Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 76a458c

Browse files
author
Connell, Joseph
committedJun 14, 2025·
Merge remote-tracking branch 'upstream/dev' into databricks-plugin
2 parents 5d1625e + 4022c65 commit 76a458c

File tree

13 files changed

+202
-167
lines changed

13 files changed

+202
-167
lines changed
 

‎client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { default as AutoComplete } from "antd/es/auto-complete";
4343
import { RefControl } from "comps/controls/refControl";
4444
import {
4545
booleanExposingStateControl,
46+
jsonValueExposingStateControl,
4647
} from "comps/controls/codeStateControl";
4748

4849
import { getDayJSLocale } from "i18n/dayjsLocale";
@@ -88,6 +89,7 @@ const childrenMap = {
8889
autocompleteIconColor: dropdownControl(autocompleteIconColor, "blue"),
8990
componentSize: dropdownControl(componentSize, "small"),
9091
valueInItems: booleanExposingStateControl("valueInItems"),
92+
selectedOption: jsonValueExposingStateControl("selectedOption", {}),
9193
style: styleControl(InputFieldStyle , 'style'),
9294
labelStyle: styleControl(LabelStyle , 'labelStyle'),
9395
inputFieldStyle: styleControl(InputLikeStyle , 'inputFieldStyle'),
@@ -247,14 +249,18 @@ let AutoCompleteCompBase = (function () {
247249
setsearchtext(value);
248250
props.value.onChange(value);
249251
props.onEvent("change");
250-
}, [props.valueInItems, getTextInputValidate, props.value, props.onEvent]);
252+
if(!Boolean(value)) {
253+
props.selectedOption.onChange({});
254+
}
255+
}, [props.valueInItems, getTextInputValidate, props.value, props.onEvent, props.selectedOption]);
251256

252257
const handleSelect = useCallback((data: string, option: any) => {
253258
setsearchtext(option[valueOrLabel]);
254259
props.valueInItems.onChange(true);
255260
props.value.onChange(option[valueOrLabel]);
261+
props.selectedOption.onChange(option);
256262
props.onEvent("submit");
257-
}, [valueOrLabel, props.valueInItems, props.value, props.onEvent]);
263+
}, [valueOrLabel, props.valueInItems, props.value, props.onEvent, props.selectedOption]);
258264

