@nocobase/flow-engine 2.1.0-alpha.40 → 2.1.0-alpha.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +84 -32
  4. package/lib/components/subModel/LazyDropdown.js +208 -16
  5. package/lib/components/subModel/utils.d.ts +1 -0
  6. package/lib/components/subModel/utils.js +6 -2
  7. package/lib/data-source/index.d.ts +9 -0
  8. package/lib/data-source/index.js +12 -0
  9. package/lib/executor/FlowExecutor.js +0 -3
  10. package/lib/flowContext.d.ts +6 -1
  11. package/lib/flowContext.js +38 -6
  12. package/lib/flowEngine.d.ts +4 -3
  13. package/lib/flowEngine.js +72 -40
  14. package/lib/models/flowModel.js +48 -16
  15. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  16. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  17. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  18. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  19. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  20. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  21. package/lib/runjs-context/contexts/base.js +464 -29
  22. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  23. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  24. package/lib/utils/loadedPageCache.d.ts +24 -0
  25. package/lib/utils/loadedPageCache.js +139 -0
  26. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  27. package/lib/utils/parsePathnameToViewParams.js +28 -4
  28. package/lib/views/ViewNavigation.d.ts +12 -2
  29. package/lib/views/ViewNavigation.js +22 -7
  30. package/lib/views/createViewMeta.js +114 -50
  31. package/lib/views/inheritLayoutContext.d.ts +10 -0
  32. package/lib/views/inheritLayoutContext.js +50 -0
  33. package/lib/views/useDialog.js +2 -0
  34. package/lib/views/useDrawer.js +2 -0
  35. package/lib/views/usePage.js +2 -0
  36. package/package.json +4 -4
  37. package/src/FlowContextProvider.tsx +9 -1
  38. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  39. package/src/__tests__/flowContext.test.ts +23 -0
  40. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  41. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  42. package/src/__tests__/runjsContext.test.ts +18 -0
  43. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  44. package/src/__tests__/runjsLocales.test.ts +6 -5
  45. package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
  46. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +90 -38
  47. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +155 -5
  48. package/src/components/subModel/LazyDropdown.tsx +237 -16
  49. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +254 -1
  50. package/src/components/subModel/utils.ts +6 -1
  51. package/src/data-source/index.ts +18 -0
  52. package/src/executor/FlowExecutor.ts +0 -3
  53. package/src/executor/__tests__/flowExecutor.test.ts +26 -0
  54. package/src/flowContext.ts +43 -6
  55. package/src/flowEngine.ts +75 -38
  56. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  57. package/src/models/__tests__/flowModel.test.ts +46 -62
  58. package/src/models/flowModel.tsx +65 -32
  59. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  60. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  61. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  62. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  63. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  64. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  65. package/src/runjs-context/contexts/base.ts +467 -31
  66. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  67. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  68. package/src/utils/loadedPageCache.ts +147 -0
  69. package/src/utils/parsePathnameToViewParams.ts +45 -5
  70. package/src/views/ViewNavigation.ts +40 -7
  71. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  72. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  73. package/src/views/createViewMeta.ts +106 -34
  74. package/src/views/inheritLayoutContext.ts +26 -0
  75. package/src/views/useDialog.tsx +2 -0
  76. package/src/views/useDrawer.tsx +2 -0
  77. package/src/views/usePage.tsx +2 -0
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import React from 'react';
10
10
  import { FlowContext, FlowEngineContext } from './flowContext';
11
- import { FlowView } from './views/FlowView';
11
+ import { FlowView, FlowViewer } from './views/FlowView';
12
12
  export declare const FlowReactContext: React.Context<FlowContext>;
13
13
  export declare const FlowViewContext: React.Context<FlowContext>;
