@nocobase/flow-engine 2.1.0-beta.23 → 2.1.0-beta.24
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.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +595 -19
- package/lib/components/dnd/index.js +7 -5
- package/package.json +4 -4
- package/src/components/__tests__/gridDragPlanner.test.ts +426 -5
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +735 -17
- package/src/components/dnd/index.tsx +7 -3
|
@@ -85,7 +85,7 @@ const resolveDraggableHostNode = /* @__PURE__ */ __name((activatorNode) => {
|
|
|
85
85
|
`[data-has-float-menu="true"][data-float-menu-model-uid="${toolbarModelUid}"]`
|
|
86
86
|
)
|
|
87
87
|
);
|
|
88
|
-
const popupRoot = floatToolbarContainer.closest(MENU_SUBMENU_POPUP_SELECTOR);
|
|
88
|
+
const popupRoot = floatToolbarContainer == null ? void 0 : floatToolbarContainer.closest(MENU_SUBMENU_POPUP_SELECTOR);
|
|
89
89
|
if (popupRoot) {
|
|
90
90
|
return matchedHosts.find((hostNode) => hostNode.closest(MENU_SUBMENU_POPUP_SELECTOR) === popupRoot) || activatorNode;
|
|
91
91
|
}
|
|
@@ -275,7 +275,9 @@ const Droppable = /* @__PURE__ */ __name(({ model, children }) => {
|
|
|
275
275
|
const DndProvider = /* @__PURE__ */ __name(({
|
|
276
276
|
persist = true,
|
|
277
277
|
children,
|
|
278
|
+
onDragStart,
|
|
278
279
|
onDragEnd,
|
|
280
|
+
onDragCancel,
|
|
279
281
|
...restProps
|
|
280
282
|
}) => {
|
|
281
283
|
const [activeId, setActiveId] = (0, import_react.useState)(null);
|
|
@@ -311,10 +313,10 @@ const DndProvider = /* @__PURE__ */ __name(({
|
|
|
311
313
|
return /* @__PURE__ */ import_react.default.createElement(
|
|
312
314
|
import_core.DndContext,
|
|
313
315
|
{
|
|
316
|
+
...restProps,
|
|
314
317
|
onDragStart: (event) => {
|
|
315
|
-
var _a;
|
|
316
318
|
setActiveId(event.active.id);
|
|
317
|
-
|
|
319
|
+
onDragStart == null ? void 0 : onDragStart(event);
|
|
318
320
|
},
|
|
319
321
|
onDragEnd: (event) => {
|
|
320
322
|
setActiveId(null);
|
|
@@ -328,10 +330,9 @@ const DndProvider = /* @__PURE__ */ __name(({
|
|
|
328
330
|
}
|
|
329
331
|
},
|
|
330
332
|
onDragCancel: (event) => {
|
|
331
|
-
var _a;
|
|
332
333
|
setActiveId(null);
|
|
333
334
|
setDragAnchorPoint(null);
|
|
334
|
-
|
|
335
|
+
onDragCancel == null ? void 0 : onDragCancel(event);
|
|
335
336
|
},
|
|
336
337
|
...restProps
|
|
337
338
|
},
|
|
@@ -348,6 +349,7 @@ const DndProvider = /* @__PURE__ */ __name(({
|
|
|
348
349
|
activeId && /* @__PURE__ */ import_react.default.createElement(
|
|
349
350
|
"span",
|
|
350
351
|
{
|
|
352
|
+
"data-testid": "flow-drag-preview",
|
|
351
353
|
style: {
|
|
352
354
|
display: "inline-flex",
|
|
353
355
|
alignItems: "center",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.24",
|
|
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.1.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-beta.24",
|
|
12
|
+
"@nocobase/shared": "2.1.0-beta.24",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "f77b85530a2d127d9bfe4dca3a26fbb02c1139ba"
|
|
41
41
|
}
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
resolveDropIntent,
|
|
17
17
|
Point,
|
|
18
18
|
buildLayoutSnapshot,
|
|
19
|
+
normalizeGridLayout,
|
|
20
|
+
projectLayoutToLegacyRows,
|
|
21
|
+
replaceUidInGridLayout,
|
|
19
22
|
} from '../dnd/gridDragPlanner';
|
|
20
23
|
|
|
21
24
|
const rect = { top: 0, left: 0, width: 100, height: 100 };
|
|
@@ -106,15 +109,105 @@ describe('buildLayoutSnapshot', () => {
|
|
|
106
109
|
const snapshot = buildLayoutSnapshot({ container });
|
|
107
110
|
const columnEdgeSlots = snapshot.slots.filter((slot) => slot.type === 'column-edge');
|
|
108
111
|
const columnSlots = snapshot.slots.filter((slot) => slot.type === 'column');
|
|
112
|
+
const itemEdgeSlots = snapshot.slots.filter((slot) => slot.type === 'item-edge');
|
|
109
113
|
|
|
110
|
-
// 外层单行单列单项应只有
|
|
111
|
-
expect(snapshot.slots).toHaveLength(
|
|
114
|
+
// 外层单行单列单项应只有 8 个 slot:上/下 row-gap + 左/右 column-edge + before/after column + 左/右 item-edge
|
|
115
|
+
expect(snapshot.slots).toHaveLength(8);
|
|
112
116
|
expect(columnEdgeSlots).toHaveLength(2);
|
|
113
117
|
expect(columnSlots).toHaveLength(2);
|
|
118
|
+
expect(itemEdgeSlots).toHaveLength(2);
|
|
114
119
|
|
|
115
120
|
// 不应混入嵌套 grid(其 top >= 360)
|
|
116
121
|
expect(snapshot.slots.every((slot) => slot.rect.top < 300)).toBe(true);
|
|
117
122
|
});
|
|
123
|
+
|
|
124
|
+
it('does not create an empty-column slot for a cell that contains nested rows', () => {
|
|
125
|
+
const container = document.createElement('div');
|
|
126
|
+
container.setAttribute('data-grid-root', '');
|
|
127
|
+
const row = document.createElement('div');
|
|
128
|
+
row.setAttribute('data-grid-row-id', 'outer');
|
|
129
|
+
row.setAttribute('data-grid-path', JSON.stringify([{ rowId: 'outer' }]));
|
|
130
|
+
container.appendChild(row);
|
|
131
|
+
|
|
132
|
+
const leftColumn = document.createElement('div');
|
|
133
|
+
leftColumn.setAttribute('data-grid-column-row-id', 'outer');
|
|
134
|
+
leftColumn.setAttribute('data-grid-column-index', '0');
|
|
135
|
+
leftColumn.setAttribute('data-grid-path', JSON.stringify([{ rowId: 'outer', cellId: 'outer-left' }]));
|
|
136
|
+
row.appendChild(leftColumn);
|
|
137
|
+
|
|
138
|
+
const leftItem = document.createElement('div');
|
|
139
|
+
leftItem.setAttribute('data-grid-item-row-id', 'outer');
|
|
140
|
+
leftItem.setAttribute('data-grid-column-index', '0');
|
|
141
|
+
leftItem.setAttribute('data-grid-item-index', '0');
|
|
142
|
+
leftItem.setAttribute('data-grid-item-uid', 'left');
|
|
143
|
+
leftColumn.appendChild(leftItem);
|
|
144
|
+
|
|
145
|
+
const rightColumn = document.createElement('div');
|
|
146
|
+
rightColumn.setAttribute('data-grid-column-row-id', 'outer');
|
|
147
|
+
rightColumn.setAttribute('data-grid-column-index', '1');
|
|
148
|
+
rightColumn.setAttribute('data-grid-path', JSON.stringify([{ rowId: 'outer', cellId: 'outer-right' }]));
|
|
149
|
+
row.appendChild(rightColumn);
|
|
150
|
+
|
|
151
|
+
const nestedRow = document.createElement('div');
|
|
152
|
+
nestedRow.setAttribute('data-grid-row-id', 'nested');
|
|
153
|
+
nestedRow.setAttribute(
|
|
154
|
+
'data-grid-path',
|
|
155
|
+
JSON.stringify([{ rowId: 'outer', cellId: 'outer-right' }, { rowId: 'nested' }]),
|
|
156
|
+
);
|
|
157
|
+
rightColumn.appendChild(nestedRow);
|
|
158
|
+
|
|
159
|
+
const nestedColumn = document.createElement('div');
|
|
160
|
+
nestedColumn.setAttribute('data-grid-column-row-id', 'nested');
|
|
161
|
+
nestedColumn.setAttribute('data-grid-column-index', '0');
|
|
162
|
+
nestedColumn.setAttribute(
|
|
163
|
+
'data-grid-path',
|
|
164
|
+
JSON.stringify([
|
|
165
|
+
{ rowId: 'outer', cellId: 'outer-right' },
|
|
166
|
+
{ rowId: 'nested', cellId: 'nested-cell' },
|
|
167
|
+
]),
|
|
168
|
+
);
|
|
169
|
+
nestedRow.appendChild(nestedColumn);
|
|
170
|
+
|
|
171
|
+
const nestedItem = document.createElement('div');
|
|
172
|
+
nestedItem.setAttribute('data-grid-item-row-id', 'nested');
|
|
173
|
+
nestedItem.setAttribute('data-grid-column-index', '0');
|
|
174
|
+
nestedItem.setAttribute('data-grid-item-index', '0');
|
|
175
|
+
nestedItem.setAttribute('data-grid-item-uid', 'nested-item');
|
|
176
|
+
nestedColumn.appendChild(nestedItem);
|
|
177
|
+
|
|
178
|
+
mockRect(container, { top: 0, left: 0, width: 1200, height: 500 });
|
|
179
|
+
mockRect(row, { top: 10, left: 10, width: 1180, height: 420 });
|
|
180
|
+
mockRect(leftColumn, { top: 10, left: 10, width: 480, height: 420 });
|
|
181
|
+
mockRect(leftItem, { top: 20, left: 20, width: 460, height: 380 });
|
|
182
|
+
mockRect(rightColumn, { top: 10, left: 500, width: 690, height: 420 });
|
|
183
|
+
mockRect(nestedRow, { top: 20, left: 510, width: 670, height: 120 });
|
|
184
|
+
mockRect(nestedColumn, { top: 20, left: 510, width: 670, height: 120 });
|
|
185
|
+
mockRect(nestedItem, { top: 30, left: 520, width: 650, height: 90 });
|
|
186
|
+
|
|
187
|
+
const snapshot = buildLayoutSnapshot({ container });
|
|
188
|
+
|
|
189
|
+
expect(
|
|
190
|
+
snapshot.slots.some(
|
|
191
|
+
(slot) =>
|
|
192
|
+
slot.type === 'empty-column' &&
|
|
193
|
+
slot.rowId === 'outer' &&
|
|
194
|
+
slot.columnIndex === 1 &&
|
|
195
|
+
slot.rect.left === 500 &&
|
|
196
|
+
slot.rect.width === 690,
|
|
197
|
+
),
|
|
198
|
+
).toBe(false);
|
|
199
|
+
expect(snapshot.slots.some((slot) => slot.type === 'column' && slot.rowId === 'nested')).toBe(true);
|
|
200
|
+
|
|
201
|
+
const nestedBelowGap = snapshot.slots.find(
|
|
202
|
+
(slot) => slot.type === 'row-gap' && slot.targetRowId === 'nested' && slot.position === 'below',
|
|
203
|
+
);
|
|
204
|
+
expect(nestedBelowGap?.rect).toEqual({
|
|
205
|
+
top: 140,
|
|
206
|
+
left: 500,
|
|
207
|
+
width: 690,
|
|
208
|
+
height: 48,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
118
211
|
});
|
|
119
212
|
|
|
120
213
|
describe('getSlotKey', () => {
|
|
@@ -178,6 +271,184 @@ describe('getSlotKey', () => {
|
|
|
178
271
|
const key = getSlotKey(slot);
|
|
179
272
|
expect(key).toBe('empty-column:row1:0');
|
|
180
273
|
});
|
|
274
|
+
|
|
275
|
+
it('should generate unique key for item-edge slot', () => {
|
|
276
|
+
const slot: LayoutSlot = {
|
|
277
|
+
type: 'item-edge',
|
|
278
|
+
rowId: 'row1',
|
|
279
|
+
columnIndex: 0,
|
|
280
|
+
itemIndex: 1,
|
|
281
|
+
itemUid: 'block-2',
|
|
282
|
+
direction: 'right',
|
|
283
|
+
rect,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const key = getSlotKey(slot);
|
|
287
|
+
expect(key).toBe('item-edge:row1:0:block-2:right');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('GridLayoutV2 helpers', () => {
|
|
292
|
+
it('converts legacy rows to stable v2 layout and legacy projection', () => {
|
|
293
|
+
const layout = normalizeGridLayout({
|
|
294
|
+
rows: {
|
|
295
|
+
rowA: [['a'], ['b', 'c']],
|
|
296
|
+
},
|
|
297
|
+
sizes: {
|
|
298
|
+
rowA: [8, 16],
|
|
299
|
+
},
|
|
300
|
+
rowOrder: ['rowA'],
|
|
301
|
+
itemUids: ['a', 'b', 'c'],
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(layout).toMatchObject({
|
|
305
|
+
version: 2,
|
|
306
|
+
rows: [
|
|
307
|
+
{
|
|
308
|
+
id: 'rowA',
|
|
309
|
+
cells: [
|
|
310
|
+
{ id: 'rowA:cell:0', items: ['a'] },
|
|
311
|
+
{ id: 'rowA:cell:1', items: ['b', 'c'] },
|
|
312
|
+
],
|
|
313
|
+
sizes: [8, 16],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const projected = projectLayoutToLegacyRows(layout);
|
|
319
|
+
expect(projected.rows.rowA).toEqual([['a'], ['b', 'c']]);
|
|
320
|
+
expect(projected.sizes.rowA).toEqual([8, 16]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('deduplicates items, removes invalid uids, appends missing items, and fixes sizes', () => {
|
|
324
|
+
const layout = normalizeGridLayout({
|
|
325
|
+
layout: {
|
|
326
|
+
version: 2,
|
|
327
|
+
rows: [
|
|
328
|
+
{
|
|
329
|
+
id: 'rowA',
|
|
330
|
+
cells: [
|
|
331
|
+
{ id: 'cellA', items: ['a', 'b', 'a', 'ghost'] },
|
|
332
|
+
{ id: 'cellB', items: ['c'] },
|
|
333
|
+
],
|
|
334
|
+
sizes: [5],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
itemUids: ['a', 'b', 'c', 'd'],
|
|
339
|
+
generateId: () => 'row-missing',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(layout.rows[0].cells[0]).toMatchObject({ id: 'cellA', items: ['a', 'b'] });
|
|
343
|
+
expect(layout.rows[0].cells[1]).toMatchObject({ id: 'cellB', items: ['c'] });
|
|
344
|
+
expect(layout.rows[0].sizes).toHaveLength(2);
|
|
345
|
+
expect(layout.rows[0].sizes.reduce((sum, value) => sum + value, 0)).toBe(24);
|
|
346
|
+
expect(layout.rows[1]).toMatchObject({
|
|
347
|
+
id: 'row-missing',
|
|
348
|
+
cells: [{ id: 'row-missing:cell:0', items: ['d'] }],
|
|
349
|
+
sizes: [24],
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('preserves explicit empty v2 cells while removing invalid-only cells', () => {
|
|
354
|
+
const layout = normalizeGridLayout({
|
|
355
|
+
layout: {
|
|
356
|
+
version: 2,
|
|
357
|
+
rows: [
|
|
358
|
+
{
|
|
359
|
+
id: 'rowA',
|
|
360
|
+
cells: [
|
|
361
|
+
{ id: 'empty-cell', items: [] },
|
|
362
|
+
{ id: 'invalid-cell', items: ['ghost'] },
|
|
363
|
+
{ id: 'valid-cell', items: ['a'] },
|
|
364
|
+
],
|
|
365
|
+
sizes: [8, 8, 8],
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
itemUids: ['a'],
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(layout.rows[0].cells).toEqual([
|
|
373
|
+
{ id: 'empty-cell', items: [] },
|
|
374
|
+
{ id: 'valid-cell', items: ['a'] },
|
|
375
|
+
]);
|
|
376
|
+
expect(layout.rows[0].sizes).toEqual([12, 12]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('replaces uid in nested layout', () => {
|
|
380
|
+
const layout = normalizeGridLayout({
|
|
381
|
+
layout: {
|
|
382
|
+
version: 2,
|
|
383
|
+
rows: [
|
|
384
|
+
{
|
|
385
|
+
id: 'rowA',
|
|
386
|
+
cells: [
|
|
387
|
+
{
|
|
388
|
+
id: 'cellA',
|
|
389
|
+
rows: [
|
|
390
|
+
{
|
|
391
|
+
id: 'nested',
|
|
392
|
+
cells: [{ id: 'nested-cell', items: ['old'] }],
|
|
393
|
+
sizes: [24],
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
sizes: [24],
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
itemUids: ['old'],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const replaced = replaceUidInGridLayout(layout, 'old', 'new');
|
|
406
|
+
expect(projectLayoutToLegacyRows(replaced).rows.rowA).toEqual([['new']]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('projects nested layout without duplicating nested items', () => {
|
|
410
|
+
const layout = normalizeGridLayout({
|
|
411
|
+
layout: {
|
|
412
|
+
version: 2,
|
|
413
|
+
rows: [
|
|
414
|
+
{
|
|
415
|
+
id: 'rowA',
|
|
416
|
+
cells: [
|
|
417
|
+
{
|
|
418
|
+
id: 'cellA',
|
|
419
|
+
rows: [
|
|
420
|
+
{
|
|
421
|
+
id: 'nestedA',
|
|
422
|
+
cells: [{ id: 'nestedA:cell:0', items: ['a'] }],
|
|
423
|
+
sizes: [24],
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: 'nestedB',
|
|
427
|
+
cells: [
|
|
428
|
+
{ id: 'nestedB:cell:0', items: ['two'] },
|
|
429
|
+
{ id: 'nestedB:cell:1', items: ['four'] },
|
|
430
|
+
],
|
|
431
|
+
sizes: [12, 12],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: 'nestedC',
|
|
435
|
+
cells: [{ id: 'nestedC:cell:0', items: ['three'] }],
|
|
436
|
+
sizes: [24],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
sizes: [24],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
itemUids: ['a', 'two', 'four', 'three'],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const projected = projectLayoutToLegacyRows(layout);
|
|
449
|
+
expect(Object.keys(projected.rows)).toEqual(['rowA']);
|
|
450
|
+
expect(projected.rows.rowA).toEqual([['a', 'two', 'four', 'three']]);
|
|
451
|
+
});
|
|
181
452
|
});
|
|
182
453
|
|
|
183
454
|
describe('resolveDropIntent', () => {
|
|
@@ -231,7 +502,7 @@ describe('resolveDropIntent', () => {
|
|
|
231
502
|
expect(result).toEqual(targetSlot);
|
|
232
503
|
});
|
|
233
504
|
|
|
234
|
-
it('should return
|
|
505
|
+
it('should return highest-priority matching slot when multiple slots contain the point', () => {
|
|
235
506
|
const firstSlot: LayoutSlot = {
|
|
236
507
|
type: 'empty-row',
|
|
237
508
|
rect: { top: 0, left: 0, width: 500, height: 500 },
|
|
@@ -250,8 +521,7 @@ describe('resolveDropIntent', () => {
|
|
|
250
521
|
|
|
251
522
|
const point: Point = { x: 125, y: 110 };
|
|
252
523
|
const result = resolveDropIntent(point, slots);
|
|
253
|
-
|
|
254
|
-
expect(result).toEqual(firstSlot);
|
|
524
|
+
expect(result).toEqual(secondSlot);
|
|
255
525
|
});
|
|
256
526
|
|
|
257
527
|
it('should handle empty-column slot correctly', () => {
|
|
@@ -484,6 +754,112 @@ describe('simulateLayoutForSlot', () => {
|
|
|
484
754
|
expect(result.rows.rowA[2]).toEqual(['c']);
|
|
485
755
|
});
|
|
486
756
|
|
|
757
|
+
it('preserves remaining cell size ratios after removing a non-last source cell in v2 layout', () => {
|
|
758
|
+
const layout = createLayout(
|
|
759
|
+
{
|
|
760
|
+
rowA: [['a'], ['b'], ['c']],
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
rowA: [4, 8, 12],
|
|
764
|
+
},
|
|
765
|
+
);
|
|
766
|
+
layout.layout = normalizeGridLayout({
|
|
767
|
+
rows: layout.rows,
|
|
768
|
+
sizes: layout.sizes,
|
|
769
|
+
rowOrder: ['rowA'],
|
|
770
|
+
itemUids: ['a', 'b', 'c'],
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const slot: LayoutSlot = {
|
|
774
|
+
type: 'column',
|
|
775
|
+
rowId: 'rowA',
|
|
776
|
+
columnIndex: 2,
|
|
777
|
+
insertIndex: 1,
|
|
778
|
+
position: 'after',
|
|
779
|
+
path: [{ rowId: 'rowA', cellId: 'rowA:cell:2' }],
|
|
780
|
+
rect,
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'a', layout });
|
|
784
|
+
|
|
785
|
+
expect(result.layout!.rows[0].cells.map((cell) => cell.items)).toEqual([['b'], ['c', 'a']]);
|
|
786
|
+
expect(result.layout!.rows[0].sizes).toEqual([10, 14]);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('splits a stacked cell around item-edge right with stable generated ids', () => {
|
|
790
|
+
const layout = createLayout(
|
|
791
|
+
{
|
|
792
|
+
rowA: [['a', 'two', 'three', 'd']],
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
rowA: [24],
|
|
796
|
+
},
|
|
797
|
+
);
|
|
798
|
+
layout.layout = normalizeGridLayout({
|
|
799
|
+
rows: layout.rows,
|
|
800
|
+
sizes: layout.sizes,
|
|
801
|
+
rowOrder: ['rowA'],
|
|
802
|
+
itemUids: ['a', 'two', 'three', 'd', 'four'],
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const slot: LayoutSlot = {
|
|
806
|
+
type: 'item-edge',
|
|
807
|
+
rowId: 'rowA',
|
|
808
|
+
columnIndex: 0,
|
|
809
|
+
itemIndex: 1,
|
|
810
|
+
itemUid: 'two',
|
|
811
|
+
direction: 'right',
|
|
812
|
+
path: [{ rowId: 'rowA', cellId: 'rowA:cell:0' }],
|
|
813
|
+
rect,
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const generatedIds = new Map<string, string>();
|
|
817
|
+
const generateId = (key: string) => `id:${key}`;
|
|
818
|
+
const first = simulateLayoutForSlot({ slot, sourceUid: 'four', layout, generatedIds, generateId });
|
|
819
|
+
const second = simulateLayoutForSlot({ slot, sourceUid: 'four', layout, generatedIds, generateId });
|
|
820
|
+
|
|
821
|
+
expect(first.layout).toEqual(second.layout);
|
|
822
|
+
const nestedRows = first.layout!.rows[0].cells[0].rows!;
|
|
823
|
+
expect(nestedRows.map((row) => row.cells.map((cell) => cell.items))).toEqual([
|
|
824
|
+
[['a']],
|
|
825
|
+
[['two'], ['four']],
|
|
826
|
+
[['three']],
|
|
827
|
+
[['d']],
|
|
828
|
+
]);
|
|
829
|
+
expect(nestedRows[1].sizes).toEqual([12, 12]);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('treats dragging an item to its own item-edge as no-op', () => {
|
|
833
|
+
const layout = createLayout(
|
|
834
|
+
{
|
|
835
|
+
rowA: [['two']],
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
rowA: [24],
|
|
839
|
+
},
|
|
840
|
+
);
|
|
841
|
+
layout.layout = normalizeGridLayout({
|
|
842
|
+
rows: layout.rows,
|
|
843
|
+
sizes: layout.sizes,
|
|
844
|
+
rowOrder: ['rowA'],
|
|
845
|
+
itemUids: ['two'],
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const slot: LayoutSlot = {
|
|
849
|
+
type: 'item-edge',
|
|
850
|
+
rowId: 'rowA',
|
|
851
|
+
columnIndex: 0,
|
|
852
|
+
itemIndex: 0,
|
|
853
|
+
itemUid: 'two',
|
|
854
|
+
direction: 'right',
|
|
855
|
+
path: [{ rowId: 'rowA', cellId: 'rowA:cell:0' }],
|
|
856
|
+
rect,
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'two', layout });
|
|
860
|
+
expect(result.layout).toEqual(layout.layout);
|
|
861
|
+
});
|
|
862
|
+
|
|
487
863
|
it('handles row-gap slot below position', () => {
|
|
488
864
|
const layout = createLayout(
|
|
489
865
|
{
|
|
@@ -719,4 +1095,49 @@ describe('simulateLayoutForSlot', () => {
|
|
|
719
1095
|
const total = result.sizes.rowA.reduce((sum, size) => sum + size, 0);
|
|
720
1096
|
expect(total).toBe(24);
|
|
721
1097
|
});
|
|
1098
|
+
|
|
1099
|
+
it('preserves explicit empty v2 cells when moving another item', () => {
|
|
1100
|
+
const layout: GridLayoutData = {
|
|
1101
|
+
rows: {},
|
|
1102
|
+
sizes: {},
|
|
1103
|
+
layout: {
|
|
1104
|
+
version: 2,
|
|
1105
|
+
rows: [
|
|
1106
|
+
{
|
|
1107
|
+
id: 'rowA',
|
|
1108
|
+
cells: [
|
|
1109
|
+
{ id: 'empty-cell', items: [] },
|
|
1110
|
+
{ id: 'source-cell', items: ['source'] },
|
|
1111
|
+
{ id: 'target-cell', items: ['target'] },
|
|
1112
|
+
],
|
|
1113
|
+
sizes: [8, 8, 8],
|
|
1114
|
+
},
|
|
1115
|
+
],
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
const slot: LayoutSlot = {
|
|
1120
|
+
type: 'column-edge',
|
|
1121
|
+
rowId: 'rowA',
|
|
1122
|
+
columnIndex: 2,
|
|
1123
|
+
direction: 'right',
|
|
1124
|
+
rect,
|
|
1125
|
+
path: [{ rowId: 'rowA', cellId: 'target-cell' }],
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const result = simulateLayoutForSlot({
|
|
1129
|
+
slot,
|
|
1130
|
+
sourceUid: 'source',
|
|
1131
|
+
layout,
|
|
1132
|
+
generateId: (key) => key,
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
expect(result.layout?.rows[0].cells).toEqual([
|
|
1136
|
+
{ id: 'empty-cell', items: [] },
|
|
1137
|
+
{ id: 'target-cell', items: ['target'] },
|
|
1138
|
+
{ id: 'column-edge:rowA:2:right:cell', items: ['source'] },
|
|
1139
|
+
]);
|
|
1140
|
+
expect(result.layout?.rows[0].sizes).toHaveLength(3);
|
|
1141
|
+
expect(result.layout?.rows[0].sizes.reduce((sum, size) => sum + size, 0)).toBe(24);
|
|
1142
|
+
});
|
|
722
1143
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { act, render, screen } from '@testing-library/react';
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { FlowEngine } from '../../../flowEngine';
|
|
14
|
+
import { FlowEngineProvider } from '../../../provider';
|
|
15
|
+
import { DndProvider } from '../index';
|
|
16
|
+
|
|
17
|
+
let latestDndProps: any = null;
|
|
18
|
+
|
|
19
|
+
vi.mock('@dnd-kit/core', () => ({
|
|
20
|
+
DndContext: ({ children, ...props }: any) => {
|
|
21
|
+
latestDndProps = props;
|
|
22
|
+
return <div data-testid="dnd-context">{children}</div>;
|
|
23
|
+
},
|
|
24
|
+
DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
|
|
25
|
+
useDraggable: () => ({
|
|
26
|
+
attributes: {},
|
|
27
|
+
listeners: {},
|
|
28
|
+
setNodeRef: vi.fn(),
|
|
29
|
+
}),
|
|
30
|
+
useDroppable: () => ({
|
|
31
|
+
active: null,
|
|
32
|
+
isOver: false,
|
|
33
|
+
setNodeRef: vi.fn(),
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const renderDndProvider = (
|
|
38
|
+
props: React.ComponentProps<typeof DndProvider> = {},
|
|
39
|
+
setupEngine?: (engine: FlowEngine) => void,
|
|
40
|
+
) => {
|
|
41
|
+
const engine = new FlowEngine();
|
|
42
|
+
setupEngine?.(engine);
|
|
43
|
+
return render(
|
|
44
|
+
<FlowEngineProvider engine={engine}>
|
|
45
|
+
<DndProvider {...props}>
|
|
46
|
+
<div>content</div>
|
|
47
|
+
</DndProvider>
|
|
48
|
+
</FlowEngineProvider>,
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe('DndProvider', () => {
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
latestDndProps = null;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps the drag overlay visible when a custom onDragStart is provided', () => {
|
|
58
|
+
const onDragStart = vi.fn();
|
|
59
|
+
renderDndProvider({ onDragStart });
|
|
60
|
+
|
|
61
|
+
act(() => {
|
|
62
|
+
latestDndProps.onDragStart({ active: { id: 'block-1' } });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(onDragStart).toHaveBeenCalledWith({ active: { id: 'block-1' } });
|
|
66
|
+
expect(screen.getByTestId('flow-drag-preview')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByText('Dragging')).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('clears the drag overlay when custom drag callbacks finish', () => {
|
|
71
|
+
const onDragStart = vi.fn();
|
|
72
|
+
const onDragEnd = vi.fn();
|
|
73
|
+
const onDragCancel = vi.fn();
|
|
74
|
+
renderDndProvider({ onDragStart, onDragEnd, onDragCancel });
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
latestDndProps.onDragStart({ active: { id: 'block-1' } });
|
|
78
|
+
});
|
|
79
|
+
expect(screen.getByText('Dragging')).toBeInTheDocument();
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
latestDndProps.onDragEnd({ active: { id: 'block-1' }, over: null });
|
|
83
|
+
});
|
|
84
|
+
expect(onDragEnd).toHaveBeenCalledWith({ active: { id: 'block-1' }, over: null });
|
|
85
|
+
expect(screen.queryByText('Dragging')).not.toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
act(() => {
|
|
88
|
+
latestDndProps.onDragStart({ active: { id: 'block-1' } });
|
|
89
|
+
});
|
|
90
|
+
expect(screen.getByText('Dragging')).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
act(() => {
|
|
93
|
+
latestDndProps.onDragCancel({ active: { id: 'block-1' } });
|
|
94
|
+
});
|
|
95
|
+
expect(onDragCancel).toHaveBeenCalledWith({ active: { id: 'block-1' } });
|
|
96
|
+
expect(screen.queryByText('Dragging')).not.toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
});
|