@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.40
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/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +613 -21
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -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/LazyDropdown.js +96 -39
- 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 +9 -3
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +75 -0
- package/lib/data-source/index.js +247 -5
- package/lib/executor/FlowExecutor.js +32 -9
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +3 -0
- package/lib/flowContext.js +43 -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/index.d.ts +2 -0
- package/lib/index.js +7 -0
- 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 +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- 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/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.js +6 -2
- 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 +5 -11
- package/lib/views/usePage.js +302 -144
- 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 +82 -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/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- 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 +558 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +758 -19
- package/src/components/dnd/index.tsx +305 -28
- 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/LazyDropdown.tsx +107 -43
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +319 -36
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +7 -1
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +68 -1
- package/src/data-source/index.ts +304 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +47 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- 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 +47 -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 +62 -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 +5 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/randomId.ts +48 -0
- 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 +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +6 -2
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- 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 +365 -179
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import { render, cleanup, waitFor } from '@testing-library/react';
|
|
12
|
+
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
|
13
13
|
import { App, ConfigProvider } from 'antd';
|
|
14
14
|
import { FlowEngine } from '../../flowEngine';
|
|
15
15
|
import { FlowModel, ModelRenderMode } from '../../models/flowModel';
|
|
@@ -94,6 +94,16 @@ const clickDeleteFromLastDropdown = async () => {
|
|
|
94
94
|
menu.onClick?.({ key: 'delete' });
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
const getHost = (element: HTMLElement) => element.closest('[data-has-float-menu="true"]') as HTMLDivElement;
|
|
98
|
+
|
|
99
|
+
const hoverHostAndClickDelete = async (element: HTMLElement) => {
|
|
100
|
+
const host = getHost(element);
|
|
101
|
+
if (host) {
|
|
102
|
+
fireEvent.mouseEnter(host);
|
|
103
|
+
}
|
|
104
|
+
await clickDeleteFromLastDropdown();
|
|
105
|
+
};
|
|
106
|
+
|
|
97
107
|
// ---------------- Tests ----------------
|
|
98
108
|
describe('Delete problematic model via FlowSettings menu', () => {
|
|
99
109
|
beforeEach(() => {
|
|
@@ -114,13 +124,13 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
const engine = new FlowEngine();
|
|
117
|
-
engine.flowSettings.forceEnable();
|
|
127
|
+
await engine.flowSettings.forceEnable();
|
|
118
128
|
engine.registerModels({ BrokenModel });
|
|
119
129
|
const model = engine.createModel({ use: 'BrokenModel', uid: 'broken-top-2' }) as BrokenModel;
|
|
120
130
|
// satisfy FlowsFloatContextMenu styles
|
|
121
131
|
model.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
122
132
|
|
|
123
|
-
render(
|
|
133
|
+
const { findByTestId } = render(
|
|
124
134
|
<ConfigProvider>
|
|
125
135
|
<App>
|
|
126
136
|
<FlowEngineProvider engine={engine}>
|
|
@@ -130,7 +140,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
130
140
|
</ConfigProvider>,
|
|
131
141
|
);
|
|
132
142
|
|
|
133
|
-
await
|
|
143
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
134
144
|
expect(engine.getModel(model.uid)).toBeUndefined();
|
|
135
145
|
});
|
|
136
146
|
|
|
@@ -154,7 +164,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
const engine = new FlowEngine();
|
|
157
|
-
engine.flowSettings.forceEnable();
|
|
167
|
+
await engine.flowSettings.forceEnable();
|
|
158
168
|
engine.registerModels({ ParentModel, BrokenChild });
|
|
159
169
|
const parent = engine.createModel({ use: 'ParentModel', uid: 'parent-3' }) as ParentModel;
|
|
160
170
|
const child = engine.createModel({ use: 'BrokenChild', uid: 'child-3' }) as BrokenChild;
|
|
@@ -163,7 +173,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
163
173
|
parent.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
164
174
|
child.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
165
175
|
|
|
166
|
-
render(
|
|
176
|
+
const { findByTestId } = render(
|
|
167
177
|
<ConfigProvider>
|
|
168
178
|
<App>
|
|
169
179
|
<FlowEngineProvider engine={engine}>
|
|
@@ -173,7 +183,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
173
183
|
</ConfigProvider>,
|
|
174
184
|
);
|
|
175
185
|
|
|
176
|
-
await
|
|
186
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
177
187
|
expect(engine.getModel(child.uid)).toBeUndefined();
|
|
178
188
|
const remain = (parent.subModels as any).items || [];
|
|
179
189
|
expect(remain.find((m: FlowModel) => m.uid === child.uid)).toBeUndefined();
|
|
@@ -200,7 +210,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
200
210
|
}
|
|
201
211
|
|
|
202
212
|
const engine = new FlowEngine();
|
|
203
|
-
engine.flowSettings.forceEnable();
|
|
213
|
+
await engine.flowSettings.forceEnable();
|
|
204
214
|
engine.registerModels({ ParentModel, RenderFnChild });
|
|
205
215
|
const parent = engine.createModel({ use: 'ParentModel', uid: 'parent-4' }) as ParentModel;
|
|
206
216
|
const child = engine.createModel({ use: 'RenderFnChild', uid: 'cell-4' }) as RenderFnChild;
|
|
@@ -208,7 +218,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
208
218
|
parent.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
209
219
|
child.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
210
220
|
|
|
211
|
-
render(
|
|
221
|
+
const { findByTestId } = render(
|
|
212
222
|
<ConfigProvider>
|
|
213
223
|
<App>
|
|
214
224
|
<FlowEngineProvider engine={engine}>
|
|
@@ -218,7 +228,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
218
228
|
</ConfigProvider>,
|
|
219
229
|
);
|
|
220
230
|
|
|
221
|
-
await
|
|
231
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
222
232
|
expect(engine.getModel(child.uid)).toBeUndefined();
|
|
223
233
|
const remain = (parent.subModels as any).cells || [];
|
|
224
234
|
expect(remain.find((m: FlowModel) => m.uid === child.uid)).toBeUndefined();
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
getSlotKey,
|
|
16
16
|
resolveDropIntent,
|
|
17
17
|
Point,
|
|
18
|
+
buildLayoutSnapshot,
|
|
19
|
+
normalizeGridLayout,
|
|
20
|
+
projectLayoutToLegacyRows,
|
|
21
|
+
replaceUidInGridLayout,
|
|
18
22
|
} from '../dnd/gridDragPlanner';
|
|
19
23
|
|
|
20
24
|
const rect = { top: 0, left: 0, width: 100, height: 100 };
|
|
@@ -29,6 +33,183 @@ const createLayout = (
|
|
|
29
33
|
rowOrder,
|
|
30
34
|
});
|
|
31
35
|
|
|
36
|
+
const createDomRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
|
|
37
|
+
return {
|
|
38
|
+
top,
|
|
39
|
+
left,
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
right: left + width,
|
|
43
|
+
bottom: top + height,
|
|
44
|
+
x: left,
|
|
45
|
+
y: top,
|
|
46
|
+
toJSON: () => ({}),
|
|
47
|
+
} as DOMRect;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mockRect = (
|
|
51
|
+
element: Element,
|
|
52
|
+
rect: {
|
|
53
|
+
top: number;
|
|
54
|
+
left: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
},
|
|
58
|
+
) => {
|
|
59
|
+
Object.defineProperty(element, 'getBoundingClientRect', {
|
|
60
|
+
configurable: true,
|
|
61
|
+
value: () => createDomRect(rect),
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe('buildLayoutSnapshot', () => {
|
|
66
|
+
it('should ignore nested grid columns/items even when rowId is duplicated', () => {
|
|
67
|
+
const container = document.createElement('div');
|
|
68
|
+
const row = document.createElement('div');
|
|
69
|
+
row.setAttribute('data-grid-row-id', 'row-1');
|
|
70
|
+
container.appendChild(row);
|
|
71
|
+
|
|
72
|
+
const column = document.createElement('div');
|
|
73
|
+
column.setAttribute('data-grid-column-row-id', 'row-1');
|
|
74
|
+
column.setAttribute('data-grid-column-index', '0');
|
|
75
|
+
row.appendChild(column);
|
|
76
|
+
|
|
77
|
+
const item = document.createElement('div');
|
|
78
|
+
item.setAttribute('data-grid-item-row-id', 'row-1');
|
|
79
|
+
item.setAttribute('data-grid-column-index', '0');
|
|
80
|
+
item.setAttribute('data-grid-item-index', '0');
|
|
81
|
+
column.appendChild(item);
|
|
82
|
+
|
|
83
|
+
// 在外层 item 内构建一个嵌套 grid,并复用相同 rowId/columnIndex
|
|
84
|
+
const nestedRow = document.createElement('div');
|
|
85
|
+
nestedRow.setAttribute('data-grid-row-id', 'row-1');
|
|
86
|
+
item.appendChild(nestedRow);
|
|
87
|
+
|
|
88
|
+
const nestedColumn = document.createElement('div');
|
|
89
|
+
nestedColumn.setAttribute('data-grid-column-row-id', 'row-1');
|
|
90
|
+
nestedColumn.setAttribute('data-grid-column-index', '0');
|
|
91
|
+
nestedRow.appendChild(nestedColumn);
|
|
92
|
+
|
|
93
|
+
const nestedItem = document.createElement('div');
|
|
94
|
+
nestedItem.setAttribute('data-grid-item-row-id', 'row-1');
|
|
95
|
+
nestedItem.setAttribute('data-grid-column-index', '0');
|
|
96
|
+
nestedItem.setAttribute('data-grid-item-index', '0');
|
|
97
|
+
nestedColumn.appendChild(nestedItem);
|
|
98
|
+
|
|
99
|
+
mockRect(container, { top: 0, left: 0, width: 600, height: 600 });
|
|
100
|
+
mockRect(row, { top: 10, left: 10, width: 320, height: 120 });
|
|
101
|
+
mockRect(column, { top: 10, left: 10, width: 320, height: 120 });
|
|
102
|
+
mockRect(item, { top: 20, left: 20, width: 300, height: 80 });
|
|
103
|
+
|
|
104
|
+
// 嵌套 grid 给一个明显偏离的位置,用于判断是否被错误命中
|
|
105
|
+
mockRect(nestedRow, { top: 360, left: 360, width: 200, height: 120 });
|
|
106
|
+
mockRect(nestedColumn, { top: 360, left: 360, width: 200, height: 120 });
|
|
107
|
+
mockRect(nestedItem, { top: 370, left: 370, width: 180, height: 90 });
|
|
108
|
+
|
|
109
|
+
const snapshot = buildLayoutSnapshot({ container });
|
|
110
|
+
const columnEdgeSlots = snapshot.slots.filter((slot) => slot.type === 'column-edge');
|
|
111
|
+
const columnSlots = snapshot.slots.filter((slot) => slot.type === 'column');
|
|
112
|
+
const itemEdgeSlots = snapshot.slots.filter((slot) => slot.type === 'item-edge');
|
|
113
|
+
|
|
114
|
+
// 外层单行单列单项应只有 8 个 slot:上/下 row-gap + 左/右 column-edge + before/after column + 左/右 item-edge
|
|
115
|
+
expect(snapshot.slots).toHaveLength(8);
|
|
116
|
+
expect(columnEdgeSlots).toHaveLength(2);
|
|
117
|
+
expect(columnSlots).toHaveLength(2);
|
|
118
|
+
expect(itemEdgeSlots).toHaveLength(2);
|
|
119
|
+
|
|
120
|
+
// 不应混入嵌套 grid(其 top >= 360)
|
|
121
|
+
expect(snapshot.slots.every((slot) => slot.rect.top < 300)).toBe(true);
|
|
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
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
32
213
|
describe('getSlotKey', () => {
|
|
33
214
|
it('should generate unique key for column slot', () => {
|
|
34
215
|
const slot: LayoutSlot = {
|
|
@@ -90,6 +271,184 @@ describe('getSlotKey', () => {
|
|
|
90
271
|
const key = getSlotKey(slot);
|
|
91
272
|
expect(key).toBe('empty-column:row1:0');
|
|
92
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
|
+
});
|
|
93
452
|
});
|
|
94
453
|
|
|
95
454
|
describe('resolveDropIntent', () => {
|
|
@@ -143,7 +502,7 @@ describe('resolveDropIntent', () => {
|
|
|
143
502
|
expect(result).toEqual(targetSlot);
|
|
144
503
|
});
|
|
145
504
|
|
|
146
|
-
it('should return
|
|
505
|
+
it('should return highest-priority matching slot when multiple slots contain the point', () => {
|
|
147
506
|
const firstSlot: LayoutSlot = {
|
|
148
507
|
type: 'empty-row',
|
|
149
508
|
rect: { top: 0, left: 0, width: 500, height: 500 },
|
|
@@ -162,8 +521,7 @@ describe('resolveDropIntent', () => {
|
|
|
162
521
|
|
|
163
522
|
const point: Point = { x: 125, y: 110 };
|
|
164
523
|
const result = resolveDropIntent(point, slots);
|
|
165
|
-
|
|
166
|
-
expect(result).toEqual(firstSlot);
|
|
524
|
+
expect(result).toEqual(secondSlot);
|
|
167
525
|
});
|
|
168
526
|
|
|
169
527
|
it('should handle empty-column slot correctly', () => {
|
|
@@ -396,6 +754,158 @@ describe('simulateLayoutForSlot', () => {
|
|
|
396
754
|
expect(result.rows.rowA[2]).toEqual(['c']);
|
|
397
755
|
});
|
|
398
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('keeps nested column insertion target when removing a sibling collapses the original path', () => {
|
|
833
|
+
const layout = createLayout(
|
|
834
|
+
{
|
|
835
|
+
vyvfw2jw071: [['6ad3ccaabd5', 'ff8b4b57f65']],
|
|
836
|
+
ablhoqw51gb: [['21b422021b8']],
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
vyvfw2jw071: [24],
|
|
840
|
+
ablhoqw51gb: [24],
|
|
841
|
+
},
|
|
842
|
+
['vyvfw2jw071', 'ablhoqw51gb'],
|
|
843
|
+
);
|
|
844
|
+
layout.layout = normalizeGridLayout({
|
|
845
|
+
rows: layout.rows,
|
|
846
|
+
sizes: layout.sizes,
|
|
847
|
+
rowOrder: layout.rowOrder,
|
|
848
|
+
itemUids: ['6ad3ccaabd5', 'ff8b4b57f65', '21b422021b8'],
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
const slot: LayoutSlot = {
|
|
852
|
+
type: 'column',
|
|
853
|
+
rowId: 'll5vo5pzj3u',
|
|
854
|
+
columnIndex: 0,
|
|
855
|
+
insertIndex: 1,
|
|
856
|
+
position: 'after',
|
|
857
|
+
path: [
|
|
858
|
+
{ rowId: 'vyvfw2jw071', cellId: 'vyvfw2jw071:cell:0' },
|
|
859
|
+
{ rowId: 'll5vo5pzj3u', cellId: 'ghy612j5zzg' },
|
|
860
|
+
],
|
|
861
|
+
rect,
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'ff8b4b57f65', layout });
|
|
865
|
+
|
|
866
|
+
expect(result.layout!.rows).toMatchObject([
|
|
867
|
+
{
|
|
868
|
+
id: 'vyvfw2jw071',
|
|
869
|
+
cells: [{ items: ['6ad3ccaabd5', 'ff8b4b57f65'] }],
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
id: 'ablhoqw51gb',
|
|
873
|
+
cells: [{ items: ['21b422021b8'] }],
|
|
874
|
+
},
|
|
875
|
+
]);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it('treats dragging an item to its own item-edge as no-op', () => {
|
|
879
|
+
const layout = createLayout(
|
|
880
|
+
{
|
|
881
|
+
rowA: [['two']],
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
rowA: [24],
|
|
885
|
+
},
|
|
886
|
+
);
|
|
887
|
+
layout.layout = normalizeGridLayout({
|
|
888
|
+
rows: layout.rows,
|
|
889
|
+
sizes: layout.sizes,
|
|
890
|
+
rowOrder: ['rowA'],
|
|
891
|
+
itemUids: ['two'],
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const slot: LayoutSlot = {
|
|
895
|
+
type: 'item-edge',
|
|
896
|
+
rowId: 'rowA',
|
|
897
|
+
columnIndex: 0,
|
|
898
|
+
itemIndex: 0,
|
|
899
|
+
itemUid: 'two',
|
|
900
|
+
direction: 'right',
|
|
901
|
+
path: [{ rowId: 'rowA', cellId: 'rowA:cell:0' }],
|
|
902
|
+
rect,
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'two', layout });
|
|
906
|
+
expect(result.layout).toEqual(layout.layout);
|
|
907
|
+
});
|
|
908
|
+
|
|
399
909
|
it('handles row-gap slot below position', () => {
|
|
400
910
|
const layout = createLayout(
|
|
401
911
|
{
|
|
@@ -631,4 +1141,49 @@ describe('simulateLayoutForSlot', () => {
|
|
|
631
1141
|
const total = result.sizes.rowA.reduce((sum, size) => sum + size, 0);
|
|
632
1142
|
expect(total).toBe(24);
|
|
633
1143
|
});
|
|
1144
|
+
|
|
1145
|
+
it('preserves explicit empty v2 cells when moving another item', () => {
|
|
1146
|
+
const layout: GridLayoutData = {
|
|
1147
|
+
rows: {},
|
|
1148
|
+
sizes: {},
|
|
1149
|
+
layout: {
|
|
1150
|
+
version: 2,
|
|
1151
|
+
rows: [
|
|
1152
|
+
{
|
|
1153
|
+
id: 'rowA',
|
|
1154
|
+
cells: [
|
|
1155
|
+
{ id: 'empty-cell', items: [] },
|
|
1156
|
+
{ id: 'source-cell', items: ['source'] },
|
|
1157
|
+
{ id: 'target-cell', items: ['target'] },
|
|
1158
|
+
],
|
|
1159
|
+
sizes: [8, 8, 8],
|
|
1160
|
+
},
|
|
1161
|
+
],
|
|
1162
|
+
},
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const slot: LayoutSlot = {
|
|
1166
|
+
type: 'column-edge',
|
|
1167
|
+
rowId: 'rowA',
|
|
1168
|
+
columnIndex: 2,
|
|
1169
|
+
direction: 'right',
|
|
1170
|
+
rect,
|
|
1171
|
+
path: [{ rowId: 'rowA', cellId: 'target-cell' }],
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const result = simulateLayoutForSlot({
|
|
1175
|
+
slot,
|
|
1176
|
+
sourceUid: 'source',
|
|
1177
|
+
layout,
|
|
1178
|
+
generateId: (key) => key,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
expect(result.layout?.rows[0].cells).toEqual([
|
|
1182
|
+
{ id: 'empty-cell', items: [] },
|
|
1183
|
+
{ id: 'target-cell', items: ['target'] },
|
|
1184
|
+
{ id: 'column-edge:rowA:2:right:cell', items: ['source'] },
|
|
1185
|
+
]);
|
|
1186
|
+
expect(result.layout?.rows[0].sizes).toHaveLength(3);
|
|
1187
|
+
expect(result.layout?.rows[0].sizes.reduce((sum, size) => sum + size, 0)).toBe(24);
|
|
1188
|
+
});
|
|
634
1189
|
});
|