@nocobase/flow-engine 2.1.0-beta.22 → 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/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +2 -0
- package/lib/components/FlowModelRenderer.js +2 -0
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +595 -19
- package/lib/components/dnd/index.d.ts +19 -1
- package/lib/components/dnd/index.js +243 -23
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +20 -1
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +4 -0
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +21 -8
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +2 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +100 -32
- 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/data-source/index.d.ts +73 -0
- package/lib/data-source/index.js +205 -1
- package/lib/flowContext.d.ts +2 -0
- package/lib/flowI18n.js +2 -1
- 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 +11 -9
- package/lib/models/flowModel.js +48 -9
- package/lib/provider.js +38 -23
- package/package.json +4 -4
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +6 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- 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 +291 -27
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +25 -1
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +24 -5
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +94 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +171 -2
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +2 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +112 -32
- package/src/components/subModel/index.ts +1 -0
- package/src/data-source/__tests__/index.test.ts +34 -1
- package/src/data-source/index.ts +252 -2
- package/src/flowContext.ts +2 -0
- package/src/flowI18n.ts +2 -1
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/flowModel.tsx +85 -23
- package/src/provider.tsx +41 -25
|
@@ -0,0 +1,44 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
import { resolveOverlayAnchorTransform } from '../dnd';
|
|
12
|
+
|
|
13
|
+
describe('resolveOverlayAnchorTransform', () => {
|
|
14
|
+
it('should keep the original transform when anchor point is missing', () => {
|
|
15
|
+
const transform = { x: 24, y: 36, scaleX: 1, scaleY: 1 };
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
resolveOverlayAnchorTransform({
|
|
19
|
+
activeId: 'menu-item-1',
|
|
20
|
+
active: { id: 'menu-item-1' },
|
|
21
|
+
transform,
|
|
22
|
+
activeNodeRect: { top: 80, left: 120 },
|
|
23
|
+
dragAnchorPoint: null,
|
|
24
|
+
}),
|
|
25
|
+
).toEqual(transform);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should align the overlay origin to the pointer position when dragging from toolbar handle', () => {
|
|
29
|
+
expect(
|
|
30
|
+
resolveOverlayAnchorTransform({
|
|
31
|
+
activeId: 'menu-item-1',
|
|
32
|
+
active: { id: 'menu-item-1' },
|
|
33
|
+
transform: { x: 20, y: 30, scaleX: 1, scaleY: 1 },
|
|
34
|
+
activeNodeRect: { top: 200, left: 100 },
|
|
35
|
+
dragAnchorPoint: { x: 180, y: 260 },
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({
|
|
38
|
+
x: 100,
|
|
39
|
+
y: 90,
|
|
40
|
+
scaleX: 1,
|
|
41
|
+
scaleY: 1,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -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
|
+
});
|