@nocobase/flow-engine 2.0.0-alpha.63 → 2.0.0-alpha.65
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/acl/Acl.js +13 -3
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +53 -1
- package/lib/data-source/index.js +3 -0
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +156 -22
- package/lib/flowContext.js +97 -0
- package/lib/provider.js +2 -1
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +21 -21
- package/lib/types.d.ts +15 -0
- package/lib/views/useDialog.js +1 -1
- package/package.json +4 -4
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/acl/Acl.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
- package/src/components/dnd/gridDragPlanner.ts +60 -0
- package/src/data-source/index.ts +3 -0
- package/src/executor/FlowExecutor.ts +193 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +128 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +356 -0
- package/src/models/__tests__/flowModel.test.ts +16 -0
- package/src/provider.tsx +2 -1
- package/src/scheduler/ModelOperationScheduler.ts +23 -21
- package/src/types.ts +26 -1
- package/src/views/useDialog.tsx +1 -1
|
@@ -125,32 +125,32 @@ const _ModelOperationScheduler = class _ModelOperationScheduler {
|
|
|
125
125
|
bindEngineLifecycle() {
|
|
126
126
|
const emitter = this.engine.emitter;
|
|
127
127
|
if (!emitter || typeof emitter.on !== "function") return;
|
|
128
|
-
const onCreated = /* @__PURE__ */ __name((e) => {
|
|
129
|
-
this.processLifecycleEvent(e.uid, { ...e, type: "created" });
|
|
128
|
+
const onCreated = /* @__PURE__ */ __name(async (e) => {
|
|
129
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: "created" });
|
|
130
130
|
}, "onCreated");
|
|
131
131
|
emitter.on("model:created", onCreated);
|
|
132
132
|
this.unbindHandlers.push(() => emitter.off("model:created", onCreated));
|
|
133
|
-
const onMounted = /* @__PURE__ */ __name((e) => {
|
|
134
|
-
this.processLifecycleEvent(e.uid, { ...e, type: "mounted" });
|
|
133
|
+
const onMounted = /* @__PURE__ */ __name(async (e) => {
|
|
134
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: "mounted" });
|
|
135
135
|
}, "onMounted");
|
|
136
136
|
emitter.on("model:mounted", onMounted);
|
|
137
137
|
this.unbindHandlers.push(() => emitter.off("model:mounted", onMounted));
|
|
138
|
-
const onGenericBeforeStart = /* @__PURE__ */ __name((e) => {
|
|
139
|
-
this.processLifecycleEvent(e.uid, { ...e, type: "event:beforeRender:start" });
|
|
138
|
+
const onGenericBeforeStart = /* @__PURE__ */ __name(async (e) => {
|
|
139
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: "event:beforeRender:start" });
|
|
140
140
|
}, "onGenericBeforeStart");
|
|
141
141
|
emitter.on("model:event:beforeRender:start", onGenericBeforeStart);
|
|
142
142
|
this.unbindHandlers.push(() => emitter.off("model:event:beforeRender:start", onGenericBeforeStart));
|
|
143
|
-
const onGenericBeforeEnd = /* @__PURE__ */ __name((e) => {
|
|
144
|
-
this.processLifecycleEvent(e.uid, { ...e, type: "event:beforeRender:end" });
|
|
143
|
+
const onGenericBeforeEnd = /* @__PURE__ */ __name(async (e) => {
|
|
144
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: "event:beforeRender:end" });
|
|
145
145
|
}, "onGenericBeforeEnd");
|
|
146
146
|
emitter.on("model:event:beforeRender:end", onGenericBeforeEnd);
|
|
147
147
|
this.unbindHandlers.push(() => emitter.off("model:event:beforeRender:end", onGenericBeforeEnd));
|
|
148
|
-
const onUnmounted = /* @__PURE__ */ __name((e) => {
|
|
149
|
-
this.processLifecycleEvent(e.uid, { ...e, type: "unmounted" });
|
|
148
|
+
const onUnmounted = /* @__PURE__ */ __name(async (e) => {
|
|
149
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: "unmounted" });
|
|
150
150
|
}, "onUnmounted");
|
|
151
151
|
emitter.on("model:unmounted", onUnmounted);
|
|
152
152
|
this.unbindHandlers.push(() => emitter.off("model:unmounted", onUnmounted));
|
|
153
|
-
const onDestroyed = /* @__PURE__ */ __name((e) => {
|
|
153
|
+
const onDestroyed = /* @__PURE__ */ __name(async (e) => {
|
|
154
154
|
const targetBucket = this.itemsByTargetUid.get(e.uid);
|
|
155
155
|
const event = { ...e, type: "destroyed" };
|
|
156
156
|
if (targetBucket && targetBucket.size) {
|
|
@@ -159,7 +159,7 @@ const _ModelOperationScheduler = class _ModelOperationScheduler {
|
|
|
159
159
|
const it = this.itemsById.get(id);
|
|
160
160
|
if (!it) continue;
|
|
161
161
|
if (this.shouldTrigger(it.options.when, event)) {
|
|
162
|
-
|
|
162
|
+
await this.tryExecuteOnce(id, event);
|
|
163
163
|
} else {
|
|
164
164
|
this.internalCancel(id);
|
|
165
165
|
}
|
|
@@ -177,14 +177,14 @@ const _ModelOperationScheduler = class _ModelOperationScheduler {
|
|
|
177
177
|
if (this.subscribedEventNames.has(name)) return;
|
|
178
178
|
this.subscribedEventNames.add(name);
|
|
179
179
|
const emitter = this.engine.emitter;
|
|
180
|
-
const onStart = /* @__PURE__ */ __name((e) => {
|
|
181
|
-
this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:start` });
|
|
180
|
+
const onStart = /* @__PURE__ */ __name(async (e) => {
|
|
181
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:start` });
|
|
182
182
|
}, "onStart");
|
|
183
|
-
const onEnd = /* @__PURE__ */ __name((e) => {
|
|
184
|
-
this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:end` });
|
|
183
|
+
const onEnd = /* @__PURE__ */ __name(async (e) => {
|
|
184
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:end` });
|
|
185
185
|
}, "onEnd");
|
|
186
|
-
const onError = /* @__PURE__ */ __name((e) => {
|
|
187
|
-
this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:error` });
|
|
186
|
+
const onError = /* @__PURE__ */ __name(async (e) => {
|
|
187
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:error` });
|
|
188
188
|
}, "onError");
|
|
189
189
|
emitter.on(`model:event:${name}:start`, onStart);
|
|
190
190
|
emitter.on(`model:event:${name}:end`, onEnd);
|
|
@@ -195,11 +195,11 @@ const _ModelOperationScheduler = class _ModelOperationScheduler {
|
|
|
195
195
|
}
|
|
196
196
|
parseEventWhen(when) {
|
|
197
197
|
if (!when || typeof when !== "string") return null;
|
|
198
|
-
const m = /^event:(
|
|
198
|
+
const m = /^event:(.+):(start|end|error)$/.exec(when);
|
|
199
199
|
if (!m) return null;
|
|
200
200
|
return { name: m[1], phase: m[2] };
|
|
201
201
|
}
|
|
202
|
-
processLifecycleEvent(targetUid, event) {
|
|
202
|
+
async processLifecycleEvent(targetUid, event) {
|
|
203
203
|
const targetBucket = this.itemsByTargetUid.get(targetUid);
|
|
204
204
|
if (!targetBucket || targetBucket.size === 0) return;
|
|
205
205
|
const ids = Array.from(targetBucket.keys());
|
|
@@ -208,7 +208,7 @@ const _ModelOperationScheduler = class _ModelOperationScheduler {
|
|
|
208
208
|
if (!item) continue;
|
|
209
209
|
const should = this.shouldTrigger(item.options.when, event);
|
|
210
210
|
if (!should) continue;
|
|
211
|
-
|
|
211
|
+
await this.tryExecuteOnce(id, event);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
shouldTrigger(when, event) {
|
package/lib/types.d.ts
CHANGED
|
@@ -166,12 +166,27 @@ export interface ActionDefinition<TModel extends FlowModel = FlowModel, TCtx ext
|
|
|
166
166
|
* - 允许扩展字符串以保持向后兼容。
|
|
167
167
|
*/
|
|
168
168
|
export type FlowEventName = 'click' | 'submit' | 'reset' | 'remove' | 'openView' | 'dropdownOpen' | 'popupScroll' | 'search' | 'customRequest' | 'collapseToggle' | (string & {});
|
|
169
|
+
/**
|
|
170
|
+
* 事件流的执行时机(phase)。
|
|
171
|
+
*
|
|
172
|
+
* 说明:
|
|
173
|
+
* - 缺省(phase 未配置)表示保持现有行为;
|
|
174
|
+
* - 当配置了 phase 时,运行时会将其映射为 `scheduleModelOperation` 的 `when` 锚点;
|
|
175
|
+
* - phase 同时适用于动态事件流(实例级)与静态流(内置)。
|
|
176
|
+
*/
|
|
177
|
+
export type FlowEventPhase = 'beforeAllFlows' | 'afterAllFlows' | 'beforeFlow' | 'afterFlow' | 'beforeStep' | 'afterStep';
|
|
169
178
|
/**
|
|
170
179
|
* Flow 事件类型(供 FlowDefinitionOptions.on 使用)。
|
|
171
180
|
*/
|
|
172
181
|
export type FlowEvent<TModel extends FlowModel = FlowModel> = FlowEventName | {
|
|
173
182
|
eventName: FlowEventName;
|
|
174
183
|
defaultParams?: Record<string, any>;
|
|
184
|
+
/** 动态事件流的执行时机(默认 beforeAllFlows) */
|
|
185
|
+
phase?: FlowEventPhase;
|
|
186
|
+
/** phase 为 beforeFlow/afterFlow/beforeStep/afterStep 时使用 */
|
|
187
|
+
flowKey?: string;
|
|
188
|
+
/** phase 为 beforeStep/afterStep 时使用 */
|
|
189
|
+
stepKey?: string;
|
|
175
190
|
};
|
|
176
191
|
/**
|
|
177
192
|
* 事件分发选项。
|
package/lib/views/useDialog.js
CHANGED
|
@@ -170,9 +170,9 @@ function useDialog() {
|
|
|
170
170
|
className: "nb-dialog-overflow-hidden",
|
|
171
171
|
ref: dialogRef,
|
|
172
172
|
hidden: (_d = (_c2 = config.inputArgs) == null ? void 0 : _c2.hidden) == null ? void 0 : _d.value,
|
|
173
|
-
...config,
|
|
174
173
|
footer: currentFooter,
|
|
175
174
|
header: currentHeader,
|
|
175
|
+
...config,
|
|
176
176
|
onCancel: () => {
|
|
177
177
|
currentDialog.close(config.result);
|
|
178
178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.65",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.0.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.0.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-alpha.65",
|
|
12
|
+
"@nocobase/shared": "2.0.0-alpha.65",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dayjs": "^1.11.9",
|
|
15
15
|
"dompurify": "^3.0.2",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "NocoBase Team",
|
|
38
38
|
"license": "AGPL-3.0",
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "790dd7ba2bb5b1dc905163f11c7b492a5b862365"
|
|
40
40
|
}
|
|
@@ -14,11 +14,6 @@ import { FlowEngine } from '../flowEngine';
|
|
|
14
14
|
import { FlowEngineProvider, useFlowEngine } from '../provider';
|
|
15
15
|
|
|
16
16
|
describe('FlowEngineProvider/useFlowEngine', () => {
|
|
17
|
-
it('throws without provider', () => {
|
|
18
|
-
const run = () => renderHook(() => useFlowEngine());
|
|
19
|
-
expect(run).toThrow(/FlowEngineProvider/);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
17
|
it('returns engine within provider', () => {
|
|
23
18
|
const engine = new FlowEngine();
|
|
24
19
|
const wrapper = ({ children }: any) => <FlowEngineProvider engine={engine}>{children}</FlowEngineProvider>;
|
package/src/acl/Acl.tsx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { omit } from 'lodash';
|
|
9
|
+
import _, { omit } from 'lodash';
|
|
10
10
|
import { FlowEngine } from '../flowEngine';
|
|
11
11
|
import { FlowModel } from '../models/flowModel';
|
|
12
12
|
|
|
@@ -31,11 +31,11 @@ export class ACL {
|
|
|
31
31
|
constructor(private flowEngine: FlowEngine) {}
|
|
32
32
|
|
|
33
33
|
setData(data: Record<string, any>) {
|
|
34
|
-
this.data = data;
|
|
34
|
+
this.data = _.cloneDeep(data);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
setMeta(data: Record<string, any>) {
|
|
38
|
-
this.meta = data;
|
|
38
|
+
this.meta = _.cloneDeep(data);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async load() {
|
|
@@ -19,9 +19,14 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const rect = { top: 0, left: 0, width: 100, height: 100 };
|
|
21
21
|
|
|
22
|
-
const createLayout = (
|
|
22
|
+
const createLayout = (
|
|
23
|
+
rows: Record<string, string[][]>,
|
|
24
|
+
sizes: Record<string, number[]>,
|
|
25
|
+
rowOrder?: string[],
|
|
26
|
+
): GridLayoutData => ({
|
|
23
27
|
rows,
|
|
24
28
|
sizes,
|
|
29
|
+
rowOrder,
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
describe('getSlotKey', () => {
|
|
@@ -275,6 +280,7 @@ describe('simulateLayoutForSlot', () => {
|
|
|
275
280
|
rowA: [24],
|
|
276
281
|
rowB: [24],
|
|
277
282
|
},
|
|
283
|
+
['rowA', 'rowB'],
|
|
278
284
|
);
|
|
279
285
|
|
|
280
286
|
const slot: LayoutSlot = {
|
|
@@ -315,6 +321,33 @@ describe('simulateLayoutForSlot', () => {
|
|
|
315
321
|
expect(result.sizes['row-new']).toEqual([24]);
|
|
316
322
|
});
|
|
317
323
|
|
|
324
|
+
it('removes empty source row when moving item into empty container slot', () => {
|
|
325
|
+
const layout = createLayout(
|
|
326
|
+
{
|
|
327
|
+
rowA: [['block-x']],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
rowA: [24],
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const slot: LayoutSlot = {
|
|
335
|
+
type: 'empty-row',
|
|
336
|
+
rect,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = simulateLayoutForSlot({
|
|
340
|
+
slot,
|
|
341
|
+
sourceUid: 'block-x',
|
|
342
|
+
layout,
|
|
343
|
+
generateRowId: () => 'row-new',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(result.rows['row-new']).toEqual([['block-x']]);
|
|
347
|
+
expect(result.rows.rowA).toBeUndefined();
|
|
348
|
+
expect(result.sizes.rowA).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
318
351
|
it('handles column slot with after position', () => {
|
|
319
352
|
const layout = createLayout(
|
|
320
353
|
{
|
|
@@ -373,6 +406,7 @@ describe('simulateLayoutForSlot', () => {
|
|
|
373
406
|
rowA: [24],
|
|
374
407
|
rowB: [24],
|
|
375
408
|
},
|
|
409
|
+
['rowA', 'rowB'],
|
|
376
410
|
);
|
|
377
411
|
|
|
378
412
|
const slot: LayoutSlot = {
|
|
@@ -392,6 +426,112 @@ describe('simulateLayoutForSlot', () => {
|
|
|
392
426
|
expect(Object.keys(result.rows)).toEqual(['rowA', 'row-inserted', 'rowB']);
|
|
393
427
|
});
|
|
394
428
|
|
|
429
|
+
it('inserts row into rowOrder when dropping below target row', () => {
|
|
430
|
+
const layout = createLayout(
|
|
431
|
+
{
|
|
432
|
+
rowA: [['a']],
|
|
433
|
+
rowB: [['b']],
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
rowA: [24],
|
|
437
|
+
rowB: [24],
|
|
438
|
+
},
|
|
439
|
+
['rowA', 'rowB'],
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const slot: LayoutSlot = {
|
|
443
|
+
type: 'row-gap',
|
|
444
|
+
targetRowId: 'rowA',
|
|
445
|
+
position: 'below',
|
|
446
|
+
rect,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const result = simulateLayoutForSlot({
|
|
450
|
+
slot,
|
|
451
|
+
sourceUid: 'c',
|
|
452
|
+
layout,
|
|
453
|
+
generateRowId: () => 'row-new',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('maintains rowOrder and inserts new row before target when provided', () => {
|
|
460
|
+
const layout = createLayout(
|
|
461
|
+
{
|
|
462
|
+
rowA: [['a']],
|
|
463
|
+
rowB: [['b']],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
rowA: [24],
|
|
467
|
+
rowB: [24],
|
|
468
|
+
},
|
|
469
|
+
['rowA', 'rowB'],
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const slot: LayoutSlot = {
|
|
473
|
+
type: 'row-gap',
|
|
474
|
+
targetRowId: 'rowB',
|
|
475
|
+
position: 'above',
|
|
476
|
+
rect,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const result = simulateLayoutForSlot({
|
|
480
|
+
slot,
|
|
481
|
+
sourceUid: 'c',
|
|
482
|
+
layout,
|
|
483
|
+
generateRowId: () => 'row-new',
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
|
|
487
|
+
expect(result.rows).toEqual({
|
|
488
|
+
rowA: [['a']],
|
|
489
|
+
'row-new': [['c']],
|
|
490
|
+
rowB: [['b']],
|
|
491
|
+
});
|
|
492
|
+
expect(result.sizes).toEqual({
|
|
493
|
+
rowA: [24],
|
|
494
|
+
'row-new': [24],
|
|
495
|
+
rowB: [24],
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('derives rowOrder from rows when missing and removes empty rows from order', () => {
|
|
500
|
+
const layout = createLayout(
|
|
501
|
+
{
|
|
502
|
+
row1: [['a']],
|
|
503
|
+
row2: [['b']],
|
|
504
|
+
row3: [['c']],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
row1: [24],
|
|
508
|
+
row2: [24],
|
|
509
|
+
row3: [24],
|
|
510
|
+
},
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const slot: LayoutSlot = {
|
|
514
|
+
type: 'column',
|
|
515
|
+
rowId: 'row1',
|
|
516
|
+
columnIndex: 0,
|
|
517
|
+
insertIndex: 0,
|
|
518
|
+
position: 'before',
|
|
519
|
+
rect,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const result = simulateLayoutForSlot({ slot, sourceUid: 'b', layout });
|
|
523
|
+
|
|
524
|
+
expect(result.rowOrder).toEqual(['row1', 'row3']);
|
|
525
|
+
expect(result.rows).toEqual({
|
|
526
|
+
row1: [['b', 'a']],
|
|
527
|
+
row3: [['c']],
|
|
528
|
+
});
|
|
529
|
+
expect(result.sizes).toEqual({
|
|
530
|
+
row1: [24],
|
|
531
|
+
row3: [24],
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
395
535
|
it('handles empty-column slot by replacing empty column', () => {
|
|
396
536
|
const layout = createLayout(
|
|
397
537
|
{
|
|
@@ -46,6 +46,7 @@ export interface Point {
|
|
|
46
46
|
export interface GridLayoutData {
|
|
47
47
|
rows: Record<string, string[][]>;
|
|
48
48
|
sizes: Record<string, number[]>;
|
|
49
|
+
rowOrder?: string[];
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface ColumnSlot {
|
|
@@ -142,6 +143,49 @@ export interface LayoutSnapshot {
|
|
|
142
143
|
containerRect: Rect;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
const deriveRowOrder = (rows: Record<string, string[][]>, provided?: string[]) => {
|
|
147
|
+
const order: string[] = [];
|
|
148
|
+
const used = new Set<string>();
|
|
149
|
+
|
|
150
|
+
(provided || Object.keys(rows)).forEach((rowId) => {
|
|
151
|
+
if (rows[rowId] && !used.has(rowId)) {
|
|
152
|
+
order.push(rowId);
|
|
153
|
+
used.add(rowId);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
Object.keys(rows).forEach((rowId) => {
|
|
158
|
+
if (!used.has(rowId)) {
|
|
159
|
+
order.push(rowId);
|
|
160
|
+
used.add(rowId);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return order;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const normalizeRowsWithOrder = (rows: Record<string, string[][]>, order: string[]) => {
|
|
168
|
+
const next: Record<string, string[][]> = {};
|
|
169
|
+
order.forEach((rowId) => {
|
|
170
|
+
if (rows[rowId]) {
|
|
171
|
+
next[rowId] = rows[rowId];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
Object.keys(rows).forEach((rowId) => {
|
|
175
|
+
if (!next[rowId]) {
|
|
176
|
+
next[rowId] = rows[rowId];
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return next;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const ensureRowOrder = (layout: GridLayoutData) => {
|
|
183
|
+
const order = deriveRowOrder(layout.rows, layout.rowOrder);
|
|
184
|
+
layout.rowOrder = order;
|
|
185
|
+
layout.rows = normalizeRowsWithOrder(layout.rows, order);
|
|
186
|
+
return order;
|
|
187
|
+
};
|
|
188
|
+
|
|
145
189
|
export interface BuildLayoutSnapshotOptions {
|
|
146
190
|
container: HTMLElement | null;
|
|
147
191
|
}
|
|
@@ -465,10 +509,12 @@ const removeItemFromLayout = (layout: GridLayoutData, uidValue: string) => {
|
|
|
465
509
|
if (columns.length === 0) {
|
|
466
510
|
delete layout.rows[rowId];
|
|
467
511
|
delete layout.sizes[rowId];
|
|
512
|
+
ensureRowOrder(layout);
|
|
468
513
|
return;
|
|
469
514
|
}
|
|
470
515
|
|
|
471
516
|
normalizeRowSizes(rowId, layout);
|
|
517
|
+
ensureRowOrder(layout);
|
|
472
518
|
};
|
|
473
519
|
|
|
474
520
|
const toIntSizes = (weights: number[], count: number): number[] => {
|
|
@@ -592,8 +638,10 @@ export const simulateLayoutForSlot = ({
|
|
|
592
638
|
const cloned: GridLayoutData = {
|
|
593
639
|
rows: _.cloneDeep(layout.rows),
|
|
594
640
|
sizes: _.cloneDeep(layout.sizes),
|
|
641
|
+
rowOrder: layout.rowOrder ? [...layout.rowOrder] : undefined,
|
|
595
642
|
};
|
|
596
643
|
|
|
644
|
+
ensureRowOrder(cloned);
|
|
597
645
|
removeItemFromLayout(cloned, sourceUid);
|
|
598
646
|
|
|
599
647
|
const createRowId = generateRowId ?? uid;
|
|
@@ -638,8 +686,16 @@ export const simulateLayoutForSlot = ({
|
|
|
638
686
|
case 'row-gap': {
|
|
639
687
|
const newRowId = createRowId();
|
|
640
688
|
const rowPosition: 'before' | 'after' = slot.position === 'above' ? 'before' : 'after';
|
|
689
|
+
const currentOrder = deriveRowOrder(cloned.rows, cloned.rowOrder);
|
|
641
690
|
cloned.rows = insertRow(cloned.rows, slot.targetRowId, newRowId, rowPosition, [[sourceUid]]);
|
|
642
691
|
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
692
|
+
const targetIndex = currentOrder.indexOf(slot.targetRowId);
|
|
693
|
+
const insertIndex =
|
|
694
|
+
targetIndex === -1 ? currentOrder.length : rowPosition === 'before' ? targetIndex : targetIndex + 1;
|
|
695
|
+
const nextOrder = [...currentOrder];
|
|
696
|
+
nextOrder.splice(insertIndex, 0, newRowId);
|
|
697
|
+
cloned.rowOrder = nextOrder;
|
|
698
|
+
cloned.rows = normalizeRowsWithOrder(cloned.rows, nextOrder);
|
|
643
699
|
break;
|
|
644
700
|
}
|
|
645
701
|
case 'empty-row': {
|
|
@@ -649,11 +705,15 @@ export const simulateLayoutForSlot = ({
|
|
|
649
705
|
[newRowId]: [[sourceUid]],
|
|
650
706
|
};
|
|
651
707
|
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
708
|
+
const currentOrder = deriveRowOrder(cloned.rows, cloned.rowOrder);
|
|
709
|
+
cloned.rowOrder = [...currentOrder.filter((id) => id !== newRowId), newRowId];
|
|
710
|
+
cloned.rows = normalizeRowsWithOrder(cloned.rows, cloned.rowOrder);
|
|
652
711
|
break;
|
|
653
712
|
}
|
|
654
713
|
default:
|
|
655
714
|
break;
|
|
656
715
|
}
|
|
657
716
|
|
|
717
|
+
ensureRowOrder(cloned);
|
|
658
718
|
return cloned;
|
|
659
719
|
};
|