259265
const handleFocus = useCallback(() => {
260266
setActivationFlag(true);
@@ -313,17 +319,7 @@ let AutoCompleteCompBase = (function () {
313319
.setPropertyViewFn((children) => {
314320
return (
315321
<>
316-
<Section>
317-
{children.autoCompleteType.getView() === 'normal' &&
318-
children.prefixIcon.propertyView({
319-
label: trans('button.prefixIcon'),
320-
})}
321-
{children.autoCompleteType.getView() === 'normal' &&
322-
children.suffixIcon.propertyView({
323-
label: trans('button.suffixIcon'),
324-
})}
325-
{allowClearPropertyView(children)}
326-
</Section>
322+
<TextInputBasicSection {...children} />
327323
<Section name={trans('autoComplete.SectionDataName')}>
328324
{children.items.propertyView({
329325
label: trans('autoComplete.value'),
@@ -351,25 +347,35 @@ let AutoCompleteCompBase = (function () {
351347
label: trans('autoComplete.ignoreCase'),
352348
})
353349
)}
354-
{children.filterOptionsByInput.getView() && (
355-
children.valueOrLabel.propertyView({
356-
label: trans('autoComplete.checkedValueFrom'),
357-
radioButton: true,
358-
})
359-
)}
350+
{children.valueOrLabel.propertyView({
351+
label: trans('autoComplete.checkedValueFrom'),
352+
radioButton: true,
353+
})}
360354
</Section>
361-
<TextInputBasicSection {...children} />
362355

363356
<FormDataPropertyView {...children} />
364357
{children.label.getPropertyView()}
365358

366359
<TextInputInteractionSection {...children} />
367360

368-
{<TextInputValidationSection {...children} />}
369361

370362
<Section name={sectionNames.layout}>
371363
{hiddenPropertyView(children)}
372364
</Section>
365+
366+
<Section name={sectionNames.advanced}>
367+
{children.autoCompleteType.getView() === 'normal' &&
368+
children.prefixIcon.propertyView({
369+
label: trans('button.prefixIcon'),
370+
})}
371+
{children.autoCompleteType.getView() === 'normal' &&
372+
children.suffixIcon.propertyView({
373+
label: trans('button.suffixIcon'),
374+
})}
375+
{allowClearPropertyView(children)}
376+
</Section>
377+
378+
{<TextInputValidationSection {...children} />}
373379

374380
<Section name={sectionNames.style}>
375381
{children.style.getPropertyView()}
@@ -389,9 +395,6 @@ let AutoCompleteCompBase = (function () {
389395
>
390396
{children.animationStyle.getPropertyView()}
391397
</Section>
392-
<Section name={sectionNames.advanced}>
393-
{children.tabIndex.propertyView({ label: trans("prop.tabIndex") })}
394-
</Section>
395398
</>
396399
);
397400
})
@@ -415,6 +418,7 @@ AutoCompleteCompBase = class extends AutoCompleteCompBase {
415418
export const AutoCompleteComp = withExposingConfigs(AutoCompleteCompBase, [
416419
new NameConfig("value", trans("export.inputValueDesc")),
417420
new NameConfig("valueInItems", trans("autoComplete.valueInItems")),
421+
new NameConfig("selectedOption", trans("autoComplete.selectedOption")),
418422
NameConfigPlaceHolder,
419423
NameConfigRequired,
420424
...TextInputConfigs,

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDateComp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ const Wrapper = styled.div`
127127

128128
export function formatDate(date: string, format: string) {
129129
let mom = dayjs(date);
130-
if (isNumber(Number(date)) && date !== "") {
130+
if (isNumber(Number(date)) && !isNaN(Number(date)) && date !== "") {
131131
mom = dayjs(Number(date));
132132
}
133133
if (!mom.isValid()) {

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const LinkEventOptions = [clickEvent] as const;
1919

2020
const childrenMap = {
2121
text: StringControl,
22+
onClick: ActionSelectorControlInContext,
2223
onEvent: eventHandlerControl(LinkEventOptions),
2324
disabled: BoolCodeControl,
2425
style: styleControl(TableColumnLinkStyle),
@@ -37,12 +38,12 @@ const StyledLink = styled.a<{ $disabled: boolean }>`
3738
`;
3839

3940
// Memoized link component
40-
export const ColumnLink = React.memo(({ disabled, label, onEvent }: { disabled: boolean; label: string; onEvent?: (eventName: string) => void }) => {
41+
export const ColumnLink = React.memo(({ disabled, label, onClick, onEvent }: { disabled: boolean; label: string; onClick?: () => void; onEvent?: (eventName: string) => void }) => {
4142
const handleClick = useCallback(() => {
42-
if (!disabled && onEvent) {
43-
onEvent("click");
44-
}
45-
}, [disabled, onEvent]);
43+
if (disabled) return;
44+
onClick?.();
45+
// onEvent?.("click");
46+
}, [disabled, onClick, onEvent]);
4647

4748
return (
4849
<StyledLink
@@ -109,7 +110,7 @@ export const LinkComp = (function () {
109110
childrenMap,
110111
(props, dispatch) => {
111112
const value = props.changeValue ?? getBaseValue(props, dispatch);
112-
return <ColumnLink disabled={props.disabled} label={value} onEvent={props.onEvent} />;
113+
return <ColumnLink disabled={props.disabled} label={value} onClick={props.onClick} />;
113114
},
114115
(nodeValue) => nodeValue.text.value,
115116
getBaseValue
@@ -128,7 +129,11 @@ export const LinkComp = (function () {
128129
tooltip: ColumnValueTooltip,
129130
})}
130131
{disabledPropertyView(children)}
131-
{children.onEvent.propertyView()}
132+
{/* {children.onEvent.propertyView()} */}
133+
{children.onClick.propertyView({
134+
label: trans("table.action"),
135+
placement: "table",
136+
})}
132137
</>
133138
))
134139
.setStylePropertyViewFn((children) => (

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const OptionItem = new MultiCompBuilder(
6363
})}
6464
{hiddenPropertyView(children)}
6565
{disabledPropertyView(children)}
66-
{children.onEvent.propertyView()}
66+
{/* {children.onEvent.propertyView()} */}
6767
</>
6868
);
6969
})
@@ -76,9 +76,9 @@ const MenuItem = React.memo(({ option, index, onMainEvent }: { option: any; inde
7676
if (option.onClick) {
7777
option.onClick();
7878
}
79-
if (option.onEvent) {
80-
option.onEvent("click");
81-
}
79+
// if (option.onEvent) {
80+
// option.onEvent("click");
81+
// }
8282
// Trigger the main component's event handler
8383
if (onMainEvent) {
8484
onMainEvent("click");

‎client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,21 @@ import { CSSProperties } from "react";
1414
import { RecordConstructorToComp } from "lowcoder-core";
1515
import { ToViewReturn } from "@lowcoder-ee/comps/generators/multi";
1616
import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl";
17+
import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators";
1718

19+
export const fixOldActionData = (oldData: any) => {
20+
if (!oldData) return oldData;
21+
if (Boolean(oldData.onClick)) {
22+
return {
23+
...oldData,
24+
onClick: [{
25+
name: "click",
26+
handler: oldData.onClick,
27+
}],
28+
};
29+
}
30+
return oldData;
31+
}
1832
export const ColumnValueTooltip = trans("table.columnValueTooltip");
1933

2034
export const ButtonTypeOptions = [
@@ -37,7 +51,7 @@ const ButtonEventOptions = [clickEvent] as const;
3751
const childrenMap = {
3852
text: StringControl,
3953
buttonType: dropdownControl(ButtonTypeOptions, "primary"),
40-
onEvent: eventHandlerControl(ButtonEventOptions),
54+
onClick: eventHandlerControl(ButtonEventOptions),
4155
loading: BoolCodeControl,
4256
disabled: BoolCodeControl,
4357
prefixIcon: IconControl,
@@ -52,8 +66,8 @@ const ButtonStyled = React.memo(({ props }: { props: ToViewReturn<RecordConstruc
5266
const iconOnly = !hasText && (hasPrefixIcon || hasSuffixIcon);
5367

5468
const handleClick = useCallback((e: React.MouseEvent) => {
55-
props.onEvent("click");
56-
}, [props.onEvent]);
69+
props.onClick?.("click");
70+
}, [props.onClick]);
5771

5872
const buttonStyle = useMemo(() => ({
5973
margin: 0,
@@ -79,7 +93,7 @@ const ButtonStyled = React.memo(({ props }: { props: ToViewReturn<RecordConstruc
7993
);
8094
});
8195

82-
export const ButtonComp = (function () {
96+
const ButtonCompTmp = (function () {
8397
return new ColumnTypeCompBuilder(
8498
childrenMap,
8599
(props) => <ButtonStyled props={props} />,
@@ -103,8 +117,10 @@ export const ButtonComp = (function () {
103117
})}
104118
{loadingPropertyView(children)}
105119
{disabledPropertyView(children)}
106-
{children.onEvent.propertyView()}
120+
{children.onClick.propertyView()}
107121
</>
108122
))
109123
.build();
110124
})();
125+
126+
export const ButtonComp = migrateOldData(ButtonCompTmp, fixOldActionData);

‎client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,26 +194,27 @@ const ColumnPropertyView = React.memo(({
194194
summaryRowIndex: number;
195195
}) => {
196196
const selectedColumn = comp.children.render.getSelectedComp();
197-
198197
const columnType = useMemo(() =>
199198
selectedColumn.getComp().children.compType.getView(),
200199
[selectedColumn]
201200
);
202-
201+
202+
const initialColumns = useMemo(() =>
203+
selectedColumn.getParams()?.initialColumns as OptionType[] || [],
204+
[selectedColumn]
205+
);
206+
203207
const columnValue = useMemo(() => {
204208
const column = selectedColumn.getComp().toJsonValue();
205209
if (column.comp?.hasOwnProperty('src')) {
206210
return (column.comp as any).src;
207211
} else if (column.comp?.hasOwnProperty('text')) {
208-
return (column.comp as any).text;
212+
const value = (column.comp as any).text;
213+
const isDynamicValue = initialColumns.find((column) => column.value === value);
214+
return !isDynamicValue ? '{{currentCell}}' : value;
209215
}
210216
return '{{currentCell}}';
211-
}, [selectedColumn]);
212-
213-
const initialColumns = useMemo(() =>
214-
selectedColumn.getParams()?.initialColumns as OptionType[] || [],
215-
[selectedColumn]
216-
);
217+
}, [selectedColumn, initialColumns]);
217218

218219
const summaryColumns = comp.children.summaryColumns.getView();
219220

‎client/packages/lowcoder/src/comps/comps/tableComp/selectionControl.tsx

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { TableOnEventView } from "./tableTypes";
77
import { OB_ROW_ORI_INDEX, RecordType } from "comps/comps/tableComp/tableUtils";
88
import { ControlNodeCompBuilder } from "comps/generators/controlCompBuilder";
99

10+
// double-click detection constants
11+
const DOUBLE_CLICK_THRESHOLD = 300; // ms
12+
let lastClickTime = 0;
13+
let clickTimer: ReturnType<typeof setTimeout>;
14+
1015
const modeOptions = [
1116
{
1217
label: trans("selectionControl.single"),
@@ -38,8 +43,9 @@ export function getSelectedRowKeys(
3843
return [selection.children.selectedRowKey.getView()];
3944
case "multiple":
4045
return selection.children.selectedRowKeys.getView();
46+
default:
47+
return [];
4148
}
42-
return [];
4349
}
4450

4551
export const SelectionControl = (function () {
@@ -50,40 +56,52 @@ export const SelectionControl = (function () {
5056
};
5157
return new ControlNodeCompBuilder(childrenMap, (props, dispatch) => {
5258
const changeSelectedRowKey = (record: RecordType) => {
53-
if (getKey(record) !== props.selectedRowKey) {
54-
dispatch(changeChildAction("selectedRowKey", getKey(record), false));
59+
const key = getKey(record);
60+
if (key !== props.selectedRowKey) {
61+
dispatch(changeChildAction("selectedRowKey", key, false));
5562
}
5663
};
64+
5765
return (onEvent: TableOnEventView) => {
66+
const handleClick = (record: RecordType) => {
67+
return () => {
68+
const now = Date.now();
69+
clearTimeout(clickTimer);
70+
if (now - lastClickTime < DOUBLE_CLICK_THRESHOLD) {
71+
72+
changeSelectedRowKey(record);
73+
onEvent("doubleClick");
74+
if (getKey(record) !== props.selectedRowKey) {
75+
onEvent("rowSelectChange");
76+
}
77+
} else {
78+
clickTimer = setTimeout(() => {
79+
changeSelectedRowKey(record);
80+
onEvent("rowClick");
81+
if (getKey(record) !== props.selectedRowKey) {
82+
onEvent("rowSelectChange");
83+
}
84+
}, DOUBLE_CLICK_THRESHOLD);
85+
}
86+
lastClickTime = now;
87+
};
88+
};
89+
5890
if (props.mode === "single" || props.mode === "close") {
5991
return {
6092
rowKey: getKey,
6193
rowClassName: (record: RecordType, index: number, indent: number) => {
62-
// Turn off row selection mode, only do visual shutdown, selectedRow still takes effect
6394
if (props.mode === "close") {
6495
return "";
6596
}
6697
return getKey(record) === props.selectedRowKey ? "ant-table-row-selected" : "";
6798
},
68-
onRow: (record: RecordType, index: number | undefined) => {
69-
return {
70-
onClick: () => {
71-
changeSelectedRowKey(record);
72-
onEvent("rowClick");
73-
if (getKey(record) !== props.selectedRowKey) {
74-
onEvent("rowSelectChange");
75-
}
76-
},
77-
onDoubleClick: () => {
78-
onEvent("doubleClick");
79-
if (getKey(record) !== props.selectedRowKey) {
80-
onEvent("rowSelectChange");
81-
}
82-
}
83-
};
84-
},
99+
onRow: (record: RecordType, index: number | undefined) => ({
100+
onClick: handleClick(record),
101+
}),
85102
};
86103
}
104+
87105
const result: TableRowSelection<any> = {
88106
type: "checkbox",
89107
selectedRowKeys: props.selectedRowKeys,
@@ -92,7 +110,6 @@ export const SelectionControl = (function () {
92110
dispatch(changeChildAction("selectedRowKeys", selectedRowKeys as string[], false));
93111
onEvent("rowSelectChange");
94112
},
95-
// click checkbox also trigger row click event
96113
onSelect: (record: RecordType) => {
97114
changeSelectedRowKey(record);
98115
onEvent("rowClick");
@@ -101,18 +118,9 @@ export const SelectionControl = (function () {
101118
return {
102119
rowKey: getKey,
103120
rowSelection: result,
104-
onRow: (record: RecordType) => {
105-
return {
106-
onClick: () => {
107-
changeSelectedRowKey(record);
108-
onEvent("rowClick");
109-
},
110-
onDoubleClick: () => {
111-
changeSelectedRowKey(record);
112-
onEvent("doubleClick");
113-
}
114-
};
115-
},
121+
onRow: (record: RecordType) => ({
122+
onClick: handleClick(record),
123+
}),
116124
};
117125
};
118126
})
@@ -123,4 +131,4 @@ export const SelectionControl = (function () {
123131
})
124132
)
125133
.build();
126-
})();
134+
})();

‎client/packages/lowcoder/src/comps/comps/tableComp/tableSummaryComp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export const TableSummary = memo(function TableSummary(props: {
203203

204204
const visibleColumns = useMemo(() => {
205205
let cols = columns.filter(col => !col.getView().hide);
206-
if (dynamicColumn) {
206+
if (dynamicColumn && dynamicColumnConfig?.length) {
207207
cols = cols.filter(col => {
208208
const colView = col.getView();
209209
return dynamicColumnConfig.includes(colView.isCustom ? colView.title : colView.dataIndex)

‎client/packages/lowcoder/src/comps/hooks/localStorageComp.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isEmpty } from "lodash";
33
import { simpleMultiComp, stateComp, withViewFn } from "../generators";
44
import { NameConfig, withExposingConfigs } from "../generators/withExposing";
55
import { JSONObject } from "../../util/jsonTypes";
6-
import { useEffect } from "react";
6+
import { useEffect, useMemo, useCallback } from "react";
77
import isEqual from "fast-deep-equal";
88
import { trans } from "i18n";
99
import log from "loglevel";
@@ -13,7 +13,22 @@ const APP_STORE_NAMESPACE = "lowcoder_app_local_storage";
1313
const LocalStorageCompBase = withViewFn(
1414
simpleMultiComp({ values: stateComp<JSONObject>({}) }),
1515
(comp) => {
16-
// add custom event listener to update values reactively
16+
const originStore = localStorage.getItem(APP_STORE_NAMESPACE) || "{}";
17+
18+
let parseStore = {};
19+
try {
20+
parseStore = JSON.parse(originStore);
21+
} catch (e) {
22+
log.error("application local storage invalid");
23+
}
24+
25+
useEffect(() => {
26+
const value = comp.children.values.value;
27+
if (!isEqual(value, parseStore)) {
28+
comp.children.values.dispatchChangeValueAction(parseStore);
29+
}
30+
}, [parseStore]);
31+
1732
useEffect(() => {
1833
const handler = () => {
1934
try {
@@ -28,9 +43,6 @@ const LocalStorageCompBase = withViewFn(
2843
// Add listener on mount
2944
window.addEventListener("lowcoder-localstorage-updated", handler);
3045

31-
// Run once on mount to initialize
32-
handler();
33-
3446
return () => {
3547
window.removeEventListener("lowcoder-localstorage-updated", handler);
3648
};

‎client/packages/lowcoder/src/comps/queries/queryComp/queryPropertyView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ export const QueryGeneralPropertyView = (props: {
239239
comp.children.datasourceId.dispatchChangeValueAction(QUICK_REST_API_ID);
240240
}
241241

242+
if (datasourceType === 'js' && datasourceId === '') {
243+
datasourceId = JS_CODE_ID;
244+
comp.children.datasourceId.dispatchChangeValueAction(JS_CODE_ID);
245+
}
246+
242247
const triggerOptions = useMemo(() => {
243248
if (datasourceType === "js" || datasourceType === "streamApi") {
244249
return JSTriggerTypeOptions;

‎client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ export const en = {
591591
"chartBorderColor": "Border Color",
592592
"chartTextColor": "Text Color",
593593
"detailSize": "Detail Size",
594+
"hideColumn": "Hide Column",
594595

595596
"radiusTip": "Specifies the radius of the element's corners. Example: 5px, 50%, or 1em.",
596597
"gapTip": "Specifies the gap between rows and columns in a grid or flex container. Example: 10px, 1rem, or 5%.",

‎client/packages/lowcoder/src/pages/editor/editorView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,6 @@ function EditorView(props: EditorViewProps) {
498498

499499
return () => {
500500
window.removeEventListener(eventType, updateSize);
501-
savePanelStatus(panelStatus);
502-
saveEditorModeStatus(editorModeStatus);
503501
};
504502
}, [panelStatus, editorModeStatus]);
505503

@@ -553,6 +551,8 @@ function EditorView(props: EditorViewProps) {
553551
setShowShortcutList(false);
554552
setMenuKey(SiderKey.State);
555553
setHeight(undefined);
554+
savePanelStatus(panelStatus);
555+
saveEditorModeStatus(editorModeStatus);
556556
};
557557
}, []);
558558

‎server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java

Lines changed: 57 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,128 +12,111 @@
1212
import java.io.IOException;
1313
import java.nio.file.Files;
1414
import java.nio.file.Path;
15-
import java.util.ArrayList;
16-
import java.util.Iterator;
17-
import java.util.List;
18-
import java.util.ServiceLoader;
15+
import java.util.*;
1916

2017
@Slf4j
2118
@RequiredArgsConstructor
2219
@Component
23-
public class PathBasedPluginLoader implements PluginLoader
24-
{
20+
public class PathBasedPluginLoader implements PluginLoader {
2521
private final CommonConfig common;
2622
private final ApplicationHome applicationHome;
27-
23+
24+
// Cache for plugin JAR paths to avoid redundant filesystem scans
25+
private static final Map<String, List<String>> cachedPluginJars = new HashMap<>();
26+
2827
@Override
29-
public List<LowcoderPlugin> loadPlugins()
30-
{
28+
public List<LowcoderPlugin> loadPlugins() {
3129
List<LowcoderPlugin> plugins = new ArrayList<>();
32-
30+
31+
// Find plugin JARs using caching
3332
List<String> pluginJars = findPluginsJars();
34-
if (pluginJars.isEmpty())
35-
{
33+
if (pluginJars.isEmpty()) {
34+
log.debug("No plugin JARs found.");
3635
return plugins;
3736
}
3837

39-
for (String pluginJar : pluginJars)
40-
{
38+
// Load plugins from JARs
39+
pluginJars.parallelStream().forEach(pluginJar -> {
4140
log.debug("Inspecting plugin jar candidate: {}", pluginJar);
4241
List<LowcoderPlugin> loadedPlugins = loadPluginCandidates(pluginJar);
43-
if (loadedPlugins.isEmpty())
44-
{
42+
if (loadedPlugins.isEmpty()) {
4543
log.debug(" - no plugins found in the jar file");
44+
} else {
45+
synchronized (plugins) {
46+
plugins.addAll(loadedPlugins);
47+
}
4648
}
47-
else
48-
{
49-
for (LowcoderPlugin plugin : loadedPlugins)
50-
{
51-
plugins.add(plugin);
52-
}
53-
}
54-
}
55-
49+
});
50+
5651
return plugins;
5752
}
58-
59-
protected List<String> findPluginsJars()
60-
{
53+
54+
protected List<String> findPluginsJars() {
55+
String cacheKey = common.getPluginDirs().toString();
56+
57+
// Use cached JAR paths if available
58+
if (cachedPluginJars.containsKey(cacheKey)) {
59+
log.debug("Using cached plugin jar candidates for key: {}", cacheKey);
60+
return cachedPluginJars.get(cacheKey);
61+
}
62+
6163
List<String> candidates = new ArrayList<>();
62-
if (CollectionUtils.isNotEmpty(common.getPluginDirs()))
63-
{
64-
for (String pluginDir : common.getPluginDirs())
65-
{
64+
if (CollectionUtils.isNotEmpty(common.getPluginDirs())) {
65+
for (String pluginDir : common.getPluginDirs()) {
6666
final Path pluginPath = getAbsoluteNormalizedPath(pluginDir);
67-
if (pluginPath != null)
68-
{
67+
if (pluginPath != null) {
6968
candidates.addAll(findPluginCandidates(pluginPath));
7069
}
7170
}
7271
}
73-
72+
73+
// Cache the results
74+
cachedPluginJars.put(cacheKey, candidates);
7475
return candidates;
7576
}
7677

77-
78-
protected List<String> findPluginCandidates(Path pluginsDir)
79-
{
80-
List<String> pluginCandidates = new ArrayList<>();
81-
try
82-
{
83-
Files.walk(pluginsDir)
84-
.filter(Files::isRegularFile)
85-
.filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar"))
86-
.forEach(path -> pluginCandidates.add(path.toString()));
87-
}
88-
catch(IOException cause)
89-
{
78+
protected List<String> findPluginCandidates(Path pluginsDir) {
79+
try {
80+
return Files.walk(pluginsDir)
81+
.filter(Files::isRegularFile)
82+
.filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar"))
83+
.map(Path::toString)
84+
.toList(); // Use Java 16+ `toList()` for better performance
85+
} catch (IOException cause) {
9086
log.error("Error walking plugin folder! - {}", cause.getMessage());
87+
return Collections.emptyList();
9188
}
92-
93-
return pluginCandidates;
9489
}
95-
96-
protected List<LowcoderPlugin> loadPluginCandidates(String pluginJar)
97-
{
90+
91+
protected List<LowcoderPlugin> loadPluginCandidates(String pluginJar) {
9892
List<LowcoderPlugin> pluginCandidates = new ArrayList<>();
9993

100-
try
101-
{
94+
try {
10295
Path pluginPath = Path.of(pluginJar);
10396
PluginClassLoader pluginClassLoader = new PluginClassLoader(pluginPath.getFileName().toString(), pluginPath);
10497

10598
ServiceLoader<LowcoderPlugin> pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader);
106-
if (pluginServices != null )
107-
{
108-
Iterator<LowcoderPlugin> pluginIterator = pluginServices.iterator();
109-
while(pluginIterator.hasNext())
110-
{
111-
LowcoderPlugin plugin = pluginIterator.next();
99+
if (pluginServices != null) {
100+
for (LowcoderPlugin plugin : pluginServices) {
112101
log.debug(" - loaded plugin: {} - {}", plugin.pluginId(), plugin.description());
113102
pluginCandidates.add(plugin);
114103
}
115104
}
116-
}
117-
catch(Throwable cause)
118-
{
105+
} catch (Throwable cause) {
119106
log.warn("Error loading plugin!", cause);
120107
}
121-
108+
122109
return pluginCandidates;
123110
}
124-
125-
private Path getAbsoluteNormalizedPath(String path)
126-
{
127-
if (StringUtils.isNotBlank(path))
128-
{
111+
112+
private Path getAbsoluteNormalizedPath(String path) {
113+
if (StringUtils.isNotBlank(path)) {
129114
Path absPath = Path.of(path);
130-
if (!absPath.isAbsolute())
131-
{
115+
if (!absPath.isAbsolute()) {
132116
absPath = Path.of(applicationHome.getDir().getAbsolutePath(), absPath.toString());
133117
}
134118
return absPath.normalize().toAbsolutePath();
135119
}
136-
137120
return null;
138121
}
139-
}
122+
}

0 commit comments

Comments
 (0)
Please sign in to comment.