14
14
  export declare function FlowContextProvider(props: {
@@ -22,3 +22,7 @@ export declare const FlowViewContextProvider: React.MemoExoticComponent<(props:
22
22
  export declare function useFlowContext<T = FlowEngineContext>(): T;
23
23
  export declare function useFlowViewContext<T = FlowEngineContext>(): T;
24
24
  export declare function useFlowView(): FlowView;
25
+ /**
26
+ * Access the `FlowViewer` that opens new drawers / modals / pages (`viewer.drawer({...})`, `viewer.modal({...})`, etc.). This is the counterpart to `useFlowView()`: `useFlowView()` returns the *current* mounted view (use it to close yourself, render Header/Footer slots, etc.), while `useFlowViewer()` returns the surface that lets you open a *new* view from inside any flow-context subtree.
27
+ */
28
+ export declare function useFlowViewer(): FlowViewer;
@@ -43,7 +43,8 @@ __export(FlowContextProvider_exports, {
43
43
  FlowViewContextProvider: () => FlowViewContextProvider,
44
44
  useFlowContext: () => useFlowContext,
45
45
  useFlowView: () => useFlowView,
46
- useFlowViewContext: () => useFlowViewContext
46
+ useFlowViewContext: () => useFlowViewContext,
47
+ useFlowViewer: () => useFlowViewer
47
48
  });
48
49
  module.exports = __toCommonJS(FlowContextProvider_exports);
49
50
  var import_react = __toESM(require("react"));
@@ -70,6 +71,11 @@ function useFlowView() {
70
71
  return ctx.view;
71
72
  }
72
73
  __name(useFlowView, "useFlowView");
74
+ function useFlowViewer() {
75
+ const ctx = useFlowContext();
76
+ return ctx.viewer;
77
+ }
78
+ __name(useFlowViewer, "useFlowViewer");
73
79
  // Annotate the CommonJS export names for ESM import in node:
74
80
  0 && (module.exports = {
75
81
  FlowContextProvider,
@@ -78,5 +84,6 @@ __name(useFlowView, "useFlowView");
78
84
  FlowViewContextProvider,
79
85
  useFlowContext,
80
86
  useFlowView,
81
- useFlowViewContext
87
+ useFlowViewContext,
88
+ useFlowViewer
82
89
  });
@@ -183,6 +183,13 @@ const getToolbarPopupContainer = /* @__PURE__ */ __name((triggerNode) => {
183
183
  }
184
184
  return triggerNode.closest(TOOLBAR_ICONS_SELECTOR) || triggerNode.closest(TOOLBAR_CONTAINER_SELECTOR);
185
185
  }, "getToolbarPopupContainer");
186
+ const removeExtraMenuItemClickHandlers = /* @__PURE__ */ __name((item) => {
187
+ const { onClick: _onClick, children, ...rest } = item;
188
+ return {
189
+ ...rest,
190
+ children: (children == null ? void 0 : children.length) ? children.map(removeExtraMenuItemClickHandlers) : void 0
191
+ };
192
+ }, "removeExtraMenuItemClickHandlers");
186
193
  const DefaultSettingsIcon = /* @__PURE__ */ __name(({
187
194
  model,
188
195
  showDeleteButton = true,
@@ -200,8 +207,17 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
200
207
  const [visible, setVisible] = (0, import_react.useState)(false);
201
208
  const [refreshTick, setRefreshTick] = (0, import_react.useState)(0);
202
209
  const [extraMenuItems, setExtraMenuItems] = (0, import_react.useState)([]);
210
+ const [extraMenuItemsLoaded, setExtraMenuItemsLoaded] = (0, import_react.useState)(false);
203
211
  const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = (0, import_react.useState)([]);
204
212
  const [isLoading, setIsLoading] = (0, import_react.useState)(true);
213
+ const commonExtras = (0, import_react.useMemo)(
214
+ () => extraMenuItems.filter((it) => it.group === "common-actions").sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)),
215
+ [extraMenuItems]
216
+ );
217
+ const hasCommonActions = showCopyUidButton || showDeleteButton || commonExtras.length > 0;
218
+ const shouldDeferConfigLoading = flattenSubMenus && menuLevels > 1 && hasCommonActions;
219
+ const shouldWaitForCommonActionProbe = flattenSubMenus && menuLevels > 1 && !showCopyUidButton && !showDeleteButton && !extraMenuItemsLoaded;
220
+ const canRenderIcon = hasCommonActions || !isLoading && configurableFlowsAndSteps.length > 0;
205
221
  const closeDropdown = (0, import_react.useCallback)(() => {
206
222
  setVisible(false);
207
223
  onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(false);
@@ -233,24 +249,28 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
233
249
  let mounted = true;
234
250
  const loadExtras = /* @__PURE__ */ __name(async () => {
235
251
  var _a;
236
- const allExtras = [];
237
- const modelsToProcess = [];
238
- walkSubModels(model, { maxDepth: menuLevels, arrayLimit: 50, mode: "stack" }, (targetModel, { modelKey }) => {
239
- modelsToProcess.push({ model: targetModel, modelKey });
240
- });
241
- for (const { model: targetModel, modelKey } of modelsToProcess) {
242
- const Cls = targetModel.constructor;
243
- const extras = await ((_a = Cls.getExtraMenuItems) == null ? void 0 : _a.call(Cls, targetModel, t));
244
- if (extras == null ? void 0 : extras.length) {
245
- allExtras.push(
246
- ...extras.map((item) => ({
247
- ...item,
248
- key: modelKey ? `${modelKey}:${item.key}` : item.key
249
- }))
250
- );
252
+ setExtraMenuItemsLoaded(false);
253
+ try {
254
+ const allExtras = [];
255
+ const modelsToProcess = [];
256
+ walkSubModels(model, { maxDepth: menuLevels, arrayLimit: 50, mode: "stack" }, (targetModel, { modelKey }) => {
257
+ modelsToProcess.push({ model: targetModel, modelKey });
258
+ });
259
+ for (const { model: targetModel, modelKey } of modelsToProcess) {
260
+ const Cls = targetModel.constructor;
261
+ const extras = await ((_a = Cls.getExtraMenuItems) == null ? void 0 : _a.call(Cls, targetModel, t));
262
+ if (extras == null ? void 0 : extras.length) {
263
+ allExtras.push(
264
+ ...extras.map((item) => ({
265
+ ...item,
266
+ key: modelKey ? `${modelKey}:${item.key}` : item.key
267
+ }))
268
+ );
269
+ }
270
+ }
271
+ if (!mounted) {
272
+ return;
251
273
  }
252
- }
253
- if (mounted) {
254
274
  const seen = /* @__PURE__ */ new Set();
255
275
  const dedupedExtras = allExtras.filter((item) => {
256
276
  if (seen.has(`${item.key}`)) {
@@ -260,15 +280,22 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
260
280
  return true;
261
281
  });
262
282
  setExtraMenuItems(dedupedExtras);
283
+ } catch (error) {
284
+ console.error("Failed to load extra menu items:", error);
285
+ if (mounted) {
286
+ setExtraMenuItems([]);
287
+ }
288
+ } finally {
289
+ if (mounted) {
290
+ setExtraMenuItemsLoaded(true);
291
+ }
263
292
  }
264
293
  }, "loadExtras");
265
- if (visible) {
266
- loadExtras();
267
- }
294
+ loadExtras();
268
295
  return () => {
269
296
  mounted = false;
270
297
  };
271
- }, [model, menuLevels, t, refreshTick, visible]);
298
+ }, [model, menuLevels, t, refreshTick]);
272
299
  const copyUidToClipboard = (0, import_react.useCallback)(
273
300
  async (uid) => {
274
301
  var _a;
@@ -513,7 +540,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
513
540
  return [];
514
541
  }
515
542
  },
516
- []
543
+ [t]
517
544
  );
518
545
  const getConfigurableFlowsAndSteps = (0, import_react.useCallback)(async () => {
519
546
  const result = [];
@@ -551,20 +578,47 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
551
578
  };
552
579
  }, [model, menuLevels, refreshTick]);
553
580
  (0, import_react.useEffect)(() => {
581
+ let mounted = true;
554
582
  const loadConfigurableFlowsAndSteps = /* @__PURE__ */ __name(async () => {
555
583
  setIsLoading(true);
584
+ if (shouldDeferConfigLoading) {
585
+ setConfigurableFlowsAndSteps([]);
586
+ }
556
587
  try {
557
588
  const flows = await getConfigurableFlowsAndSteps();
558
- setConfigurableFlowsAndSteps(flows);
589
+ if (mounted) {
590
+ setConfigurableFlowsAndSteps(flows);
591
+ }
559
592
  } catch (error) {
560
593
  console.error("Failed to load configurable flows and steps:", error);
561
- setConfigurableFlowsAndSteps([]);
594
+ if (mounted) {
595
+ setConfigurableFlowsAndSteps([]);
596
+ }
562
597
  } finally {
563
- setIsLoading(false);
598
+ if (mounted) {
599
+ setIsLoading(false);
600
+ }
564
601
  }
565
602
  }, "loadConfigurableFlowsAndSteps");
603
+ if (shouldWaitForCommonActionProbe) {
604
+ setConfigurableFlowsAndSteps([]);
605
+ setIsLoading(false);
606
+ return () => {
607
+ mounted = false;
608
+ };
609
+ }
610
+ if (!visible && shouldDeferConfigLoading) {
611
+ setConfigurableFlowsAndSteps([]);
612
+ setIsLoading(false);
613
+ return () => {
614
+ mounted = false;
615
+ };
616
+ }
566
617
  loadConfigurableFlowsAndSteps();
567
- }, [getConfigurableFlowsAndSteps, refreshTick]);
618
+ return () => {
619
+ mounted = false;
620
+ };
621
+ }, [getConfigurableFlowsAndSteps, refreshTick, shouldDeferConfigLoading, shouldWaitForCommonActionProbe, visible]);
568
622
  const menuItems = (0, import_react.useMemo)(() => {
569
623
  const items = [];
570
624
  const keyCounter = /* @__PURE__ */ new Map();
@@ -689,16 +743,15 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
689
743
  }
690
744
  }
