@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45
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/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- 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 +76 -11
- 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 +293 -52
- 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 +84 -0
- package/lib/data-source/index.js +259 -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 +46 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +392 -18
- 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 +81 -21
- 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.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +29 -5
- 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.d.ts +12 -2
- package/lib/views/ViewNavigation.js +28 -9
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- 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 +22 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +22 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +6 -5
- package/src/FlowContextProvider.tsx +9 -1
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -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__/flowEngine.removeModel.test.ts +47 -3
- 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 +99 -11
- 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 +194 -5
- 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 +332 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
- 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 +322 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +85 -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 +50 -3
- package/src/flowEngine.ts +449 -14
- 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__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +80 -37
- package/src/models/flowModel.tsx +122 -36
- 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 +28 -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 +47 -7
- 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 +46 -9
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +27 -3
- package/src/views/useDrawer.tsx +27 -3
- package/src/views/usePage.tsx +367 -179
|
@@ -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[];
|
|
@@ -41,17 +41,27 @@ export async function setupRunJSContexts() {
|
|
|
41
41
|
import('./contexts/JSCollectionActionRunJSContext'),
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
const registerBuiltins = (version: 'v1' | 'v2') => {
|
|
45
|
+
RunJSContextRegistry.register(version, '*', FlowRunJSContext);
|
|
46
|
+
RunJSContextRegistry.register(version, 'JSBlockModel', JSBlockRunJSContext, { scenes: ['block'] });
|
|
47
|
+
RunJSContextRegistry.register(version, 'JSFieldModel', JSFieldRunJSContext, { scenes: ['detail'] });
|
|
48
|
+
RunJSContextRegistry.register(version, 'JSEditableFieldModel', JSEditableFieldRunJSContext, { scenes: ['form'] });
|
|
49
|
+
RunJSContextRegistry.register(version, 'JSItemModel', JSItemRunJSContext, { scenes: ['form'] });
|
|
50
|
+
RunJSContextRegistry.register(version, 'JSItemActionModel', JSItemRunJSContext, { scenes: ['table'] });
|
|
51
|
+
RunJSContextRegistry.register(version, 'JSColumnModel', JSColumnRunJSContext, { scenes: ['table'] });
|
|
52
|
+
RunJSContextRegistry.register(version, 'FormJSFieldItemModel', FormJSFieldItemRunJSContext, { scenes: ['form'] });
|
|
53
|
+
RunJSContextRegistry.register(version, 'JSRecordActionModel', JSRecordActionRunJSContext, { scenes: ['table'] });
|
|
54
|
+
RunJSContextRegistry.register(version, 'JSCollectionActionModel', JSCollectionActionRunJSContext, {
|
|
55
|
+
scenes: ['table'],
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const versions: Array<'v1' | 'v2'> = ['v1', 'v2'];
|
|
60
|
+
for (const version of versions) {
|
|
61
|
+
registerBuiltins(version);
|
|
62
|
+
await applyRunJSContextContributions(version);
|
|
63
|
+
markRunJSContextsSetupDone(version);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
done = true;
|
|
56
|
-
markRunJSContextsSetupDone(v1);
|
|
57
67
|
}
|
|
@@ -49,6 +49,7 @@ const snippets: Record<string, RunJSSnippetLoader | undefined> = {
|
|
|
49
49
|
'scene/detail/status-tag': () => import('./scene/detail/status-tag.snippet'),
|
|
50
50
|
'scene/detail/relative-time': () => import('./scene/detail/relative-time.snippet'),
|
|
51
51
|
'scene/detail/percentage-bar': () => import('./scene/detail/percentage-bar.snippet'),
|
|
52
|
+
'scene/detail/set-field-style': () => import('./scene/detail/set-field-style.snippet'),
|
|
52
53
|
// scene/form
|
|
53
54
|
'scene/form/render-basic': () => import('./scene/form/render-basic.snippet'),
|
|
54
55
|
'scene/form/set-field-value': () => import('./scene/form/set-field-value.snippet'),
|
|
@@ -67,6 +68,7 @@ const snippets: Record<string, RunJSSnippetLoader | undefined> = {
|
|
|
67
68
|
'scene/table/iterate-selected-rows': () => import('./scene/table/iterate-selected-rows.snippet'),
|
|
68
69
|
'scene/table/destroy-selected': () => import('./scene/table/destroy-selected.snippet'),
|
|
69
70
|
'scene/table/export-selected-json': () => import('./scene/table/export-selected-json.snippet'),
|
|
71
|
+
'scene/table/set-cell-style': () => import('./scene/table/set-cell-style.snippet'),
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
export default snippets;
|
|
@@ -125,10 +127,19 @@ function normalizeScenes(def: any, key: string): string[] {
|
|
|
125
127
|
return [];
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
function normalizeSceneGroup(scene: string): string {
|
|
131
|
+
const mapping: Record<string, string> = {
|
|
132
|
+
detailFieldEvent: 'detail',
|
|
133
|
+
tableFieldEvent: 'table',
|
|
134
|
+
formFieldEvent: 'form',
|
|
135
|
+
};
|
|
136
|
+
return mapping[scene] || scene;
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
function computeGroups(def: any, key: string): string[] {
|
|
129
140
|
const scenes = normalizeScenes(def, key);
|
|
130
141
|
if (scenes.length) {
|
|
131
|
-
return scenes.map((scene) => `scene/${scene}`);
|
|
142
|
+
return Array.from(new Set(scenes.map((scene) => `scene/${normalizeSceneGroup(scene)}`)));
|
|
132
143
|
}
|
|
133
144
|
const parts = key.split('/');
|
|
134
145
|
if (!parts.length) return [];
|
|
@@ -0,0 +1,30 @@
|
|
|
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 type { SnippetModule } from '../../types';
|
|
11
|
+
const snippet: SnippetModule = {
|
|
12
|
+
contexts: ['*'],
|
|
13
|
+
scenes: ['detailFieldEvent', 'formFieldEvent'],
|
|
14
|
+
prefix: 'sn-item-style',
|
|
15
|
+
label: 'Set form item/details item style',
|
|
16
|
+
description: 'Customize form item and details item container styles',
|
|
17
|
+
locales: {
|
|
18
|
+
'zh-CN': {
|
|
19
|
+
label: '设置表单项/详情项样式',
|
|
20
|
+
description: '自定义表单项和详情项容器样式',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
content: `
|
|
24
|
+
ctx.model.props.style = {
|
|
25
|
+
background: 'red',
|
|
26
|
+
};
|
|
27
|
+
`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default snippet;
|
|
@@ -0,0 +1,34 @@
|
|
|
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 type { SnippetModule } from '../../types';
|
|
11
|
+
const snippet: SnippetModule = {
|
|
12
|
+
contexts: ['*'],
|
|
13
|
+
scenes: ['tableFieldEvent'],
|
|
14
|
+
prefix: 'sn-table-cell-style',
|
|
15
|
+
label: 'Set table cell style',
|
|
16
|
+
description: 'Customize table field cell styles with onCell',
|
|
17
|
+
locales: {
|
|
18
|
+
'zh-CN': {
|
|
19
|
+
label: '表格字段样式设置',
|
|
20
|
+
description: '通过 onCell 自定义表格字段单元格样式',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
content: `
|
|
24
|
+
ctx.model.props.onCell = (record, rowIndex) => {
|
|
25
|
+
return {
|
|
26
|
+
style: {
|
|
27
|
+
background: 'red',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default snippet;
|
|
@@ -21,7 +21,11 @@ type LifecycleType =
|
|
|
21
21
|
| `event:${string}:end`
|
|
22
22
|
| `event:${string}:error`;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
type EventPredicateWhen = ((e: LifecycleEvent) => boolean) & {
|
|
25
|
+
__eventType?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ScheduleWhen = LifecycleType | EventPredicateWhen;
|
|
25
29
|
|
|
26
30
|
export interface ScheduleOptions {
|
|
27
31
|
when?: ScheduleWhen;
|
|
@@ -37,6 +41,7 @@ export interface LifecycleEvent {
|
|
|
37
41
|
error?: any;
|
|
38
42
|
inputArgs?: Record<string, any>;
|
|
39
43
|
result?: any;
|
|
44
|
+
aborted?: boolean;
|
|
40
45
|
flowKey?: string;
|
|
41
46
|
stepKey?: string;
|
|
42
47
|
}
|
|
@@ -216,8 +221,14 @@ export class ModelOperationScheduler {
|
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
private ensureEventSubscriptionIfNeeded(when?: ScheduleWhen) {
|
|
219
|
-
|
|
220
|
-
|
|
224
|
+
const eventType =
|
|
225
|
+
typeof when === 'string'
|
|
226
|
+
? when
|
|
227
|
+
: typeof when === 'function'
|
|
228
|
+
? (when as EventPredicateWhen).__eventType
|
|
229
|
+
: undefined;
|
|
230
|
+
if (!eventType) return;
|
|
231
|
+
const parsed = this.parseEventWhen(eventType as ScheduleWhen);
|
|
221
232
|
if (!parsed) return;
|
|
222
233
|
const { name } = parsed;
|
|
223
234
|
if (this.subscribedEventNames.has(name)) return;
|
package/src/types.ts
CHANGED
|
@@ -145,6 +145,8 @@ export enum ActionScene {
|
|
|
145
145
|
ACTION_LINKAGE_RULES,
|
|
146
146
|
/** 动态事件流可用 */
|
|
147
147
|
DYNAMIC_EVENT_FLOW,
|
|
148
|
+
/** 菜单项联动规则可用 */
|
|
149
|
+
MENU_LINKAGE_RULES,
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
@@ -213,6 +215,7 @@ export interface ActionDefinition<TModel extends FlowModel = FlowModel, TCtx ext
|
|
|
213
215
|
*/
|
|
214
216
|
export type FlowEventName =
|
|
215
217
|
| 'click'
|
|
218
|
+
| 'close'
|
|
216
219
|
| 'submit'
|
|
217
220
|
| 'reset'
|
|
218
221
|
| 'remove'
|
|
@@ -387,6 +390,65 @@ export interface CreateModelOptions {
|
|
|
387
390
|
delegateToParent?: boolean;
|
|
388
391
|
[key: string]: any; // 允许额外的自定义选项
|
|
389
392
|
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* FlowModel loader result.
|
|
396
|
+
* Supports returning the model constructor directly, a default export, or a module object containing the named export.
|
|
397
|
+
*/
|
|
398
|
+
export type FlowModelLoaderResult =
|
|
399
|
+
| ModelConstructor
|
|
400
|
+
| {
|
|
401
|
+
default?: ModelConstructor;
|
|
402
|
+
[key: string]: unknown;
|
|
403
|
+
}
|
|
404
|
+
| Record<string, unknown>;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* FlowModel loader function.
|
|
408
|
+
*/
|
|
409
|
+
export type FlowModelLoader = () => Promise<FlowModelLoaderResult>;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* FlowModel loader entry (normalized internal form).
|
|
413
|
+
*/
|
|
414
|
+
export interface FlowModelLoaderEntry {
|
|
415
|
+
loader: FlowModelLoader;
|
|
416
|
+
extends?: string[];
|
|
417
|
+
// meta?: Partial<FlowModelMeta>;
|
|
418
|
+
// scenes?: string[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* FlowModel loader input (user-facing form for registerModelLoaders).
|
|
423
|
+
* The `extends` field accepts flexible formats that will be normalized to `string[]` at registration time.
|
|
424
|
+
*/
|
|
425
|
+
export interface FlowModelLoaderInput {
|
|
426
|
+
loader: FlowModelLoader;
|
|
427
|
+
extends?: string | ModelConstructor | (string | ModelConstructor)[];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* FlowModel loader entry map (normalized internal form).
|
|
432
|
+
*/
|
|
433
|
+
export type FlowModelLoaderMap = Record<string, FlowModelLoaderEntry>;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* FlowModel loader input map (user-facing form for registerModelLoaders).
|
|
437
|
+
*/
|
|
438
|
+
export type FlowModelLoaderInputMap = Record<string, FlowModelLoaderInput>;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Batch ensure result.
|
|
442
|
+
*/
|
|
443
|
+
export interface EnsureBatchResult {
|
|
444
|
+
requested: string[];
|
|
445
|
+
loaded: string[];
|
|
446
|
+
failed: Array<{
|
|
447
|
+
name: string;
|
|
448
|
+
error?: unknown;
|
|
449
|
+
}>;
|
|
450
|
+
}
|
|
451
|
+
|
|
390
452
|
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
|
|
391
453
|
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
|
|
392
454
|
save(model: T, options?: { onlyStepParams?: boolean }): Promise<Record<string, any>>;
|
|
@@ -26,6 +26,7 @@ describe('createCollectionContextMeta', () => {
|
|
|
26
26
|
{ name: 'id', type: 'integer', interface: 'number', filterable: true },
|
|
27
27
|
{ name: 'email', type: 'string', interface: 'text', filterable: true },
|
|
28
28
|
{ name: 'nickname', type: 'string', interface: 'text' }, // 未声明 filterable
|
|
29
|
+
{ name: 'rawUserPayload', type: 'json', filterable: true },
|
|
29
30
|
],
|
|
30
31
|
});
|
|
31
32
|
|
|
@@ -34,6 +35,7 @@ describe('createCollectionContextMeta', () => {
|
|
|
34
35
|
fields: [
|
|
35
36
|
{ name: 'title', type: 'string', interface: 'text', filterable: true },
|
|
36
37
|
{ name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
|
|
38
|
+
{ name: 'rawPostPayload', type: 'json', filterable: true },
|
|
37
39
|
],
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -44,8 +46,54 @@ describe('createCollectionContextMeta', () => {
|
|
|
44
46
|
const authorMeta: any = props?.author;
|
|
45
47
|
const authorFields = await authorMeta?.properties?.();
|
|
46
48
|
|
|
49
|
+
expect(props).toHaveProperty('title');
|
|
50
|
+
expect(props).toHaveProperty('author');
|
|
51
|
+
expect(props).not.toHaveProperty('rawPostPayload');
|
|
47
52
|
expect(authorFields).toBeTruthy();
|
|
48
53
|
expect(authorFields).toHaveProperty('email');
|
|
49
54
|
expect(authorFields).not.toHaveProperty('nickname');
|
|
55
|
+
expect(authorFields).not.toHaveProperty('rawUserPayload');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('keeps interfaced non-filterable fields but hides fields without interface when includeNonFilterable is true', async () => {
|
|
59
|
+
const engine = new FlowEngine();
|
|
60
|
+
const dm = engine.dataSourceManager as any;
|
|
61
|
+
dm.collectionFieldInterfaceManager = new CollectionFieldInterfaceManager([], {}, dm);
|
|
62
|
+
engine.context.defineProperty('app', { value: { dataSourceManager: dm } });
|
|
63
|
+
const ds = dm.getDataSource('main')!;
|
|
64
|
+
|
|
65
|
+
ds.addCollection({
|
|
66
|
+
name: 'users',
|
|
67
|
+
fields: [
|
|
68
|
+
{ name: 'id', type: 'integer', interface: 'number', filterable: true },
|
|
69
|
+
{ name: 'email', type: 'string', interface: 'text', filterable: true },
|
|
70
|
+
{ name: 'nickname', type: 'string', interface: 'text' },
|
|
71
|
+
{ name: 'rawUserPayload', type: 'json', filterable: true },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
ds.addCollection({
|
|
76
|
+
name: 'posts',
|
|
77
|
+
fields: [
|
|
78
|
+
{ name: 'title', type: 'string', interface: 'text', filterable: true },
|
|
79
|
+
{ name: 'internalName', type: 'string', interface: 'text' },
|
|
80
|
+
{ name: 'rawPostPayload', type: 'json', filterable: true },
|
|
81
|
+
{ name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const posts = ds.getCollection('posts')!;
|
|
86
|
+
const metaFactory = createCollectionContextMeta(posts, 'Posts', true);
|
|
87
|
+
const meta = await metaFactory();
|
|
88
|
+
const props = await (meta?.properties as any)?.();
|
|
89
|
+
const authorFields = await props?.author?.properties?.();
|
|
90
|
+
|
|
91
|
+
expect(props).toHaveProperty('title');
|
|
92
|
+
expect(props).toHaveProperty('internalName');
|
|
93
|
+
expect(props).toHaveProperty('author');
|
|
94
|
+
expect(props).not.toHaveProperty('rawPostPayload');
|
|
95
|
+
expect(authorFields).toHaveProperty('email');
|
|
96
|
+
expect(authorFields).toHaveProperty('nickname');
|
|
97
|
+
expect(authorFields).not.toHaveProperty('rawUserPayload');
|
|
50
98
|
});
|
|
51
99
|
});
|
|
@@ -102,6 +102,27 @@ describe('parsePathnameToViewParams', () => {
|
|
|
102
102
|
expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }]);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
test('should parse custom root prefix', () => {
|
|
106
|
+
const result = parsePathnameToViewParams('/embed/xxx/tab/yyy/view/zzz', { rootPrefix: 'embed' });
|
|
107
|
+
expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }, { viewUid: 'zzz' }]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should parse pathname by basePath', () => {
|
|
111
|
+
const result = parsePathnameToViewParams('/embed/xxx/tab/yyy/view/zzz', { basePath: '/embed' });
|
|
112
|
+
expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }, { viewUid: 'zzz' }]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should parse pathname by nested basePath', () => {
|
|
116
|
+
const result = parsePathnameToViewParams('/admin/settings/public-forms/xxx/view/zzz', {
|
|
117
|
+
basePath: '/admin/settings/public-forms',
|
|
118
|
+
});
|
|
119
|
+
expect(result).toEqual([{ viewUid: 'xxx' }, { viewUid: 'zzz' }]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should keep admin as default root prefix', () => {
|
|
123
|
+
expect(parsePathnameToViewParams('/embed/xxx')).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
105
126
|
test('should parse filterByTk from key-value encoded segment into object', () => {
|
|
106
127
|
const kv = encodeURIComponent('id=1&tenant=ac');
|
|
107
128
|
const path = `/admin/xxx/filterbytk/${kv}`;
|
|
@@ -109,6 +130,13 @@ describe('parsePathnameToViewParams', () => {
|
|
|
109
130
|
expect(result).toEqual([{ viewUid: 'xxx', filterByTk: { id: '1', tenant: 'ac' } }]);
|
|
110
131
|
});
|
|
111
132
|
|
|
133
|
+
test('should parse filterByTk from single key-value encoded segment into object', () => {
|
|
134
|
+
const kv = encodeURIComponent('id=1');
|
|
135
|
+
const path = `/admin/xxx/filterbytk/${kv}`;
|
|
136
|
+
const result = parsePathnameToViewParams(path);
|
|
137
|
+
expect(result).toEqual([{ viewUid: 'xxx', filterByTk: { id: '1' } }]);
|
|
138
|
+
});
|
|
139
|
+
|
|
112
140
|
test('should parse filterByTk from JSON object segment', () => {
|
|
113
141
|
const json = encodeURIComponent('{"id":"1","tenant":"ac"}');
|
|
114
142
|
const path = `/admin/xxx/filterbytk/${json}`;
|
|
@@ -41,4 +41,15 @@ describe('runjsValue utils', () => {
|
|
|
41
41
|
expect(out.someVar).toContain('');
|
|
42
42
|
expect(out.user).toContain('name');
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
it('extractUsedVariablePathsFromRunJS: extracts ctx.getVar string paths', () => {
|
|
46
|
+
const code = `
|
|
47
|
+
const phone = await ctx.getVar('ctx.item.value.phone');
|
|
48
|
+
const assignee = await ctx.getVar("ctx.user.profile.name");
|
|
49
|
+
return [phone, assignee];
|
|
50
|
+
`;
|
|
51
|
+
const out = extractUsedVariablePathsFromRunJS(code);
|
|
52
|
+
expect(out.item).toContain('value.phone');
|
|
53
|
+
expect(out.user).toContain('profile.name');
|
|
54
|
+
});
|
|
44
55
|
});
|