@nocobase/flow-engine 2.1.0-beta.2 → 2.1.0-beta.21
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/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FlowModelRenderer.d.ts +1 -1
- package/lib/components/FlowModelRenderer.js +10 -6
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.js +6 -2
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +339 -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 +272 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +247 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/data-source/index.js +6 -0
- package/lib/executor/FlowExecutor.js +31 -8
- package/lib/flowContext.js +31 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- 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/flowModel.d.ts +2 -1
- package/lib/models/flowModel.js +28 -9
- 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 +47 -1
- package/lib/utils/index.d.ts +2 -2
- package/lib/utils/index.js +4 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- 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/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 +20 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +20 -3
- package/lib/views/usePage.d.ts +2 -1
- package/lib/views/usePage.js +10 -3
- package/package.json +6 -5
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +65 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- 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/FlowModelRenderer.tsx +12 -6
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +88 -0
- package/src/components/dnd/gridDragPlanner.ts +8 -2
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +63 -9
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +468 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +609 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +358 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/utils.ts +1 -1
- package/src/data-source/index.ts +6 -0
- package/src/executor/FlowExecutor.ts +34 -9
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flowContext.ts +35 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowSettings.ts +40 -6
- 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/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/flowModel.tsx +31 -10
- 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 +60 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- 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 +11 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +25 -3
- package/src/views/useDrawer.tsx +25 -3
- package/src/views/usePage.tsx +12 -3
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getSlotKey,
|
|
16
16
|
resolveDropIntent,
|
|
17
17
|
Point,
|
|
18
|
+
buildLayoutSnapshot,
|
|
18
19
|
} from '../dnd/gridDragPlanner';
|
|
19
20
|
|
|
20
21
|
const rect = { top: 0, left: 0, width: 100, height: 100 };
|
|
@@ -29,6 +30,93 @@ const createLayout = (
|
|
|
29
30
|
rowOrder,
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
const createDomRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
|
|
34
|
+
return {
|
|
35
|
+
top,
|
|
36
|
+
left,
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
right: left + width,
|
|
40
|
+
bottom: top + height,
|
|
41
|
+
x: left,
|
|
42
|
+
y: top,
|
|
43
|
+
toJSON: () => ({}),
|
|
44
|
+
} as DOMRect;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mockRect = (
|
|
48
|
+
element: Element,
|
|
49
|
+
rect: {
|
|
50
|
+
top: number;
|
|
51
|
+
left: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
},
|
|
55
|
+
) => {
|
|
56
|
+
Object.defineProperty(element, 'getBoundingClientRect', {
|
|
57
|
+
configurable: true,
|
|
58
|
+
value: () => createDomRect(rect),
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('buildLayoutSnapshot', () => {
|
|
63
|
+
it('should ignore nested grid columns/items even when rowId is duplicated', () => {
|
|
64
|
+
const container = document.createElement('div');
|
|
65
|
+
const row = document.createElement('div');
|
|
66
|
+
row.setAttribute('data-grid-row-id', 'row-1');
|
|
67
|
+
container.appendChild(row);
|
|
68
|
+
|
|
69
|
+
const column = document.createElement('div');
|
|
70
|
+
column.setAttribute('data-grid-column-row-id', 'row-1');
|
|
71
|
+
column.setAttribute('data-grid-column-index', '0');
|
|
72
|
+
row.appendChild(column);
|
|
73
|
+
|
|
74
|
+
const item = document.createElement('div');
|
|
75
|
+
item.setAttribute('data-grid-item-row-id', 'row-1');
|
|
76
|
+
item.setAttribute('data-grid-column-index', '0');
|
|
77
|
+
item.setAttribute('data-grid-item-index', '0');
|
|
78
|
+
column.appendChild(item);
|
|
79
|
+
|
|
80
|
+
// 在外层 item 内构建一个嵌套 grid,并复用相同 rowId/columnIndex
|
|
81
|
+
const nestedRow = document.createElement('div');
|
|
82
|
+
nestedRow.setAttribute('data-grid-row-id', 'row-1');
|
|
83
|
+
item.appendChild(nestedRow);
|
|
84
|
+
|
|
85
|
+
const nestedColumn = document.createElement('div');
|
|
86
|
+
nestedColumn.setAttribute('data-grid-column-row-id', 'row-1');
|
|
87
|
+
nestedColumn.setAttribute('data-grid-column-index', '0');
|
|
88
|
+
nestedRow.appendChild(nestedColumn);
|
|
89
|
+
|
|
90
|
+
const nestedItem = document.createElement('div');
|
|
91
|
+
nestedItem.setAttribute('data-grid-item-row-id', 'row-1');
|
|
92
|
+
nestedItem.setAttribute('data-grid-column-index', '0');
|
|
93
|
+
nestedItem.setAttribute('data-grid-item-index', '0');
|
|
94
|
+
nestedColumn.appendChild(nestedItem);
|
|
95
|
+
|
|
96
|
+
mockRect(container, { top: 0, left: 0, width: 600, height: 600 });
|
|
97
|
+
mockRect(row, { top: 10, left: 10, width: 320, height: 120 });
|
|
98
|
+
mockRect(column, { top: 10, left: 10, width: 320, height: 120 });
|
|
99
|
+
mockRect(item, { top: 20, left: 20, width: 300, height: 80 });
|
|
100
|
+
|
|
101
|
+
// 嵌套 grid 给一个明显偏离的位置,用于判断是否被错误命中
|
|
102
|
+
mockRect(nestedRow, { top: 360, left: 360, width: 200, height: 120 });
|
|
103
|
+
mockRect(nestedColumn, { top: 360, left: 360, width: 200, height: 120 });
|
|
104
|
+
mockRect(nestedItem, { top: 370, left: 370, width: 180, height: 90 });
|
|
105
|
+
|
|
106
|
+
const snapshot = buildLayoutSnapshot({ container });
|
|
107
|
+
const columnEdgeSlots = snapshot.slots.filter((slot) => slot.type === 'column-edge');
|
|
108
|
+
const columnSlots = snapshot.slots.filter((slot) => slot.type === 'column');
|
|
109
|
+
|
|
110
|
+
// 外层单行单列单项应只有 6 个 slot:上/下 row-gap + 左/右 column-edge + before/after column
|
|
111
|
+
expect(snapshot.slots).toHaveLength(6);
|
|
112
|
+
expect(columnEdgeSlots).toHaveLength(2);
|
|
113
|
+
expect(columnSlots).toHaveLength(2);
|
|
114
|
+
|
|
115
|
+
// 不应混入嵌套 grid(其 top >= 360)
|
|
116
|
+
expect(snapshot.slots.every((slot) => slot.rect.top < 300)).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
32
120
|
describe('getSlotKey', () => {
|
|
33
121
|
it('should generate unique key for column slot', () => {
|
|
34
122
|
const slot: LayoutSlot = {
|
|
@@ -333,7 +333,10 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
333
333
|
|
|
334
334
|
const columnElements = Array.from(
|
|
335
335
|
container.querySelectorAll(`[data-grid-column-row-id="${rowId}"][data-grid-column-index]`),
|
|
336
|
-
)
|
|
336
|
+
).filter((el) => {
|
|
337
|
+
// 只保留当前 row 下的直接列,避免嵌套 Grid 中相同 rowId 的列混入
|
|
338
|
+
return (el as HTMLElement).closest('[data-grid-row-id]') === rowElement;
|
|
339
|
+
}) as HTMLElement[];
|
|
337
340
|
|
|
338
341
|
const sortedColumns = columnElements.sort((a, b) => {
|
|
339
342
|
const indexA = Number(a.dataset.gridColumnIndex || 0);
|
|
@@ -363,7 +366,10 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
363
366
|
|
|
364
367
|
const itemElements = Array.from(
|
|
365
368
|
columnElement.querySelectorAll(`[data-grid-item-row-id="${rowId}"][data-grid-column-index="${columnIndex}"]`),
|
|
366
|
-
)
|
|
369
|
+
).filter((el) => {
|
|
370
|
+
// 只保留当前 column 下的直接 item,避免命中更深层嵌套 column 的 item
|
|
371
|
+
return (el as HTMLElement).closest('[data-grid-column-row-id][data-grid-column-index]') === columnElement;
|
|
372
|
+
}) as HTMLElement[];
|
|
367
373
|
|
|
368
374
|
const sortedItems = itemElements.sort((a, b) => {
|
|
369
375
|
const indexA = Number(a.dataset.gridItemIndex || 0);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ExclamationCircleOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
|
11
|
+
import { css } from '@emotion/css';
|
|
11
12
|
import type { DropdownProps, MenuProps } from 'antd';
|
|
12
13
|
import { App, Dropdown, Modal, Tooltip, theme } from 'antd';
|
|
13
14
|
import React, { startTransition, useCallback, useEffect, useMemo, useState, FC } from 'react';
|
|
@@ -188,15 +189,42 @@ interface DefaultSettingsIconProps {
|
|
|
188
189
|
showCopyUidButton?: boolean;
|
|
189
190
|
menuLevels?: number; // Menu levels: 1=current model only (default), 2=include sub-models
|
|
190
191
|
flattenSubMenus?: boolean; // Whether to flatten sub-menus: false=group by model (default), true=flatten all
|
|
192
|
+
onDropdownVisibleChange?: (open: boolean) => void;
|
|
193
|
+
getPopupContainer?: DropdownProps['getPopupContainer'];
|
|
191
194
|
[key: string]: any; // Allow additional props
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
const TOOLBAR_ICONS_SELECTOR = '.nb-toolbar-container-icons';
|
|
198
|
+
const TOOLBAR_CONTAINER_SELECTOR = '.nb-toolbar-container';
|
|
199
|
+
const TOOLBAR_DROPDOWN_OVERLAY_CLASS = css`
|
|
200
|
+
width: max-content;
|
|
201
|
+
min-width: max-content;
|
|
202
|
+
|
|
203
|
+
.ant-dropdown-menu {
|
|
204
|
+
width: max-content;
|
|
205
|
+
min-width: max-content;
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const getToolbarPopupContainer = (triggerNode?: HTMLElement | null) => {
|
|
210
|
+
if (!triggerNode) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
(triggerNode.closest(TOOLBAR_ICONS_SELECTOR) as HTMLElement | null) ||
|
|
216
|
+
(triggerNode.closest(TOOLBAR_CONTAINER_SELECTOR) as HTMLElement | null)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
194
220
|
export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
195
221
|
model,
|
|
196
222
|
showDeleteButton = true,
|
|
197
223
|
showCopyUidButton = true,
|
|
198
224
|
menuLevels = 1, // 默认一级菜单
|
|
199
225
|
flattenSubMenus = true,
|
|
226
|
+
onDropdownVisibleChange,
|
|
227
|
+
getPopupContainer,
|
|
200
228
|
}) => {
|
|
201
229
|
const { message } = App.useApp();
|
|
202
230
|
const t = useMemo(() => getT(model), [model]);
|
|
@@ -210,15 +238,38 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
210
238
|
const [isLoading, setIsLoading] = useState(true);
|
|
211
239
|
const closeDropdown = useCallback(() => {
|
|
212
240
|
setVisible(false);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
onDropdownVisibleChange?.(false);
|
|
242
|
+
}, [onDropdownVisibleChange]);
|
|
243
|
+
const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
|
|
244
|
+
(triggerNode) => {
|
|
245
|
+
// 工具栏自身容器必须优先,保证鼠标从 icon 移到菜单时仍处于同一 hover 树。
|
|
246
|
+
// 弹窗场景的裁剪问题由 useFloatToolbarPortal 负责把 toolbar 挂到正确的 popup host。
|
|
247
|
+
return (
|
|
248
|
+
getToolbarPopupContainer(triggerNode) ||
|
|
249
|
+
getPopupContainer?.(triggerNode) ||
|
|
250
|
+
triggerNode?.parentElement ||
|
|
251
|
+
document.body
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
[getPopupContainer],
|
|
255
|
+
);
|
|
256
|
+
const handleOpenChange: DropdownProps['onOpenChange'] = useCallback(
|
|
257
|
+
(nextOpen: boolean, info) => {
|
|
258
|
+
if (info.source === 'trigger' || nextOpen) {
|
|
259
|
+
// 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
|
|
260
|
+
startTransition(() => {
|
|
261
|
+
setVisible(nextOpen);
|
|
262
|
+
});
|
|
263
|
+
onDropdownVisibleChange?.(nextOpen);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[onDropdownVisibleChange],
|
|
267
|
+
);
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
return () => {
|
|
270
|
+
onDropdownVisibleChange?.(false);
|
|
271
|
+
};
|
|
272
|
+
}, [onDropdownVisibleChange]);
|
|
222
273
|
const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]);
|
|
223
274
|
useEffect(() => {
|
|
224
275
|
let mounted = true;
|
|
@@ -833,6 +884,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
833
884
|
|
|
834
885
|
return (
|
|
835
886
|
<Dropdown
|
|
887
|
+
getPopupContainer={resolvePopupContainer}
|
|
888
|
+
overlayClassName={TOOLBAR_DROPDOWN_OVERLAY_CLASS}
|
|
889
|
+
overlayStyle={{ width: 'max-content', minWidth: 'max-content' }}
|
|
836
890
|
onOpenChange={handleOpenChange}
|
|
837
891
|
open={visible}
|
|
838
892
|
menu={{
|