@nocobase/flow-engine 2.0.46 → 2.0.48
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 +9 -4
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/package.json +4 -4
- package/src/__tests__/objectVariable.test.ts +24 -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 +9 -3
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
|
@@ -104,7 +104,9 @@ const Droppable = /* @__PURE__ */ __name(({ model, children }) => {
|
|
|
104
104
|
const DndProvider = /* @__PURE__ */ __name(({
|
|
105
105
|
persist = true,
|
|
106
106
|
children,
|
|
107
|
+
onDragStart,
|
|
107
108
|
onDragEnd,
|
|
109
|
+
onDragCancel,
|
|
108
110
|
...restProps
|
|
109
111
|
}) => {
|
|
110
112
|
const [activeId, setActiveId] = (0, import_react.useState)(null);
|
|
@@ -112,10 +114,10 @@ const DndProvider = /* @__PURE__ */ __name(({
|
|
|
112
114
|
return /* @__PURE__ */ import_react.default.createElement(
|
|
113
115
|
import_core.DndContext,
|
|
114
116
|
{
|
|
117
|
+
...restProps,
|
|
115
118
|
onDragStart: (event) => {
|
|
116
|
-
var _a;
|
|
117
119
|
setActiveId(event.active.id);
|
|
118
|
-
|
|
120
|
+
onDragStart == null ? void 0 : onDragStart(event);
|
|
119
121
|
},
|
|
120
122
|
onDragEnd: (event) => {
|
|
121
123
|
setActiveId(null);
|
|
@@ -127,13 +129,17 @@ const DndProvider = /* @__PURE__ */ __name(({
|
|
|
127
129
|
onDragEnd(event);
|
|
128
130
|
}
|
|
129
131
|
},
|
|
130
|
-
|
|
132
|
+
onDragCancel: (event) => {
|
|
133
|
+
setActiveId(null);
|
|
134
|
+
onDragCancel == null ? void 0 : onDragCancel(event);
|
|
135
|
+
}
|
|
131
136
|
},
|
|
132
137
|
children,
|
|
133
138
|
(0, import_react_dom.createPortal)(
|
|
134
139
|
/* @__PURE__ */ import_react.default.createElement(import_core.DragOverlay, { dropAnimation: null, zIndex: 2e3 }, activeId && /* @__PURE__ */ import_react.default.createElement(
|
|
135
140
|
"span",
|
|
136
141
|
{
|
|
142
|
+
"data-testid": "flow-drag-preview",
|
|
137
143
|
style: {
|
|
138
144
|
display: "inline-flex",
|
|
139
145
|
alignItems: "center",
|
|
@@ -143,7 +149,6 @@ const DndProvider = /* @__PURE__ */ __name(({
|
|
|
143
149
|
borderRadius: 4,
|
|
144
150
|
padding: "4px 12px",
|
|
145
151
|
color: "#1890ff",
|
|
146
|
-
// fontSize: 18,
|
|
147
152
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)"
|
|
148
153
|
}
|
|
149
154
|
},
|
|
@@ -32,6 +32,10 @@ __export(createCollectionContextMeta_exports, {
|
|
|
32
32
|
module.exports = __toCommonJS(createCollectionContextMeta_exports);
|
|
33
33
|
const RELATION_FIELD_TYPES = ["belongsTo", "hasOne", "hasMany", "belongsToMany", "belongsToArray"];
|
|
34
34
|
const NUMERIC_FIELD_TYPES = ["integer", "float", "double", "decimal"];
|
|
35
|
+
function shouldShowFieldInMeta(field, includeNonFilterable) {
|
|
36
|
+
return Boolean(field.interface && (includeNonFilterable || field.filterable));
|
|
37
|
+
}
|
|
38
|
+
__name(shouldShowFieldInMeta, "shouldShowFieldInMeta");
|
|
35
39
|
function createFieldMetadata(field, includeNonFilterable) {
|
|
36
40
|
const baseProperties = createMetaBaseProperties(field);
|
|
37
41
|
if (field.isAssociationField()) {
|
|
@@ -49,7 +53,7 @@ function createFieldMetadata(field, includeNonFilterable) {
|
|
|
49
53
|
properties: /* @__PURE__ */ __name(async () => {
|
|
50
54
|
const subProperties = {};
|
|
51
55
|
targetCollection.fields.forEach((subField) => {
|
|
52
|
-
if (includeNonFilterable
|
|
56
|
+
if (shouldShowFieldInMeta(subField, includeNonFilterable)) {
|
|
53
57
|
subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
|
|
54
58
|
}
|
|
55
59
|
});
|
|
@@ -104,7 +108,7 @@ function createCollectionContextMeta(collectionOrFactory, title, includeNonFilte
|
|
|
104
108
|
properties: /* @__PURE__ */ __name(async () => {
|
|
105
109
|
const properties = {};
|
|
106
110
|
collection.fields.forEach((field) => {
|
|
107
|
-
if (includeNonFilterable
|
|
111
|
+
if (shouldShowFieldInMeta(field, includeNonFilterable)) {
|
|
108
112
|
properties[field.name] = createFieldMetadata(field, includeNonFilterable);
|
|
109
113
|
}
|
|
110
114
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.48",
|
|
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.48",
|
|
12
|
+
"@nocobase/shared": "2.0.48",
|
|
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": "9a7e1e19953b501dedce4526a7231ea6c74dfa98"
|
|
41
41
|
}
|
|
@@ -26,6 +26,7 @@ function setupEngineWithCollections() {
|
|
|
26
26
|
fields: [
|
|
27
27
|
{ name: 'id', type: 'integer', interface: 'number' },
|
|
28
28
|
{ name: 'name', type: 'string', interface: 'text' },
|
|
29
|
+
{ name: 'rawUserPayload', type: 'json', filterable: true },
|
|
29
30
|
],
|
|
30
31
|
});
|
|
31
32
|
ds.addCollection({
|
|
@@ -41,6 +42,8 @@ function setupEngineWithCollections() {
|
|
|
41
42
|
filterTargetKey: 'id',
|
|
42
43
|
fields: [
|
|
43
44
|
{ name: 'title', type: 'string', interface: 'text' },
|
|
45
|
+
{ name: 'internalName', type: 'string', interface: 'text' },
|
|
46
|
+
{ name: 'rawPostPayload', type: 'json', filterable: true },
|
|
44
47
|
{ name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o' },
|
|
45
48
|
{ name: 'tags', type: 'belongsToMany', target: 'tags', interface: 'm2m' },
|
|
46
49
|
],
|
|
@@ -91,6 +94,27 @@ describe('objectVariable utilities', () => {
|
|
|
91
94
|
});
|
|
92
95
|
});
|
|
93
96
|
|
|
97
|
+
it('createAssociationAwareObjectMetaFactory should hide fields without interface from object variable meta', async () => {
|
|
98
|
+
const { collection } = setupEngineWithCollections();
|
|
99
|
+
const obj = { title: 'hello', internalName: 'internal', rawPostPayload: { secret: true }, author: 1 };
|
|
100
|
+
const metaFactory = createAssociationAwareObjectMetaFactory(
|
|
101
|
+
() => collection,
|
|
102
|
+
'Current object',
|
|
103
|
+
() => obj,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const meta = await metaFactory();
|
|
107
|
+
const props = await (meta?.properties as any)?.();
|
|
108
|
+
const authorFields = await props?.author?.properties?.();
|
|
109
|
+
|
|
110
|
+
expect(props).toHaveProperty('title');
|
|
111
|
+
expect(props).toHaveProperty('internalName');
|
|
112
|
+
expect(props).toHaveProperty('author');
|
|
113
|
+
expect(props).not.toHaveProperty('rawPostPayload');
|
|
114
|
+
expect(authorFields).toHaveProperty('name');
|
|
115
|
+
expect(authorFields).not.toHaveProperty('rawUserPayload');
|
|
116
|
+
});
|
|
117
|
+
|
|
94
118
|
it('integrates with FlowContext.resolveJsonTemplate to call variables:resolve with flattened contextParams', async () => {
|
|
95
119
|
const { engine, collection } = setupEngineWithCollections();
|
|
96
120
|
const obj = { author: 1 };
|
|
@@ -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
|
});
|