@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45
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.
- package/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +613 -21
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -11
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/LazyDropdown.js +293 -52
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +1 -1
- package/lib/components/subModel/utils.js +9 -3
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +84 -0
- package/lib/data-source/index.js +259 -5
- package/lib/executor/FlowExecutor.js +32 -9
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +3 -0
- package/lib/flowContext.js +46 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +392 -18
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +81 -21
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +29 -5
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.d.ts +12 -2
- package/lib/views/ViewNavigation.js +28 -9
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +22 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +22 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +6 -5
- package/src/FlowContextProvider.tsx +9 -1
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +82 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +758 -19
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +99 -11
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +194 -5
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/LazyDropdown.tsx +332 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +7 -1
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +68 -1
- package/src/data-source/index.ts +322 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +85 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +50 -3
- package/src/flowEngine.ts +449 -14
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +80 -37
- package/src/models/flowModel.tsx +122 -36
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +28 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/parsePathnameToViewParams.ts +47 -7
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +46 -9
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +27 -3
- package/src/views/useDrawer.tsx +27 -3
- package/src/views/usePage.tsx +367 -179
|
@@ -247,18 +247,75 @@ const useKeepDropdownOpen = () => {
|
|
|
247
247
|
*/
|
|
248
248
|
const useMenuSearch = () => {
|
|
249
249
|
const [searchValues, setSearchValues] = useState<Record<string, string>>({});
|
|
250
|
+
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
|
250
251
|
const [isSearching, setIsSearching] = useState(false);
|
|
252
|
+
const [composingCount, setComposingCount] = useState(0);
|
|
253
|
+
const composingKeysRef = useRef<Set<string>>(new Set());
|
|
251
254
|
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
252
255
|
|
|
253
|
-
const updateSearchValue = (key: string, value: string) => {
|
|
256
|
+
const updateSearchValue = useCallback((key: string, value: string) => {
|
|
254
257
|
setIsSearching(true);
|
|
258
|
+
setInputValues((prev) => ({ ...prev, [key]: value }));
|
|
255
259
|
setSearchValues((prev) => ({ ...prev, [key]: value }));
|
|
256
260
|
|
|
257
261
|
if (searchTimeoutRef.current) {
|
|
258
262
|
clearTimeout(searchTimeoutRef.current);
|
|
259
263
|
}
|
|
260
264
|
searchTimeoutRef.current = setTimeout(() => setIsSearching(false), 300);
|
|
261
|
-
};
|
|
265
|
+
}, []);
|
|
266
|
+
|
|
267
|
+
const startComposition = useCallback((key: string) => {
|
|
268
|
+
composingKeysRef.current.add(key);
|
|
269
|
+
setIsSearching(true);
|
|
270
|
+
setComposingCount(composingKeysRef.current.size);
|
|
271
|
+
if (searchTimeoutRef.current) {
|
|
272
|
+
clearTimeout(searchTimeoutRef.current);
|
|
273
|
+
searchTimeoutRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
277
|
+
const endComposition = useCallback(
|
|
278
|
+
(key: string, value: string) => {
|
|
279
|
+
composingKeysRef.current.delete(key);
|
|
280
|
+
setComposingCount(composingKeysRef.current.size);
|
|
281
|
+
updateSearchValue(key, value);
|
|
282
|
+
},
|
|
283
|
+
[updateSearchValue],
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const updateInputValue = useCallback((key: string, value: string) => {
|
|
287
|
+
setInputValues((prev) => ({ ...prev, [key]: value }));
|
|
288
|
+
}, []);
|
|
289
|
+
|
|
290
|
+
const clearSearchValue = useCallback((key: string) => {
|
|
291
|
+
composingKeysRef.current.delete(key);
|
|
292
|
+
setComposingCount(composingKeysRef.current.size);
|
|
293
|
+
|
|
294
|
+
setInputValues((prev) => {
|
|
295
|
+
if (!(key in prev)) return prev;
|
|
296
|
+
const next = { ...prev };
|
|
297
|
+
delete next[key];
|
|
298
|
+
return next;
|
|
299
|
+
});
|
|
300
|
+
setSearchValues((prev) => {
|
|
301
|
+
if (!(key in prev)) return prev;
|
|
302
|
+
const next = { ...prev };
|
|
303
|
+
delete next[key];
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const clearAllSearchValues = useCallback(() => {
|
|
309
|
+
composingKeysRef.current.clear();
|
|
310
|
+
setComposingCount(0);
|
|
311
|
+
setInputValues({});
|
|
312
|
+
setSearchValues({});
|
|
313
|
+
setIsSearching(false);
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
const isComposing = useCallback((key?: string) => {
|
|
317
|
+
return key ? composingKeysRef.current.has(key) : composingKeysRef.current.size > 0;
|
|
318
|
+
}, []);
|
|
262
319
|
|
|
263
320
|
useEffect(() => {
|
|
264
321
|
return () => {
|
|
@@ -270,8 +327,15 @@ const useMenuSearch = () => {
|
|
|
270
327
|
|
|
271
328
|
return {
|
|
272
329
|
searchValues,
|
|
273
|
-
|
|
330
|
+
inputValues,
|
|
331
|
+
isSearching: isSearching || composingCount > 0,
|
|
274
332
|
updateSearchValue,
|
|
333
|
+
updateInputValue,
|
|
334
|
+
startComposition,
|
|
335
|
+
endComposition,
|
|
336
|
+
clearSearchValue,
|
|
337
|
+
clearAllSearchValues,
|
|
338
|
+
isComposing,
|
|
275
339
|
};
|
|
276
340
|
};
|
|
277
341
|
|
|
@@ -358,29 +422,95 @@ const SearchInputWithAutoFocus: FC<InputProps & { visible: boolean }> = (props)
|
|
|
358
422
|
|
|
359
423
|
const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
|
|
360
424
|
|
|
425
|
+
const normalizeOpenKeys = (nextOpenKeys: string[]) => {
|
|
426
|
+
const latestKey = nextOpenKeys[nextOpenKeys.length - 1];
|
|
427
|
+
|
|
428
|
+
if (!latestKey) {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return nextOpenKeys.filter((key) => latestKey === key || latestKey.startsWith(`${key}/`));
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const getLabelSearchText = (label: React.ReactNode): string => {
|
|
436
|
+
if (label === null || label === undefined || typeof label === 'boolean') {
|
|
437
|
+
return '';
|
|
438
|
+
}
|
|
439
|
+
if (typeof label === 'string' || typeof label === 'number') {
|
|
440
|
+
return String(label);
|
|
441
|
+
}
|
|
442
|
+
if (Array.isArray(label)) {
|
|
443
|
+
return label.map(getLabelSearchText).join(' ');
|
|
444
|
+
}
|
|
445
|
+
if (React.isValidElement(label)) {
|
|
446
|
+
return getLabelSearchText(label.props.children);
|
|
447
|
+
}
|
|
448
|
+
return '';
|
|
449
|
+
};
|
|
450
|
+
|
|
361
451
|
const createSearchItem = (
|
|
362
452
|
item: Item,
|
|
363
453
|
searchKey: string,
|
|
364
454
|
currentSearchValue: string,
|
|
365
455
|
menuVisible: boolean,
|
|
366
456
|
t: (key: string) => string,
|
|
367
|
-
|
|
457
|
+
searchHandlers: {
|
|
458
|
+
updateSearchValue: (key: string, value: string) => void;
|
|
459
|
+
updateInputValue: (key: string, value: string) => void;
|
|
460
|
+
startComposition: (key: string) => void;
|
|
461
|
+
endComposition: (key: string, value: string) => void;
|
|
462
|
+
isComposing: (key: string) => boolean;
|
|
463
|
+
},
|
|
464
|
+
activateSearchSubmenu: (key: string) => void,
|
|
465
|
+
deactivateSearchSubmenu: (key: string) => void,
|
|
368
466
|
) => ({
|
|
369
467
|
key: `${searchKey}-search`,
|
|
370
468
|
type: 'group' as const,
|
|
371
469
|
label: (
|
|
372
|
-
<div>
|
|
470
|
+
<div onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
|
373
471
|
<SearchInputWithAutoFocus
|
|
374
472
|
visible={menuVisible}
|
|
375
473
|
variant="borderless"
|
|
376
474
|
allowClear
|
|
377
475
|
placeholder={t(item.searchPlaceholder || 'Search')}
|
|
378
476
|
value={currentSearchValue}
|
|
477
|
+
onFocus={(e) => {
|
|
478
|
+
e.stopPropagation();
|
|
479
|
+
}}
|
|
379
480
|
onChange={(e) => {
|
|
380
481
|
e.stopPropagation();
|
|
381
|
-
|
|
482
|
+
const value = e.target.value;
|
|
483
|
+
activateSearchSubmenu(searchKey);
|
|
484
|
+
if ((e.nativeEvent as any)?.isComposing || searchHandlers.isComposing(searchKey)) {
|
|
485
|
+
searchHandlers.updateInputValue(searchKey, value);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (!value) {
|
|
489
|
+
deactivateSearchSubmenu(searchKey);
|
|
490
|
+
}
|
|
491
|
+
searchHandlers.updateSearchValue(searchKey, value);
|
|
492
|
+
}}
|
|
493
|
+
onCompositionStart={(e) => {
|
|
494
|
+
e.stopPropagation();
|
|
495
|
+
activateSearchSubmenu(searchKey);
|
|
496
|
+
searchHandlers.startComposition(searchKey);
|
|
497
|
+
}}
|
|
498
|
+
onCompositionEnd={(e) => {
|
|
499
|
+
e.stopPropagation();
|
|
500
|
+
const value = e.currentTarget.value;
|
|
501
|
+
if (value) {
|
|
502
|
+
activateSearchSubmenu(searchKey);
|
|
503
|
+
} else {
|
|
504
|
+
deactivateSearchSubmenu(searchKey);
|
|
505
|
+
}
|
|
506
|
+
searchHandlers.endComposition(searchKey, value);
|
|
507
|
+
}}
|
|
508
|
+
onClick={(e) => {
|
|
509
|
+
e.stopPropagation();
|
|
510
|
+
}}
|
|
511
|
+
onKeyDown={(e) => {
|
|
512
|
+
e.stopPropagation();
|
|
382
513
|
}}
|
|
383
|
-
onClick={(e) => e.stopPropagation()}
|
|
384
514
|
onMouseDown={(e) => {
|
|
385
515
|
// 防止菜单聚焦丢失或页面滚动
|
|
386
516
|
e.stopPropagation();
|
|
@@ -406,18 +536,31 @@ const createEmptyItem = (itemKey: string, t: (key: string) => string) => ({
|
|
|
406
536
|
disabled: true,
|
|
407
537
|
});
|
|
408
538
|
|
|
539
|
+
const KEEP_OPEN_LABEL_STYLE: React.CSSProperties = {
|
|
540
|
+
display: 'block',
|
|
541
|
+
width: '100%',
|
|
542
|
+
};
|
|
543
|
+
|
|
409
544
|
// ==================== Main Component ====================
|
|
410
545
|
|
|
411
546
|
// 短暂保持打开状态的注册表(用于跨父节点快速重建时的恢复)
|
|
412
547
|
const DROPDOWN_PERSIST_TTL_MS = 350;
|
|
548
|
+
const SUBMENU_CLOSE_DELAY = 0.05;
|
|
549
|
+
const SUBMENU_MOTION_DISABLED = {
|
|
550
|
+
motionEnter: false,
|
|
551
|
+
motionLeave: false,
|
|
552
|
+
};
|
|
413
553
|
const dropdownPersistRegistry: Map<string, number> = new Map();
|
|
414
554
|
|
|
415
555
|
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
|
|
416
556
|
const engine = useFlowEngine();
|
|
417
557
|
const [menuVisible, setMenuVisible] = useState(false);
|
|
418
558
|
const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
|
|
559
|
+
const [activeSearchKey, setActiveSearchKey] = useState<string | null>(null);
|
|
419
560
|
const [rootItems, setRootItems] = useState<Item[]>([]);
|
|
420
561
|
const [rootLoading, setRootLoading] = useState(false);
|
|
562
|
+
const closeByOutsideClickRef = useRef(false);
|
|
563
|
+
const skipPreserveActiveSearchRef = useRef(false);
|
|
421
564
|
const dropdownMaxHeight = useNiceDropdownMaxHeight();
|
|
422
565
|
const t = engine.translate.bind(engine);
|
|
423
566
|
|
|
@@ -432,10 +575,105 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
432
575
|
openKeys,
|
|
433
576
|
refreshKeys,
|
|
434
577
|
);
|
|
435
|
-
const
|
|
578
|
+
const searchHandlers = useMenuSearch();
|
|
579
|
+
const { searchValues, inputValues, clearSearchValue, clearAllSearchValues } = searchHandlers;
|
|
436
580
|
const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
|
|
437
581
|
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
|
438
582
|
|
|
583
|
+
const closeMenu = useCallback(() => {
|
|
584
|
+
setMenuVisible(false);
|
|
585
|
+
setActiveSearchKey(null);
|
|
586
|
+
setOpenKeys(new Set());
|
|
587
|
+
clearAllSearchValues();
|
|
588
|
+
}, [clearAllSearchValues]);
|
|
589
|
+
|
|
590
|
+
const activateSearchSubmenu = useCallback((key: string) => {
|
|
591
|
+
setActiveSearchKey(key);
|
|
592
|
+
setOpenKeys((prev) => {
|
|
593
|
+
if (prev.has(key)) return prev;
|
|
594
|
+
const next = new Set(prev);
|
|
595
|
+
next.add(key);
|
|
596
|
+
return next;
|
|
597
|
+
});
|
|
598
|
+
}, []);
|
|
599
|
+
|
|
600
|
+
const deactivateSearchSubmenu = useCallback((key: string) => {
|
|
601
|
+
setActiveSearchKey((prev) => (prev === key ? null : prev));
|
|
602
|
+
}, []);
|
|
603
|
+
|
|
604
|
+
const closeActiveSearchForPath = useCallback(
|
|
605
|
+
(keyPath: string) => {
|
|
606
|
+
if (
|
|
607
|
+
!activeSearchKey ||
|
|
608
|
+
keyPath === activeSearchKey ||
|
|
609
|
+
keyPath.startsWith(`${activeSearchKey}/`) ||
|
|
610
|
+
activeSearchKey.startsWith(`${keyPath}/`)
|
|
611
|
+
) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
skipPreserveActiveSearchRef.current = true;
|
|
616
|
+
clearSearchValue(activeSearchKey);
|
|
617
|
+
setActiveSearchKey(null);
|
|
618
|
+
setOpenKeys((prev) => {
|
|
619
|
+
const next = new Set(prev);
|
|
620
|
+
next.delete(activeSearchKey);
|
|
621
|
+
return next;
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
[activeSearchKey, clearSearchValue],
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const handleMenuOpenChange = useCallback(
|
|
628
|
+
(nextOpenKeys: string[]) => {
|
|
629
|
+
let normalized = normalizeOpenKeys(nextOpenKeys);
|
|
630
|
+
if (activeSearchKey && openKeys.has(activeSearchKey) && !normalized.includes(activeSearchKey)) {
|
|
631
|
+
if (normalized.length || skipPreserveActiveSearchRef.current) {
|
|
632
|
+
clearSearchValue(activeSearchKey);
|
|
633
|
+
setActiveSearchKey(null);
|
|
634
|
+
} else {
|
|
635
|
+
normalized = [activeSearchKey];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!normalized.length && shouldPreventClose()) {
|
|
640
|
+
dropdownMenuProps.onOpenChange?.(Array.from(openKeys));
|
|
641
|
+
skipPreserveActiveSearchRef.current = false;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
Array.from(openKeys).forEach((key) => {
|
|
646
|
+
if (!normalized.includes(key)) {
|
|
647
|
+
clearSearchValue(key);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
setOpenKeys(new Set(normalized));
|
|
651
|
+
dropdownMenuProps.onOpenChange?.(normalized);
|
|
652
|
+
skipPreserveActiveSearchRef.current = false;
|
|
653
|
+
},
|
|
654
|
+
[activeSearchKey, clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose],
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (!menuVisible) return;
|
|
659
|
+
|
|
660
|
+
const markOutsideClick = (event: MouseEvent | PointerEvent) => {
|
|
661
|
+
const target = event.target as HTMLElement | null;
|
|
662
|
+
const isOutside = !target?.closest('.ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup');
|
|
663
|
+
closeByOutsideClickRef.current = isOutside;
|
|
664
|
+
if (isOutside) {
|
|
665
|
+
closeMenu();
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
document.addEventListener('pointerdown', markOutsideClick, true);
|
|
670
|
+
document.addEventListener('mousedown', markOutsideClick, true);
|
|
671
|
+
return () => {
|
|
672
|
+
document.removeEventListener('pointerdown', markOutsideClick, true);
|
|
673
|
+
document.removeEventListener('mousedown', markOutsideClick, true);
|
|
674
|
+
};
|
|
675
|
+
}, [closeMenu, menuVisible]);
|
|
676
|
+
|
|
439
677
|
// 在挂载时,若存在 persistKey 且仍在持久期内,则尝试恢复打开状态
|
|
440
678
|
useEffect(() => {
|
|
441
679
|
if (!persistKey) return;
|
|
@@ -460,6 +698,12 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
460
698
|
};
|
|
461
699
|
}, [persistKey, menuVisible]);
|
|
462
700
|
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
if (!menuVisible) {
|
|
703
|
+
setOpenKeys(new Set());
|
|
704
|
+
}
|
|
705
|
+
}, [menuVisible]);
|
|
706
|
+
|
|
463
707
|
// 加载根 items,支持同步/异步函数
|
|
464
708
|
useEffect(() => {
|
|
465
709
|
const loadRootItems = async () => {
|
|
@@ -492,21 +736,17 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
492
736
|
): any[] {
|
|
493
737
|
const searchKey = keyPath;
|
|
494
738
|
const currentSearchValue = searchValues[searchKey] || '';
|
|
739
|
+
const currentInputValue = inputValues[searchKey] ?? currentSearchValue;
|
|
495
740
|
|
|
496
741
|
// 递归过滤:当 child 为分组时,会继续向下过滤其 children;
|
|
497
742
|
// 仅保留自身匹配或存在匹配子项的分组。
|
|
498
743
|
const filteredChildren = currentSearchValue
|
|
499
744
|
? (function deepFilter(items: Item[]): Item[] {
|
|
500
745
|
const searchText = currentSearchValue.toLowerCase();
|
|
501
|
-
const tryString = (v: any) => {
|
|
502
|
-
if (!v) return '';
|
|
503
|
-
return typeof v === 'string' ? v : String(v);
|
|
504
|
-
};
|
|
505
746
|
return items
|
|
506
747
|
.map((child) => {
|
|
507
|
-
const labelStr =
|
|
508
|
-
const selfMatch =
|
|
509
|
-
labelStr.includes(searchText) || (child.key && String(child.key).toLowerCase().includes(searchText));
|
|
748
|
+
const labelStr = getLabelSearchText(child.label).toLowerCase();
|
|
749
|
+
const selfMatch = labelStr.includes(searchText);
|
|
510
750
|
if (child.type === 'group' && Array.isArray(child.children)) {
|
|
511
751
|
const nested = deepFilter(child.children);
|
|
512
752
|
if (selfMatch || nested.length > 0) {
|
|
@@ -521,7 +761,16 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
521
761
|
: children;
|
|
522
762
|
|
|
523
763
|
const resolvedFiltered = resolve(filteredChildren, [...path, item.key]);
|
|
524
|
-
const searchItem = createSearchItem(
|
|
764
|
+
const searchItem = createSearchItem(
|
|
765
|
+
item,
|
|
766
|
+
searchKey,
|
|
767
|
+
currentInputValue,
|
|
768
|
+
menuVisible,
|
|
769
|
+
t,
|
|
770
|
+
searchHandlers,
|
|
771
|
+
activateSearchSubmenu,
|
|
772
|
+
deactivateSearchSubmenu,
|
|
773
|
+
);
|
|
525
774
|
const dividerItem = { key: `${keyPath}-search-divider`, type: 'divider' as const };
|
|
526
775
|
|
|
527
776
|
if (currentSearchValue && resolvedFiltered.length === 0) {
|
|
@@ -588,56 +837,75 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
588
837
|
return { type: 'divider', key: keyPath };
|
|
589
838
|
}
|
|
590
839
|
|
|
840
|
+
const label = typeof item.label === 'string' ? t(item.label) : item.label;
|
|
841
|
+
|
|
591
842
|
// 非 group 的“子菜单”也支持本层级搜索:当 item.searchable = true 且存在 children 时
|
|
592
843
|
if (item.searchable && children) {
|
|
593
844
|
return {
|
|
594
|
-
key:
|
|
595
|
-
label
|
|
845
|
+
key: keyPath,
|
|
846
|
+
label,
|
|
596
847
|
onClick: (info: any) => {},
|
|
597
|
-
onMouseEnter: () =>
|
|
598
|
-
setOpenKeys((prev) => {
|
|
599
|
-
if (prev.has(keyPath)) return prev;
|
|
600
|
-
const next = new Set(prev);
|
|
601
|
-
next.add(keyPath);
|
|
602
|
-
return next;
|
|
603
|
-
});
|
|
604
|
-
},
|
|
848
|
+
onMouseEnter: () => closeActiveSearchForPath(keyPath),
|
|
605
849
|
children: buildSearchChildren(children, item, keyPath, path, menuVisible, resolveItems),
|
|
606
850
|
};
|
|
607
851
|
}
|
|
608
852
|
|
|
853
|
+
const itemShouldKeepOpen = !children && (item.keepDropdownOpen ?? keepDropdownOpen ?? false);
|
|
854
|
+
const handleLeafClick = (info: any) => {
|
|
855
|
+
if (children) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (itemShouldKeepOpen) {
|
|
860
|
+
requestKeepOpen();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const extendedInfo: ExtendedMenuInfo = {
|
|
864
|
+
...info,
|
|
865
|
+
key: info?.key ?? keyPath,
|
|
866
|
+
keyPath: info?.keyPath ?? [keyPath],
|
|
867
|
+
item: info?.item || item,
|
|
868
|
+
originalItem: item,
|
|
869
|
+
keepDropdownOpen: itemShouldKeepOpen,
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
menu.onClick?.(extendedInfo);
|
|
873
|
+
};
|
|
874
|
+
|
|
609
875
|
return {
|
|
610
876
|
key: keyPath,
|
|
611
|
-
label:
|
|
877
|
+
label: itemShouldKeepOpen ? (
|
|
878
|
+
<div
|
|
879
|
+
style={KEEP_OPEN_LABEL_STYLE}
|
|
880
|
+
onMouseDown={(event) => {
|
|
881
|
+
event.stopPropagation();
|
|
882
|
+
requestKeepOpen();
|
|
883
|
+
}}
|
|
884
|
+
onClick={(event) => {
|
|
885
|
+
event.stopPropagation();
|
|
886
|
+
handleLeafClick({
|
|
887
|
+
key: keyPath,
|
|
888
|
+
keyPath: [keyPath],
|
|
889
|
+
item,
|
|
890
|
+
domEvent: event,
|
|
891
|
+
});
|
|
892
|
+
}}
|
|
893
|
+
>
|
|
894
|
+
{label}
|
|
895
|
+
</div>
|
|
896
|
+
) : (
|
|
897
|
+
label
|
|
898
|
+
),
|
|
612
899
|
onClick: (info: any) => {
|
|
613
|
-
if (
|
|
900
|
+
if (!itemShouldKeepOpen) handleLeafClick(info);
|
|
901
|
+
},
|
|
902
|
+
onMouseEnter: () => closeActiveSearchForPath(keyPath),
|
|
903
|
+
onMouseDown: () => {
|
|
904
|
+
if (!itemShouldKeepOpen) {
|
|
614
905
|
return;
|
|
615
906
|
}
|
|
616
907
|
|
|
617
|
-
|
|
618
|
-
const itemShouldKeepOpen = item.keepDropdownOpen ?? keepDropdownOpen ?? false;
|
|
619
|
-
|
|
620
|
-
// 如果需要保持菜单打开,请求保持打开状态
|
|
621
|
-
if (itemShouldKeepOpen) {
|
|
622
|
-
requestKeepOpen();
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const extendedInfo: ExtendedMenuInfo = {
|
|
626
|
-
...info,
|
|
627
|
-
item: info.item || item,
|
|
628
|
-
originalItem: item,
|
|
629
|
-
keepDropdownOpen: itemShouldKeepOpen,
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
menu.onClick?.(extendedInfo);
|
|
633
|
-
},
|
|
634
|
-
onMouseEnter: () => {
|
|
635
|
-
setOpenKeys((prev) => {
|
|
636
|
-
if (prev.has(keyPath)) return prev;
|
|
637
|
-
const next = new Set(prev);
|
|
638
|
-
next.add(keyPath);
|
|
639
|
-
return next;
|
|
640
|
-
});
|
|
908
|
+
requestKeepOpen();
|
|
641
909
|
},
|
|
642
910
|
children:
|
|
643
911
|
children && children.length > 0
|
|
@@ -684,17 +952,20 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
684
952
|
placement="bottomLeft"
|
|
685
953
|
menu={{
|
|
686
954
|
...dropdownMenuProps,
|
|
955
|
+
openKeys: Array.from(openKeys),
|
|
687
956
|
items: items,
|
|
957
|
+
subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? SUBMENU_CLOSE_DELAY,
|
|
958
|
+
motion: dropdownMenuProps.motion ?? SUBMENU_MOTION_DISABLED,
|
|
688
959
|
onClick: () => {},
|
|
960
|
+
onOpenChange: handleMenuOpenChange,
|
|
689
961
|
style: {
|
|
690
962
|
maxHeight: dropdownMaxHeight,
|
|
691
963
|
overflowY: 'auto',
|
|
692
964
|
...dropdownMenuProps?.style,
|
|
693
965
|
},
|
|
694
966
|
}}
|
|
695
|
-
onOpenChange={(visible) => {
|
|
696
|
-
|
|
697
|
-
if (!visible && isSearching) {
|
|
967
|
+
onOpenChange={(visible, info) => {
|
|
968
|
+
if (!visible && activeSearchKey && info?.source === 'trigger' && !closeByOutsideClickRef.current) {
|
|
698
969
|
return;
|
|
699
970
|
}
|
|
700
971
|
|
|
@@ -703,7 +974,12 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
703
974
|
return;
|
|
704
975
|
}
|
|
705
976
|
|
|
706
|
-
|
|
977
|
+
if (!visible) {
|
|
978
|
+
closeMenu();
|
|
979
|
+
} else {
|
|
980
|
+
setMenuVisible(visible);
|
|
981
|
+
}
|
|
982
|
+
closeByOutsideClickRef.current = false;
|
|
707
983
|
}}
|
|
708
984
|
>
|
|
709
985
|
{props.children}
|