@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
|
@@ -47,6 +47,39 @@ export interface GridLayoutData {
|
|
|
47
47
|
rows: Record<string, string[][]>;
|
|
48
48
|
sizes: Record<string, number[]>;
|
|
49
49
|
rowOrder?: string[];
|
|
50
|
+
layout?: GridLayoutV2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GridLayoutV2 {
|
|
54
|
+
version: 2;
|
|
55
|
+
rows: GridRowV2[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GridRowV2 {
|
|
59
|
+
id: string;
|
|
60
|
+
cells: GridCellV2[];
|
|
61
|
+
sizes: number[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GridCellV2 {
|
|
65
|
+
id: string;
|
|
66
|
+
items?: string[];
|
|
67
|
+
rows?: GridRowV2[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GridLayoutPathEntry {
|
|
71
|
+
rowId: string;
|
|
72
|
+
cellId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type GridLayoutPath = GridLayoutPathEntry[];
|
|
76
|
+
|
|
77
|
+
export interface GridLayoutPosition {
|
|
78
|
+
path: GridLayoutPath;
|
|
79
|
+
rowIndex: number;
|
|
80
|
+
cellIndex: number;
|
|
81
|
+
itemIndex: number;
|
|
82
|
+
itemUid: string;
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
export interface ColumnSlot {
|
|
@@ -56,6 +89,7 @@ export interface ColumnSlot {
|
|
|
56
89
|
insertIndex: number;
|
|
57
90
|
position: 'before' | 'after';
|
|
58
91
|
rect: Rect;
|
|
92
|
+
path?: GridLayoutPath;
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
export interface ColumnEdgeSlot {
|
|
@@ -64,6 +98,7 @@ export interface ColumnEdgeSlot {
|
|
|
64
98
|
columnIndex: number;
|
|
65
99
|
direction: 'left' | 'right';
|
|
66
100
|
rect: Rect;
|
|
101
|
+
path?: GridLayoutPath;
|
|
67
102
|
}
|
|
68
103
|
|
|
69
104
|
export interface RowGapSlot {
|
|
@@ -71,6 +106,7 @@ export interface RowGapSlot {
|
|
|
71
106
|
targetRowId: string;
|
|
72
107
|
position: 'above' | 'below';
|
|
73
108
|
rect: Rect;
|
|
109
|
+
path?: GridLayoutPath;
|
|
74
110
|
}
|
|
75
111
|
|
|
76
112
|
export interface EmptyRowSlot {
|
|
@@ -83,9 +119,21 @@ export interface EmptyColumnSlot {
|
|
|
83
119
|
rowId: string;
|
|
84
120
|
columnIndex: number;
|
|
85
121
|
rect: Rect;
|
|
122
|
+
path?: GridLayoutPath;
|
|
86
123
|
}
|
|
87
124
|
|
|
88
|
-
export
|
|
125
|
+
export interface ItemEdgeSlot {
|
|
126
|
+
type: 'item-edge';
|
|
127
|
+
rowId: string;
|
|
128
|
+
columnIndex: number;
|
|
129
|
+
itemIndex: number;
|
|
130
|
+
itemUid: string;
|
|
131
|
+
direction: 'left' | 'right';
|
|
132
|
+
rect: Rect;
|
|
133
|
+
path?: GridLayoutPath;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type LayoutSlot = ColumnSlot | ColumnEdgeSlot | RowGapSlot | EmptyRowSlot | EmptyColumnSlot | ItemEdgeSlot;
|
|
89
137
|
|
|
90
138
|
/**
|
|
91
139
|
* 列内插入的配置
|
|
@@ -267,6 +315,39 @@ const createColumnInsertRect = (itemRect: Rect, position: 'before' | 'after'): R
|
|
|
267
315
|
});
|
|
268
316
|
};
|
|
269
317
|
|
|
318
|
+
const parseLayoutPath = (value?: string): GridLayoutPath | undefined => {
|
|
319
|
+
if (!value) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(value);
|
|
324
|
+
if (!Array.isArray(parsed)) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const path = parsed
|
|
328
|
+
.map((entry) => {
|
|
329
|
+
if (!entry || typeof entry !== 'object' || typeof entry.rowId !== 'string') {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
rowId: entry.rowId,
|
|
334
|
+
...(typeof entry.cellId === 'string' ? { cellId: entry.cellId } : {}),
|
|
335
|
+
};
|
|
336
|
+
})
|
|
337
|
+
.filter(Boolean) as GridLayoutPath;
|
|
338
|
+
return path.length ? path : undefined;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const createLegacyCellPath = (rowId: string, columnIndex: number): GridLayoutPath => [
|
|
345
|
+
{
|
|
346
|
+
rowId,
|
|
347
|
+
cellId: `${rowId}:cell:${columnIndex}`,
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
|
|
270
351
|
const expandColumnRect = (columnRect: Rect): Rect => ({
|
|
271
352
|
top: columnRect.top,
|
|
272
353
|
left: columnRect.left,
|
|
@@ -274,6 +355,12 @@ const expandColumnRect = (columnRect: Rect): Rect => ({
|
|
|
274
355
|
height: Math.max(columnRect.height, MIN_SLOT_THICKNESS),
|
|
275
356
|
});
|
|
276
357
|
|
|
358
|
+
const hasDirectNestedRows = (columnElement: HTMLElement) => {
|
|
359
|
+
return Array.from(columnElement.querySelectorAll('[data-grid-row-id]')).some((rowElement) => {
|
|
360
|
+
return (rowElement as HTMLElement).closest('[data-grid-column-row-id][data-grid-column-index]') === columnElement;
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
|
|
277
364
|
export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions): LayoutSnapshot => {
|
|
278
365
|
if (!container) {
|
|
279
366
|
return {
|
|
@@ -282,23 +369,29 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
282
369
|
};
|
|
283
370
|
}
|
|
284
371
|
|
|
285
|
-
const
|
|
372
|
+
const scope = (
|
|
373
|
+
container.hasAttribute('data-grid-root') ? container : container.querySelector('[data-grid-root]')
|
|
374
|
+
) as HTMLElement | null;
|
|
375
|
+
const layoutContainer = scope || container;
|
|
376
|
+
const containerRect = toRect(layoutContainer.getBoundingClientRect());
|
|
286
377
|
const slots: LayoutSlot[] = [];
|
|
287
378
|
|
|
288
379
|
// 获取所有行元素,但只保留直接属于当前容器的(不在子 Grid 中的)
|
|
289
|
-
const allRowElements = Array.from(
|
|
380
|
+
const allRowElements = Array.from(layoutContainer.querySelectorAll('[data-grid-row-id]'));
|
|
381
|
+
const hasGridRootScope = layoutContainer.hasAttribute('data-grid-root');
|
|
290
382
|
const rowElements = allRowElements.filter((el) => {
|
|
291
383
|
const htmlEl = el as HTMLElement;
|
|
292
|
-
|
|
384
|
+
if (hasGridRootScope) {
|
|
385
|
+
return htmlEl.closest('[data-grid-root]') === layoutContainer;
|
|
386
|
+
}
|
|
387
|
+
// 兼容旧测试和旧 DOM:只保留当前容器的直接 Grid 行。
|
|
293
388
|
let parent = htmlEl.parentElement;
|
|
294
|
-
while (parent && parent !==
|
|
389
|
+
while (parent && parent !== layoutContainer) {
|
|
295
390
|
if (parent.hasAttribute('data-grid-row-id')) {
|
|
296
|
-
// 说明这个元素在另一个 Grid 行内,是嵌套的
|
|
297
391
|
return false;
|
|
298
392
|
}
|
|
299
393
|
parent = parent.parentElement;
|
|
300
394
|
}
|
|
301
|
-
// 如果遍历到 container 都没遇到其他 grid-row,说明是直接子元素
|
|
302
395
|
return true;
|
|
303
396
|
}) as HTMLElement[];
|
|
304
397
|
|
|
@@ -315,24 +408,43 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
315
408
|
return { slots, containerRect };
|
|
316
409
|
}
|
|
317
410
|
|
|
318
|
-
|
|
411
|
+
const getRowScopeElement = (rowElement: HTMLElement) => {
|
|
412
|
+
const parent = rowElement.parentElement;
|
|
413
|
+
if (!parent || !layoutContainer.contains(parent)) {
|
|
414
|
+
return layoutContainer;
|
|
415
|
+
}
|
|
416
|
+
return parent;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const rowElementsByScope = new Map<HTMLElement, HTMLElement[]>();
|
|
420
|
+
rowElements.forEach((rowElement) => {
|
|
421
|
+
const rowScopeElement = getRowScopeElement(rowElement);
|
|
422
|
+
rowElementsByScope.set(rowScopeElement, [...(rowElementsByScope.get(rowScopeElement) || []), rowElement]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
rowElements.forEach((rowElement) => {
|
|
319
426
|
const rowId = rowElement.dataset.gridRowId;
|
|
320
427
|
if (!rowId) {
|
|
321
428
|
return;
|
|
322
429
|
}
|
|
323
430
|
const rowRect = toRect(rowElement.getBoundingClientRect());
|
|
431
|
+
const rowPath = parseLayoutPath(rowElement.dataset.gridPath) || [{ rowId }];
|
|
432
|
+
const rowScopeElement = getRowScopeElement(rowElement);
|
|
433
|
+
const rowScopeRect = toRect(rowScopeElement.getBoundingClientRect());
|
|
434
|
+
const rowElementsInScope = rowElementsByScope.get(rowScopeElement) || [];
|
|
324
435
|
|
|
325
|
-
if (
|
|
436
|
+
if (rowElementsInScope[0] === rowElement) {
|
|
326
437
|
slots.push({
|
|
327
438
|
type: 'row-gap',
|
|
328
439
|
targetRowId: rowId,
|
|
329
440
|
position: 'above',
|
|
330
|
-
rect: createRowGapRect(rowRect, 'above',
|
|
441
|
+
rect: createRowGapRect(rowRect, 'above', rowScopeRect),
|
|
442
|
+
path: rowPath,
|
|
331
443
|
});
|
|
332
444
|
}
|
|
333
445
|
|
|
334
446
|
const columnElements = Array.from(
|
|
335
|
-
|
|
447
|
+
layoutContainer.querySelectorAll(`[data-grid-column-row-id="${rowId}"][data-grid-column-index]`),
|
|
336
448
|
).filter((el) => {
|
|
337
449
|
// 只保留当前 row 下的直接列,避免嵌套 Grid 中相同 rowId 的列混入
|
|
338
450
|
return (el as HTMLElement).closest('[data-grid-row-id]') === rowElement;
|
|
@@ -347,6 +459,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
347
459
|
sortedColumns.forEach((columnElement) => {
|
|
348
460
|
const columnIndex = Number(columnElement.dataset.gridColumnIndex || 0);
|
|
349
461
|
const columnRect = toRect(columnElement.getBoundingClientRect());
|
|
462
|
+
const columnPath = parseLayoutPath(columnElement.dataset.gridPath) || createLegacyCellPath(rowId, columnIndex);
|
|
350
463
|
|
|
351
464
|
slots.push({
|
|
352
465
|
type: 'column-edge',
|
|
@@ -354,6 +467,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
354
467
|
columnIndex,
|
|
355
468
|
direction: 'left',
|
|
356
469
|
rect: createColumnEdgeRect(columnRect, 'left'),
|
|
470
|
+
path: columnPath,
|
|
357
471
|
});
|
|
358
472
|
|
|
359
473
|
slots.push({
|
|
@@ -362,6 +476,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
362
476
|
columnIndex,
|
|
363
477
|
direction: 'right',
|
|
364
478
|
rect: createColumnEdgeRect(columnRect, 'right'),
|
|
479
|
+
path: columnPath,
|
|
365
480
|
});
|
|
366
481
|
|
|
367
482
|
const itemElements = Array.from(
|
|
@@ -378,11 +493,16 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
378
493
|
});
|
|
379
494
|
|
|
380
495
|
if (sortedItems.length === 0) {
|
|
496
|
+
if (hasDirectNestedRows(columnElement)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
381
500
|
slots.push({
|
|
382
501
|
type: 'empty-column',
|
|
383
502
|
rowId,
|
|
384
503
|
columnIndex,
|
|
385
504
|
rect: expandColumnRect(columnRect),
|
|
505
|
+
path: columnPath,
|
|
386
506
|
});
|
|
387
507
|
return;
|
|
388
508
|
}
|
|
@@ -395,10 +515,35 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
395
515
|
insertIndex: 0,
|
|
396
516
|
position: 'before',
|
|
397
517
|
rect: createColumnInsertRect(firstItemRect, 'before'),
|
|
518
|
+
path: columnPath,
|
|
398
519
|
});
|
|
399
520
|
|
|
400
521
|
sortedItems.forEach((itemElement, itemIndex) => {
|
|
401
522
|
const itemRect = toRect(itemElement.getBoundingClientRect());
|
|
523
|
+
const itemPath = parseLayoutPath(itemElement.dataset.gridPath) || columnPath;
|
|
524
|
+
const itemUid = itemElement.dataset.gridItemUid || '';
|
|
525
|
+
slots.push({
|
|
526
|
+
type: 'item-edge',
|
|
527
|
+
rowId,
|
|
528
|
+
columnIndex,
|
|
529
|
+
itemIndex,
|
|
530
|
+
itemUid,
|
|
531
|
+
direction: 'left',
|
|
532
|
+
rect: createColumnEdgeRect(itemRect, 'left'),
|
|
533
|
+
path: itemPath,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
slots.push({
|
|
537
|
+
type: 'item-edge',
|
|
538
|
+
rowId,
|
|
539
|
+
columnIndex,
|
|
540
|
+
itemIndex,
|
|
541
|
+
itemUid,
|
|
542
|
+
direction: 'right',
|
|
543
|
+
rect: createColumnEdgeRect(itemRect, 'right'),
|
|
544
|
+
path: itemPath,
|
|
545
|
+
});
|
|
546
|
+
|
|
402
547
|
slots.push({
|
|
403
548
|
type: 'column',
|
|
404
549
|
rowId,
|
|
@@ -406,6 +551,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
406
551
|
insertIndex: itemIndex + 1,
|
|
407
552
|
position: 'after',
|
|
408
553
|
rect: createColumnInsertRect(itemRect, 'after'),
|
|
554
|
+
path: itemPath,
|
|
409
555
|
});
|
|
410
556
|
});
|
|
411
557
|
});
|
|
@@ -414,7 +560,8 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
414
560
|
type: 'row-gap',
|
|
415
561
|
targetRowId: rowId,
|
|
416
562
|
position: 'below',
|
|
417
|
-
rect: createRowGapRect(rowRect, 'below',
|
|
563
|
+
rect: createRowGapRect(rowRect, 'below', rowScopeRect),
|
|
564
|
+
path: rowPath,
|
|
418
565
|
});
|
|
419
566
|
});
|
|
420
567
|
|
|
@@ -436,6 +583,8 @@ export const getSlotKey = (slot: LayoutSlot): string => {
|
|
|
436
583
|
return `${slot.type}`;
|
|
437
584
|
case 'empty-column':
|
|
438
585
|
return `${slot.type}:${slot.rowId}:${slot.columnIndex}`;
|
|
586
|
+
case 'item-edge':
|
|
587
|
+
return `${slot.type}:${slot.rowId}:${slot.columnIndex}:${slot.itemUid}:${slot.direction}`;
|
|
439
588
|
}
|
|
440
589
|
};
|
|
441
590
|
|
|
@@ -454,21 +603,45 @@ const distanceToRect = (point: Point, rect: Rect): number => {
|
|
|
454
603
|
return Math.sqrt(dx * dx + dy * dy);
|
|
455
604
|
};
|
|
456
605
|
|
|
606
|
+
const slotPriority: Record<LayoutSlot['type'], number> = {
|
|
607
|
+
'item-edge': 5,
|
|
608
|
+
'column-edge': 4,
|
|
609
|
+
column: 3,
|
|
610
|
+
'row-gap': 2,
|
|
611
|
+
'empty-column': 1,
|
|
612
|
+
'empty-row': 0,
|
|
613
|
+
};
|
|
614
|
+
|
|
457
615
|
export const resolveDropIntent = (point: Point, slots: LayoutSlot[]): LayoutSlot | null => {
|
|
458
616
|
if (!slots.length) {
|
|
459
617
|
return null;
|
|
460
618
|
}
|
|
461
619
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
620
|
+
let bestInsideSlot: LayoutSlot | null = null;
|
|
621
|
+
let bestInsidePriority = Number.NEGATIVE_INFINITY;
|
|
622
|
+
slots.forEach((slot) => {
|
|
623
|
+
if (!isPointInsideRect(point, slot.rect)) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const priority = slotPriority[slot.type];
|
|
627
|
+
if (priority > bestInsidePriority) {
|
|
628
|
+
bestInsidePriority = priority;
|
|
629
|
+
bestInsideSlot = slot;
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (bestInsideSlot) {
|
|
634
|
+
return bestInsideSlot;
|
|
465
635
|
}
|
|
466
636
|
|
|
467
637
|
let closest: LayoutSlot | null = null;
|
|
468
638
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
469
639
|
slots.forEach((slot) => {
|
|
470
640
|
const distance = distanceToRect(point, slot.rect);
|
|
471
|
-
if (
|
|
641
|
+
if (
|
|
642
|
+
distance < minDistance ||
|
|
643
|
+
(distance === minDistance && closest && slotPriority[slot.type] > slotPriority[closest.type])
|
|
644
|
+
) {
|
|
472
645
|
minDistance = distance;
|
|
473
646
|
closest = slot;
|
|
474
647
|
}
|
|
@@ -528,7 +701,10 @@ const toIntSizes = (weights: number[], count: number): number[] => {
|
|
|
528
701
|
return [];
|
|
529
702
|
}
|
|
530
703
|
|
|
531
|
-
const normalizedWeights =
|
|
704
|
+
const normalizedWeights = Array.from({ length: count }, (_, index) => {
|
|
705
|
+
const weight = weights[index];
|
|
706
|
+
return Number.isFinite(weight) && weight > 0 ? weight : 1;
|
|
707
|
+
});
|
|
532
708
|
const total = normalizedWeights.reduce((sum, weight) => sum + weight, 0) || count;
|
|
533
709
|
const ratios = normalizedWeights.map((weight) => weight / total);
|
|
534
710
|
const raw = ratios.map((ratio) => ratio * DEFAULT_GRID_COLUMNS);
|
|
@@ -569,6 +745,294 @@ const toIntSizes = (weights: number[], count: number): number[] => {
|
|
|
569
745
|
return floors;
|
|
570
746
|
};
|
|
571
747
|
|
|
748
|
+
const EMPTY_COLUMN_VALUE = 'EMPTY_COLUMN';
|
|
749
|
+
|
|
750
|
+
const createTopLevelRow = (itemUid: string, id: string): GridRowV2 => ({
|
|
751
|
+
id,
|
|
752
|
+
cells: [
|
|
753
|
+
{
|
|
754
|
+
id: `${id}:cell:0`,
|
|
755
|
+
items: [itemUid],
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
sizes: [DEFAULT_GRID_COLUMNS],
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const convertLegacyRowsToLayout = (
|
|
762
|
+
rows: Record<string, string[][]> = {},
|
|
763
|
+
sizes: Record<string, number[]> = {},
|
|
764
|
+
rowOrder?: string[],
|
|
765
|
+
): GridLayoutV2 => {
|
|
766
|
+
const order = deriveRowOrder(rows, rowOrder);
|
|
767
|
+
return {
|
|
768
|
+
version: 2,
|
|
769
|
+
rows: order
|
|
770
|
+
.map((rowId) => {
|
|
771
|
+
const cells = (rows[rowId] || []).map((items, columnIndex) => ({
|
|
772
|
+
id: `${rowId}:cell:${columnIndex}`,
|
|
773
|
+
items: [...items],
|
|
774
|
+
}));
|
|
775
|
+
return {
|
|
776
|
+
id: rowId,
|
|
777
|
+
cells,
|
|
778
|
+
sizes: toIntSizes(sizes[rowId] || new Array(cells.length).fill(1), cells.length),
|
|
779
|
+
};
|
|
780
|
+
})
|
|
781
|
+
.filter((row) => row.cells.length > 0),
|
|
782
|
+
};
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const collectCellItems = (cell: GridCellV2): string[] => {
|
|
786
|
+
if (Array.isArray(cell.items)) {
|
|
787
|
+
return cell.items;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return (cell.rows || []).flatMap((row) => row.cells.flatMap((childCell) => collectCellItems(childCell)));
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
export const projectLayoutToLegacyRows = (layout: GridLayoutV2): GridLayoutData => {
|
|
794
|
+
const rows: Record<string, string[][]> = {};
|
|
795
|
+
const sizes: Record<string, number[]> = {};
|
|
796
|
+
const rowOrder: string[] = [];
|
|
797
|
+
|
|
798
|
+
const appendRows = (sourceRows: GridRowV2[], prefix = '') => {
|
|
799
|
+
sourceRows.forEach((row) => {
|
|
800
|
+
const rowId = prefix ? `${prefix}/${row.id}` : row.id;
|
|
801
|
+
const cells = row.cells.map((cell) => collectCellItems(cell)).filter((items) => items.length > 0);
|
|
802
|
+
if (cells.length > 0) {
|
|
803
|
+
rows[rowId] = cells;
|
|
804
|
+
sizes[rowId] = toIntSizes(row.sizes || new Array(cells.length).fill(1), cells.length);
|
|
805
|
+
rowOrder.push(rowId);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
appendRows(layout.rows || []);
|
|
811
|
+
return { rows, sizes, rowOrder, layout };
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const normalizeGridRows = (
|
|
815
|
+
rows: GridRowV2[],
|
|
816
|
+
options: {
|
|
817
|
+
validUids?: Set<string>;
|
|
818
|
+
seenUids: Set<string>;
|
|
819
|
+
},
|
|
820
|
+
): GridRowV2[] => {
|
|
821
|
+
return (Array.isArray(rows) ? rows : [])
|
|
822
|
+
.map((row, rowIndex) => {
|
|
823
|
+
const rawCells = Array.isArray(row?.cells) ? row.cells : [];
|
|
824
|
+
const rawSizes = Array.isArray(row?.sizes) ? row.sizes : [];
|
|
825
|
+
const cellsWithSizes = rawCells
|
|
826
|
+
.map((cell, cellIndex) => {
|
|
827
|
+
const id =
|
|
828
|
+
typeof cell?.id === 'string' && cell.id ? cell.id : `${row?.id || `row:${rowIndex}`}:cell:${cellIndex}`;
|
|
829
|
+
const size = rawSizes[cellIndex];
|
|
830
|
+
if (Array.isArray(cell?.rows) && cell.rows.length > 0) {
|
|
831
|
+
const childRows = normalizeGridRows(cell.rows, options);
|
|
832
|
+
if (childRows.length > 0) {
|
|
833
|
+
return { cell: { id, rows: childRows } as GridCellV2, size };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const rawItems = Array.isArray(cell?.items) ? cell.items : undefined;
|
|
838
|
+
const items = (rawItems || [])
|
|
839
|
+
.filter((itemUid) => typeof itemUid === 'string' && itemUid)
|
|
840
|
+
.filter((itemUid) => !options.validUids || options.validUids.has(itemUid) || itemUid === EMPTY_COLUMN_VALUE)
|
|
841
|
+
.filter((itemUid) => {
|
|
842
|
+
if (itemUid === EMPTY_COLUMN_VALUE) {
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
if (options.seenUids.has(itemUid)) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
options.seenUids.add(itemUid);
|
|
849
|
+
return true;
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (rawItems && (items.length > 0 || rawItems.length === 0)) {
|
|
853
|
+
return { cell: { id, items } as GridCellV2, size };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return null;
|
|
857
|
+
})
|
|
858
|
+
.filter(Boolean) as { cell: GridCellV2; size: number }[];
|
|
859
|
+
const cells = cellsWithSizes.map((entry) => entry.cell);
|
|
860
|
+
|
|
861
|
+
if (cells.length === 0) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
id: typeof row?.id === 'string' && row.id ? row.id : `row:${rowIndex}`,
|
|
867
|
+
cells,
|
|
868
|
+
sizes: toIntSizes(
|
|
869
|
+
cellsWithSizes.map((entry) => entry.size),
|
|
870
|
+
cells.length,
|
|
871
|
+
),
|
|
872
|
+
} as GridRowV2;
|
|
873
|
+
})
|
|
874
|
+
.filter(Boolean) as GridRowV2[];
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const collapseCell = (cell: GridCellV2): GridCellV2 | null => {
|
|
878
|
+
if (cell.rows?.length) {
|
|
879
|
+
const rows = collapseRows(cell.rows);
|
|
880
|
+
if (rows.length === 0) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
if (rows.length === 1 && rows[0].cells.length === 1 && rows[0].sizes[0] === DEFAULT_GRID_COLUMNS) {
|
|
884
|
+
return {
|
|
885
|
+
id: cell.id,
|
|
886
|
+
..._.omit(rows[0].cells[0], ['id']),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
return { id: cell.id, rows };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (Array.isArray(cell.items)) {
|
|
893
|
+
return { id: cell.id, items: cell.items.filter(Boolean) };
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const collapseRows = (rows: GridRowV2[]): GridRowV2[] => {
|
|
899
|
+
return rows
|
|
900
|
+
.map((row) => {
|
|
901
|
+
const cellsWithSizes = row.cells
|
|
902
|
+
.map((cell, index) => {
|
|
903
|
+
const collapsed = collapseCell(cell);
|
|
904
|
+
return collapsed ? { cell: collapsed, size: row.sizes?.[index] } : null;
|
|
905
|
+
})
|
|
906
|
+
.filter(Boolean) as { cell: GridCellV2; size: number }[];
|
|
907
|
+
const cells = cellsWithSizes.map((entry) => entry.cell);
|
|
908
|
+
if (cells.length === 0) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
id: row.id,
|
|
913
|
+
cells,
|
|
914
|
+
sizes: toIntSizes(
|
|
915
|
+
cellsWithSizes.map((entry) => entry.size),
|
|
916
|
+
cells.length,
|
|
917
|
+
),
|
|
918
|
+
};
|
|
919
|
+
})
|
|
920
|
+
.filter(Boolean) as GridRowV2[];
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
export const normalizeGridLayout = ({
|
|
924
|
+
layout,
|
|
925
|
+
rows,
|
|
926
|
+
sizes,
|
|
927
|
+
rowOrder,
|
|
928
|
+
itemUids,
|
|
929
|
+
generateId = uid,
|
|
930
|
+
logger,
|
|
931
|
+
gridUid,
|
|
932
|
+
}: {
|
|
933
|
+
layout?: GridLayoutV2 | null;
|
|
934
|
+
rows?: Record<string, string[][]>;
|
|
935
|
+
sizes?: Record<string, number[]>;
|
|
936
|
+
rowOrder?: string[];
|
|
937
|
+
itemUids?: string[];
|
|
938
|
+
generateId?: () => string;
|
|
939
|
+
logger?: Pick<Console, 'warn'>;
|
|
940
|
+
gridUid?: string;
|
|
941
|
+
}): GridLayoutV2 => {
|
|
942
|
+
const validUids = itemUids ? new Set(itemUids) : undefined;
|
|
943
|
+
if (validUids) {
|
|
944
|
+
validUids.add(EMPTY_COLUMN_VALUE);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
const source =
|
|
949
|
+
layout?.version === 2
|
|
950
|
+
? _.cloneDeep(layout)
|
|
951
|
+
: convertLegacyRowsToLayout(rows || {}, sizes || {}, rowOrder || Object.keys(rows || {}));
|
|
952
|
+
const seenUids = new Set<string>();
|
|
953
|
+
const next: GridLayoutV2 = {
|
|
954
|
+
version: 2,
|
|
955
|
+
rows: collapseRows(
|
|
956
|
+
normalizeGridRows(source.rows || [], {
|
|
957
|
+
validUids,
|
|
958
|
+
seenUids,
|
|
959
|
+
}),
|
|
960
|
+
),
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
if (itemUids?.length) {
|
|
964
|
+
itemUids.forEach((itemUid) => {
|
|
965
|
+
if (itemUid === EMPTY_COLUMN_VALUE || seenUids.has(itemUid)) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const rowId = generateId();
|
|
969
|
+
next.rows.push(createTopLevelRow(itemUid, rowId));
|
|
970
|
+
seenUids.add(itemUid);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return next;
|
|
975
|
+
} catch (error) {
|
|
976
|
+
logger?.warn?.(`[GridModel] Failed to normalize grid layout${gridUid ? ` (${gridUid})` : ''}.`, error);
|
|
977
|
+
return {
|
|
978
|
+
version: 2,
|
|
979
|
+
rows: (itemUids || [])
|
|
980
|
+
.filter((itemUid) => itemUid !== EMPTY_COLUMN_VALUE)
|
|
981
|
+
.map((itemUid) => createTopLevelRow(itemUid, generateId())),
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
export const replaceUidInGridLayout = (layout: GridLayoutV2, fromUid: string, toUid: string): GridLayoutV2 => {
|
|
987
|
+
const replaceRows = (rows: GridRowV2[]): GridRowV2[] =>
|
|
988
|
+
rows.map((row) => ({
|
|
989
|
+
...row,
|
|
990
|
+
cells: row.cells.map((cell) => {
|
|
991
|
+
if (cell.rows) {
|
|
992
|
+
return { ...cell, rows: replaceRows(cell.rows) };
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
...cell,
|
|
996
|
+
items: (cell.items || []).map((itemUid) => (itemUid === fromUid ? toUid : itemUid)),
|
|
997
|
+
};
|
|
998
|
+
}),
|
|
999
|
+
}));
|
|
1000
|
+
return { version: 2, rows: replaceRows(_.cloneDeep(layout.rows || [])) };
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
export const findModelUidLayoutPosition = (layout: GridLayoutV2, uidValue: string): GridLayoutPosition | null => {
|
|
1004
|
+
const visitRows = (rows: GridRowV2[], parentPath: GridLayoutPath): GridLayoutPosition | null => {
|
|
1005
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
1006
|
+
const row = rows[rowIndex];
|
|
1007
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
1008
|
+
const cell = row.cells[cellIndex];
|
|
1009
|
+
const path = [...parentPath, { rowId: row.id, cellId: cell.id }];
|
|
1010
|
+
if (cell.items) {
|
|
1011
|
+
const itemIndex = cell.items.indexOf(uidValue);
|
|
1012
|
+
if (itemIndex !== -1) {
|
|
1013
|
+
return {
|
|
1014
|
+
path,
|
|
1015
|
+
rowIndex,
|
|
1016
|
+
cellIndex,
|
|
1017
|
+
itemIndex,
|
|
1018
|
+
itemUid: uidValue,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (cell.rows) {
|
|
1023
|
+
const result = visitRows(cell.rows, path);
|
|
1024
|
+
if (result) {
|
|
1025
|
+
return result;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
return visitRows(layout.rows || [], []);
|
|
1034
|
+
};
|
|
1035
|
+
|
|
572
1036
|
const normalizeRowSizes = (rowId: string, layout: GridLayoutData) => {
|
|
573
1037
|
const columns = layout.rows[rowId];
|
|
574
1038
|
if (!columns || columns.length === 0) {
|
|
@@ -628,11 +1092,251 @@ const distributeSizesWithNewColumn = (
|
|
|
628
1092
|
return toIntSizes(weights, columnCount);
|
|
629
1093
|
};
|
|
630
1094
|
|
|
1095
|
+
const resolveCellPath = (layout: GridLayoutV2, slot: LayoutSlot): GridLayoutPath | undefined => {
|
|
1096
|
+
if ('path' in slot && slot.path?.length) {
|
|
1097
|
+
return slot.path;
|
|
1098
|
+
}
|
|
1099
|
+
if ('rowId' in slot && 'columnIndex' in slot) {
|
|
1100
|
+
return createLegacyCellPath(slot.rowId, slot.columnIndex);
|
|
1101
|
+
}
|
|
1102
|
+
return undefined;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
const findRowListByPath = (layout: GridLayoutV2, path?: GridLayoutPath): GridRowV2[] => {
|
|
1106
|
+
if (!path || path.length <= 1) {
|
|
1107
|
+
return layout.rows;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
let rows = layout.rows;
|
|
1111
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
1112
|
+
const entry = path[i];
|
|
1113
|
+
const row = rows.find((candidate) => candidate.id === entry.rowId);
|
|
1114
|
+
const cell = row?.cells.find((candidate) => candidate.id === entry.cellId);
|
|
1115
|
+
if (!cell?.rows) {
|
|
1116
|
+
return rows;
|
|
1117
|
+
}
|
|
1118
|
+
rows = cell.rows;
|
|
1119
|
+
}
|
|
1120
|
+
return rows;
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
const findCellByPath = (layout: GridLayoutV2, path?: GridLayoutPath) => {
|
|
1124
|
+
if (!path?.length) {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
let rows = layout.rows;
|
|
1129
|
+
for (let i = 0; i < path.length; i += 1) {
|
|
1130
|
+
const entry = path[i];
|
|
1131
|
+
const rowIndex = rows.findIndex((candidate) => candidate.id === entry.rowId);
|
|
1132
|
+
const row = rows[rowIndex];
|
|
1133
|
+
if (!row || !entry.cellId) {
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
const cellIndex = row.cells.findIndex((candidate) => candidate.id === entry.cellId);
|
|
1137
|
+
const cell = row.cells[cellIndex];
|
|
1138
|
+
if (!cell) {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
if (i === path.length - 1) {
|
|
1142
|
+
return { rows, row, cell, rowIndex, cellIndex };
|
|
1143
|
+
}
|
|
1144
|
+
rows = cell.rows || [];
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const removeItemFromGridLayout = (layout: GridLayoutV2, sourceUid: string) => {
|
|
1150
|
+
const removeFromRows = (rows: GridRowV2[]): GridRowV2[] =>
|
|
1151
|
+
rows
|
|
1152
|
+
.map((row) => {
|
|
1153
|
+
const cellsWithSizes = row.cells
|
|
1154
|
+
.map((cell, index) => {
|
|
1155
|
+
if (cell.rows) {
|
|
1156
|
+
const childRows = removeFromRows(cell.rows);
|
|
1157
|
+
return childRows.length ? { cell: { ...cell, rows: childRows }, size: row.sizes?.[index] } : null;
|
|
1158
|
+
}
|
|
1159
|
+
const currentItems = cell.items || [];
|
|
1160
|
+
const hadSourceUid = currentItems.includes(sourceUid);
|
|
1161
|
+
const items = currentItems.filter((itemUid) => itemUid !== sourceUid);
|
|
1162
|
+
if (hadSourceUid && !items.length) {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
return { cell: { ...cell, items }, size: row.sizes?.[index] };
|
|
1166
|
+
})
|
|
1167
|
+
.filter(Boolean) as { cell: GridCellV2; size: number }[];
|
|
1168
|
+
const cells = cellsWithSizes.map((entry) => entry.cell);
|
|
1169
|
+
|
|
1170
|
+
return cells.length
|
|
1171
|
+
? {
|
|
1172
|
+
...row,
|
|
1173
|
+
cells,
|
|
1174
|
+
sizes: toIntSizes(
|
|
1175
|
+
cellsWithSizes.map((entry) => entry.size),
|
|
1176
|
+
cells.length,
|
|
1177
|
+
),
|
|
1178
|
+
}
|
|
1179
|
+
: null;
|
|
1180
|
+
})
|
|
1181
|
+
.filter(Boolean) as GridRowV2[];
|
|
1182
|
+
|
|
1183
|
+
layout.rows = collapseRows(removeFromRows(layout.rows));
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
const createSingleCellRow = (itemUid: string, rowId: string, cellId: string): GridRowV2 => ({
|
|
1187
|
+
id: rowId,
|
|
1188
|
+
cells: [{ id: cellId, items: [itemUid] }],
|
|
1189
|
+
sizes: [DEFAULT_GRID_COLUMNS],
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const getGeneratedId = (
|
|
1193
|
+
key: string,
|
|
1194
|
+
options?: {
|
|
1195
|
+
generatedIds?: Map<string, string>;
|
|
1196
|
+
generateId?: (key: string) => string;
|
|
1197
|
+
},
|
|
1198
|
+
) => {
|
|
1199
|
+
const existing = options?.generatedIds?.get(key);
|
|
1200
|
+
if (existing) {
|
|
1201
|
+
return existing;
|
|
1202
|
+
}
|
|
1203
|
+
const value = options?.generateId?.(key) || uid();
|
|
1204
|
+
options?.generatedIds?.set(key, value);
|
|
1205
|
+
return value;
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const simulateGridLayoutForSlot = ({
|
|
1209
|
+
slot,
|
|
1210
|
+
sourceUid,
|
|
1211
|
+
layout,
|
|
1212
|
+
generatedIds,
|
|
1213
|
+
generateId,
|
|
1214
|
+
}: SimulateLayoutOptions): GridLayoutV2 => {
|
|
1215
|
+
const original = normalizeGridLayout({
|
|
1216
|
+
layout: layout.layout,
|
|
1217
|
+
rows: layout.rows,
|
|
1218
|
+
sizes: layout.sizes,
|
|
1219
|
+
rowOrder: layout.rowOrder,
|
|
1220
|
+
});
|
|
1221
|
+
const cloned = _.cloneDeep(original);
|
|
1222
|
+
const slotKey = getSlotKey(slot);
|
|
1223
|
+
const sourcePosition = findModelUidLayoutPosition(cloned, sourceUid);
|
|
1224
|
+
if (slot.type === 'item-edge' && slot.itemUid === sourceUid) {
|
|
1225
|
+
return cloned;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const targetPath = resolveCellPath(cloned, slot);
|
|
1229
|
+
const targetItemUid = slot.type === 'item-edge' ? slot.itemUid : undefined;
|
|
1230
|
+
removeItemFromGridLayout(cloned, sourceUid);
|
|
1231
|
+
|
|
1232
|
+
switch (slot.type) {
|
|
1233
|
+
case 'column': {
|
|
1234
|
+
const target = findCellByPath(cloned, targetPath);
|
|
1235
|
+
if (!target) {
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
if (target.cell.rows) {
|
|
1239
|
+
target.cell.rows.push(
|
|
1240
|
+
createTopLevelRow(sourceUid, getGeneratedId(`${slotKey}:row`, { generatedIds, generateId })),
|
|
1241
|
+
);
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
target.cell.items ||= [];
|
|
1245
|
+
const insertIndex = Math.max(0, Math.min(slot.insertIndex, target.cell.items.length));
|
|
1246
|
+
target.cell.items.splice(insertIndex, 0, sourceUid);
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
case 'empty-column': {
|
|
1250
|
+
const target = findCellByPath(cloned, targetPath);
|
|
1251
|
+
if (target) {
|
|
1252
|
+
delete target.cell.rows;
|
|
1253
|
+
target.cell.items = [sourceUid];
|
|
1254
|
+
}
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
case 'column-edge': {
|
|
1258
|
+
const target = findCellByPath(cloned, targetPath);
|
|
1259
|
+
if (!target) {
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
const insertIndex = slot.direction === 'left' ? target.cellIndex : target.cellIndex + 1;
|
|
1263
|
+
const cellId = getGeneratedId(`${slotKey}:cell`, { generatedIds, generateId });
|
|
1264
|
+
target.row.cells.splice(insertIndex, 0, { id: cellId, items: [sourceUid] });
|
|
1265
|
+
target.row.sizes = distributeSizesWithNewColumn(target.row.sizes, insertIndex, target.row.cells.length);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case 'row-gap': {
|
|
1269
|
+
const rows = findRowListByPath(cloned, slot.path);
|
|
1270
|
+
const targetIndex = rows.findIndex((row) => row.id === slot.targetRowId);
|
|
1271
|
+
const insertIndex = targetIndex === -1 ? rows.length : slot.position === 'above' ? targetIndex : targetIndex + 1;
|
|
1272
|
+
const rowId = getGeneratedId(`${slotKey}:row`, { generatedIds, generateId });
|
|
1273
|
+
rows.splice(insertIndex, 0, createSingleCellRow(sourceUid, rowId, `${rowId}:cell:0`));
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case 'empty-row': {
|
|
1277
|
+
const rowId = getGeneratedId(`${slotKey}:row`, { generatedIds, generateId });
|
|
1278
|
+
cloned.rows.push(createSingleCellRow(sourceUid, rowId, `${rowId}:cell:0`));
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
case 'item-edge': {
|
|
1282
|
+
if (!targetItemUid) {
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
const target = findCellByPath(cloned, targetPath);
|
|
1286
|
+
if (!target?.cell.items) {
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
const targetIndex = target.cell.items.indexOf(targetItemUid);
|
|
1290
|
+
if (targetIndex === -1) {
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const rows: GridRowV2[] = [];
|
|
1295
|
+
target.cell.items.forEach((itemUid, index) => {
|
|
1296
|
+
if (index === targetIndex) {
|
|
1297
|
+
const rowId = getGeneratedId(`${slotKey}:target-row`, { generatedIds, generateId });
|
|
1298
|
+
const leftItem = slot.direction === 'left' ? sourceUid : itemUid;
|
|
1299
|
+
const rightItem = slot.direction === 'left' ? itemUid : sourceUid;
|
|
1300
|
+
rows.push({
|
|
1301
|
+
id: rowId,
|
|
1302
|
+
cells: [
|
|
1303
|
+
{ id: getGeneratedId(`${slotKey}:target-cell:0`, { generatedIds, generateId }), items: [leftItem] },
|
|
1304
|
+
{ id: getGeneratedId(`${slotKey}:target-cell:1`, { generatedIds, generateId }), items: [rightItem] },
|
|
1305
|
+
],
|
|
1306
|
+
sizes: [12, 12],
|
|
1307
|
+
});
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const rowId = getGeneratedId(`${slotKey}:row:${index}`, { generatedIds, generateId });
|
|
1312
|
+
rows.push(createSingleCellRow(itemUid, rowId, `${rowId}:cell:0`));
|
|
1313
|
+
});
|
|
1314
|
+
delete target.cell.items;
|
|
1315
|
+
target.cell.rows = rows;
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
default:
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const normalized = normalizeGridLayout({ layout: cloned });
|
|
1323
|
+
if (sourcePosition && isSameGridLayout(normalized, original)) {
|
|
1324
|
+
return original;
|
|
1325
|
+
}
|
|
1326
|
+
return normalized;
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
export const isSameGridLayout = (a: GridLayoutV2, b: GridLayoutV2): boolean => {
|
|
1330
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1331
|
+
};
|
|
1332
|
+
|
|
631
1333
|
export interface SimulateLayoutOptions {
|
|
632
1334
|
slot: LayoutSlot;
|
|
633
1335
|
sourceUid: string;
|
|
634
1336
|
layout: GridLayoutData;
|
|
635
1337
|
generateRowId?: () => string;
|
|
1338
|
+
generatedIds?: Map<string, string>;
|
|
1339
|
+
generateId?: (key: string) => string;
|
|
636
1340
|
}
|
|
637
1341
|
|
|
638
1342
|
export const simulateLayoutForSlot = ({
|
|
@@ -640,7 +1344,21 @@ export const simulateLayoutForSlot = ({
|
|
|
640
1344
|
sourceUid,
|
|
641
1345
|
layout,
|
|
642
1346
|
generateRowId,
|
|
1347
|
+
generatedIds,
|
|
1348
|
+
generateId,
|
|
643
1349
|
}: SimulateLayoutOptions): GridLayoutData => {
|
|
1350
|
+
if (layout.layout || slot.type === 'item-edge' || ('path' in slot && slot.path?.length)) {
|
|
1351
|
+
const nextLayout = simulateGridLayoutForSlot({
|
|
1352
|
+
slot,
|
|
1353
|
+
sourceUid,
|
|
1354
|
+
layout,
|
|
1355
|
+
generateRowId,
|
|
1356
|
+
generatedIds,
|
|
1357
|
+
generateId,
|
|
1358
|
+
});
|
|
1359
|
+
return projectLayoutToLegacyRows(nextLayout);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
644
1362
|
const cloned: GridLayoutData = {
|
|
645
1363
|
rows: _.cloneDeep(layout.rows),
|
|
646
1364
|
sizes: _.cloneDeep(layout.sizes),
|