@nocobase/flow-engine 2.1.0-beta.2 → 2.1.0-beta.21
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/FlowModelRenderer.d.ts +1 -1
- package/lib/components/FlowModelRenderer.js +10 -6
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.js +6 -2
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +339 -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 +272 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +247 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/data-source/index.js +6 -0
- package/lib/executor/FlowExecutor.js +31 -8
- package/lib/flowContext.js +31 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- 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/flowModel.d.ts +2 -1
- package/lib/models/flowModel.js +28 -9
- 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/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__/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/FlowModelRenderer.tsx +12 -6
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +88 -0
- package/src/components/dnd/gridDragPlanner.ts +8 -2
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +63 -9
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +468 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +609 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +358 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/utils.ts +1 -1
- package/src/data-source/index.ts +6 -0
- package/src/executor/FlowExecutor.ts +34 -9
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flowContext.ts +35 -3
- package/src/flowEngine.ts +445 -11
- 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/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/flowModel.tsx +31 -10
- 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__/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/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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { lazy as reactLazy } from 'react';
|
|
11
|
+
|
|
12
|
+
type LazyComponentType<M extends Record<string, any>, K extends keyof M> = {
|
|
13
|
+
[P in K]: M[P];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function lazy<M extends Record<'default', any>>(factory: () => Promise<M>): M['default'];
|
|
17
|
+
|
|
18
|
+
export function lazy<M extends Record<string, any>, K extends keyof M = keyof M>(
|
|
19
|
+
factory: () => Promise<M>,
|
|
20
|
+
...componentNames: K[]
|
|
21
|
+
): LazyComponentType<M, K>;
|
|
22
|
+
|
|
23
|
+
export function lazy<M extends Record<string, any>, K extends keyof M>(
|
|
24
|
+
factory: () => Promise<M>,
|
|
25
|
+
...componentNames: K[]
|
|
26
|
+
) {
|
|
27
|
+
if (componentNames.length === 0) {
|
|
28
|
+
const LazyComponent = reactLazy(() =>
|
|
29
|
+
factory().then((module) => ({
|
|
30
|
+
default: module.default,
|
|
31
|
+
})),
|
|
32
|
+
);
|
|
33
|
+
const Component = (props) => (
|
|
34
|
+
<React.Suspense fallback={null}>
|
|
35
|
+
<LazyComponent {...props} />
|
|
36
|
+
</React.Suspense>
|
|
37
|
+
);
|
|
38
|
+
return Component;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return componentNames.reduce(
|
|
42
|
+
(acc, name) => {
|
|
43
|
+
const LazyComponent = reactLazy(() =>
|
|
44
|
+
factory().then((module) => ({
|
|
45
|
+
default: module[name],
|
|
46
|
+
})),
|
|
47
|
+
);
|
|
48
|
+
acc[name] = ((props) => (
|
|
49
|
+
<React.Suspense fallback={null}>
|
|
50
|
+
<LazyComponent {...props} />
|
|
51
|
+
</React.Suspense>
|
|
52
|
+
)) as M[K];
|
|
53
|
+
return acc;
|
|
54
|
+
},
|
|
55
|
+
{} as LazyComponentType<M, K>,
|
|
56
|
+
);
|
|
57
|
+
}
|
package/src/locale/en-US.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"Failed to destroy model after creation error": "Failed to destroy model after creation error",
|
|
35
35
|
"Failed to get action {{action}}": "Failed to get action {{action}}",
|
|
36
36
|
"Failed to get configurable flows for model {{model}}": "Failed to get configurable flows for model {{model}}",
|
|
37
|
+
"Attributes are unavailable before selecting a record": "Attributes are unavailable before selecting a record",
|
|
37
38
|
"Failed to import FormDialog": "Failed to import FormDialog",
|
|
38
39
|
"Failed to import FormDialog or FormStep": "Failed to import FormDialog or FormStep",
|
|
39
40
|
"Failed to import Formily components": "Failed to import Formily components",
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"Failed to destroy model after creation error": "创建错误后销毁模型失败",
|
|
32
32
|
"Failed to get action {{action}}": "获取 action '{{action}}' 失败",
|
|
33
33
|
"Failed to get configurable flows for model {{model}}": "获取模型 '{{model}}' 的可配置 flows 失败",
|
|
34
|
+
"Attributes are unavailable before selecting a record": "选择记录之前,当前项属性不可用",
|
|
34
35
|
"Failed to import FormDialog": "导入 FormDialog 失败",
|
|
35
36
|
"Failed to import FormDialog or FormStep": "导入 FormDialog 或 FormStep 失败",
|
|
36
37
|
"Failed to import Formily components": "导入 Formily 组件失败",
|
|
@@ -105,6 +105,45 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
|
|
|
105
105
|
expect(calls).toEqual(['static-a', 'dynamic']);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test("phase='afterAllFlows': skips when event aborted by ctx.exitAll()", async () => {
|
|
109
|
+
const engine = new FlowEngine();
|
|
110
|
+
class M extends FlowModel {}
|
|
111
|
+
engine.registerModels({ M });
|
|
112
|
+
|
|
113
|
+
const calls: string[] = [];
|
|
114
|
+
|
|
115
|
+
M.registerFlow({
|
|
116
|
+
key: 'S',
|
|
117
|
+
on: { eventName: 'go' },
|
|
118
|
+
steps: {
|
|
119
|
+
a: { handler: async () => void calls.push('static-a') } as any,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const model = engine.createModel({ use: 'M' });
|
|
124
|
+
model.registerFlow('Abort', {
|
|
125
|
+
on: { eventName: 'go' },
|
|
126
|
+
sort: -10,
|
|
127
|
+
steps: {
|
|
128
|
+
d: {
|
|
129
|
+
handler: async (ctx: any) => {
|
|
130
|
+
calls.push('abort');
|
|
131
|
+
ctx.exitAll();
|
|
132
|
+
},
|
|
133
|
+
} as any,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
model.registerFlow('AfterAll', {
|
|
137
|
+
on: { eventName: 'go', phase: 'afterAllFlows' },
|
|
138
|
+
steps: {
|
|
139
|
+
d: { handler: async () => void calls.push('after-all') } as any,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await model.dispatchEvent('go');
|
|
144
|
+
expect(calls).toEqual(['abort']);
|
|
145
|
+
});
|
|
146
|
+
|
|
108
147
|
test("phase='beforeFlow': instance flow runs before the target static flow", async () => {
|
|
109
148
|
const engine = new FlowEngine();
|
|
110
149
|
class M extends FlowModel {}
|
|
@@ -161,6 +200,39 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
|
|
|
161
200
|
expect(calls).toEqual(['static-a', 'static-b', 'dynamic']);
|
|
162
201
|
});
|
|
163
202
|
|
|
203
|
+
test("phase='afterFlow': skips when anchor flow is aborted by ctx.exitAll()", async () => {
|
|
204
|
+
const engine = new FlowEngine();
|
|
205
|
+
class M extends FlowModel {}
|
|
206
|
+
engine.registerModels({ M });
|
|
207
|
+
|
|
208
|
+
const calls: string[] = [];
|
|
209
|
+
|
|
210
|
+
M.registerFlow({
|
|
211
|
+
key: 'S',
|
|
212
|
+
on: { eventName: 'go' },
|
|
213
|
+
steps: {
|
|
214
|
+
a: {
|
|
215
|
+
handler: async (ctx: any) => {
|
|
216
|
+
calls.push('static-a');
|
|
217
|
+
ctx.exitAll();
|
|
218
|
+
},
|
|
219
|
+
} as any,
|
|
220
|
+
b: { handler: async () => void calls.push('static-b') } as any,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const model = engine.createModel({ use: 'M' });
|
|
225
|
+
model.registerFlow('D', {
|
|
226
|
+
on: { eventName: 'go', phase: 'afterFlow', flowKey: 'S' },
|
|
227
|
+
steps: {
|
|
228
|
+
d: { handler: async () => void calls.push('dynamic') } as any,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await model.dispatchEvent('go');
|
|
233
|
+
expect(calls).toEqual(['static-a']);
|
|
234
|
+
});
|
|
235
|
+
|
|
164
236
|
test("phase='beforeStep': instance flow runs before the target static step", async () => {
|
|
165
237
|
const engine = new FlowEngine();
|
|
166
238
|
class M extends FlowModel {}
|
|
@@ -217,6 +289,39 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
|
|
|
217
289
|
expect(calls).toEqual(['static-a', 'dynamic', 'static-b']);
|
|
218
290
|
});
|
|
219
291
|
|
|
292
|
+
test("phase='afterStep': skips when anchor step is aborted by ctx.exitAll()", async () => {
|
|
293
|
+
const engine = new FlowEngine();
|
|
294
|
+
class M extends FlowModel {}
|
|
295
|
+
engine.registerModels({ M });
|
|
296
|
+
|
|
297
|
+
const calls: string[] = [];
|
|
298
|
+
|
|
299
|
+
M.registerFlow({
|
|
300
|
+
key: 'S',
|
|
301
|
+
on: { eventName: 'go' },
|
|
302
|
+
steps: {
|
|
303
|
+
a: {
|
|
304
|
+
handler: async (ctx: any) => {
|
|
305
|
+
calls.push('static-a');
|
|
306
|
+
ctx.exitAll();
|
|
307
|
+
},
|
|
308
|
+
} as any,
|
|
309
|
+
b: { handler: async () => void calls.push('static-b') } as any,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const model = engine.createModel({ use: 'M' });
|
|
314
|
+
model.registerFlow('D', {
|
|
315
|
+
on: { eventName: 'go', phase: 'afterStep', flowKey: 'S', stepKey: 'a' },
|
|
316
|
+
steps: {
|
|
317
|
+
d: { handler: async () => void calls.push('dynamic') } as any,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await model.dispatchEvent('go');
|
|
322
|
+
expect(calls).toEqual(['static-a']);
|
|
323
|
+
});
|
|
324
|
+
|
|
220
325
|
test("phase='beforeFlow': ctx.exitAll() stops anchor flow and subsequent flows", async () => {
|
|
221
326
|
const engine = new FlowEngine();
|
|
222
327
|
class M extends FlowModel {}
|
|
@@ -430,6 +535,115 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
|
|
|
430
535
|
expect(calls).toEqual(['static-a', 'dynamic']);
|
|
431
536
|
});
|
|
432
537
|
|
|
538
|
+
test('fallback to event:end (missing anchor) skips when event aborted by ctx.exitAll()', async () => {
|
|
539
|
+
const engine = new FlowEngine();
|
|
540
|
+
class M extends FlowModel {}
|
|
541
|
+
engine.registerModels({ M });
|
|
542
|
+
|
|
543
|
+
const calls: string[] = [];
|
|
544
|
+
|
|
545
|
+
M.registerFlow({
|
|
546
|
+
key: 'S',
|
|
547
|
+
on: { eventName: 'go' },
|
|
548
|
+
steps: {
|
|
549
|
+
a: { handler: async () => void calls.push('static-a') } as any,
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const model = engine.createModel({ use: 'M' });
|
|
554
|
+
model.registerFlow('Abort', {
|
|
555
|
+
on: { eventName: 'go' },
|
|
556
|
+
sort: -10,
|
|
557
|
+
steps: {
|
|
558
|
+
d: {
|
|
559
|
+
handler: async (ctx: any) => {
|
|
560
|
+
calls.push('abort');
|
|
561
|
+
ctx.exitAll();
|
|
562
|
+
},
|
|
563
|
+
} as any,
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
model.registerFlow('FallbackToEnd', {
|
|
567
|
+
on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'missing' },
|
|
568
|
+
steps: {
|
|
569
|
+
d: { handler: async () => void calls.push('fallback-end') } as any,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
await model.dispatchEvent('go');
|
|
574
|
+
expect(calls).toEqual(['abort']);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('event:end is still emitted with aborted=true when exitAll happens', async () => {
|
|
578
|
+
const engine = new FlowEngine();
|
|
579
|
+
class M extends FlowModel {}
|
|
580
|
+
engine.registerModels({ M });
|
|
581
|
+
|
|
582
|
+
const endEvents: any[] = [];
|
|
583
|
+
const onEnd = (payload: any) => {
|
|
584
|
+
endEvents.push(payload);
|
|
585
|
+
};
|
|
586
|
+
engine.emitter.on('model:event:go:end', onEnd);
|
|
587
|
+
|
|
588
|
+
const model = engine.createModel({ use: 'M' });
|
|
589
|
+
model.registerFlow('Abort', {
|
|
590
|
+
on: { eventName: 'go' },
|
|
591
|
+
steps: {
|
|
592
|
+
d: {
|
|
593
|
+
handler: async (ctx: any) => {
|
|
594
|
+
ctx.exitAll();
|
|
595
|
+
},
|
|
596
|
+
} as any,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
await model.dispatchEvent('go');
|
|
601
|
+
engine.emitter.off('model:event:go:end', onEnd);
|
|
602
|
+
|
|
603
|
+
expect(endEvents).toHaveLength(1);
|
|
604
|
+
expect(endEvents[0]?.aborted).toBe(true);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test('flow:end/step:end are emitted with aborted=true when exitAll happens', async () => {
|
|
608
|
+
const engine = new FlowEngine();
|
|
609
|
+
class M extends FlowModel {}
|
|
610
|
+
engine.registerModels({ M });
|
|
611
|
+
|
|
612
|
+
const flowEndEvents: any[] = [];
|
|
613
|
+
const stepEndEvents: any[] = [];
|
|
614
|
+
const onFlowEnd = (payload: any) => {
|
|
615
|
+
flowEndEvents.push(payload);
|
|
616
|
+
};
|
|
617
|
+
const onStepEnd = (payload: any) => {
|
|
618
|
+
stepEndEvents.push(payload);
|
|
619
|
+
};
|
|
620
|
+
engine.emitter.on('model:event:go:flow:S:end', onFlowEnd);
|
|
621
|
+
engine.emitter.on('model:event:go:flow:S:step:a:end', onStepEnd);
|
|
622
|
+
|
|
623
|
+
M.registerFlow({
|
|
624
|
+
key: 'S',
|
|
625
|
+
on: { eventName: 'go' },
|
|
626
|
+
steps: {
|
|
627
|
+
a: {
|
|
628
|
+
handler: async (ctx: any) => {
|
|
629
|
+
ctx.exitAll();
|
|
630
|
+
},
|
|
631
|
+
} as any,
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const model = engine.createModel({ use: 'M' });
|
|
636
|
+
await model.dispatchEvent('go');
|
|
637
|
+
|
|
638
|
+
engine.emitter.off('model:event:go:flow:S:end', onFlowEnd);
|
|
639
|
+
engine.emitter.off('model:event:go:flow:S:step:a:end', onStepEnd);
|
|
640
|
+
|
|
641
|
+
expect(flowEndEvents).toHaveLength(1);
|
|
642
|
+
expect(stepEndEvents).toHaveLength(1);
|
|
643
|
+
expect(flowEndEvents[0]?.aborted).toBe(true);
|
|
644
|
+
expect(stepEndEvents[0]?.aborted).toBe(true);
|
|
645
|
+
});
|
|
646
|
+
|
|
433
647
|
test('multiple flows on same anchor: executes by flow.sort asc (stable)', async () => {
|
|
434
648
|
const engine = new FlowEngine();
|
|
435
649
|
class M extends FlowModel {}
|
package/src/models/flowModel.tsx
CHANGED
|
@@ -11,8 +11,6 @@ import { batch, define, observable, observe } from '@formily/reactive';
|
|
|
11
11
|
import _ from 'lodash';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { uid } from 'uid/secure';
|
|
14
|
-
import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
|
|
15
|
-
import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
|
|
16
14
|
import { Emitter } from '../emitter';
|
|
17
15
|
import { InstanceFlowRegistry } from '../flow-registry/InstanceFlowRegistry';
|
|
18
16
|
import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
|
|
@@ -36,7 +34,9 @@ import type {
|
|
|
36
34
|
import { IModelComponentProps, ReadonlyModelProps } from '../types';
|
|
37
35
|
import { isInheritedFrom, setupRuntimeContextSteps } from '../utils';
|
|
38
36
|
// import { FlowExitAllException } from '../utils/exceptions';
|
|
39
|
-
import { Typography } from 'antd
|
|
37
|
+
import { Typography } from 'antd';
|
|
38
|
+
import type { MenuProps } from 'antd';
|
|
39
|
+
import { observer } from '..';
|
|
40
40
|
import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
|
|
41
41
|
import { buildSubModelItem } from '../components/subModel/utils';
|
|
42
42
|
import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
|
|
@@ -46,8 +46,6 @@ import { FlowSettingsOpenOptions } from '../flowSettings';
|
|
|
46
46
|
import type { ScheduleOptions } from '../scheduler/ModelOperationScheduler';
|
|
47
47
|
import type { DispatchEventOptions, EventDefinition } from '../types';
|
|
48
48
|
import { ForkFlowModel } from './forkFlowModel';
|
|
49
|
-
import type { MenuProps } from 'antd';
|
|
50
|
-
import { observer } from '..';
|
|
51
49
|
|
|
52
50
|
// 使用 WeakMap 为每个类缓存一个 ModelActionRegistry 实例
|
|
53
51
|
const classActionRegistries = new WeakMap<typeof FlowModel, ModelActionRegistry>();
|
|
@@ -88,6 +86,16 @@ type ExtraMenuItemEntry = {
|
|
|
88
86
|
|
|
89
87
|
const classMenuExtensions = new WeakMap<typeof FlowModel, Set<ExtraMenuItemEntry>>();
|
|
90
88
|
|
|
89
|
+
async function loadOpenStepSettingsDialog() {
|
|
90
|
+
const mod = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
|
|
91
|
+
return mod.openStepSettingsDialog;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function loadOpenRequiredParamsStepFormDialog() {
|
|
95
|
+
const mod = await import('../components/settings/wrappers/contextual/StepRequiredSettingsDialog');
|
|
96
|
+
return mod.openRequiredParamsStepFormDialog;
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
export enum ModelRenderMode {
|
|
92
100
|
ReactElement = 'reactElement',
|
|
93
101
|
RenderFunction = 'renderFunction',
|
|
@@ -208,11 +216,14 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
208
216
|
if (changed.type === 'set' && _.isEqual(changed.value, changed.oldValue)) {
|
|
209
217
|
return;
|
|
210
218
|
}
|
|
219
|
+
const hasLastAutoRun = !!this._lastAutoRunParams;
|
|
211
220
|
|
|
212
221
|
if (this.flowEngine) {
|
|
213
222
|
this.invalidateFlowCache('beforeRender');
|
|
214
223
|
}
|
|
215
|
-
|
|
224
|
+
if (hasLastAutoRun) {
|
|
225
|
+
this._rerunLastAutoRun();
|
|
226
|
+
}
|
|
216
227
|
this.forks.forEach((fork) => {
|
|
217
228
|
fork.rerender();
|
|
218
229
|
});
|
|
@@ -858,6 +869,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
858
869
|
}
|
|
859
870
|
}, 100);
|
|
860
871
|
|
|
872
|
+
private resetAutoRunState(): void {
|
|
873
|
+
this._rerunLastAutoRun?.cancel?.();
|
|
874
|
+
this._lastAutoRunParams = null;
|
|
875
|
+
}
|
|
876
|
+
|
|
861
877
|
/**
|
|
862
878
|
* 通用事件分发钩子:开始
|
|
863
879
|
* 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
|
|
@@ -951,7 +967,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
951
967
|
}
|
|
952
968
|
|
|
953
969
|
// 创建缓存的响应式包装器组件工厂(只创建一次)
|
|
954
|
-
const createReactiveWrapper = (modelInstance:
|
|
970
|
+
const createReactiveWrapper = (modelInstance: FlowModel) => {
|
|
955
971
|
const ReactiveWrapper = observer(() => {
|
|
956
972
|
// 触发响应式更新的关键属性访问(读取 run/渲染目标的 props)
|
|
957
973
|
const renderTarget = modelInstance;
|
|
@@ -977,6 +993,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
977
993
|
model: renderTarget,
|
|
978
994
|
});
|
|
979
995
|
return () => {
|
|
996
|
+
renderTarget.resetAutoRunState();
|
|
980
997
|
if (typeof renderTarget.onUnmount === 'function') {
|
|
981
998
|
renderTarget.onUnmount();
|
|
982
999
|
}
|
|
@@ -1369,7 +1386,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1369
1386
|
* @param {string} stepKey 步骤的唯一标识符
|
|
1370
1387
|
* @returns {void}
|
|
1371
1388
|
*/
|
|
1372
|
-
openStepSettingsDialog(flowKey: string, stepKey: string) {
|
|
1389
|
+
async openStepSettingsDialog(flowKey: string, stepKey: string) {
|
|
1373
1390
|
// 创建流程运行时上下文
|
|
1374
1391
|
const flow = this.getFlow(flowKey);
|
|
1375
1392
|
const step = flow?.steps?.[stepKey];
|
|
@@ -1383,7 +1400,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1383
1400
|
setupRuntimeContextSteps(ctx, flow.steps, this, flowKey);
|
|
1384
1401
|
ctx.defineProperty('currentStep', { value: step });
|
|
1385
1402
|
|
|
1386
|
-
|
|
1403
|
+
const openStepSettingsDialog = await loadOpenStepSettingsDialog();
|
|
1404
|
+
|
|
1405
|
+
return openStepSettingsDialog({
|
|
1387
1406
|
model: this,
|
|
1388
1407
|
flowKey,
|
|
1389
1408
|
stepKey,
|
|
@@ -1399,7 +1418,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1399
1418
|
* @returns {Promise<any>} 返回表单提交的值
|
|
1400
1419
|
*/
|
|
1401
1420
|
async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string) {
|
|
1402
|
-
|
|
1421
|
+
const openRequiredParamsStepFormDialog = await loadOpenRequiredParamsStepFormDialog();
|
|
1422
|
+
|
|
1423
|
+
return openRequiredParamsStepFormDialog({
|
|
1403
1424
|
model: this,
|
|
1404
1425
|
dialogWidth,
|
|
1405
1426
|
dialogTitle,
|
|
@@ -208,4 +208,86 @@ describe('observer', () => {
|
|
|
208
208
|
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
209
209
|
expect(screen.queryByText('Count: 1')).not.toBeInTheDocument();
|
|
210
210
|
});
|
|
211
|
+
|
|
212
|
+
it('should flush pending update without TDZ error when context becomes active before timer callback runs', async () => {
|
|
213
|
+
vi.useFakeTimers();
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const model = observable({ count: 0 });
|
|
217
|
+
const pageActive = observable.ref(false);
|
|
218
|
+
const tabActive = observable.ref(true);
|
|
219
|
+
|
|
220
|
+
const context = {
|
|
221
|
+
pageActive,
|
|
222
|
+
tabActive,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
(useFlowContext as any).mockReturnValue(context);
|
|
226
|
+
|
|
227
|
+
const Component = observer(() => <div>Count: {model.count}</div>);
|
|
228
|
+
|
|
229
|
+
render(<Component />);
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
232
|
+
|
|
233
|
+
act(() => {
|
|
234
|
+
model.count++;
|
|
235
|
+
pageActive.value = true;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await act(async () => {
|
|
239
|
+
await vi.runAllTimersAsync();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
|
243
|
+
} finally {
|
|
244
|
+
vi.useRealTimers();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should cleanup pending timer and listener on unmount', async () => {
|
|
249
|
+
vi.useFakeTimers();
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const model = observable({ count: 0 });
|
|
253
|
+
const pageActive = observable.ref(false);
|
|
254
|
+
const tabActive = observable.ref(true);
|
|
255
|
+
const renderSpy = vi.fn();
|
|
256
|
+
|
|
257
|
+
const context = {
|
|
258
|
+
pageActive,
|
|
259
|
+
tabActive,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
(useFlowContext as any).mockReturnValue(context);
|
|
263
|
+
|
|
264
|
+
const Component = observer(() => {
|
|
265
|
+
renderSpy(model.count);
|
|
266
|
+
return <div>Count: {model.count}</div>;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const { unmount } = render(<Component />);
|
|
270
|
+
|
|
271
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
273
|
+
|
|
274
|
+
act(() => {
|
|
275
|
+
model.count++;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
unmount();
|
|
279
|
+
|
|
280
|
+
act(() => {
|
|
281
|
+
pageActive.value = true;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await act(async () => {
|
|
285
|
+
await vi.runAllTimersAsync();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
289
|
+
} finally {
|
|
290
|
+
vi.useRealTimers();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
211
293
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { observer as originalObserver, IObserverOptions, ReactFC } from '@formily/reactive-react';
|
|
11
11
|
import React, { useMemo, useEffect, useRef } from 'react';
|
|
12
12
|
import { useFlowContext } from '../FlowContextProvider';
|
|
13
|
-
import {
|
|
13
|
+
import { reaction } from '@formily/reactive';
|
|
14
14
|
import { FlowEngineContext } from '..';
|
|
15
15
|
|
|
16
16
|
type ObserverComponentProps<P, Options extends IObserverOptions> = Options extends {
|
|
@@ -30,12 +30,67 @@ export const observer = <P, Options extends IObserverOptions = IObserverOptions>
|
|
|
30
30
|
const ctxRef = useRef(ctx);
|
|
31
31
|
ctxRef.current = ctx;
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// 保存延迟更新的监听器,避免重复创建监听。
|
|
34
34
|
const pendingDisposerRef = useRef<(() => void) | null>(null);
|
|
35
|
+
// 保存延迟创建监听器的定时器,避免组件卸载后仍继续调度。
|
|
36
|
+
const pendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 清理挂起的可见性监听器。
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* clearPendingDisposer();
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
const clearPendingDisposer = () => {
|
|
47
|
+
if (pendingDisposerRef.current) {
|
|
48
|
+
pendingDisposerRef.current();
|
|
49
|
+
pendingDisposerRef.current = null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 清理挂起的定时器。
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* clearPendingTimer();
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
const clearPendingTimer = () => {
|
|
62
|
+
if (pendingTimerRef.current) {
|
|
63
|
+
clearTimeout(pendingTimerRef.current);
|
|
64
|
+
pendingTimerRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 判断当前页面与标签页是否允许立即更新。
|
|
70
|
+
*
|
|
71
|
+
* @returns 当前上下文是否处于可更新状态。
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* if (isContextActive()) {
|
|
75
|
+
* updater();
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
const isContextActive = () => {
|
|
80
|
+
const pageActive = getPageActive(ctxRef.current);
|
|
81
|
+
const tabActive = ctxRef.current?.tabActive?.value;
|
|
82
|
+
|
|
83
|
+
return pageActive !== false && tabActive !== false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 组件卸载时统一清理所有挂起任务,避免异步回调在卸载后继续运行。
|
|
37
87
|
useEffect(() => {
|
|
38
88
|
return () => {
|
|
89
|
+
if (pendingTimerRef.current) {
|
|
90
|
+
clearTimeout(pendingTimerRef.current);
|
|
91
|
+
pendingTimerRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
39
94
|
if (pendingDisposerRef.current) {
|
|
40
95
|
pendingDisposerRef.current();
|
|
41
96
|
pendingDisposerRef.current = null;
|
|
@@ -47,38 +102,45 @@ export const observer = <P, Options extends IObserverOptions = IObserverOptions>
|
|
|
47
102
|
() =>
|
|
48
103
|
originalObserver(Component, {
|
|
49
104
|
scheduler(updater) {
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
if (!isContextActive()) {
|
|
106
|
+
if (pendingTimerRef.current || pendingDisposerRef.current) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 通过异步任务打断同步调度,避免连续触发时形成递归更新。
|
|
111
|
+
pendingTimerRef.current = setTimeout(() => {
|
|
112
|
+
pendingTimerRef.current = null;
|
|
52
113
|
|
|
53
|
-
if (pageActive === false || tabActive === false) {
|
|
54
|
-
// Avoid stack overflow
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
// If there is already a pending updater, do nothing
|
|
57
114
|
if (pendingDisposerRef.current) {
|
|
58
115
|
return;
|
|
59
116
|
}
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
118
|
+
if (isContextActive()) {
|
|
119
|
+
updater();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 只监听组合后的“是否可更新”状态,条件恢复后执行一次并立即销毁。
|
|
124
|
+
pendingDisposerRef.current = reaction(
|
|
125
|
+
() => isContextActive(),
|
|
126
|
+
(active) => {
|
|
127
|
+
if (!active) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
clearPendingDisposer();
|
|
67
132
|
updater();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'FlowObserverPendingUpdate',
|
|
136
|
+
},
|
|
137
|
+
);
|
|
73
138
|
});
|
|
74
139
|
return;
|
|
75
140
|
}
|
|
76
141
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
pendingDisposerRef.current();
|
|
80
|
-
pendingDisposerRef.current = null;
|
|
81
|
-
}
|
|
142
|
+
clearPendingTimer();
|
|
143
|
+
clearPendingDisposer();
|
|
82
144
|
|
|
83
145
|
updater();
|
|
84
146
|
},
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// 为避免在模块初始化阶段引入 FlowContext(从而触发循环依赖),不要在顶层导入各类 RunJSContext。
|
|
11
11
|
// 在需要默认映射时(首次 resolve)再使用 createRequire 同步加载对应模块。
|
|
12
12
|
|
|
13
|
-
export type RunJSVersion = 'v1' | (string & {});
|
|
13
|
+
export type RunJSVersion = 'v1' | 'v2' | (string & {});
|
|
14
14
|
export type RunJSContextCtor = new (delegate: any) => any;
|
|
15
15
|
export type RunJSContextMeta = {
|
|
16
16
|
scenes?: string[];
|