@nocobase/flow-engine 2.0.0-beta.8 → 2.0.0

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 (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. package/src/views/viewEvents.ts +55 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export type RunJSValue = {
11
+ code: string;
12
+ version?: string;
13
+ };
14
+
15
+ const RUNJS_ALLOWED_KEYS = new Set(['code', 'version']);
16
+
17
+ /**
18
+ * Strictly detect RunJSValue to avoid conflicting with normal constant objects.
19
+ * - MUST be a plain object (not array)
20
+ * - MUST include string `code`
21
+ * - MAY include string `version`
22
+ * - MUST NOT include any other enumerable keys
23
+ */
24
+ export function isRunJSValue(value: any): value is RunJSValue {
25
+ if (!value || typeof value !== 'object') return false;
26
+ if (Array.isArray(value)) return false;
27
+ const keys = Object.keys(value);
28
+ if (!keys.includes('code')) return false;
29
+ if (typeof (value as any).code !== 'string') return false;
30
+ if ('version' in value && (value as any).version != null && typeof (value as any).version !== 'string') return false;
31
+ for (const k of keys) {
32
+ if (!RUNJS_ALLOWED_KEYS.has(k)) return false;
33
+ }
34
+ return true;
35
+ }
36
+
37
+ export function normalizeRunJSValue(value: RunJSValue): Required<RunJSValue> {
38
+ return {
39
+ code: String(value?.code ?? ''),
40
+ version: String(value?.version ?? 'v1'),
41
+ };
42
+ }
43
+
44
+ function stripStringsAndComments(code: string): string {
45
+ // Keep template literals untouched (may include ${} expressions).
46
+ let out = '';
47
+ let state: 'code' | 'single' | 'double' | 'line' | 'block' = 'code';
48
+ for (let i = 0; i < code.length; i++) {
49
+ const ch = code[i];
50
+ const next = i + 1 < code.length ? code[i + 1] : '';
51
+
52
+ if (state === 'code') {
53
+ if (ch === '/' && next === '/') {
54
+ out += ' ';
55
+ i++;
56
+ state = 'line';
57
+ continue;
58
+ }
59
+ if (ch === '/' && next === '*') {
60
+ out += ' ';
61
+ i++;
62
+ state = 'block';
63
+ continue;
64
+ }
65
+ if (ch === "'") {
66
+ out += ' ';
67
+ state = 'single';
68
+ continue;
69
+ }
70
+ if (ch === '"') {
71
+ out += ' ';
72
+ state = 'double';
73
+ continue;
74
+ }
75
+ out += ch;
76
+ continue;
77
+ }
78
+
79
+ if (state === 'line') {
80
+ if (ch === '\n') {
81
+ out += '\n';
82
+ state = 'code';
83
+ } else {
84
+ out += ' ';
85
+ }
86
+ continue;
87
+ }
88
+
89
+ if (state === 'block') {
90
+ if (ch === '*' && next === '/') {
91
+ out += ' ';
92
+ i++;
93
+ state = 'code';
94
+ } else {
95
+ out += ch === '\n' ? '\n' : ' ';
96
+ }
97
+ continue;
98
+ }
99
+
100
+ if (state === 'single') {
101
+ if (ch === '\\') {
102
+ out += ' ';
103
+ i++;
104
+ continue;
105
+ }
106
+ if (ch === "'") {
107
+ out += ' ';
108
+ state = 'code';
109
+ } else {
110
+ out += ch === '\n' ? '\n' : ' ';
111
+ }
112
+ continue;
113
+ }
114
+
115
+ // state === 'double'
116
+ if (ch === '\\') {
117
+ out += ' ';
118
+ i++;
119
+ continue;
120
+ }
121
+ if (ch === '"') {
122
+ out += ' ';
123
+ state = 'code';
124
+ } else {
125
+ out += ch === '\n' ? '\n' : ' ';
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+
131
+ function stripComments(code: string): string {
132
+ let out = '';
133
+ let state: 'code' | 'single' | 'double' | 'line' | 'block' = 'code';
134
+ for (let i = 0; i < code.length; i++) {
135
+ const ch = code[i];
136
+ const next = i + 1 < code.length ? code[i + 1] : '';
137
+
138
+ if (state === 'code') {
139
+ if (ch === '/' && next === '/') {
140
+ out += ' ';
141
+ i++;
142
+ state = 'line';
143
+ continue;
144
+ }
145
+ if (ch === '/' && next === '*') {
146
+ out += ' ';
147
+ i++;
148
+ state = 'block';
149
+ continue;
150
+ }
151
+ if (ch === "'") {
152
+ out += ch;
153
+ state = 'single';
154
+ continue;
155
+ }
156
+ if (ch === '"') {
157
+ out += ch;
158
+ state = 'double';
159
+ continue;
160
+ }
161
+ out += ch;
162
+ continue;
163
+ }
164
+
165
+ if (state === 'line') {
166
+ if (ch === '\n') {
167
+ out += '\n';
168
+ state = 'code';
169
+ } else {
170
+ out += ' ';
171
+ }
172
+ continue;
173
+ }
174
+
175
+ if (state === 'block') {
176
+ if (ch === '*' && next === '/') {
177
+ out += ' ';
178
+ i++;
179
+ state = 'code';
180
+ } else {
181
+ out += ch === '\n' ? '\n' : ' ';
182
+ }
183
+ continue;
184
+ }
185
+
186
+ if (state === 'single') {
187
+ out += ch;
188
+ if (ch === '\\') {
189
+ const nextCh = i + 1 < code.length ? code[i + 1] : '';
190
+ if (nextCh) {
191
+ out += nextCh;
192
+ i++;
193
+ }
194
+ continue;
195
+ }
196
+ if (ch === "'") {
197
+ state = 'code';
198
+ }
199
+ continue;
200
+ }
201
+
202
+ // state === 'double'
203
+ out += ch;
204
+ if (ch === '\\') {
205
+ const nextCh = i + 1 < code.length ? code[i + 1] : '';
206
+ if (nextCh) {
207
+ out += nextCh;
208
+ i++;
209
+ }
210
+ continue;
211
+ }
212
+ if (ch === '"') {
213
+ state = 'code';
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+
219
+ function normalizeSubPath(raw: string): { subPath: string; wildcard: boolean } {
220
+ if (!raw) return { subPath: '', wildcard: false };
221
+ let s = raw;
222
+ // Convert simple string literal keys: ['a'] / ["a"] -> .a
223
+ s = s.replace(/\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]\]/g, '.$1');
224
+
225
+ // Any remaining bracket access with non-numeric content is considered dynamic -> wildcard.
226
+ const bracketRe = /\[([^\]]+)\]/g;
227
+ let m: RegExpExecArray | null;
228
+ while ((m = bracketRe.exec(s))) {
229
+ const inner = String(m[1] ?? '').trim();
230
+ if (/^\d+$/.test(inner)) continue;
231
+ if (/^['"][a-zA-Z_$][a-zA-Z0-9_$]*['"]$/.test(inner)) continue;
232
+ return { subPath: s.startsWith('.') ? s.slice(1) : s, wildcard: true };
233
+ }
234
+
235
+ if (s.startsWith('.')) s = s.slice(1);
236
+ return { subPath: s, wildcard: false };
237
+ }
238
+
239
+ /**
240
+ * Heuristic extraction of ctx variable usage from RunJS code.
241
+ *
242
+ * Returns a map: varName -> string[] subPaths
243
+ * - subPath '' means the variable root is used (or dependency is dynamic), caller MAY treat it as wildcard.
244
+ * - Only best-effort parsing; correctness prefers over-approximation.
245
+ */
246
+ export function extractUsedVariablePathsFromRunJS(code: string): Record<string, string[]> {
247
+ if (typeof code !== 'string' || !code.trim()) return {};
248
+ const src = stripStringsAndComments(code);
249
+ const srcWithStrings = stripComments(code);
250
+ const usage = new Map<string, Set<string>>();
251
+
252
+ const add = (varName: string, subPath: string) => {
253
+ if (!varName) return;
254
+ const set = usage.get(varName) || new Set<string>();
255
+ set.add(subPath || '');
256
+ usage.set(varName, set);
257
+ };
258
+
259
+ // dot form: ctx.foo.bar / ctx.foo[0].bar (excluding ctx.method(...))
260
+ const dotRe = /ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*(?:(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\[[^\]]+\]))*)(?!\s*\()/g;
261
+ let match: RegExpExecArray | null;
262
+ while ((match = dotRe.exec(src))) {
263
+ const pathAfterCtx = match[1] || '';
264
+ const firstKeyMatch = pathAfterCtx.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
265
+ if (!firstKeyMatch) continue;
266
+ const firstKey = firstKeyMatch[1];
267
+ const rest = pathAfterCtx.slice(firstKey.length);
268
+ const { subPath, wildcard } = normalizeSubPath(rest);
269
+ add(firstKey, wildcard ? '' : subPath);
270
+ }
271
+
272
+ // bracket root: ctx['foo'].bar / ctx["foo"][0] (excluding ctx['method'](...))
273
+ const bracketRootRe =
274
+ /ctx\s*\[\s*(['"])([a-zA-Z_$][a-zA-Z0-9_$]*)\1\s*\]((?:(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\[[^\]]+\]))*)(?!\s*\()/g;
275
+ while ((match = bracketRootRe.exec(srcWithStrings))) {
276
+ const varName = match[2] || '';
277
+ const rest = match[3] || '';
278
+ const { subPath, wildcard } = normalizeSubPath(rest);
279
+ add(varName, wildcard ? '' : subPath);
280
+ }
281
+
282
+ const out: Record<string, string[]> = {};
283
+ for (const [k, set] of usage.entries()) {
284
+ out[k] = Array.from(set);
285
+ }
286
+ return out;
287
+ }
@@ -9,12 +9,72 @@
9
9
 
10
10
  /**
11
11
  * 统一的安全全局对象代理:window/document/navigator
12
- * - window:仅允许常用的定时器、console、Math、Date、FormData、addEventListener、open(安全包装)、location(安全代理)
12
+ * - window:仅允许常用的定时器、console、Math、Date、FormData、Blob、URL、addEventListener、open(安全包装)、location(安全代理)
13
13
  * - document:仅允许 createElement/querySelector/querySelectorAll
14
14
  * - navigator:仅提供极少量低风险能力(clipboard.writeText、onLine、language、languages)
15
15
  * - 不允许随意访问未声明的属性,最小权限原则
16
16
  */
17
17
 
18
+ type RunJSSafeGlobalsRegistry = {
19
+ windowAllow: Set<string>;
20
+ documentAllow: Set<string>;
21
+ };
22
+
23
+ function getRunJSSafeGlobalsRegistry(): RunJSSafeGlobalsRegistry {
24
+ const g: any = globalThis as any;
25
+ if (g.__nocobaseRunJSSafeGlobalsRegistry?.windowAllow && g.__nocobaseRunJSSafeGlobalsRegistry?.documentAllow) {
26
+ return g.__nocobaseRunJSSafeGlobalsRegistry as RunJSSafeGlobalsRegistry;
27
+ }
28
+ const reg: RunJSSafeGlobalsRegistry = {
29
+ windowAllow: new Set<string>(),
30
+ documentAllow: new Set<string>(),
31
+ };
32
+ g.__nocobaseRunJSSafeGlobalsRegistry = reg;
33
+ return reg;
34
+ }
35
+
36
+ export function registerRunJSSafeWindowGlobals(keys: Iterable<string> | null | undefined): void {
37
+ if (!keys) return;
38
+ const reg = getRunJSSafeGlobalsRegistry();
39
+ for (const k of keys) {
40
+ if (typeof k !== 'string') continue;
41
+ const key = k.trim();
42
+ if (!key) continue;
43
+ reg.windowAllow.add(key);
44
+ }
45
+ }
46
+
47
+ export function registerRunJSSafeDocumentGlobals(keys: Iterable<string> | null | undefined): void {
48
+ if (!keys) return;
49
+ const reg = getRunJSSafeGlobalsRegistry();
50
+ for (const k of keys) {
51
+ if (typeof k !== 'string') continue;
52
+ const key = k.trim();
53
+ if (!key) continue;
54
+ reg.documentAllow.add(key);
55
+ }
56
+ }
57
+
58
+ export function __resetRunJSSafeGlobalsRegistryForTests(): void {
59
+ const g: any = globalThis as any;
60
+ if (g.__nocobaseRunJSSafeGlobalsRegistry) {
61
+ try {
62
+ g.__nocobaseRunJSSafeGlobalsRegistry.windowAllow?.clear?.();
63
+ g.__nocobaseRunJSSafeGlobalsRegistry.documentAllow?.clear?.();
64
+ } catch {
65
+ // ignore
66
+ }
67
+ }
68
+ }
69
+
70
+ function isAllowedDynamicWindowKey(key: string): boolean {
71
+ return getRunJSSafeGlobalsRegistry().windowAllow.has(key);
72
+ }
73
+
74
+ function isAllowedDynamicDocumentKey(key: string): boolean {
75
+ return getRunJSSafeGlobalsRegistry().documentAllow.has(key);
76
+ }
77
+
18
78
  export function createSafeWindow(extra?: Record<string, any>) {
19
79
  // 解析相对 URL 使用脱敏 base(不含 query/hash),避免在解析时泄露敏感信息
20
80
  const getSafeBaseHref = () => `${window.location.origin}${window.location.pathname}`;
@@ -151,6 +211,8 @@ export function createSafeWindow(extra?: Record<string, any>) {
151
211
  Math,
152
212
  Date,
153
213
  FormData,
214
+ ...(typeof Blob !== 'undefined' ? { Blob } : {}),
215
+ ...(typeof URL !== 'undefined' ? { URL } : {}),
154
216
  // 事件侦听仅绑定到真实 window,便于少量需要的全局监听
155
217
  addEventListener: addEventListener.bind(window),
156
218
  // 安全的 window.open 代理
@@ -160,15 +222,44 @@ export function createSafeWindow(extra?: Record<string, any>) {
160
222
  ...(extra || {}),
161
223
  };
162
224
 
163
- return new Proxy(
164
- {},
165
- {
166
- get(_target, prop: string) {
167
- if (prop in allowedGlobals) return allowedGlobals[prop];
168
- throw new Error(`Access to global property "${prop}" is not allowed.`);
169
- },
225
+ const target: Record<string, any> = Object.create(null);
226
+
227
+ return new Proxy(target, {
228
+ get(t, prop: string | symbol) {
229
+ if (typeof prop !== 'string') {
230
+ return Reflect.get(t, prop);
231
+ }
232
+
233
+ if (prop in allowedGlobals) return allowedGlobals[prop];
234
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return (t as any)[prop];
235
+ if (isAllowedDynamicWindowKey(prop)) {
236
+ const v = (window as any)[prop];
237
+ // Bind functions to the real window to avoid Illegal invocation
238
+ if (typeof v === 'function') return v.bind(window);
239
+ return v;
240
+ }
241
+
242
+ throw new Error(`Access to global property "${prop}" is not allowed.`);
170
243
  },
171
- );
244
+ set(t, prop: string | symbol, value: any) {
245
+ if (typeof prop !== 'string') {
246
+ Reflect.set(t, prop, value);
247
+ return true;
248
+ }
249
+ if (prop in allowedGlobals) {
250
+ throw new Error(`Mutation of global property "${prop}" is not allowed.`);
251
+ }
252
+ (t as any)[prop] = value;
253
+ return true;
254
+ },
255
+ has(t, prop: string | symbol) {
256
+ if (typeof prop !== 'string') return Reflect.has(t, prop);
257
+ if (prop in allowedGlobals) return true;
258
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return true;
259
+ if (isAllowedDynamicWindowKey(prop)) return true;
260
+ return false;
261
+ },
262
+ });
172
263
  }
173
264
 
174
265
  export function createSafeDocument(extra?: Record<string, any>) {
@@ -178,15 +269,43 @@ export function createSafeDocument(extra?: Record<string, any>) {
178
269
  querySelectorAll: document.querySelectorAll.bind(document),
179
270
  ...(extra || {}),
180
271
  };
181
- return new Proxy(
182
- {},
183
- {
184
- get(_target, prop: string) {
185
- if (prop in allowed) return allowed[prop];
186
- throw new Error(`Access to document property "${prop}" is not allowed.`);
187
- },
272
+ const target: Record<string, any> = Object.create(null);
273
+ return new Proxy(target, {
274
+ get(t, prop: string | symbol) {
275
+ if (typeof prop !== 'string') {
276
+ return Reflect.get(t, prop);
277
+ }
278
+
279
+ if (prop in allowed) return allowed[prop];
280
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return (t as any)[prop];
281
+ if (isAllowedDynamicDocumentKey(prop)) {
282
+ const v = (document as any)[prop];
283
+ // Bind functions to the real document to avoid Illegal invocation
284
+ if (typeof v === 'function') return v.bind(document);
285
+ return v;
286
+ }
287
+
288
+ throw new Error(`Access to document property "${prop}" is not allowed.`);
188
289
  },
189
- );
290
+ set(t, prop: string | symbol, value: any) {
291
+ if (typeof prop !== 'string') {
292
+ Reflect.set(t, prop, value);
293
+ return true;
294
+ }
295
+ if (prop in allowed) {
296
+ throw new Error(`Mutation of document property "${prop}" is not allowed.`);
297
+ }
298
+ (t as any)[prop] = value;
299
+ return true;
300
+ },
301
+ has(t, prop: string | symbol) {
302
+ if (typeof prop !== 'string') return Reflect.has(t, prop);
303
+ if (prop in allowed) return true;
304
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return true;
305
+ if (isAllowedDynamicDocumentKey(prop)) return true;
306
+ return false;
307
+ },
308
+ });
190
309
  }
191
310
 
192
311
  export function createSafeNavigator(extra?: Record<string, any>) {
@@ -233,3 +352,55 @@ export function createSafeNavigator(extra?: Record<string, any>) {
233
352
  },
234
353
  );
235
354
  }
355
+
356
+ /**
357
+ * Create a safe globals object for RunJS execution.
358
+ *
359
+ * - Always tries to provide `navigator`
360
+ * - Best-effort provides `window` and `document` in browser environments
361
+ * - Never throws (so callers can decide how to handle missing globals)
362
+ */
363
+ export function createSafeRunJSGlobals(extraGlobals?: Record<string, any>): Record<string, any> {
364
+ const globals: Record<string, any> = {};
365
+
366
+ try {
367
+ const navigator = createSafeNavigator();
368
+ globals.navigator = navigator;
369
+ try {
370
+ globals.window = createSafeWindow({ navigator });
371
+ } catch {
372
+ // ignore when window is not available (e.g. SSR/tests)
373
+ }
374
+ } catch {
375
+ // ignore
376
+ }
377
+
378
+ try {
379
+ globals.document = createSafeDocument();
380
+ } catch {
381
+ // ignore when document is not available (e.g. SSR/tests)
382
+ }
383
+
384
+ return extraGlobals ? { ...globals, ...extraGlobals } : globals;
385
+ }
386
+
387
+ /**
388
+ * Execute RunJS with safe globals (window/document/navigator).
389
+ *
390
+ * Keeps `this` binding by calling `ctx.runjs(...)` instead of passing bare function references.
391
+ */
392
+ export async function runjsWithSafeGlobals(
393
+ ctx: unknown,
394
+ code: string,
395
+ options?: any,
396
+ extraGlobals?: Record<string, any>,
397
+ ): Promise<any> {
398
+ if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return undefined;
399
+ const runjs = (ctx as { runjs?: unknown }).runjs;
400
+ if (typeof runjs !== 'function') return undefined;
401
+ return (ctx as { runjs: (code: string, variables?: Record<string, any>, options?: any) => Promise<any> }).runjs(
402
+ code,
403
+ createSafeRunJSGlobals(extraGlobals),
404
+ options,
405
+ );
406
+ }
@@ -277,3 +277,82 @@ export async function shouldHideStepInSettings<TModel extends FlowModel = FlowMo
277
277
 
278
278
  return !!hideInSettings;
279
279
  }
280
+
281
+ /**
282
+ * 解析步骤在设置菜单中的禁用状态与提示文案。
283
+ * - 支持 StepDefinition.disabledInSettings 与 ActionDefinition.disabledInSettings(step 优先)。
284
+ * - 支持 StepDefinition.disabledReasonInSettings 与 ActionDefinition.disabledReasonInSettings(step 优先)。
285
+ * - 以上属性均支持静态值与函数(接收 FlowRuntimeContext)。
286
+ */
287
+ export async function resolveStepDisabledInSettings<TModel extends FlowModel = FlowModel>(
288
+ model: TModel,
289
+ flow: any,
290
+ step: StepDefinition,
291
+ ): Promise<{ disabled: boolean; reason?: string }> {
292
+ if (!step) return { disabled: false };
293
+
294
+ let disabledInSettings = step.disabledInSettings;
295
+ let disabledReasonInSettings = step.disabledReasonInSettings;
296
+
297
+ if ((typeof disabledInSettings === 'undefined' || typeof disabledReasonInSettings === 'undefined') && step.use) {
298
+ try {
299
+ const action = model.getAction?.(step.use);
300
+ if (typeof disabledInSettings === 'undefined') {
301
+ disabledInSettings = action?.disabledInSettings;
302
+ }
303
+ if (typeof disabledReasonInSettings === 'undefined') {
304
+ disabledReasonInSettings = action?.disabledReasonInSettings;
305
+ }
306
+ } catch (error) {
307
+ console.warn(`Failed to get action ${step.use}:`, error);
308
+ }
309
+ }
310
+
311
+ let ctx: FlowRuntimeContext<TModel> | null = null;
312
+ const getContext = () => {
313
+ if (ctx) return ctx;
314
+ ctx = new FlowRuntimeContext(model, flow.key, 'settings');
315
+ setupRuntimeContextSteps(ctx, flow.steps, model, flow.key);
316
+ ctx.defineProperty('currentStep', { value: step });
317
+ return ctx;
318
+ };
319
+
320
+ let disabled = false;
321
+ if (typeof disabledInSettings === 'function') {
322
+ try {
323
+ disabled = !!(await disabledInSettings(getContext() as any));
324
+ } catch (error) {
325
+ console.warn(`Error evaluating disabledInSettings for step '${step.key || ''}' in flow '${flow.key}':`, error);
326
+ return { disabled: false };
327
+ }
328
+ } else {
329
+ disabled = !!disabledInSettings;
330
+ }
331
+
332
+ if (!disabled) {
333
+ return { disabled: false };
334
+ }
335
+
336
+ let reason: string | undefined;
337
+ if (typeof disabledReasonInSettings === 'function') {
338
+ try {
339
+ const resolved = await disabledReasonInSettings(getContext() as any);
340
+ if (typeof resolved !== 'undefined' && resolved !== null && resolved !== '') {
341
+ reason = String(resolved);
342
+ }
343
+ } catch (error) {
344
+ console.warn(
345
+ `Error evaluating disabledReasonInSettings for step '${step.key || ''}' in flow '${flow.key}':`,
346
+ error,
347
+ );
348
+ }
349
+ } else if (
350
+ typeof disabledReasonInSettings !== 'undefined' &&
351
+ disabledReasonInSettings !== null &&
352
+ disabledReasonInSettings !== ''
353
+ ) {
354
+ reason = String(disabledReasonInSettings);
355
+ }
356
+
357
+ return { disabled: true, reason };
358
+ }
@@ -13,7 +13,7 @@ import { render, act, waitFor, screen } from '@testing-library/react';
13
13
  import { FlowEngine } from '../../flowEngine';
14
14
  import { FlowEngineProvider } from '../../provider';
15
15
  import { FlowViewer } from '../FlowView';
16
- import { usePage } from '../usePage';
16
+ import { usePage, GLOBAL_EMBED_CONTAINER_ID } from '../usePage';
17
17
  import { App, ConfigProvider } from 'antd';
18
18
 
19
19
  describe('FlowViewer zIndex with usePage', () => {
@@ -130,4 +130,57 @@ describe('FlowViewer zIndex with usePage', () => {
130
130
 
131
131
  unmount();
132
132
  });
133
+
134
+ it('replaces previous embed view when using global #nocobase-embed-container target', async () => {
135
+ let api: { open: (config: any, flowContext: any) => any } | undefined;
136
+
137
+ function TestApp({ onReady }: { onReady: (page: any) => void }) {
138
+ const [page, pageHolder] = usePage() as [{ open: (config: any, flowContext: any) => any }, React.ReactNode];
139
+
140
+ React.useEffect(() => {
141
+ onReady(page);
142
+ }, [page, onReady]);
143
+
144
+ return <>{pageHolder}</>;
145
+ }
146
+
147
+ const Wrapper: React.FC<{ onReady: (page: any) => void }> = ({ onReady }) => (
148
+ <ConfigProvider>
149
+ <App>
150
+ <FlowEngineProvider engine={engine}>
151
+ <TestApp onReady={onReady} />
152
+ </FlowEngineProvider>
153
+ </App>
154
+ </ConfigProvider>
155
+ );
156
+
157
+ const target = document.createElement('div');
158
+ target.id = GLOBAL_EMBED_CONTAINER_ID;
159
+ document.body.appendChild(target);
160
+
161
+ const { unmount } = render(
162
+ <Wrapper
163
+ onReady={(page) => {
164
+ api = page;
165
+ }}
166
+ />,
167
+ );
168
+
169
+ await waitFor(() => expect(api).toBeDefined());
170
+
171
+ await act(async () => {
172
+ api!.open({ target, content: <div data-testid="page1">Page 1</div> }, engine.context);
173
+ });
174
+ await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
175
+
176
+ // Opening page2 into the global embed container should destroy page1 (replace behavior).
177
+ await act(async () => {
178
+ api!.open({ target, content: <div data-testid="page2">Page 2</div> }, engine.context);
179
+ });
180
+ await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
181
+ expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
182
+
183
+ unmount();
184
+ document.body.removeChild(target);
185
+ });
133
186
  });