@nocobase/flow-engine 2.0.0-beta.2 → 2.0.0-beta.20
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/BlockScopedFlowEngine.js +0 -1
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +2 -1
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +53 -1
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +11 -3
- package/lib/components/variables/VariableInput.js +8 -2
- package/lib/data-source/index.js +6 -0
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +156 -22
- package/lib/flowContext.d.ts +4 -1
- package/lib/flowContext.js +176 -107
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +38 -0
- package/lib/flowSettings.js +12 -10
- package/lib/index.d.ts +3 -0
- package/lib/index.js +16 -0
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.d.ts +7 -0
- package/lib/models/flowModel.js +66 -1
- package/lib/provider.js +7 -6
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +1 -0
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/base.js +10 -4
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +21 -21
- package/lib/types.d.ts +15 -0
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +10 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/safeGlobals.d.ts +5 -9
- package/lib/utils/safeGlobals.js +129 -17
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +8 -3
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +8 -1
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowSettings.open.test.tsx +69 -15
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/acl/Acl.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
- package/src/components/dnd/gridDragPlanner.ts +60 -0
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +11 -3
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +63 -4
- package/src/components/variables/VariableInput.tsx +8 -2
- package/src/data-source/index.ts +6 -0
- package/src/executor/FlowExecutor.ts +193 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +234 -118
- package/src/flowEngine.ts +41 -0
- package/src/flowSettings.ts +12 -11
- package/src/index.ts +10 -0
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +356 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +16 -0
- package/src/models/flowModel.tsx +94 -1
- package/src/provider.tsx +9 -7
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +8 -3
- package/src/runjs-context/contexts/base.ts +9 -2
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +23 -21
- package/src/types.ts +26 -1
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/safeGlobals.test.ts +49 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/safeGlobals.ts +133 -16
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +9 -2
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -139,6 +139,72 @@ describe('FlowExecutor', () => {
|
|
|
139
139
|
expect(submitHandler).not.toHaveBeenCalled();
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("dispatchEvent('click') skips instance flows when triggerByRouter is true", async () => {
|
|
143
|
+
class MyModel extends FlowModel {}
|
|
144
|
+
|
|
145
|
+
const globalHandler = vi.fn().mockResolvedValue('global-ok');
|
|
146
|
+
MyModel.registerFlow('globalClick', {
|
|
147
|
+
on: 'click',
|
|
148
|
+
steps: {
|
|
149
|
+
s: { handler: globalHandler },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const instanceHandler = vi.fn().mockResolvedValue('instance-ok');
|
|
154
|
+
const model = new MyModel({
|
|
155
|
+
uid: 'm-click-router-replay',
|
|
156
|
+
flowEngine: engine,
|
|
157
|
+
flowRegistry: {
|
|
158
|
+
instanceClick: {
|
|
159
|
+
on: 'click',
|
|
160
|
+
steps: {
|
|
161
|
+
s: { handler: instanceHandler },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
stepParams: {},
|
|
166
|
+
subModels: {},
|
|
167
|
+
} as FlowModelOptions);
|
|
168
|
+
|
|
169
|
+
await engine.executor.dispatchEvent(model, 'click', { triggerByRouter: true }, { sequential: true });
|
|
170
|
+
|
|
171
|
+
expect(globalHandler).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(instanceHandler).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("dispatchEvent('click') keeps instance flows when triggerByRouter is not true", async () => {
|
|
176
|
+
class MyModel extends FlowModel {}
|
|
177
|
+
|
|
178
|
+
const globalHandler = vi.fn().mockResolvedValue('global-ok');
|
|
179
|
+
MyModel.registerFlow('globalClick', {
|
|
180
|
+
on: 'click',
|
|
181
|
+
steps: {
|
|
182
|
+
s: { handler: globalHandler },
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const instanceHandler = vi.fn().mockResolvedValue('instance-ok');
|
|
187
|
+
const model = new MyModel({
|
|
188
|
+
uid: 'm-click-normal',
|
|
189
|
+
flowEngine: engine,
|
|
190
|
+
flowRegistry: {
|
|
191
|
+
instanceClick: {
|
|
192
|
+
on: 'click',
|
|
193
|
+
steps: {
|
|
194
|
+
s: { handler: instanceHandler },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
stepParams: {},
|
|
199
|
+
subModels: {},
|
|
200
|
+
} as FlowModelOptions);
|
|
201
|
+
|
|
202
|
+
await engine.executor.dispatchEvent(model, 'click', { triggerByRouter: false }, { sequential: true });
|
|
203
|
+
|
|
204
|
+
expect(globalHandler).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect(instanceHandler).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
142
208
|
it('dispatchEvent default parallel does not stop on exitAll', async () => {
|
|
143
209
|
const calls: string[] = [];
|
|
144
210
|
const mkFlow = (key: string, opts?: { exitAll?: boolean }) => ({
|
package/src/flowContext.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { APIClient } from '@nocobase/sdk';
|
|
|
13
13
|
import type { Router } from '@remix-run/router';
|
|
14
14
|
import { MessageInstance } from 'antd/es/message/interface';
|
|
15
15
|
import * as antd from 'antd';
|
|
16
|
-
import * as antdIcons from '@ant-design/icons';
|
|
17
16
|
import type { HookAPI } from 'antd/es/modal/useModal';
|
|
18
17
|
import { NotificationInstance } from 'antd/es/notification/interface';
|
|
19
18
|
import _ from 'lodash';
|
|
@@ -38,17 +37,23 @@ import {
|
|
|
38
37
|
extractPropertyPath,
|
|
39
38
|
extractUsedVariablePaths,
|
|
40
39
|
FlowExitException,
|
|
40
|
+
isCssFile,
|
|
41
|
+
prepareRunJsCode,
|
|
41
42
|
resolveDefaultParams,
|
|
42
43
|
resolveExpressions,
|
|
44
|
+
resolveModuleUrl,
|
|
43
45
|
} from './utils';
|
|
44
46
|
import { FlowExitAllException } from './utils/exceptions';
|
|
45
47
|
import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
|
46
48
|
import type { RecordRef } from './utils/serverContextParams';
|
|
47
49
|
import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
|
|
50
|
+
import { inferRecordRef } from './utils/variablesParams';
|
|
48
51
|
import { FlowView, FlowViewer } from './views/FlowView';
|
|
49
52
|
import { RunJSContextRegistry, getModelClassName } from './runjs-context/registry';
|
|
50
53
|
import { createEphemeralContext } from './utils/createEphemeralContext';
|
|
51
54
|
import dayjs from 'dayjs';
|
|
55
|
+
import { externalReactRender, setupRunJSLibs } from './runjsLibs';
|
|
56
|
+
import { runjsImportAsync, runjsImportModule, runjsRequireAsync } from './utils/runjsModuleLoader';
|
|
52
57
|
|
|
53
58
|
// Helper: detect a RecordRef-like object
|
|
54
59
|
function isRecordRefLike(val: any): boolean {
|
|
@@ -71,6 +76,61 @@ function filterBuilderOutputByPaths(built: any, neededPaths: string[]): any {
|
|
|
71
76
|
return undefined;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
// Helper: extract top-level segment of a subpath (e.g. 'a.b' -> 'a', 'tags[0].name' -> 'tags')
|
|
80
|
+
function topLevelOf(subPath: string): string | undefined {
|
|
81
|
+
if (!subPath) return undefined;
|
|
82
|
+
const m = String(subPath).match(/^([^.[]+)/);
|
|
83
|
+
return m?.[1];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Helper: infer selects (fields/appends) from usage paths (mirrors server-side inferSelectsFromUsage)
|
|
87
|
+
function inferSelectsFromUsage(paths: string[] = []): { generatedAppends?: string[]; generatedFields?: string[] } {
|
|
88
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
89
|
+
return { generatedAppends: undefined, generatedFields: undefined };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const appendSet = new Set<string>();
|
|
93
|
+
const fieldSet = new Set<string>();
|
|
94
|
+
|
|
95
|
+
const normalizePath = (raw: string): string => {
|
|
96
|
+
if (!raw) return '';
|
|
97
|
+
let s = String(raw);
|
|
98
|
+
// remove numeric indexes like [0]
|
|
99
|
+
s = s.replace(/\[(?:\d+)\]/g, '');
|
|
100
|
+
// normalize string indexes like ["name"] / ['name'] into .name
|
|
101
|
+
s = s.replace(/\[(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')\]/g, (_m, g1, g2) => `.${(g1 || g2) as string}`);
|
|
102
|
+
s = s.replace(/\.\.+/g, '.');
|
|
103
|
+
s = s.replace(/^\./, '').replace(/\.$/, '');
|
|
104
|
+
return s;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (let path of paths) {
|
|
108
|
+
if (!path) continue;
|
|
109
|
+
// drop leading numeric index like [0].name
|
|
110
|
+
while (/^\[(\d+)\](\.|$)/.test(path)) {
|
|
111
|
+
path = path.replace(/^\[(\d+)\]\.?/, '');
|
|
112
|
+
}
|
|
113
|
+
const norm = normalizePath(path);
|
|
114
|
+
if (!norm) continue;
|
|
115
|
+
const segments = norm.split('.').filter(Boolean);
|
|
116
|
+
if (segments.length === 0) continue;
|
|
117
|
+
|
|
118
|
+
if (segments.length === 1) {
|
|
119
|
+
fieldSet.add(segments[0]);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
124
|
+
appendSet.add(segments.slice(0, i + 1).join('.'));
|
|
125
|
+
}
|
|
126
|
+
fieldSet.add(segments.join('.'));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const generatedAppends = appendSet.size ? Array.from(appendSet) : undefined;
|
|
130
|
+
const generatedFields = fieldSet.size ? Array.from(fieldSet) : undefined;
|
|
131
|
+
return { generatedAppends, generatedFields };
|
|
132
|
+
}
|
|
133
|
+
|
|
74
134
|
type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>;
|
|
75
135
|
|
|
76
136
|
export interface MetaTreeNode {
|
|
@@ -78,6 +138,7 @@ export interface MetaTreeNode {
|
|
|
78
138
|
title: string;
|
|
79
139
|
type: string;
|
|
80
140
|
interface?: string;
|
|
141
|
+
options?: any;
|
|
81
142
|
uiSchema?: ISchema;
|
|
82
143
|
render?: (props: any) => JSX.Element;
|
|
83
144
|
// display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级
|
|
@@ -95,6 +156,7 @@ export interface PropertyMeta {
|
|
|
95
156
|
type: string;
|
|
96
157
|
title: string;
|
|
97
158
|
interface?: string;
|
|
159
|
+
options?: any;
|
|
98
160
|
uiSchema?: ISchema; // TODO: 这个是不是压根没必要啊?
|
|
99
161
|
render?: (props: any) => JSX.Element; // 自定义渲染函数
|
|
100
162
|
// 用于 VariableInput 的排序:数值越大,显示越靠前;相同值保持稳定顺序
|
|
@@ -679,6 +741,7 @@ export class FlowContext {
|
|
|
679
741
|
title: metaOrFactory.title || initialTitle, // 初始使用 name 作为 title
|
|
680
742
|
type: 'object', // 初始类型
|
|
681
743
|
interface: undefined,
|
|
744
|
+
options: undefined,
|
|
682
745
|
uiSchema: undefined,
|
|
683
746
|
paths,
|
|
684
747
|
parentTitles: parentTitles.length > 0 ? parentTitles : undefined,
|
|
@@ -710,6 +773,7 @@ export class FlowContext {
|
|
|
710
773
|
node.title = finalTitle;
|
|
711
774
|
node.type = meta?.type;
|
|
712
775
|
node.interface = meta?.interface;
|
|
776
|
+
node.options = meta?.options;
|
|
713
777
|
node.uiSchema = meta?.uiSchema;
|
|
714
778
|
// parentTitles 保持不变,因为它不包含自身 title
|
|
715
779
|
|
|
@@ -742,6 +806,7 @@ export class FlowContext {
|
|
|
742
806
|
title: nodeTitle,
|
|
743
807
|
type: metaOrFactory.type,
|
|
744
808
|
interface: metaOrFactory.interface,
|
|
809
|
+
options: metaOrFactory.options,
|
|
745
810
|
uiSchema: metaOrFactory.uiSchema,
|
|
746
811
|
paths,
|
|
747
812
|
parentTitles: parentTitles.length > 0 ? parentTitles : undefined,
|
|
@@ -908,7 +973,7 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
908
973
|
declare dataSourceManager: DataSourceManager;
|
|
909
974
|
declare requireAsync: (url: string) => Promise<any>;
|
|
910
975
|
declare importAsync: (url: string) => Promise<any>;
|
|
911
|
-
declare createJSRunner: (options?: JSRunnerOptions) => JSRunner
|
|
976
|
+
declare createJSRunner: (options?: JSRunnerOptions) => Promise<JSRunner>;
|
|
912
977
|
declare pageInfo: { version?: 'v1' | 'v2' };
|
|
913
978
|
/**
|
|
914
979
|
* @deprecated use `resolveJsonTemplate` instead
|
|
@@ -939,6 +1004,25 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
939
1004
|
declare location: Location;
|
|
940
1005
|
declare sql: FlowSQLRepository;
|
|
941
1006
|
declare logger: pino.Logger;
|
|
1007
|
+
|
|
1008
|
+
constructor() {
|
|
1009
|
+
super();
|
|
1010
|
+
this.defineMethod(
|
|
1011
|
+
'runjs',
|
|
1012
|
+
async function (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) {
|
|
1013
|
+
const { preprocessTemplates, ...runnerOptions } = options || {};
|
|
1014
|
+
const mergedGlobals = { ...(runnerOptions?.globals || {}), ...(variables || {}) };
|
|
1015
|
+
const runner = await this.createJSRunner({
|
|
1016
|
+
...(runnerOptions || {}),
|
|
1017
|
+
globals: mergedGlobals,
|
|
1018
|
+
});
|
|
1019
|
+
// Enable by default; use `preprocessTemplates: false` to explicitly disable.
|
|
1020
|
+
const shouldPreprocessTemplates = preprocessTemplates !== false;
|
|
1021
|
+
const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
|
|
1022
|
+
return runner.run(jsCode);
|
|
1023
|
+
},
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
942
1026
|
}
|
|
943
1027
|
|
|
944
1028
|
class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
@@ -987,14 +1071,6 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
987
1071
|
this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
|
|
988
1072
|
return i18n.translate(keyOrTemplate, options);
|
|
989
1073
|
});
|
|
990
|
-
this.defineMethod('runjs', async (code, variables, options?: JSRunnerOptions) => {
|
|
991
|
-
const mergedGlobals = { ...(options?.globals || {}), ...(variables || {}) };
|
|
992
|
-
const runner = (await (this as any).createJSRunner({
|
|
993
|
-
...(options || {}),
|
|
994
|
-
globals: mergedGlobals,
|
|
995
|
-
})) as JSRunner;
|
|
996
|
-
return runner.run(code);
|
|
997
|
-
});
|
|
998
1074
|
this.defineMethod('renderJson', function (template: any) {
|
|
999
1075
|
return this.resolveJsonTemplate(template);
|
|
1000
1076
|
});
|
|
@@ -1030,6 +1106,22 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1030
1106
|
const needServer = Object.keys(serverVarPaths).length > 0;
|
|
1031
1107
|
let serverResolved = template;
|
|
1032
1108
|
if (needServer) {
|
|
1109
|
+
const inferRecordRefWithMeta = (ctx: any): RecordRef | undefined => {
|
|
1110
|
+
const ref = inferRecordRef(ctx as any);
|
|
1111
|
+
if (ref) return ref as RecordRef;
|
|
1112
|
+
try {
|
|
1113
|
+
const tk = ctx?.resource?.getMeta?.('currentFilterByTk');
|
|
1114
|
+
if (typeof tk === 'undefined' || tk === null) return undefined;
|
|
1115
|
+
const collection =
|
|
1116
|
+
ctx?.collection?.name || ctx?.resource?.getResourceName?.()?.split?.('.')?.slice?.(-1)?.[0];
|
|
1117
|
+
if (!collection) return undefined;
|
|
1118
|
+
const dataSourceKey = ctx?.collection?.dataSourceKey || ctx?.resource?.getDataSourceKey?.();
|
|
1119
|
+
return { collection, dataSourceKey, filterByTk: tk } as RecordRef;
|
|
1120
|
+
} catch (_) {
|
|
1121
|
+
return undefined;
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1033
1125
|
const collectFromMeta = async (): Promise<Record<string, any>> => {
|
|
1034
1126
|
const out: Record<string, any> = {};
|
|
1035
1127
|
try {
|
|
@@ -1069,7 +1161,62 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1069
1161
|
};
|
|
1070
1162
|
|
|
1071
1163
|
const inputFromMeta = await collectFromMeta();
|
|
1072
|
-
const autoInput = { ...inputFromMeta }
|
|
1164
|
+
const autoInput = { ...inputFromMeta } as Record<string, any>;
|
|
1165
|
+
|
|
1166
|
+
// Special-case: formValues
|
|
1167
|
+
// If server needs to resolve some formValues paths but meta params only cover association anchors
|
|
1168
|
+
// (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
|
|
1169
|
+
// inject a top-level record anchor (formValues -> { collection, filterByTk, fields/appends }) so server can fetch DB values.
|
|
1170
|
+
// This anchor MUST be selective (fields/appends derived from serverVarPaths['formValues']) to avoid server overriding
|
|
1171
|
+
// client-only values for configured form fields in the same template.
|
|
1172
|
+
try {
|
|
1173
|
+
const varName = 'formValues';
|
|
1174
|
+
const neededPaths = serverVarPaths[varName] || [];
|
|
1175
|
+
if (neededPaths.length) {
|
|
1176
|
+
const requiredTop = new Set<string>();
|
|
1177
|
+
for (const p of neededPaths) {
|
|
1178
|
+
const top = topLevelOf(p);
|
|
1179
|
+
if (top) requiredTop.add(top);
|
|
1180
|
+
}
|
|
1181
|
+
const metaOut = inputFromMeta?.[varName];
|
|
1182
|
+
const builtTop = new Set<string>();
|
|
1183
|
+
if (metaOut && typeof metaOut === 'object' && !Array.isArray(metaOut) && !isRecordRefLike(metaOut)) {
|
|
1184
|
+
Object.keys(metaOut).forEach((k) => builtTop.add(k));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const missing = [...requiredTop].filter((k) => !builtTop.has(k));
|
|
1188
|
+
if (missing.length) {
|
|
1189
|
+
const ref = inferRecordRefWithMeta(this);
|
|
1190
|
+
if (ref) {
|
|
1191
|
+
const { generatedFields, generatedAppends } = inferSelectsFromUsage(neededPaths);
|
|
1192
|
+
const recordRef: RecordRef = {
|
|
1193
|
+
...ref,
|
|
1194
|
+
fields: generatedFields,
|
|
1195
|
+
appends: generatedAppends,
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// Preserve existing association anchors by lifting them to dotted keys before overwriting formValues
|
|
1199
|
+
const existing = autoInput[varName];
|
|
1200
|
+
if (
|
|
1201
|
+
existing &&
|
|
1202
|
+
typeof existing === 'object' &&
|
|
1203
|
+
!Array.isArray(existing) &&
|
|
1204
|
+
!isRecordRefLike(existing)
|
|
1205
|
+
) {
|
|
1206
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
1207
|
+
autoInput[`${varName}.${k}`] = v;
|
|
1208
|
+
}
|
|
1209
|
+
delete autoInput[varName];
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
autoInput[varName] = recordRef;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
} catch (_) {
|
|
1217
|
+
// ignore
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1073
1220
|
const autoContextParams = Object.keys(autoInput).length
|
|
1074
1221
|
? _buildServerContextParams(this, autoInput)
|
|
1075
1222
|
: undefined;
|
|
@@ -1181,7 +1328,8 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1181
1328
|
user: this.user,
|
|
1182
1329
|
}),
|
|
1183
1330
|
});
|
|
1184
|
-
this.defineMethod('loadCSS', async (
|
|
1331
|
+
this.defineMethod('loadCSS', async (href: string) => {
|
|
1332
|
+
const url = resolveModuleUrl(href);
|
|
1185
1333
|
return new Promise((resolve, reject) => {
|
|
1186
1334
|
// Check if CSS is already loaded
|
|
1187
1335
|
const existingLink = document.querySelector(`link[href="${url}"]`);
|
|
@@ -1199,53 +1347,19 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1199
1347
|
});
|
|
1200
1348
|
});
|
|
1201
1349
|
this.defineMethod('requireAsync', async (url: string) => {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
reject(new Error('requirejs is not available'));
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
this.requirejs(
|
|
1208
|
-
[url],
|
|
1209
|
-
(...args: any[]) => {
|
|
1210
|
-
resolve(args[0]);
|
|
1211
|
-
},
|
|
1212
|
-
reject,
|
|
1213
|
-
);
|
|
1214
|
-
});
|
|
1350
|
+
const u = resolveModuleUrl(url, { raw: true });
|
|
1351
|
+
return await runjsRequireAsync(this.requirejs, u);
|
|
1215
1352
|
});
|
|
1216
1353
|
// 动态按 URL 加载 ESM 模块
|
|
1217
1354
|
// - 使用 Vite / Webpack ignore 注释,避免被预打包或重写
|
|
1218
1355
|
// - 返回模块命名空间对象(包含 default 与命名导出)
|
|
1219
|
-
this.defineMethod('importAsync', async (url: string)
|
|
1220
|
-
|
|
1221
|
-
|
|
1356
|
+
this.defineMethod('importAsync', async function (this: any, url: string) {
|
|
1357
|
+
// 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
|
|
1358
|
+
if (isCssFile(url)) {
|
|
1359
|
+
return this.loadCSS(url);
|
|
1222
1360
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
g.__nocobaseImportAsyncCache = g.__nocobaseImportAsyncCache || new Map<string, Promise<any>>();
|
|
1226
|
-
const cache: Map<string, Promise<any>> = g.__nocobaseImportAsyncCache;
|
|
1227
|
-
if (cache.has(u)) return cache.get(u)!;
|
|
1228
|
-
// 尝试使用原生 dynamic import(加上 vite/webpack 的 ignore 注释)
|
|
1229
|
-
const nativeImport = () => import(/* @vite-ignore */ /* webpackIgnore: true */ u);
|
|
1230
|
-
// 兜底方案:通过 eval 在运行时构造 import,避免被打包器接管
|
|
1231
|
-
const evalImport = () => {
|
|
1232
|
-
const importer = (0, eval)('u => import(u)');
|
|
1233
|
-
return importer(u);
|
|
1234
|
-
};
|
|
1235
|
-
const p = (async () => {
|
|
1236
|
-
try {
|
|
1237
|
-
return await nativeImport();
|
|
1238
|
-
} catch (err: any) {
|
|
1239
|
-
// 常见于打包产物仍然拦截了 dynamic import 或开发态插件未识别 ignore 注释
|
|
1240
|
-
try {
|
|
1241
|
-
return await evalImport();
|
|
1242
|
-
} catch (err2) {
|
|
1243
|
-
throw err2 || err;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
})();
|
|
1247
|
-
cache.set(u, p);
|
|
1248
|
-
return p;
|
|
1361
|
+
|
|
1362
|
+
return await runjsImportModule(this, url, { importer: runjsImportAsync });
|
|
1249
1363
|
});
|
|
1250
1364
|
this.defineMethod('createJSRunner', async function (options?: JSRunnerOptions) {
|
|
1251
1365
|
try {
|
|
@@ -1254,16 +1368,10 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1254
1368
|
} catch (_) {
|
|
1255
1369
|
// ignore if setup is not available
|
|
1256
1370
|
}
|
|
1257
|
-
const version =
|
|
1371
|
+
const version = options?.version || 'v1';
|
|
1258
1372
|
const modelClass = getModelClassName(this);
|
|
1259
|
-
const Ctor =
|
|
1260
|
-
|
|
1261
|
-
(RunJSContextRegistry.resolve(version, '*') as any) ||
|
|
1262
|
-
FlowRunJSContext;
|
|
1263
|
-
let runCtx: any;
|
|
1264
|
-
if (Ctor) {
|
|
1265
|
-
runCtx = new Ctor(this);
|
|
1266
|
-
}
|
|
1373
|
+
const Ctor: new (delegate: any) => any = RunJSContextRegistry.resolve(version, modelClass) || FlowRunJSContext;
|
|
1374
|
+
const runCtx = new Ctor(this);
|
|
1267
1375
|
const globals: Record<string, any> = { ctx: runCtx, ...(options?.globals || {}) };
|
|
1268
1376
|
const { timeoutMs } = options || {};
|
|
1269
1377
|
return new JSRunner({ globals, timeoutMs });
|
|
@@ -1401,13 +1509,6 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1401
1509
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1402
1510
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1403
1511
|
});
|
|
1404
|
-
this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
|
|
1405
|
-
const runner = await this.createJSRunner({
|
|
1406
|
-
globals: variables,
|
|
1407
|
-
version: options?.version,
|
|
1408
|
-
});
|
|
1409
|
-
return runner.run(code);
|
|
1410
|
-
});
|
|
1411
1512
|
this.defineProperty('model', {
|
|
1412
1513
|
value: model,
|
|
1413
1514
|
});
|
|
@@ -1538,7 +1639,7 @@ export class FlowForkModelContext extends BaseFlowModelContext {
|
|
|
1538
1639
|
throw new Error('Invalid FlowModel instance');
|
|
1539
1640
|
}
|
|
1540
1641
|
super();
|
|
1541
|
-
this.addDelegate(
|
|
1642
|
+
this.addDelegate(this.master.context);
|
|
1542
1643
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1543
1644
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1544
1645
|
});
|
|
@@ -1553,13 +1654,6 @@ export class FlowForkModelContext extends BaseFlowModelContext {
|
|
|
1553
1654
|
return stableRef;
|
|
1554
1655
|
},
|
|
1555
1656
|
});
|
|
1556
|
-
this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
|
|
1557
|
-
const runner = await this.createJSRunner({
|
|
1558
|
-
globals: variables,
|
|
1559
|
-
version: options?.version,
|
|
1560
|
-
});
|
|
1561
|
-
return runner.run(code);
|
|
1562
|
-
});
|
|
1563
1657
|
}
|
|
1564
1658
|
}
|
|
1565
1659
|
|
|
@@ -1614,13 +1708,6 @@ export class FlowRuntimeContext<
|
|
|
1614
1708
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1615
1709
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1616
1710
|
});
|
|
1617
|
-
this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
|
|
1618
|
-
const runner = await this.createJSRunner({
|
|
1619
|
-
globals: variables,
|
|
1620
|
-
version: options?.version,
|
|
1621
|
-
});
|
|
1622
|
-
return runner.run(code);
|
|
1623
|
-
});
|
|
1624
1711
|
}
|
|
1625
1712
|
|
|
1626
1713
|
protected _getOwnProperty(key: string): any {
|
|
@@ -1717,6 +1804,7 @@ function __runjsDeepMerge(base: any, patch: any) {
|
|
|
1717
1804
|
}
|
|
1718
1805
|
return out;
|
|
1719
1806
|
}
|
|
1807
|
+
|
|
1720
1808
|
export class FlowRunJSContext extends FlowContext {
|
|
1721
1809
|
constructor(delegate: FlowContext) {
|
|
1722
1810
|
super();
|
|
@@ -1735,19 +1823,10 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1735
1823
|
return this.engine.reactView.createRoot(realContainer as HTMLElement, options);
|
|
1736
1824
|
},
|
|
1737
1825
|
};
|
|
1826
|
+
ReactDOMShim.__nbRunjsInternalShim = true;
|
|
1738
1827
|
this.defineProperty('ReactDOM', { value: ReactDOMShim });
|
|
1739
1828
|
|
|
1740
|
-
|
|
1741
|
-
// - 新增库应优先挂载到 ctx.libs.xxx
|
|
1742
|
-
// - 同时保留顶层别名(如 ctx.React / ctx.antd),以兼容历史代码
|
|
1743
|
-
const libs = Object.freeze({
|
|
1744
|
-
React,
|
|
1745
|
-
ReactDOM: ReactDOMShim,
|
|
1746
|
-
antd,
|
|
1747
|
-
dayjs,
|
|
1748
|
-
antdIcons,
|
|
1749
|
-
});
|
|
1750
|
-
this.defineProperty('libs', { value: libs });
|
|
1829
|
+
setupRunJSLibs(this);
|
|
1751
1830
|
|
|
1752
1831
|
// Convenience: ctx.render(<App />[, container])
|
|
1753
1832
|
// - container defaults to ctx.element if available
|
|
@@ -1767,16 +1846,37 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1767
1846
|
globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
|
|
1768
1847
|
const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
|
|
1769
1848
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1849
|
+
const disposeEntry = (entry: any) => {
|
|
1850
|
+
if (!entry) return;
|
|
1851
|
+
if (entry.disposeTheme && typeof entry.disposeTheme === 'function') {
|
|
1852
|
+
try {
|
|
1853
|
+
entry.disposeTheme();
|
|
1854
|
+
} catch (_) {
|
|
1855
|
+
// ignore
|
|
1856
|
+
}
|
|
1857
|
+
entry.disposeTheme = undefined;
|
|
1858
|
+
}
|
|
1859
|
+
const root = entry.root || entry;
|
|
1860
|
+
if (root && typeof root.unmount === 'function') {
|
|
1774
1861
|
try {
|
|
1775
|
-
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1862
|
+
root.unmount();
|
|
1863
|
+
} catch (_) {
|
|
1864
|
+
// ignore
|
|
1778
1865
|
}
|
|
1779
1866
|
}
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
const unmountContainerRoot = () => {
|
|
1870
|
+
const existing = rootMap.get(containerEl);
|
|
1871
|
+
if (existing) {
|
|
1872
|
+
disposeEntry(existing);
|
|
1873
|
+
rootMap.delete(containerEl);
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
// If vnode is string (HTML), unmount react root and set sanitized HTML
|
|
1878
|
+
if (typeof vnode === 'string') {
|
|
1879
|
+
unmountContainerRoot();
|
|
1780
1880
|
const proxy: any = new ElementProxy(containerEl);
|
|
1781
1881
|
proxy.innerHTML = String(vnode ?? '');
|
|
1782
1882
|
return null;
|
|
@@ -1788,26 +1888,42 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1788
1888
|
(vnode as any).nodeType &&
|
|
1789
1889
|
((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
|
|
1790
1890
|
) {
|
|
1791
|
-
|
|
1792
|
-
if (existingRoot && typeof existingRoot.unmount === 'function') {
|
|
1793
|
-
try {
|
|
1794
|
-
existingRoot.unmount();
|
|
1795
|
-
} finally {
|
|
1796
|
-
rootMap.delete(containerEl);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1891
|
+
unmountContainerRoot();
|
|
1799
1892
|
while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
|
|
1800
1893
|
containerEl.appendChild(vnode as any);
|
|
1801
1894
|
return null;
|
|
1802
1895
|
}
|
|
1803
1896
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1897
|
+
// 注意:rootMap 是“全局按容器复用”的(key=containerEl)。
|
|
1898
|
+
// 若不同 RunJS ctx 复用同一个 containerEl,且 ReactDOM 实例引用也相同,
|
|
1899
|
+
// 则会复用到旧 entry,进而复用旧 ctx 创建的 autorun(闭包捕获旧 ctx),造成:
|
|
1900
|
+
// 1) 旧 ctx 的 reaction 继续驱动新渲染(跨 ctx 复用风险)
|
|
1901
|
+
// 2) 新 ctx 的主题变化不再触发 rerender
|
|
1902
|
+
// 3) 旧 ctx 被 entry/autorun 间接持有,无法被 GC(内存泄漏)
|
|
1903
|
+
// 因此这里把 ownerKey(当前 ctx)也纳入复用判断;owner 变化时必须重建 entry。
|
|
1904
|
+
const rendererKey = this.ReactDOM;
|
|
1905
|
+
const ownerKey = this;
|
|
1906
|
+
let entry = rootMap.get(containerEl);
|
|
1907
|
+
if (!entry || entry.rendererKey !== rendererKey || entry.ownerKey !== ownerKey) {
|
|
1908
|
+
if (entry) {
|
|
1909
|
+
disposeEntry(entry);
|
|
1910
|
+
rootMap.delete(containerEl);
|
|
1911
|
+
}
|
|
1912
|
+
const root = this.ReactDOM.createRoot(containerEl);
|
|
1913
|
+
entry = { rendererKey, ownerKey, root, disposeTheme: undefined, lastVnode: undefined };
|
|
1914
|
+
rootMap.set(containerEl, entry);
|
|
1808
1915
|
}
|
|
1809
|
-
|
|
1810
|
-
return
|
|
1916
|
+
|
|
1917
|
+
return externalReactRender({
|
|
1918
|
+
ctx: this,
|
|
1919
|
+
entry,
|
|
1920
|
+
vnode,
|
|
1921
|
+
containerEl,
|
|
1922
|
+
rootMap,
|
|
1923
|
+
unmountContainerRoot,
|
|
1924
|
+
internalReact: React,
|
|
1925
|
+
internalAntd: antd,
|
|
1926
|
+
});
|
|
1811
1927
|
},
|
|
1812
1928
|
);
|
|
1813
1929
|
}
|
|
@@ -1828,7 +1944,7 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1828
1944
|
const self = this as any as Function;
|
|
1829
1945
|
let cacheForClass = __runjsDocCache.get(self);
|
|
1830
1946
|
const cacheKey = String(locale || 'default');
|
|
1831
|
-
if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey)
|
|
1947
|
+
if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey) as RunJSDocMeta;
|
|
1832
1948
|
const chain: Function[] = [];
|
|
1833
1949
|
let cur: any = self;
|
|
1834
1950
|
while (cur && cur.prototype) {
|
package/src/flowEngine.ts
CHANGED
|
@@ -119,6 +119,18 @@ export class FlowEngine {
|
|
|
119
119
|
|
|
120
120
|
private _resources = new Map<string, typeof FlowResource>();
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Data change registry used to coordinate "refresh on active" across view-scoped engines.
|
|
124
|
+
*
|
|
125
|
+
* Keyed by: dataSourceKey -> resourceName -> version.
|
|
126
|
+
* - mark: increments version
|
|
127
|
+
* - get: returns current version (default 0)
|
|
128
|
+
*
|
|
129
|
+
* NOTE: ViewScopedFlowEngine proxies delegate non-local fields/methods to parents, so this
|
|
130
|
+
* registry naturally lives on the root engine instance and is shared across the whole view stack.
|
|
131
|
+
*/
|
|
132
|
+
private _dataSourceDirtyVersions: Map<string, Map<string, number>> = new Map();
|
|
133
|
+
|
|
122
134
|
/**
|
|
123
135
|
* 引擎事件总线(目前用于模型生命周期等事件)。
|
|
124
136
|
* ViewScopedFlowEngine 持有自己的实例,实现作用域隔离。
|
|
@@ -198,6 +210,35 @@ export class FlowEngine {
|
|
|
198
210
|
}
|
|
199
211
|
}
|
|
200
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Mark a data source resource as "dirty" (changed).
|
|
215
|
+
* This is used by data blocks to decide whether to refresh when a view becomes active.
|
|
216
|
+
*/
|
|
217
|
+
public markDataSourceDirty(dataSourceKey: string, resourceName: string): number {
|
|
218
|
+
const dsKey = String(dataSourceKey || 'main');
|
|
219
|
+
const resName = String(resourceName || '');
|
|
220
|
+
if (!resName) return this.getDataSourceDirtyVersion(dsKey, resName);
|
|
221
|
+
|
|
222
|
+
const ds = this._dataSourceDirtyVersions.get(dsKey) || new Map<string, number>();
|
|
223
|
+
if (!this._dataSourceDirtyVersions.has(dsKey)) {
|
|
224
|
+
this._dataSourceDirtyVersions.set(dsKey, ds);
|
|
225
|
+
}
|
|
226
|
+
const next = (ds.get(resName) || 0) + 1;
|
|
227
|
+
ds.set(resName, next);
|
|
228
|
+
return next;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get current dirty version for a data source resource.
|
|
233
|
+
* Returns 0 when no writes have been recorded.
|
|
234
|
+
*/
|
|
235
|
+
public getDataSourceDirtyVersion(dataSourceKey: string, resourceName: string): number {
|
|
236
|
+
const dsKey = String(dataSourceKey || 'main');
|
|
237
|
+
const resName = String(resourceName || '');
|
|
238
|
+
if (!resName) return 0;
|
|
239
|
+
return this._dataSourceDirtyVersions.get(dsKey)?.get(resName) || 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
201
242
|
/** 在目标模型生命周期达成时执行操作(仅在 View 引擎本地存储计划) */
|
|
202
243
|
public scheduleModelOperation(
|
|
203
244
|
fromModelOrUid: FlowModel | string,
|