@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.
@@ -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
- void this.tryExecuteOnce(id, event);
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:([^:]+):(start|end|error)$/.exec(when);
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
- void this.tryExecuteOnce(id, event);
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
  * 事件分发选项。
@@ -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.63",
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.63",
12
- "@nocobase/shared": "2.0.0-alpha.63",
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": "0d5e9eb45e4d0f3ba70d7561a0d296d12a4d8bf8"
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 = (rows: Record<string, string[][]>, sizes: Record<string, number[]>): GridLayoutData => ({
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
  };
@@ -815,6 +815,9 @@ export class CollectionField {
815
815
  if (typeof v !== 'object') {
816
816
  return v;
817
817
  }
818
+ if (v.value === null || v.value === undefined) {
819
+ return v;
820
+ }
818
821
  return {
819
822
  ...v,
820
823
  value: Number(v.value),