691
745
  return items;
692
- }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, t]);
746
+ }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, message, model, t]);
693
747
  const finalMenuItems = (0, import_react.useMemo)(() => {
694
748
  const items = [...menuItems];
695
- const commonExtras = extraMenuItems.filter((it) => it.group === "common-actions").sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
696
749
  if (showCopyUidButton || showDeleteButton || commonExtras.length > 0) {
697
750
  items.push({
698
751
  type: "divider"
699
752
  });
700
753
  if (commonExtras.length > 0) {
701
- items.push(...commonExtras);
754
+ items.push(...commonExtras.map(removeExtraMenuItemClickHandlers));
702
755
  }
703
756
  if (showCopyUidButton && model.uid) {
704
757
  items.push({
@@ -714,9 +767,8 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
714
767
  }
715
768
  }
716
769
  return items;
717
- }, [menuItems, showCopyUidButton, showDeleteButton, model.uid, model.destroy, t, extraMenuItems]);
718
- const hasExtras = extraMenuItems.some((it) => it.group === "common-actions");
719
- if (isLoading || configurableFlowsAndSteps.length === 0 && !showDeleteButton && !showCopyUidButton && !hasExtras) {
770
+ }, [menuItems, showCopyUidButton, showDeleteButton, commonExtras, model.uid, model.destroy, t]);
771
+ if (!canRenderIcon) {
720
772
  return null;
721
773
  }
722
774
  if (!model || !model.uid) {
@@ -169,16 +169,66 @@ const useKeepDropdownOpen = /* @__PURE__ */ __name(() => {
169
169
  }, "useKeepDropdownOpen");
170
170
  const useMenuSearch = /* @__PURE__ */ __name(() => {
171
171
  const [searchValues, setSearchValues] = (0, import_react.useState)({});
172
+ const [inputValues, setInputValues] = (0, import_react.useState)({});
172
173
  const [isSearching, setIsSearching] = (0, import_react.useState)(false);
174
+ const [composingCount, setComposingCount] = (0, import_react.useState)(0);
175
+ const composingKeysRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
173
176
  const searchTimeoutRef = (0, import_react.useRef)(null);
174
- const updateSearchValue = /* @__PURE__ */ __name((key, value) => {
177
+ const updateSearchValue = (0, import_react.useCallback)((key, value) => {
175
178
  setIsSearching(true);
179
+ setInputValues((prev) => ({ ...prev, [key]: value }));
176
180
  setSearchValues((prev) => ({ ...prev, [key]: value }));
177
181
  if (searchTimeoutRef.current) {
178
182
  clearTimeout(searchTimeoutRef.current);
179
183
  }
180
184
  searchTimeoutRef.current = setTimeout(() => setIsSearching(false), 300);
181
- }, "updateSearchValue");
185
+ }, []);
186
+ const startComposition = (0, import_react.useCallback)((key) => {
187
+ composingKeysRef.current.add(key);
188
+ setIsSearching(true);
189
+ setComposingCount(composingKeysRef.current.size);
190
+ if (searchTimeoutRef.current) {
191
+ clearTimeout(searchTimeoutRef.current);
192
+ searchTimeoutRef.current = null;
193
+ }
194
+ }, []);
195
+ const endComposition = (0, import_react.useCallback)(
196
+ (key, value) => {
197
+ composingKeysRef.current.delete(key);
198
+ setComposingCount(composingKeysRef.current.size);
199
+ updateSearchValue(key, value);
200
+ },
201
+ [updateSearchValue]
202
+ );
203
+ const updateInputValue = (0, import_react.useCallback)((key, value) => {
204
+ setInputValues((prev) => ({ ...prev, [key]: value }));
205
+ }, []);
206
+ const clearSearchValue = (0, import_react.useCallback)((key) => {
207
+ composingKeysRef.current.delete(key);
208
+ setComposingCount(composingKeysRef.current.size);
209
+ setInputValues((prev) => {
210
+ if (!(key in prev)) return prev;
211
+ const next = { ...prev };
212
+ delete next[key];
213
+ return next;
214
+ });
215
+ setSearchValues((prev) => {
216
+ if (!(key in prev)) return prev;
217
+ const next = { ...prev };
218
+ delete next[key];
219
+ return next;
220
+ });
221
+ }, []);
222
+ const clearAllSearchValues = (0, import_react.useCallback)(() => {
223
+ composingKeysRef.current.clear();
224
+ setComposingCount(0);
225
+ setInputValues({});
226
+ setSearchValues({});
227
+ setIsSearching(false);
228
+ }, []);
229
+ const isComposing = (0, import_react.useCallback)((key) => {
230
+ return key ? composingKeysRef.current.has(key) : composingKeysRef.current.size > 0;
231
+ }, []);
182
232
  (0, import_react.useEffect)(() => {
183
233
  return () => {
184
234
  if (searchTimeoutRef.current) {
@@ -188,8 +238,15 @@ const useMenuSearch = /* @__PURE__ */ __name(() => {
188
238
  }, []);
189
239
  return {
190
240
  searchValues,
191
- isSearching,
192
- updateSearchValue
241
+ inputValues,
242
+ isSearching: isSearching || composingCount > 0,
243
+ updateSearchValue,
244
+ updateInputValue,
245
+ startComposition,
246
+ endComposition,
247
+ clearSearchValue,
248
+ clearAllSearchValues,
249
+ isComposing
193
250
  };
194
251
  }, "useMenuSearch");
195
252
  const useSubmenuStyles = /* @__PURE__ */ __name((menuVisible, dropdownMaxHeight) => {
@@ -275,10 +332,10 @@ const getLabelSearchText = /* @__PURE__ */ __name((label) => {
275
332
  }
276
333
  return "";
277
334
  }, "getLabelSearchText");
278
- const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchValue, menuVisible, t, updateSearchValue) => ({
335
+ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchValue, menuVisible, t, searchHandlers, activateSearchSubmenu, deactivateSearchSubmenu, shouldActivateSearchSubmenu) => ({
279
336
  key: `${searchKey}-search`,
280
337
  type: "group",
281
- label: /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(
338
+ label: /* @__PURE__ */ import_react.default.createElement("div", { onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation() }, /* @__PURE__ */ import_react.default.createElement(
282
339
  SearchInputWithAutoFocus,
283
340
  {
284
341
  visible: menuVisible,
@@ -286,11 +343,50 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
286
343
  allowClear: true,
287
344
  placeholder: t(item.searchPlaceholder || "Search"),
288
345
  value: currentSearchValue,
346
+ onFocus: (e) => {
347
+ e.stopPropagation();
348
+ },
289
349
  onChange: (e) => {
350
+ var _a;
351
+ e.stopPropagation();
352
+ const value = e.target.value;
353
+ if (shouldActivateSearchSubmenu) {
354
+ activateSearchSubmenu(searchKey);
355
+ }
356
+ if (((_a = e.nativeEvent) == null ? void 0 : _a.isComposing) || searchHandlers.isComposing(searchKey)) {
357
+ searchHandlers.updateInputValue(searchKey, value);
358
+ return;
359
+ }
360
+ if (!value && shouldActivateSearchSubmenu) {
361
+ deactivateSearchSubmenu(searchKey);
362
+ }
363
+ searchHandlers.updateSearchValue(searchKey, value);
364
+ },
365
+ onCompositionStart: (e) => {
366
+ e.stopPropagation();
367
+ if (shouldActivateSearchSubmenu) {
368
+ activateSearchSubmenu(searchKey);
369
+ }
370
+ searchHandlers.startComposition(searchKey);
371
+ },
372
+ onCompositionEnd: (e) => {
373
+ e.stopPropagation();
374
+ const value = e.currentTarget.value;
375
+ if (shouldActivateSearchSubmenu) {
376
+ if (value) {
377
+ activateSearchSubmenu(searchKey);
378
+ } else {
379
+ deactivateSearchSubmenu(searchKey);
380
+ }
381
+ }
382
+ searchHandlers.endComposition(searchKey, value);
383
+ },
384
+ onClick: (e) => {
385
+ e.stopPropagation();
386
+ },
387
+ onKeyDown: (e) => {
290
388
  e.stopPropagation();
291
- updateSearchValue(searchKey, e.target.value);
292
389
  },
293
- onClick: (e) => e.stopPropagation(),
294
390
  onMouseDown: (e) => {
295
391
  e.stopPropagation();
296
392
  },
@@ -313,13 +409,21 @@ const KEEP_OPEN_LABEL_STYLE = {
313
409
  width: "100%"
314
410
  };
315
411
  const DROPDOWN_PERSIST_TTL_MS = 350;
412
+ const SUBMENU_CLOSE_DELAY = 0.05;
413
+ const SUBMENU_MOTION_DISABLED = {
414
+ motionEnter: false,
415
+ motionLeave: false
416
+ };
316
417
  const dropdownPersistRegistry = /* @__PURE__ */ new Map();
317
418
  const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
318
419
  const engine = (0, import_provider.useFlowEngine)();
319
420
  const [menuVisible, setMenuVisible] = (0, import_react.useState)(false);
320
421
  const [openKeys, setOpenKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set());
422
+ const [activeSearchKey, setActiveSearchKey] = (0, import_react.useState)(null);
321
423
  const [rootItems, setRootItems] = (0, import_react.useState)([]);
322
424
  const [rootLoading, setRootLoading] = (0, import_react.useState)(false);
425
+ const closeByOutsideClickRef = (0, import_react.useRef)(false);
426
+ const skipPreserveActiveSearchRef = (0, import_react.useRef)(false);
323
427
  const dropdownMaxHeight = useNiceDropdownMaxHeight();
324
428
  const t = engine.translate.bind(engine);
325
429
  const { items: menuItems, keepDropdownOpen, persistKey, stateVersion, refreshKeys, ...dropdownMenuProps } = menu;
@@ -330,22 +434,89 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
330
434
  openKeys,
331
435
  refreshKeys
332
436
  );
333
- const { searchValues, isSearching, updateSearchValue } = useMenuSearch();
437
+ const searchHandlers = useMenuSearch();
438
+ const { searchValues, inputValues, clearSearchValue, clearAllSearchValues } = searchHandlers;
334
439
  const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
335
440
  useSubmenuStyles(menuVisible, dropdownMaxHeight);
441
+ const closeMenu = (0, import_react.useCallback)(() => {
442
+ setMenuVisible(false);
443
+ setActiveSearchKey(null);
444
+ setOpenKeys(/* @__PURE__ */ new Set());
445
+ clearAllSearchValues();
446
+ }, [clearAllSearchValues]);
447
+ const activateSearchSubmenu = (0, import_react.useCallback)((key) => {
448
+ setActiveSearchKey(key);
449
+ setOpenKeys((prev) => {
450
+ if (prev.has(key)) return prev;
451
+ const next = new Set(prev);
452
+ next.add(key);
453
+ return next;
454
+ });
455
+ }, []);
456
+ const deactivateSearchSubmenu = (0, import_react.useCallback)((key) => {
457
+ setActiveSearchKey((prev) => prev === key ? null : prev);
458
+ }, []);
459
+ const closeActiveSearchForPath = (0, import_react.useCallback)(
460
+ (keyPath) => {
461
+ if (!activeSearchKey || keyPath === activeSearchKey || keyPath.startsWith(`${activeSearchKey}/`) || activeSearchKey.startsWith(`${keyPath}/`)) {
462
+ return;
463
+ }
464
+ skipPreserveActiveSearchRef.current = true;
465
+ clearSearchValue(activeSearchKey);
466
+ setActiveSearchKey(null);
467
+ setOpenKeys((prev) => {
468
+ const next = new Set(prev);
469
+ next.delete(activeSearchKey);
470
+ return next;
471
+ });
472
+ },
473
+ [activeSearchKey, clearSearchValue]
474
+ );
336
475
  const handleMenuOpenChange = (0, import_react.useCallback)(
337
476
  (nextOpenKeys) => {
338
477
  var _a, _b;
339
- if (!nextOpenKeys.length && shouldPreventClose()) {
478
+ let normalized = normalizeOpenKeys(nextOpenKeys);
479
+ if (activeSearchKey && openKeys.has(activeSearchKey) && !normalized.includes(activeSearchKey)) {
480
+ if (normalized.length || skipPreserveActiveSearchRef.current) {
481
+ clearSearchValue(activeSearchKey);
482
+ setActiveSearchKey(null);
483
+ } else {
484
+ normalized = [activeSearchKey];
485
+ }
486
+ }
487
+ if (!normalized.length && shouldPreventClose()) {
340
488
  (_a = dropdownMenuProps.onOpenChange) == null ? void 0 : _a.call(dropdownMenuProps, Array.from(openKeys));
489
+ skipPreserveActiveSearchRef.current = false;
341
490
  return;
342
491
  }
343
- const normalized = normalizeOpenKeys(nextOpenKeys);
492
+ Array.from(openKeys).forEach((key) => {
493
+ if (!normalized.includes(key)) {
494
+ clearSearchValue(key);
495
+ }
496
+ });
344
497
  setOpenKeys(new Set(normalized));
345
498
  (_b = dropdownMenuProps.onOpenChange) == null ? void 0 : _b.call(dropdownMenuProps, normalized);
499
+ skipPreserveActiveSearchRef.current = false;
346
500
  },
347
- [dropdownMenuProps, openKeys, shouldPreventClose]
501
+ [activeSearchKey, clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose]
348
502
  );
503
+ (0, import_react.useEffect)(() => {
504
+ if (!menuVisible) return;
505
+ const markOutsideClick = /* @__PURE__ */ __name((event) => {
506
+ const target = event.target;
507
+ const isOutside = !(target == null ? void 0 : target.closest(".ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup"));
508
+ closeByOutsideClickRef.current = isOutside;
509
+ if (isOutside) {
510
+ closeMenu();
511
+ }
512
+ }, "markOutsideClick");
513
+ document.addEventListener("pointerdown", markOutsideClick, true);
514
+ document.addEventListener("mousedown", markOutsideClick, true);
515
+ return () => {
516
+ document.removeEventListener("pointerdown", markOutsideClick, true);
517
+ document.removeEventListener("mousedown", markOutsideClick, true);
518
+ };
519
+ }, [closeMenu, menuVisible]);
349
520
  (0, import_react.useEffect)(() => {
350
521
  if (!persistKey) return;
351
522
  const until = dropdownPersistRegistry.get(persistKey) || 0;
@@ -391,6 +562,8 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
391
562
  function buildSearchChildren(children, item, keyPath, path, menuVisible2, resolve) {
392
563
  const searchKey = keyPath;
393
564
  const currentSearchValue = searchValues[searchKey] || "";
565
+ const currentInputValue = inputValues[searchKey] ?? currentSearchValue;
566
+ const shouldActivateSearchSubmenu = !(item.type === "group" && path.length === 0);
394
567
  const filteredChildren = currentSearchValue ? (/* @__PURE__ */ __name(function deepFilter(items2) {
395
568
  const searchText = currentSearchValue.toLowerCase();
396
569
  return items2.map((child) => {
@@ -407,7 +580,17 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
407
580
  }).filter(Boolean);
408
581
  }, "deepFilter"))(children) : children;
409
582
  const resolvedFiltered = resolve(filteredChildren, [...path, item.key]);
410
- const searchItem = createSearchItem(item, searchKey, currentSearchValue, menuVisible2, t, updateSearchValue);
583
+ const searchItem = createSearchItem(
584
+ item,
585
+ searchKey,
586
+ currentInputValue,
587
+ menuVisible2,
588
+ t,
589
+ searchHandlers,
590
+ activateSearchSubmenu,
591
+ deactivateSearchSubmenu,
592
+ shouldActivateSearchSubmenu
593
+ );
411
594
  const dividerItem = { key: `${keyPath}-search-divider`, type: "divider" };
412
595
  if (currentSearchValue && resolvedFiltered.length === 0) {
413
596
  return [searchItem, dividerItem, createEmptyItem(keyPath, t)];
@@ -466,6 +649,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
466
649
  label,
467
650
  onClick: /* @__PURE__ */ __name((info) => {
468
651
  }, "onClick"),
652
+ onMouseEnter: /* @__PURE__ */ __name(() => closeActiveSearchForPath(keyPath), "onMouseEnter"),
469
653
  children: buildSearchChildren(children, item, keyPath, path, menuVisible, resolveItems)
470
654
  };
471
655
  }
@@ -513,6 +697,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
513
697
  onClick: /* @__PURE__ */ __name((info) => {
514
698
  if (!itemShouldKeepOpen) handleLeafClick(info);
515
699
  }, "onClick"),
700
+ onMouseEnter: /* @__PURE__ */ __name(() => closeActiveSearchForPath(keyPath), "onMouseEnter"),
516
701
  onMouseDown: /* @__PURE__ */ __name(() => {
517
702
  if (!itemShouldKeepOpen) {
518
703
  return;
@@ -555,6 +740,8 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
555
740
  ...dropdownMenuProps,
556
741
  openKeys: Array.from(openKeys),
557
742
  items,
743
+ subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? SUBMENU_CLOSE_DELAY,
744
+ motion: dropdownMenuProps.motion ?? SUBMENU_MOTION_DISABLED,
558
745
  onClick: /* @__PURE__ */ __name(() => {
559
746
  }, "onClick"),
560
747
  onOpenChange: handleMenuOpenChange,
@@ -564,14 +751,19 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
564
751
  ...dropdownMenuProps == null ? void 0 : dropdownMenuProps.style
565
752
  }
566
753
  },
567
- onOpenChange: (visible) => {
568
- if (!visible && isSearching) {
754
+ onOpenChange: (visible, info) => {
755
+ if (!visible && activeSearchKey && (info == null ? void 0 : info.source) === "trigger" && !closeByOutsideClickRef.current) {
569
756
  return;
570
757
  }
571
758
  if (!visible && shouldPreventClose()) {
572
759
  return;
573
760
  }
574
- setMenuVisible(visible);
761
+ if (!visible) {
762
+ closeMenu();
763
+ } else {
764
+ setMenuVisible(visible);
765
+ }
766
+ closeByOutsideClickRef.current = false;
575
767
  }
576
768
  },
577
769
  props.children
@@ -19,6 +19,7 @@ export interface BuildFieldChildrenOptions {
19
19
  fieldUseModel?: string | ((field: any) => string);
20
20
  collection?: Collection;
21
21
  associationPathName?: string;
22
+ maxAssociationFieldDepth?: number;
22
23
  /**
23
24
  * 点击这些子项后,除自身路径外,还需要联动刷新的其他菜单路径前缀
24
25
  */
@@ -231,14 +231,18 @@ function buildSubModelGroups(subModelBaseClasses = []) {
231
231
  }
232
232
  __name(buildSubModelGroups, "buildSubModelGroups");
233
233
  function buildWrapperFieldChildren(ctx, options) {
234
- var _a;
235
- const { useModel, fieldUseModel, associationPathName, refreshTargets } = options;
234
+ var _a, _b;
235
+ const { useModel, fieldUseModel, associationPathName, refreshTargets, maxAssociationFieldDepth = 2 } = options;
236
236
  const collection = options.collection || ctx.model["collection"] || ctx.collection;
237
237
  const fields = collection.getFields();
238
238
  const defaultItemKeys = ["fieldSettings", "init"];
239
239
  const children = [];
240
+ const associationDepth = associationPathName ? associationPathName.split(".").filter(Boolean).length : 0;
240
241
  for (const f of fields) {
241
242
  if (!((_a = f == null ? void 0 : f.options) == null ? void 0 : _a.interface)) continue;
243
+ if (associationDepth >= maxAssociationFieldDepth && (((_b = f.isAssociationField) == null ? void 0 : _b.call(f)) || f.target || f.targetCollection)) {
244
+ continue;
245
+ }
242
246
  const fieldPath = associationPathName ? `${associationPathName}.${f.name}` : f.name;
243
247
  const childUse = typeof fieldUseModel === "function" ? fieldUseModel(f) : fieldUseModel ?? "FieldModel";
244
248
  if (childUse) {
@@ -38,7 +38,13 @@ export declare class DataSourceManager {
38
38
  }>) => void;
39
39
  addFieldInterfaceComponentOption?: (name: string, option: any) => void;
40
40
  addFieldInterfaceOperator?: (name: string, operator: any) => void;
41
+ registerFieldFilterOperator?: (operator: any) => void;
42
+ registerFieldFilterOperatorGroup?: (name: string, operators?: any[]) => void;
43
+ addFieldFilterOperatorsToGroup?: (name: string, operators?: any[]) => void;
41
44
  getFieldInterface?: (name: string) => any;
45
+ registerFieldInterfaceConfigure?: (options: unknown) => void;
46
+ getFieldInterfaceConfigure?: (name: string, collectionInfo?: unknown) => unknown;
47
+ getFieldInterfaceConfigureProperties?: (name: string, collectionInfo?: any) => Record<string, any>;
42
48
  };
43
49
  loaders: Map<string, DataSourceLoader>;
44
50
  loadedKeys: Set<string>;
@@ -56,6 +62,9 @@ export declare class DataSourceManager {
56
62
  }>): void;
57
63
  addFieldInterfaceComponentOption(name: string, option: any): void;
58
64
  addFieldInterfaceOperator(name: string, operator: any): void;
65
+ registerFieldFilterOperator(operator: any): void;
66
+ registerFieldFilterOperatorGroup(name: string, operators?: any[]): void;
67
+ addFieldFilterOperatorsToGroup(name: string, operators?: any[]): void;
59
68
  registerLoader(key: string, loader: DataSourceLoader): void;
60
69
  removeLoader(key: string): void;
61
70
  addDataSource(ds: DataSource | DataSourceOptions): void;
@@ -89,6 +89,18 @@ const _DataSourceManager = class _DataSourceManager {
89
89
  var _a, _b;
90
90
  (_b = (_a = this.collectionFieldInterfaceManager) == null ? void 0 : _a.addFieldInterfaceOperator) == null ? void 0 : _b.call(_a, name, operator);
91
91
  }
92
+ registerFieldFilterOperator(operator) {
93
+ var _a, _b;
94
+ (_b = (_a = this.collectionFieldInterfaceManager) == null ? void 0 : _a.registerFieldFilterOperator) == null ? void 0 : _b.call(_a, operator);
95
+ }
96
+ registerFieldFilterOperatorGroup(name, operators = []) {
97
+ var _a, _b;
98
+ (_b = (_a = this.collectionFieldInterfaceManager) == null ? void 0 : _a.registerFieldFilterOperatorGroup) == null ? void 0 : _b.call(_a, name, operators);
99
+ }
100
+ addFieldFilterOperatorsToGroup(name, operators = []) {
101
+ var _a, _b;
102
+ (_b = (_a = this.collectionFieldInterfaceManager) == null ? void 0 : _a.addFieldFilterOperatorsToGroup) == null ? void 0 : _b.call(_a, name, operators);
103
+ }
92
104
  registerLoader(key, loader) {
93
105
  this.loaders.set(key, loader);
94
106
  }
@@ -153,9 +153,6 @@ const _FlowExecutor = class _FlowExecutor {
153
153
  const stepDefaultParams = await (0, import_utils.resolveDefaultParams)(step.defaultParams, runtimeCtx);
154
154
  combinedParams = { ...stepDefaultParams };
155
155
  } else {
156
- flowContext.logger.warn(
157
- `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`
158
- );
159
156
  continue;
160
157
  }
161
158
  const modelStepParams = model.getStepParams(flowKey, stepKey);