@nocobase/flow-engine 2.0.13 → 2.0.15
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/lib/components/dnd/gridDragPlanner.js +6 -2
- package/lib/components/subModel/AddSubModelButton.js +15 -0
- package/package.json +4 -4
- package/src/components/__tests__/gridDragPlanner.test.ts +88 -0
- package/src/components/dnd/gridDragPlanner.ts +8 -2
- package/src/components/subModel/AddSubModelButton.tsx +16 -0
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +50 -0
|
@@ -220,7 +220,9 @@ const buildLayoutSnapshot = /* @__PURE__ */ __name(({ container }) => {
|
|
|
220
220
|
}
|
|
221
221
|
const columnElements = Array.from(
|
|
222
222
|
container.querySelectorAll(`[data-grid-column-row-id="${rowId}"][data-grid-column-index]`)
|
|
223
|
-
)
|
|
223
|
+
).filter((el) => {
|
|
224
|
+
return el.closest("[data-grid-row-id]") === rowElement;
|
|
225
|
+
});
|
|
224
226
|
const sortedColumns = columnElements.sort((a, b) => {
|
|
225
227
|
const indexA = Number(a.dataset.gridColumnIndex || 0);
|
|
226
228
|
const indexB = Number(b.dataset.gridColumnIndex || 0);
|
|
@@ -245,7 +247,9 @@ const buildLayoutSnapshot = /* @__PURE__ */ __name(({ container }) => {
|
|
|
245
247
|
});
|
|
246
248
|
const itemElements = Array.from(
|
|
247
249
|
columnElement.querySelectorAll(`[data-grid-item-row-id="${rowId}"][data-grid-column-index="${columnIndex}"]`)
|
|
248
|
-
)
|
|
250
|
+
).filter((el) => {
|
|
251
|
+
return el.closest("[data-grid-column-row-id][data-grid-column-index]") === columnElement;
|
|
252
|
+
});
|
|
249
253
|
const sortedItems = itemElements.sort((a, b) => {
|
|
250
254
|
const indexA = Number(a.dataset.gridItemIndex || 0);
|
|
251
255
|
const indexB = Number(b.dataset.gridItemIndex || 0);
|
|
@@ -376,6 +376,21 @@ const AddSubModelButtonCore = /* @__PURE__ */ __name(function AddSubModelButton(
|
|
|
376
376
|
}),
|
|
377
377
|
[model, subModelKey, subModelType]
|
|
378
378
|
);
|
|
379
|
+
import_react.default.useEffect(() => {
|
|
380
|
+
var _a, _b, _c;
|
|
381
|
+
const handleSubModelChanged = /* @__PURE__ */ __name(() => {
|
|
382
|
+
setRefreshTick((x) => x + 1);
|
|
383
|
+
}, "handleSubModelChanged");
|
|
384
|
+
(_a = model.emitter) == null ? void 0 : _a.on("onSubModelAdded", handleSubModelChanged);
|
|
385
|
+
(_b = model.emitter) == null ? void 0 : _b.on("onSubModelRemoved", handleSubModelChanged);
|
|
386
|
+
(_c = model.emitter) == null ? void 0 : _c.on("onSubModelReplaced", handleSubModelChanged);
|
|
387
|
+
return () => {
|
|
388
|
+
var _a2, _b2, _c2;
|
|
389
|
+
(_a2 = model.emitter) == null ? void 0 : _a2.off("onSubModelAdded", handleSubModelChanged);
|
|
390
|
+
(_b2 = model.emitter) == null ? void 0 : _b2.off("onSubModelRemoved", handleSubModelChanged);
|
|
391
|
+
(_c2 = model.emitter) == null ? void 0 : _c2.off("onSubModelReplaced", handleSubModelChanged);
|
|
392
|
+
};
|
|
393
|
+
}, [model]);
|
|
379
394
|
const onClick = /* @__PURE__ */ __name(async (info) => {
|
|
380
395
|
const clickedItem = info.originalItem || info;
|
|
381
396
|
const item = clickedItem.originalItem || clickedItem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.0.
|
|
12
|
-
"@nocobase/shared": "2.0.
|
|
11
|
+
"@nocobase/sdk": "2.0.15",
|
|
12
|
+
"@nocobase/shared": "2.0.15",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dayjs": "^1.11.9",
|
|
15
15
|
"dompurify": "^3.0.2",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "NocoBase Team",
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "6262fea1782dd92e3950266cc3ccc5eac18f2689"
|
|
40
40
|
}
|
|
@@ -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);
|
|
@@ -542,6 +542,22 @@ const AddSubModelButtonCore = function AddSubModelButton({
|
|
|
542
542
|
[model, subModelKey, subModelType],
|
|
543
543
|
);
|
|
544
544
|
|
|
545
|
+
React.useEffect(() => {
|
|
546
|
+
const handleSubModelChanged = () => {
|
|
547
|
+
setRefreshTick((x) => x + 1);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
model.emitter?.on('onSubModelAdded', handleSubModelChanged);
|
|
551
|
+
model.emitter?.on('onSubModelRemoved', handleSubModelChanged);
|
|
552
|
+
model.emitter?.on('onSubModelReplaced', handleSubModelChanged);
|
|
553
|
+
|
|
554
|
+
return () => {
|
|
555
|
+
model.emitter?.off('onSubModelAdded', handleSubModelChanged);
|
|
556
|
+
model.emitter?.off('onSubModelRemoved', handleSubModelChanged);
|
|
557
|
+
model.emitter?.off('onSubModelReplaced', handleSubModelChanged);
|
|
558
|
+
};
|
|
559
|
+
}, [model]);
|
|
560
|
+
|
|
545
561
|
// 点击处理逻辑
|
|
546
562
|
const onClick = async (info: any) => {
|
|
547
563
|
const clickedItem = info.originalItem || info;
|
|
@@ -995,6 +995,56 @@ describe('AddSubModelButton - toggle interactions', () => {
|
|
|
995
995
|
const subModels = ((parent.subModels as any).items as FlowModel[]) || [];
|
|
996
996
|
expect(subModels).toHaveLength(1);
|
|
997
997
|
});
|
|
998
|
+
|
|
999
|
+
it('updates toggle state after external sub model removal', async () => {
|
|
1000
|
+
const engine = new FlowEngine();
|
|
1001
|
+
engine.flowSettings.forceEnable();
|
|
1002
|
+
|
|
1003
|
+
class ToggleParent extends FlowModel {}
|
|
1004
|
+
class ToggleChild extends FlowModel {}
|
|
1005
|
+
|
|
1006
|
+
engine.registerModels({ ToggleParent, ToggleChild });
|
|
1007
|
+
const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-external-remove' });
|
|
1008
|
+
const existing = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-external-remove' });
|
|
1009
|
+
parent.addSubModel('items', existing);
|
|
1010
|
+
|
|
1011
|
+
render(
|
|
1012
|
+
<FlowEngineProvider engine={engine}>
|
|
1013
|
+
<ConfigProvider>
|
|
1014
|
+
<App>
|
|
1015
|
+
<AddSubModelButton
|
|
1016
|
+
model={parent}
|
|
1017
|
+
subModelKey="items"
|
|
1018
|
+
items={[
|
|
1019
|
+
{
|
|
1020
|
+
key: 'toggle-child',
|
|
1021
|
+
label: 'Toggle Child',
|
|
1022
|
+
toggleable: true,
|
|
1023
|
+
useModel: 'ToggleChild',
|
|
1024
|
+
createModelOptions: { use: 'ToggleChild' },
|
|
1025
|
+
},
|
|
1026
|
+
]}
|
|
1027
|
+
>
|
|
1028
|
+
Toggle Menu
|
|
1029
|
+
</AddSubModelButton>
|
|
1030
|
+
</App>
|
|
1031
|
+
</ConfigProvider>
|
|
1032
|
+
</FlowEngineProvider>,
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
await act(async () => {
|
|
1036
|
+
await userEvent.click(screen.getByText('Toggle Menu'));
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
await waitFor(() => expect(screen.getByText('Toggle Child')).toBeInTheDocument());
|
|
1040
|
+
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
|
|
1041
|
+
|
|
1042
|
+
await act(async () => {
|
|
1043
|
+
await existing.destroy();
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'));
|
|
1047
|
+
});
|
|
998
1048
|
});
|
|
999
1049
|
|
|
1000
1050
|
// ========================
|