@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.
Files changed (124) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/JSRunner.d.ts +6 -0
  3. package/lib/JSRunner.js +2 -1
  4. package/lib/ViewScopedFlowEngine.js +3 -0
  5. package/lib/acl/Acl.js +13 -3
  6. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  7. package/lib/components/dnd/gridDragPlanner.js +53 -1
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +11 -3
  10. package/lib/components/variables/VariableInput.js +8 -2
  11. package/lib/data-source/index.js +6 -0
  12. package/lib/executor/FlowExecutor.d.ts +2 -1
  13. package/lib/executor/FlowExecutor.js +156 -22
  14. package/lib/flowContext.d.ts +4 -1
  15. package/lib/flowContext.js +176 -107
  16. package/lib/flowEngine.d.ts +21 -0
  17. package/lib/flowEngine.js +38 -0
  18. package/lib/flowSettings.js +12 -10
  19. package/lib/index.d.ts +3 -0
  20. package/lib/index.js +16 -0
  21. package/lib/models/CollectionFieldModel.d.ts +1 -0
  22. package/lib/models/CollectionFieldModel.js +3 -2
  23. package/lib/models/flowModel.d.ts +7 -0
  24. package/lib/models/flowModel.js +66 -1
  25. package/lib/provider.js +7 -6
  26. package/lib/resources/baseRecordResource.d.ts +5 -0
  27. package/lib/resources/baseRecordResource.js +24 -0
  28. package/lib/resources/multiRecordResource.d.ts +1 -0
  29. package/lib/resources/multiRecordResource.js +11 -4
  30. package/lib/resources/singleRecordResource.js +2 -0
  31. package/lib/resources/sqlResource.d.ts +1 -0
  32. package/lib/resources/sqlResource.js +8 -3
  33. package/lib/runjs-context/contexts/base.js +10 -4
  34. package/lib/runjsLibs.d.ts +28 -0
  35. package/lib/runjsLibs.js +532 -0
  36. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  37. package/lib/scheduler/ModelOperationScheduler.js +21 -21
  38. package/lib/types.d.ts +15 -0
  39. package/lib/utils/createCollectionContextMeta.js +1 -0
  40. package/lib/utils/index.d.ts +2 -0
  41. package/lib/utils/index.js +10 -0
  42. package/lib/utils/params-resolvers.js +16 -9
  43. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  44. package/lib/utils/resolveModuleUrl.js +65 -0
  45. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  46. package/lib/utils/runjsModuleLoader.js +422 -0
  47. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  48. package/lib/utils/runjsTemplateCompat.js +743 -0
  49. package/lib/utils/safeGlobals.d.ts +5 -9
  50. package/lib/utils/safeGlobals.js +129 -17
  51. package/lib/views/createViewMeta.d.ts +0 -7
  52. package/lib/views/createViewMeta.js +19 -70
  53. package/lib/views/index.d.ts +1 -2
  54. package/lib/views/index.js +4 -3
  55. package/lib/views/useDialog.js +8 -3
  56. package/lib/views/useDrawer.js +7 -2
  57. package/lib/views/usePage.d.ts +4 -0
  58. package/lib/views/usePage.js +43 -6
  59. package/lib/views/usePopover.js +4 -1
  60. package/lib/views/viewEvents.d.ts +17 -0
  61. package/lib/views/viewEvents.js +90 -0
  62. package/package.json +4 -4
  63. package/src/BlockScopedFlowEngine.ts +2 -5
  64. package/src/JSRunner.ts +8 -1
  65. package/src/ViewScopedFlowEngine.ts +4 -0
  66. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  67. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  68. package/src/__tests__/flowSettings.open.test.tsx +69 -15
  69. package/src/__tests__/provider.test.tsx +0 -5
  70. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  71. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  72. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  73. package/src/acl/Acl.tsx +3 -3
  74. package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
  75. package/src/components/dnd/gridDragPlanner.ts +60 -0
  76. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  77. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  78. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +11 -3
  79. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +63 -4
  80. package/src/components/variables/VariableInput.tsx +8 -2
  81. package/src/data-source/index.ts +6 -0
  82. package/src/executor/FlowExecutor.ts +193 -23
  83. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  84. package/src/flowContext.ts +234 -118
  85. package/src/flowEngine.ts +41 -0
  86. package/src/flowSettings.ts +12 -11
  87. package/src/index.ts +10 -0
  88. package/src/models/CollectionFieldModel.tsx +3 -1
  89. package/src/models/__tests__/dispatchEvent.when.test.ts +356 -0
  90. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  91. package/src/models/__tests__/flowModel.test.ts +16 -0
  92. package/src/models/flowModel.tsx +94 -1
  93. package/src/provider.tsx +9 -7
  94. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  95. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  96. package/src/resources/baseRecordResource.ts +31 -0
  97. package/src/resources/multiRecordResource.ts +11 -4
  98. package/src/resources/singleRecordResource.ts +3 -0
  99. package/src/resources/sqlResource.ts +8 -3
  100. package/src/runjs-context/contexts/base.ts +9 -2
  101. package/src/runjsLibs.ts +622 -0
  102. package/src/scheduler/ModelOperationScheduler.ts +23 -21
  103. package/src/types.ts +26 -1
  104. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  105. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  106. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  107. package/src/utils/__tests__/safeGlobals.test.ts +49 -2
  108. package/src/utils/createCollectionContextMeta.ts +1 -0
  109. package/src/utils/index.ts +6 -0
  110. package/src/utils/params-resolvers.ts +23 -9
  111. package/src/utils/resolveModuleUrl.ts +91 -0
  112. package/src/utils/runjsModuleLoader.ts +553 -0
  113. package/src/utils/runjsTemplateCompat.ts +828 -0
  114. package/src/utils/safeGlobals.ts +133 -16
  115. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  116. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  117. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  118. package/src/views/createViewMeta.ts +22 -75
  119. package/src/views/index.tsx +1 -2
  120. package/src/views/useDialog.tsx +9 -2
  121. package/src/views/useDrawer.tsx +8 -1
  122. package/src/views/usePage.tsx +51 -5
  123. package/src/views/usePopover.tsx +4 -1
  124. 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 }) => ({
@@ -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 (url: string) => {
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
- return new Promise((resolve, reject) => {
1203
- if (!this.requirejs) {
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
- if (!url || typeof url !== 'string') {
1221
- throw new Error('invalid url');
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
- const u = url.trim();
1224
- const g = globalThis as any;
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 = (options?.version as any) || 'v1';
1371
+ const version = options?.version || 'v1';
1258
1372
  const modelClass = getModelClassName(this);
1259
- const Ctor =
1260
- (RunJSContextRegistry.resolve(version, modelClass) as any) ||
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((this.master as any).context);
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
- // 为第三方/通用库提供统一命名空间:ctx.libs
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
- // If vnode is string (HTML), unmount react root and set sanitized HTML
1771
- if (typeof vnode === 'string') {
1772
- const existingRoot = rootMap.get(containerEl);
1773
- if (existingRoot && typeof existingRoot.unmount === 'function') {
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
- existingRoot.unmount();
1776
- } finally {
1777
- rootMap.delete(containerEl);
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
- const existingRoot = rootMap.get(containerEl);
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
- let root = rootMap.get(containerEl);
1805
- if (!root) {
1806
- root = this.ReactDOM.createRoot(containerEl);
1807
- rootMap.set(containerEl, root);
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
- root.render(vnode as any);
1810
- return root;
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,