@nocobase/flow-engine 2.0.33 → 2.0.35
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/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/reactive/observer.js +46 -16
- package/package.json +4 -4
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
package/lib/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/lib/locale/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export declare const locales: {
|
|
|
43
43
|
"Failed to destroy model after creation error": string;
|
|
44
44
|
"Failed to get action {{action}}": string;
|
|
45
45
|
"Failed to get configurable flows for model {{model}}": string;
|
|
46
|
+
"Attributes are unavailable before selecting a record": string;
|
|
46
47
|
"Failed to import FormDialog": string;
|
|
47
48
|
"Failed to import FormDialog or FormStep": string;
|
|
48
49
|
"Failed to import Formily components": string;
|
|
@@ -120,6 +121,7 @@ export declare const locales: {
|
|
|
120
121
|
"Failed to destroy model after creation error": string;
|
|
121
122
|
"Failed to get action {{action}}": string;
|
|
122
123
|
"Failed to get configurable flows for model {{model}}": string;
|
|
124
|
+
"Attributes are unavailable before selecting a record": string;
|
|
123
125
|
"Failed to import FormDialog": string;
|
|
124
126
|
"Failed to import FormDialog or FormStep": string;
|
|
125
127
|
"Failed to import Formily components": string;
|
package/lib/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 组件失败",
|
package/lib/reactive/observer.js
CHANGED
|
@@ -51,8 +51,31 @@ const observer = /* @__PURE__ */ __name((Component, options) => {
|
|
|
51
51
|
const ctxRef = (0, import_react.useRef)(ctx);
|
|
52
52
|
ctxRef.current = ctx;
|
|
53
53
|
const pendingDisposerRef = (0, import_react.useRef)(null);
|
|
54
|
+
const pendingTimerRef = (0, import_react.useRef)(null);
|
|
55
|
+
const clearPendingDisposer = /* @__PURE__ */ __name(() => {
|
|
56
|
+
if (pendingDisposerRef.current) {
|
|
57
|
+
pendingDisposerRef.current();
|
|
58
|
+
pendingDisposerRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
}, "clearPendingDisposer");
|
|
61
|
+
const clearPendingTimer = /* @__PURE__ */ __name(() => {
|
|
62
|
+
if (pendingTimerRef.current) {
|
|
63
|
+
clearTimeout(pendingTimerRef.current);
|
|
64
|
+
pendingTimerRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
}, "clearPendingTimer");
|
|
67
|
+
const isContextActive = /* @__PURE__ */ __name(() => {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
const pageActive = getPageActive(ctxRef.current);
|
|
70
|
+
const tabActive = (_b = (_a = ctxRef.current) == null ? void 0 : _a.tabActive) == null ? void 0 : _b.value;
|
|
71
|
+
return pageActive !== false && tabActive !== false;
|
|
72
|
+
}, "isContextActive");
|
|
54
73
|
(0, import_react.useEffect)(() => {
|
|
55
74
|
return () => {
|
|
75
|
+
if (pendingTimerRef.current) {
|
|
76
|
+
clearTimeout(pendingTimerRef.current);
|
|
77
|
+
pendingTimerRef.current = null;
|
|
78
|
+
}
|
|
56
79
|
if (pendingDisposerRef.current) {
|
|
57
80
|
pendingDisposerRef.current();
|
|
58
81
|
pendingDisposerRef.current = null;
|
|
@@ -62,30 +85,37 @@ const observer = /* @__PURE__ */ __name((Component, options) => {
|
|
|
62
85
|
const ObservedComponent = (0, import_react.useMemo)(
|
|
63
86
|
() => (0, import_reactive_react.observer)(Component, {
|
|
64
87
|
scheduler(updater) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
setTimeout(() => {
|
|
88
|
+
if (!isContextActive()) {
|
|
89
|
+
if (pendingTimerRef.current || pendingDisposerRef.current) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
pendingTimerRef.current = setTimeout(() => {
|
|
93
|
+
pendingTimerRef.current = null;
|
|
70
94
|
if (pendingDisposerRef.current) {
|
|
71
95
|
return;
|
|
72
96
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
if (isContextActive()) {
|
|
98
|
+
updater();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
pendingDisposerRef.current = (0, import_reactive.reaction)(
|
|
102
|
+
() => isContextActive(),
|
|
103
|
+
(active) => {
|
|
104
|
+
if (!active) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
clearPendingDisposer();
|
|
76
108
|
updater();
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "FlowObserverPendingUpdate"
|
|
79
112
|
}
|
|
80
|
-
|
|
81
|
-
pendingDisposerRef.current = disposer;
|
|
113
|
+
);
|
|
82
114
|
});
|
|
83
115
|
return;
|
|
84
116
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
pendingDisposerRef.current = null;
|
|
88
|
-
}
|
|
117
|
+
clearPendingTimer();
|
|
118
|
+
clearPendingDisposer();
|
|
89
119
|
updater();
|
|
90
120
|
},
|
|
91
121
|
...options
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.35",
|
|
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.
|
|
12
|
-
"@nocobase/shared": "2.0.
|
|
11
|
+
"@nocobase/sdk": "2.0.35",
|
|
12
|
+
"@nocobase/shared": "2.0.35",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "724e9b28057c9033b9d57bdbeda5a331c9dcc646"
|
|
41
41
|
}
|
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 组件失败",
|
|
@@ -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
|
},
|