@nocobase/flow-engine 2.1.0-alpha.3 → 2.1.0-alpha.30
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/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +601 -21
- package/lib/components/dnd/index.d.ts +19 -1
- package/lib/components/dnd/index.js +243 -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 +68 -10
- 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/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 +2 -2
- package/lib/data-source/index.d.ts +73 -0
- package/lib/data-source/index.js +211 -1
- package/lib/executor/FlowExecutor.js +31 -8
- package/lib/flowContext.d.ts +2 -0
- package/lib/flowContext.js +31 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowI18n.js +2 -1
- 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/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 +78 -18
- 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 +47 -1
- package/lib/utils/createCollectionContextMeta.js +6 -2
- 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__/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/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- 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 +512 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +743 -19
- package/src/components/dnd/index.tsx +291 -27
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
- 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 +189 -3
- 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/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +1 -1
- package/src/data-source/__tests__/index.test.ts +34 -1
- package/src/data-source/index.ts +258 -2
- package/src/executor/FlowExecutor.ts +34 -9
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flowContext.ts +37 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowI18n.ts +2 -1
- 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/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__/flowModel.test.ts +19 -3
- package/src/models/flowModel.tsx +119 -33
- 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 +60 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -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/createCollectionContextMeta.ts +6 -2
- 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
package/lib/views/useDrawer.js
CHANGED
|
@@ -52,6 +52,7 @@ var import_viewEvents = require("./viewEvents");
|
|
|
52
52
|
var import_provider = require("../provider");
|
|
53
53
|
var import_ViewScopedFlowEngine = require("../ViewScopedFlowEngine");
|
|
54
54
|
var import_variablesParams = require("../utils/variablesParams");
|
|
55
|
+
var import_runViewBeforeClose = require("./runViewBeforeClose");
|
|
55
56
|
function useDrawer() {
|
|
56
57
|
const holderRef = React.useRef(null);
|
|
57
58
|
const drawerList = React.useMemo(() => import__.observable.shallow({ value: [] }), []);
|
|
@@ -122,12 +123,16 @@ function useDrawer() {
|
|
|
122
123
|
} else {
|
|
123
124
|
ctx.addDelegate(flowContext.engine.context);
|
|
124
125
|
}
|
|
126
|
+
let destroyed = false;
|
|
125
127
|
const currentDrawer = {
|
|
126
128
|
type: "drawer",
|
|
127
129
|
inputArgs: config.inputArgs || {},
|
|
128
130
|
preventClose: !!config.preventClose,
|
|
131
|
+
beforeClose: void 0,
|
|
129
132
|
destroy: /* @__PURE__ */ __name((result) => {
|
|
130
133
|
var _a2, _b2, _c, _d;
|
|
134
|
+
if (destroyed) return;
|
|
135
|
+
destroyed = true;
|
|
131
136
|
(_a2 = config.onClose) == null ? void 0 : _a2.call(config);
|
|
132
137
|
(_b2 = drawerRef.current) == null ? void 0 : _b2.destroy();
|
|
133
138
|
closeFunc == null ? void 0 : closeFunc();
|
|
@@ -141,16 +146,21 @@ function useDrawer() {
|
|
|
141
146
|
var _a2;
|
|
142
147
|
return (_a2 = drawerRef.current) == null ? void 0 : _a2.update(newConfig);
|
|
143
148
|
}, "update"),
|
|
144
|
-
close: /* @__PURE__ */ __name((result, force) => {
|
|
149
|
+
close: /* @__PURE__ */ __name(async (result, force) => {
|
|
145
150
|
var _a2, _b2;
|
|
146
151
|
if (config.preventClose && !force) {
|
|
147
|
-
return;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const shouldClose = await (0, import_runViewBeforeClose.runViewBeforeClose)(currentDrawer, { result, force });
|
|
155
|
+
if (!shouldClose) {
|
|
156
|
+
return false;
|
|
148
157
|
}
|
|
149
158
|
if (config.triggerByRouter && ((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.navigation) == null ? void 0 : _b2.back)) {
|
|
150
159
|
config.inputArgs.navigation.back();
|
|
151
|
-
return;
|
|
160
|
+
return true;
|
|
152
161
|
}
|
|
153
162
|
currentDrawer.destroy(result);
|
|
163
|
+
return true;
|
|
154
164
|
}, "close"),
|
|
155
165
|
Footer: FooterComponent,
|
|
156
166
|
Header: HeaderComponent,
|
|
@@ -173,6 +183,13 @@ function useDrawer() {
|
|
|
173
183
|
get: /* @__PURE__ */ __name(() => currentDrawer, "get"),
|
|
174
184
|
resolveOnServer: (0, import_variablesParams.createViewRecordResolveOnServer)(ctx, () => (0, import_variablesParams.getViewRecordFromParent)(flowContext, ctx))
|
|
175
185
|
});
|
|
186
|
+
scopedEngine.setDestroyView(() => {
|
|
187
|
+
var _a2, _b2;
|
|
188
|
+
if (config.triggerByRouter && ((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.navigation) == null ? void 0 : _b2.back)) {
|
|
189
|
+
config.inputArgs.navigation.back();
|
|
190
|
+
}
|
|
191
|
+
currentDrawer.destroy();
|
|
192
|
+
});
|
|
176
193
|
(0, import_createViewMeta.registerPopupVariable)(ctx, currentDrawer);
|
|
177
194
|
const DrawerWithContext = React.memo(
|
|
178
195
|
(0, import__.observer)((props) => {
|
package/lib/views/usePage.d.ts
CHANGED
|
@@ -16,9 +16,10 @@ export declare function usePage(): (React.JSX.Element | {
|
|
|
16
16
|
type: "embed";
|
|
17
17
|
inputArgs: any;
|
|
18
18
|
preventClose: boolean;
|
|
19
|
+
beforeClose: any;
|
|
19
20
|
destroy: (result?: any) => void;
|
|
20
21
|
update: (newConfig: any) => void;
|
|
21
|
-
close: (result?: any, force?: boolean) =>
|
|
22
|
+
close: (result?: any, force?: boolean) => Promise<boolean>;
|
|
22
23
|
Header: React.FC<{
|
|
23
24
|
title?: React.ReactNode;
|
|
24
25
|
extra?: React.ReactNode;
|
package/lib/views/usePage.js
CHANGED
|
@@ -54,6 +54,7 @@ var import_viewEvents = require("./viewEvents");
|
|
|
54
54
|
var import_provider = require("../provider");
|
|
55
55
|
var import_ViewScopedFlowEngine = require("../ViewScopedFlowEngine");
|
|
56
56
|
var import_variablesParams = require("../utils/variablesParams");
|
|
57
|
+
var import_runViewBeforeClose = require("./runViewBeforeClose");
|
|
57
58
|
let uuid = 0;
|
|
58
59
|
const GLOBAL_EMBED_CONTAINER_ID = "nocobase-embed-container";
|
|
59
60
|
const EMBED_REPLACING_DATA_KEY = "nocobaseEmbedReplacing";
|
|
@@ -131,6 +132,7 @@ function usePage() {
|
|
|
131
132
|
type: "embed",
|
|
132
133
|
inputArgs: viewInputArgs,
|
|
133
134
|
preventClose: !!config.preventClose,
|
|
135
|
+
beforeClose: void 0,
|
|
134
136
|
destroy: /* @__PURE__ */ __name((result) => {
|
|
135
137
|
var _a2, _b2, _c2, _d, _e;
|
|
136
138
|
(_a2 = config.onClose) == null ? void 0 : _a2.call(config);
|
|
@@ -152,16 +154,21 @@ function usePage() {
|
|
|
152
154
|
var _a2;
|
|
153
155
|
return (_a2 = pageRef.current) == null ? void 0 : _a2.update(newConfig);
|
|
154
156
|
}, "update"),
|
|
155
|
-
close: /* @__PURE__ */ __name((result, force) => {
|
|
157
|
+
close: /* @__PURE__ */ __name(async (result, force) => {
|
|
156
158
|
var _a2, _b2;
|
|
157
159
|
if (preventClose && !force) {
|
|
158
|
-
return;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const shouldClose = await (0, import_runViewBeforeClose.runViewBeforeClose)(currentPage, { result, force });
|
|
163
|
+
if (!shouldClose) {
|
|
164
|
+
return false;
|
|
159
165
|
}
|
|
160
166
|
if (config.triggerByRouter && ((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.navigation) == null ? void 0 : _b2.back)) {
|
|
161
167
|
config.inputArgs.navigation.back();
|
|
162
|
-
return;
|
|
168
|
+
return true;
|
|
163
169
|
}
|
|
164
170
|
currentPage.destroy(result);
|
|
171
|
+
return true;
|
|
165
172
|
}, "close"),
|
|
166
173
|
Header: HeaderComponent,
|
|
167
174
|
Footer: FooterComponent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.30",
|
|
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,9 +8,10 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.1.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.1.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-alpha.30",
|
|
12
|
+
"@nocobase/shared": "2.1.0-alpha.30",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
|
+
"axios": "^1.7.0",
|
|
14
15
|
"dayjs": "^1.11.9",
|
|
15
16
|
"dompurify": "^3.0.2",
|
|
16
17
|
"lodash": "^4.x",
|
|
@@ -35,6 +36,6 @@
|
|
|
35
36
|
"workflow"
|
|
36
37
|
],
|
|
37
38
|
"author": "NocoBase Team",
|
|
38
|
-
"license": "
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"license": "Apache-2.0",
|
|
40
|
+
"gitHead": "292ae0ad87f195ed201b274902d21ecd96f5ddd0"
|
|
40
41
|
}
|
package/src/JSRunner.ts
CHANGED
|
@@ -16,12 +16,74 @@ export interface JSRunnerOptions {
|
|
|
16
16
|
version?: string;
|
|
17
17
|
/**
|
|
18
18
|
* Enable RunJS template compatibility preprocessing for `{{ ... }}`.
|
|
19
|
-
* When enabled
|
|
19
|
+
* When enabled (or falling back to version default),
|
|
20
20
|
* the code will be rewritten to call `ctx.resolveJsonTemplate(...)` at runtime.
|
|
21
21
|
*/
|
|
22
22
|
preprocessTemplates?: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Decide whether RunJS `{{ ... }}` compatibility preprocessing should run.
|
|
27
|
+
*
|
|
28
|
+
* Priority:
|
|
29
|
+
* 1. Explicit `preprocessTemplates` option always wins.
|
|
30
|
+
* 2. Otherwise, `version === 'v2'` disables preprocessing.
|
|
31
|
+
* 3. Fallback keeps v1-compatible behavior (enabled).
|
|
32
|
+
*/
|
|
33
|
+
export function shouldPreprocessRunJSTemplates(
|
|
34
|
+
options?: Pick<JSRunnerOptions, 'preprocessTemplates' | 'version'>,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (typeof options?.preprocessTemplates === 'boolean') {
|
|
37
|
+
return options.preprocessTemplates;
|
|
38
|
+
}
|
|
39
|
+
return options?.version !== 'v2';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Heuristic: detect likely bare `{{ctx.xxx}}` usage in executable positions (not quoted string literals).
|
|
43
|
+
const BARE_CTX_TEMPLATE_RE = /(^|[=(:,[\s)])(\{\{\s*(ctx(?:\.|\[|\?\.)[^}]*)\s*\}\})/m;
|
|
44
|
+
|
|
45
|
+
function extractDeprecatedCtxTemplateUsage(code: string): { placeholder: string; expression: string } | null {
|
|
46
|
+
const src = String(code || '');
|
|
47
|
+
const m = src.match(BARE_CTX_TEMPLATE_RE);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
const placeholder = String(m[2] || '').trim();
|
|
50
|
+
const expression = String(m[3] || '').trim();
|
|
51
|
+
if (!placeholder || !expression) return null;
|
|
52
|
+
return { placeholder, expression };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shouldHintCtxTemplateSyntax(err: any, usage: { placeholder: string; expression: string } | null): boolean {
|
|
56
|
+
const isSyntaxError = err instanceof SyntaxError || String((err as any)?.name || '') === 'SyntaxError';
|
|
57
|
+
if (!isSyntaxError) return false;
|
|
58
|
+
if (!usage) return false;
|
|
59
|
+
const msg = String((err as any)?.message || err || '');
|
|
60
|
+
return /unexpected token/i.test(msg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toCtxTemplateSyntaxHintError(
|
|
64
|
+
err: any,
|
|
65
|
+
usage: {
|
|
66
|
+
placeholder: string;
|
|
67
|
+
expression: string;
|
|
68
|
+
},
|
|
69
|
+
): Error {
|
|
70
|
+
const hint = `"${usage.placeholder}" has been deprecated and cannot be used as executable RunJS syntax. Use await ctx.getVar("${usage.expression}") instead, or keep "${usage.placeholder}" as a plain string.`;
|
|
71
|
+
const out = new SyntaxError(hint);
|
|
72
|
+
try {
|
|
73
|
+
(out as any).cause = err;
|
|
74
|
+
} catch (_) {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
// Hint-only error: avoid leaking internal bundle line numbers from stack parsers in preview UI.
|
|
79
|
+
(out as any).__runjsHideLocation = true;
|
|
80
|
+
out.stack = `${out.name}: ${out.message}`;
|
|
81
|
+
} catch (_) {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
25
87
|
export class JSRunner {
|
|
26
88
|
private globals: Record<string, any>;
|
|
27
89
|
private timeoutMs: number;
|
|
@@ -118,11 +180,13 @@ export class JSRunner {
|
|
|
118
180
|
if (err instanceof FlowExitAllException) {
|
|
119
181
|
throw err;
|
|
120
182
|
}
|
|
121
|
-
|
|
183
|
+
const usage = extractDeprecatedCtxTemplateUsage(code);
|
|
184
|
+
const outErr = shouldHintCtxTemplateSyntax(err, usage) && usage ? toCtxTemplateSyntaxHintError(err, usage) : err;
|
|
185
|
+
console.error(outErr);
|
|
122
186
|
return {
|
|
123
187
|
success: false,
|
|
124
|
-
error:
|
|
125
|
-
timeout:
|
|
188
|
+
error: outErr,
|
|
189
|
+
timeout: (outErr as any)?.message === 'Execution timed out',
|
|
126
190
|
};
|
|
127
191
|
}
|
|
128
192
|
}
|
|
@@ -62,6 +62,10 @@ export function createViewScopedEngine(parent: FlowEngine): FlowEngine {
|
|
|
62
62
|
'_nextEngine',
|
|
63
63
|
// getModel 需要在本地执行以确保全局查找时正确遍历整个引擎栈
|
|
64
64
|
'getModel',
|
|
65
|
+
// 视图销毁回调需要在本地存储,每个视图引擎有自己的销毁逻辑
|
|
66
|
+
'_destroyView',
|
|
67
|
+
'setDestroyView',
|
|
68
|
+
'destroyView',
|
|
65
69
|
]);
|
|
66
70
|
|
|
67
71
|
const handler: ProxyHandler<FlowEngine> = {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
-
import { JSRunner } from '../JSRunner';
|
|
11
|
+
import { JSRunner, shouldPreprocessRunJSTemplates } from '../JSRunner';
|
|
12
12
|
import { createSafeWindow } from '../utils';
|
|
13
13
|
|
|
14
14
|
describe('JSRunner', () => {
|
|
@@ -30,6 +30,18 @@ describe('JSRunner', () => {
|
|
|
30
30
|
vi.restoreAllMocks();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it('shouldPreprocessRunJSTemplates: explicit option has highest priority', () => {
|
|
34
|
+
expect(shouldPreprocessRunJSTemplates({ version: 'v2', preprocessTemplates: true })).toBe(true);
|
|
35
|
+
expect(shouldPreprocessRunJSTemplates({ version: 'v1', preprocessTemplates: false })).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('shouldPreprocessRunJSTemplates: falls back to version policy', () => {
|
|
39
|
+
expect(shouldPreprocessRunJSTemplates({ version: 'v1' })).toBe(true);
|
|
40
|
+
expect(shouldPreprocessRunJSTemplates({ version: 'v2' })).toBe(false);
|
|
41
|
+
expect(shouldPreprocessRunJSTemplates({})).toBe(true);
|
|
42
|
+
expect(shouldPreprocessRunJSTemplates()).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
33
45
|
it('executes simple code and returns value', async () => {
|
|
34
46
|
const runner = new JSRunner();
|
|
35
47
|
const result = await runner.run('return 1 + 2 + 3');
|
|
@@ -152,6 +164,20 @@ describe('JSRunner', () => {
|
|
|
152
164
|
expect((result.error as Error).message).toBe('Execution timed out');
|
|
153
165
|
});
|
|
154
166
|
|
|
167
|
+
it('returns friendly hint when bare {{ctx.xxx}} appears in syntax error', async () => {
|
|
168
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
169
|
+
const runner = new JSRunner();
|
|
170
|
+
const result = await runner.run('const z = {{ctx.user.id}}');
|
|
171
|
+
expect(result.success).toBe(false);
|
|
172
|
+
expect(result.error).toBeInstanceOf(SyntaxError);
|
|
173
|
+
const msg = String((result.error as any)?.message || '');
|
|
174
|
+
expect(msg).toContain('"{{ctx.user.id}}" has been deprecated');
|
|
175
|
+
expect(msg).toContain('await ctx.getVar("ctx.user.id")');
|
|
176
|
+
expect(msg).not.toContain('(at ');
|
|
177
|
+
expect((result.error as any)?.__runjsHideLocation).toBe(true);
|
|
178
|
+
expect(spy).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
155
181
|
it('skips execution when URL contains skipRunJs=true', async () => {
|
|
156
182
|
// 模拟预览模式下通过 URL 参数跳过代码执行
|
|
157
183
|
if (typeof window !== 'undefined' && typeof window.history?.pushState === 'function') {
|
|
@@ -189,4 +189,170 @@ describe('FlowEngine', () => {
|
|
|
189
189
|
expect(mounted?.uid).toBe('c3');
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
describe('getSubclassesOfAsync', () => {
|
|
194
|
+
it('should return async-loaded subclasses matching extends declaration', async () => {
|
|
195
|
+
class AsyncSubModelD extends BaseModel {}
|
|
196
|
+
class AsyncSubModelE extends BaseModel {}
|
|
197
|
+
|
|
198
|
+
engine.registerModelLoaders({
|
|
199
|
+
AsyncSubModelD: {
|
|
200
|
+
extends: 'BaseModel',
|
|
201
|
+
loader: async () => ({ AsyncSubModelD }),
|
|
202
|
+
},
|
|
203
|
+
AsyncSubModelE: {
|
|
204
|
+
extends: 'BaseModel',
|
|
205
|
+
loader: async () => ({ AsyncSubModelE }),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
210
|
+
|
|
211
|
+
// Sync-registered subclasses
|
|
212
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
213
|
+
expect(result.has('SubModelB')).toBe(true);
|
|
214
|
+
expect(result.has('SubModelC')).toBe(true);
|
|
215
|
+
// Async-loaded subclasses
|
|
216
|
+
expect(result.has('AsyncSubModelD')).toBe(true);
|
|
217
|
+
expect(result.has('AsyncSubModelE')).toBe(true);
|
|
218
|
+
// Base class excluded
|
|
219
|
+
expect(result.has('BaseModel')).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should merge sync-registered and async-loaded subclasses', async () => {
|
|
223
|
+
class AsyncSubModel extends BaseModel {}
|
|
224
|
+
|
|
225
|
+
engine.registerModelLoaders({
|
|
226
|
+
AsyncSubModel: {
|
|
227
|
+
extends: 'BaseModel',
|
|
228
|
+
loader: async () => ({ AsyncSubModel }),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await engine.getSubclassesOfAsync('BaseModel');
|
|
233
|
+
|
|
234
|
+
// Sync: SubModelA, SubModelB, SubModelC
|
|
235
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
236
|
+
expect(result.has('SubModelB')).toBe(true);
|
|
237
|
+
expect(result.has('SubModelC')).toBe(true);
|
|
238
|
+
// Async
|
|
239
|
+
expect(result.has('AsyncSubModel')).toBe(true);
|
|
240
|
+
expect(result.size).toBe(4);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should support extends as string array (multiple parents)', async () => {
|
|
244
|
+
class AnotherBase extends FlowModel {}
|
|
245
|
+
class MultiParentModel extends BaseModel {}
|
|
246
|
+
|
|
247
|
+
engine.registerModels({ AnotherBase });
|
|
248
|
+
engine.registerModelLoaders({
|
|
249
|
+
MultiParentModel: {
|
|
250
|
+
extends: ['BaseModel', 'AnotherBase'],
|
|
251
|
+
loader: async () => ({ MultiParentModel }),
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const resultBase = await engine.getSubclassesOfAsync(BaseModel);
|
|
256
|
+
expect(resultBase.has('MultiParentModel')).toBe(true);
|
|
257
|
+
|
|
258
|
+
// Also found by AnotherBase (even though actual inheritance is from BaseModel, not AnotherBase)
|
|
259
|
+
// The extends declaration triggers loading, but isInheritedFrom validation will exclude it from AnotherBase results
|
|
260
|
+
const resultAnother = await engine.getSubclassesOfAsync(AnotherBase);
|
|
261
|
+
expect(resultAnother.has('MultiParentModel')).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should support extends as ModelConstructor', async () => {
|
|
265
|
+
class AsyncCtorSubModel extends BaseModel {}
|
|
266
|
+
|
|
267
|
+
engine.registerModelLoaders({
|
|
268
|
+
AsyncCtorSubModel: {
|
|
269
|
+
extends: BaseModel,
|
|
270
|
+
loader: async () => ({ AsyncCtorSubModel }),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
275
|
+
expect(result.has('AsyncCtorSubModel')).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should validate actual inheritance and warn on mismatch', async () => {
|
|
279
|
+
class UnrelatedModel extends FlowModel {}
|
|
280
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
281
|
+
|
|
282
|
+
engine.registerModelLoaders({
|
|
283
|
+
UnrelatedModel: {
|
|
284
|
+
extends: 'BaseModel',
|
|
285
|
+
loader: async () => ({ UnrelatedModel }),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
290
|
+
expect(result.has('UnrelatedModel')).toBe(false);
|
|
291
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
292
|
+
expect.stringContaining("declares extends 'BaseModel' but does not actually inherit from it"),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
warnSpy.mockRestore();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should resolve base class from loaders if not in _modelClasses', async () => {
|
|
299
|
+
const freshEngine = new FlowEngine();
|
|
300
|
+
|
|
301
|
+
class LazyBase extends FlowModel {}
|
|
302
|
+
class LazySub extends LazyBase {}
|
|
303
|
+
|
|
304
|
+
freshEngine.registerModelLoaders({
|
|
305
|
+
LazyBase: {
|
|
306
|
+
loader: async () => ({ LazyBase }),
|
|
307
|
+
},
|
|
308
|
+
LazySub: {
|
|
309
|
+
extends: 'LazyBase',
|
|
310
|
+
loader: async () => ({ LazySub }),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await freshEngine.getSubclassesOfAsync('LazyBase');
|
|
315
|
+
expect(result.has('LazySub')).toBe(true);
|
|
316
|
+
expect(result.size).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should return empty Map when base class cannot be found', async () => {
|
|
320
|
+
const result = await engine.getSubclassesOfAsync('NonExistentModel');
|
|
321
|
+
expect(result.size).toBe(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should support filter parameter on both sync and async sources', async () => {
|
|
325
|
+
class FilteredAsyncModel extends BaseModel {}
|
|
326
|
+
|
|
327
|
+
engine.registerModelLoaders({
|
|
328
|
+
FilteredAsyncModel: {
|
|
329
|
+
extends: 'BaseModel',
|
|
330
|
+
loader: async () => ({ FilteredAsyncModel }),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const result = await engine.getSubclassesOfAsync(BaseModel, (_ModelClass, name) => name.startsWith('SubModelA'));
|
|
335
|
+
|
|
336
|
+
// Only SubModelA passes the filter (SubModelB, SubModelC, FilteredAsyncModel excluded)
|
|
337
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
338
|
+
expect(result.has('SubModelB')).toBe(false);
|
|
339
|
+
expect(result.has('SubModelC')).toBe(false);
|
|
340
|
+
expect(result.has('FilteredAsyncModel')).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should not include loaders without extends declaration', async () => {
|
|
344
|
+
class NoExtendsModel extends BaseModel {}
|
|
345
|
+
|
|
346
|
+
engine.registerModelLoaders({
|
|
347
|
+
NoExtendsModel: {
|
|
348
|
+
loader: async () => ({ NoExtendsModel }),
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
353
|
+
// Only sync-registered subclasses; NoExtendsModel has no extends, so not discovered
|
|
354
|
+
expect(result.has('NoExtendsModel')).toBe(false);
|
|
355
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
192
358
|
});
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
11
12
|
import { FlowContext, FlowRuntimeContext, FlowRunJSContext, type PropertyMetaFactory } from '../flowContext';
|
|
12
13
|
import { FlowEngine } from '../flowEngine';
|
|
13
14
|
import { FlowModel } from '../models/flowModel';
|
|
@@ -1630,6 +1631,69 @@ describe('runAction delegation from runtime context', () => {
|
|
|
1630
1631
|
});
|
|
1631
1632
|
});
|
|
1632
1633
|
|
|
1634
|
+
describe('FlowContext request defaults', () => {
|
|
1635
|
+
class RequestModel extends FlowModel {}
|
|
1636
|
+
|
|
1637
|
+
afterEach(() => {
|
|
1638
|
+
vi.restoreAllMocks();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
const createRequestContext = () => {
|
|
1642
|
+
const engine = new FlowEngine();
|
|
1643
|
+
engine.registerModels({ RequestModel });
|
|
1644
|
+
|
|
1645
|
+
const apiRequest = vi.fn(async (options) => options);
|
|
1646
|
+
const app = {
|
|
1647
|
+
getApiUrl(pathname = '') {
|
|
1648
|
+
return 'https://app.example.com/api/'.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, '');
|
|
1649
|
+
},
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
engine.context.defineProperty('api', { value: { request: apiRequest } as any });
|
|
1653
|
+
engine.context.defineProperty('app', { value: app });
|
|
1654
|
+
|
|
1655
|
+
const model = engine.createModel({ use: 'RequestModel' });
|
|
1656
|
+
const ctx = new FlowRuntimeContext(model, 'flow');
|
|
1657
|
+
const directAxiosRequest = vi.spyOn(axios, 'request').mockResolvedValue({ data: {} } as any);
|
|
1658
|
+
|
|
1659
|
+
return { ctx, apiRequest, directAxiosRequest };
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
it.each([
|
|
1663
|
+
['apiClient', 'users:list', 'api'],
|
|
1664
|
+
['apiClient', '/api/users:list', 'api'],
|
|
1665
|
+
['apiClient', 'https://app.example.com/api/users:list', 'api'],
|
|
1666
|
+
['direct axios', 'https://app.example.com/custom-api/users', 'axios'],
|
|
1667
|
+
])('should use %s for %s', async (_target, url, expected) => {
|
|
1668
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1669
|
+
|
|
1670
|
+
await ctx.request({ url, method: 'get' });
|
|
1671
|
+
|
|
1672
|
+
if (expected === 'api') {
|
|
1673
|
+
expect(apiRequest).toHaveBeenCalledTimes(1);
|
|
1674
|
+
expect(directAxiosRequest).not.toHaveBeenCalled();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1679
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it('should use direct axios for cross-origin absolute urls', async () => {
|
|
1683
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1684
|
+
|
|
1685
|
+
await ctx.request({ url: 'https://api.example.com/users', method: 'get', skipAuth: false });
|
|
1686
|
+
|
|
1687
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1688
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1689
|
+
expect(directAxiosRequest.mock.calls[0][0]).toMatchObject({
|
|
1690
|
+
url: 'https://api.example.com/users',
|
|
1691
|
+
method: 'get',
|
|
1692
|
+
skipAuth: false,
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1633
1697
|
describe('FlowContext delayed meta loading', () => {
|
|
1634
1698
|
// 测试场景:属性定义时 meta 为异步函数,首次访问时延迟加载
|
|
1635
1699
|
// 输入:属性带有异步 meta 函数
|