@nocobase/flow-engine 2.1.0-alpha.3 → 2.1.0-alpha.30
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/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +601 -21
- package/lib/components/dnd/index.d.ts +19 -1
- package/lib/components/dnd/index.js +243 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +68 -10
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- 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/components/subModel/utils.js +2 -2
- package/lib/data-source/index.d.ts +73 -0
- package/lib/data-source/index.js +211 -1
- package/lib/executor/FlowExecutor.js +31 -8
- package/lib/flowContext.d.ts +2 -0
- package/lib/flowContext.js +31 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- 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 +13 -10
- package/lib/models/flowModel.js +78 -18
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- package/lib/types.d.ts +47 -1
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +2 -2
- package/lib/utils/index.js +4 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +20 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +20 -3
- package/lib/views/usePage.d.ts +2 -1
- package/lib/views/usePage.js +10 -3
- package/package.json +6 -5
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +65 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +512 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +743 -19
- package/src/components/dnd/index.tsx +291 -27
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +189 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +1 -1
- package/src/data-source/__tests__/index.test.ts +34 -1
- package/src/data-source/index.ts +258 -2
- package/src/executor/FlowExecutor.ts +34 -9
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flowContext.ts +37 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/__tests__/flowModel.test.ts +19 -3
- package/src/models/flowModel.tsx +119 -33
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +60 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +2 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +11 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +25 -3
- package/src/views/useDrawer.tsx +25 -3
- package/src/views/usePage.tsx +12 -3
|
@@ -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,25 +408,47 @@ 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
|
-
|
|
336
|
-
)
|
|
447
|
+
layoutContainer.querySelectorAll(`[data-grid-column-row-id="${rowId}"][data-grid-column-index]`),
|
|
448
|
+
).filter((el) => {
|
|
449
|
+
// 只保留当前 row 下的直接列,避免嵌套 Grid 中相同 rowId 的列混入
|
|
450
|
+
return (el as HTMLElement).closest('[data-grid-row-id]') === rowElement;
|
|
451
|
+
}) as HTMLElement[];
|
|
337
452
|
|
|
338
453
|
const sortedColumns = columnElements.sort((a, b) => {
|
|
339
454
|
const indexA = Number(a.dataset.gridColumnIndex || 0);
|
|
@@ -344,6 +459,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
344
459
|
sortedColumns.forEach((columnElement) => {
|
|
345
460
|
const columnIndex = Number(columnElement.dataset.gridColumnIndex || 0);
|
|
346
461
|
const columnRect = toRect(columnElement.getBoundingClientRect());
|
|
462
|
+
const columnPath = parseLayoutPath(columnElement.dataset.gridPath) || createLegacyCellPath(rowId, columnIndex);
|
|
347
463
|
|
|
348
464
|
slots.push({
|
|
349
465
|
type: 'column-edge',
|
|
@@ -351,6 +467,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
351
467
|
columnIndex,
|
|
352
468
|
direction: 'left',
|
|
353
469
|
rect: createColumnEdgeRect(columnRect, 'left'),
|
|
470
|
+
path: columnPath,
|
|
354
471
|
});
|
|
355
472
|
|
|
356
473
|
slots.push({
|
|
@@ -359,11 +476,15 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
359
476
|
columnIndex,
|
|
360
477
|
direction: 'right',
|
|
361
478
|
rect: createColumnEdgeRect(columnRect, 'right'),
|
|
479
|
+
path: columnPath,
|
|
362
480
|
});
|
|
363
481
|
|
|
364
482
|
const itemElements = Array.from(
|
|
365
483
|
columnElement.querySelectorAll(`[data-grid-item-row-id="${rowId}"][data-grid-column-index="${columnIndex}"]`),
|
|
366
|
-
)
|
|
484
|
+
).filter((el) => {
|
|
485
|
+
// 只保留当前 column 下的直接 item,避免命中更深层嵌套 column 的 item
|
|
486
|
+
return (el as HTMLElement).closest('[data-grid-column-row-id][data-grid-column-index]') === columnElement;
|
|
487
|
+
}) as HTMLElement[];
|
|
367
488
|
|
|
368
489
|
const sortedItems = itemElements.sort((a, b) => {
|
|
369
490
|
const indexA = Number(a.dataset.gridItemIndex || 0);
|
|
@@ -372,11 +493,16 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
372
493
|
});
|
|
373
494
|
|
|
374
495
|
if (sortedItems.length === 0) {
|
|
496
|
+
if (hasDirectNestedRows(columnElement)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
375
500
|
slots.push({
|
|
376
501
|
type: 'empty-column',
|
|
377
502
|
rowId,
|
|
378
503
|
columnIndex,
|
|
379
504
|
rect: expandColumnRect(columnRect),
|
|
505
|
+
path: columnPath,
|
|
380
506
|
});
|
|
381
507
|
return;
|
|
382
508
|
}
|
|
@@ -389,10 +515,35 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
389
515
|
insertIndex: 0,
|
|
390
516
|
position: 'before',
|
|
391
517
|
rect: createColumnInsertRect(firstItemRect, 'before'),
|
|
518
|
+
path: columnPath,
|
|
392
519
|
});
|
|
393
520
|
|
|
394
521
|
sortedItems.forEach((itemElement, itemIndex) => {
|
|
395
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
|
+
|
|
396
547
|
slots.push({
|
|
397
548
|
type: 'column',
|
|
398
549
|
rowId,
|
|
@@ -400,6 +551,7 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
400
551
|
insertIndex: itemIndex + 1,
|
|
401
552
|
position: 'after',
|
|
402
553
|
rect: createColumnInsertRect(itemRect, 'after'),
|
|
554
|
+
path: itemPath,
|
|
403
555
|
});
|
|
404
556
|
});
|
|
405
557
|
});
|
|
@@ -408,7 +560,8 @@ export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions):
|
|
|
408
560
|
type: 'row-gap',
|
|
409
561
|
targetRowId: rowId,
|
|
410
562
|
position: 'below',
|
|
411
|
-
rect: createRowGapRect(rowRect, 'below',
|
|
563
|
+
rect: createRowGapRect(rowRect, 'below', rowScopeRect),
|
|
564
|
+
path: rowPath,
|
|
412
565
|
});
|
|
413
566
|
});
|
|
414
567
|
|
|
@@ -430,6 +583,8 @@ export const getSlotKey = (slot: LayoutSlot): string => {
|
|
|
430
583
|
return `${slot.type}`;
|
|
431
584
|
case 'empty-column':
|
|
432
585
|
return `${slot.type}:${slot.rowId}:${slot.columnIndex}`;
|
|
586
|
+
case 'item-edge':
|
|
587
|
+
return `${slot.type}:${slot.rowId}:${slot.columnIndex}:${slot.itemUid}:${slot.direction}`;
|
|
433
588
|
}
|
|
434
589
|
};
|
|
435
590
|
|
|
@@ -448,21 +603,45 @@ const distanceToRect = (point: Point, rect: Rect): number => {
|
|
|
448
603
|
return Math.sqrt(dx * dx + dy * dy);
|
|
449
604
|
};
|
|
450
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
|
+
|
|
451
615
|
export const resolveDropIntent = (point: Point, slots: LayoutSlot[]): LayoutSlot | null => {
|
|
452
616
|
if (!slots.length) {
|
|
453
617
|
return null;
|
|
454
618
|
}
|
|
455
619
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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;
|
|
459
635
|
}
|
|
460
636
|
|
|
461
637
|
let closest: LayoutSlot | null = null;
|
|
462
638
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
463
639
|
slots.forEach((slot) => {
|
|
464
640
|
const distance = distanceToRect(point, slot.rect);
|
|
465
|
-
if (
|
|
641
|
+
if (
|
|
642
|
+
distance < minDistance ||
|
|
643
|
+
(distance === minDistance && closest && slotPriority[slot.type] > slotPriority[closest.type])
|
|
644
|
+
) {
|
|
466
645
|
minDistance = distance;
|
|
467
646
|
closest = slot;
|
|
468
647
|
}
|
|
@@ -522,7 +701,10 @@ const toIntSizes = (weights: number[], count: number): number[] => {
|
|
|
522
701
|
return [];
|
|
523
702
|
}
|
|
524
703
|
|
|
525
|
-
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
|
+
});
|
|
526
708
|
const total = normalizedWeights.reduce((sum, weight) => sum + weight, 0) || count;
|
|
527
709
|
const ratios = normalizedWeights.map((weight) => weight / total);
|
|
528
710
|
const raw = ratios.map((ratio) => ratio * DEFAULT_GRID_COLUMNS);
|
|
@@ -563,6 +745,294 @@ const toIntSizes = (weights: number[], count: number): number[] => {
|
|
|
563
745
|
return floors;
|
|
564
746
|
};
|
|
565
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
|
+
|
|
566
1036
|
const normalizeRowSizes = (rowId: string, layout: GridLayoutData) => {
|
|
567
1037
|
const columns = layout.rows[rowId];
|
|
568
1038
|
if (!columns || columns.length === 0) {
|
|
@@ -622,11 +1092,251 @@ const distributeSizesWithNewColumn = (
|
|
|
622
1092
|
return toIntSizes(weights, columnCount);
|
|
623
1093
|
};
|
|
624
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
|
+
|
|
625
1333
|
export interface SimulateLayoutOptions {
|
|
626
1334
|
slot: LayoutSlot;
|
|
627
1335
|
sourceUid: string;
|
|
628
1336
|
layout: GridLayoutData;
|
|
629
1337
|
generateRowId?: () => string;
|
|
1338
|
+
generatedIds?: Map<string, string>;
|
|
1339
|
+
generateId?: (key: string) => string;
|
|
630
1340
|
}
|
|
631
1341
|
|
|
632
1342
|
export const simulateLayoutForSlot = ({
|
|
@@ -634,7 +1344,21 @@ export const simulateLayoutForSlot = ({
|
|
|
634
1344
|
sourceUid,
|
|
635
1345
|
layout,
|
|
636
1346
|
generateRowId,
|
|
1347
|
+
generatedIds,
|
|
1348
|
+
generateId,
|
|
637
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
|
+
|
|
638
1362
|
const cloned: GridLayoutData = {
|
|
639
1363
|
rows: _.cloneDeep(layout.rows),
|
|
640
1364
|
sizes: _.cloneDeep(layout.sizes),
|