@nocobase/flow-engine 2.0.0-beta.9 → 2.0.1

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,88 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowRunJSContext } from '../flowContext';
11
+ import { RunJSContextRegistry, type RunJSVersion } from './registry';
12
+
13
+ export type RunJSContextContributionApi = {
14
+ version: RunJSVersion;
15
+ RunJSContextRegistry: typeof RunJSContextRegistry;
16
+ FlowRunJSContext: typeof FlowRunJSContext;
17
+ };
18
+
19
+ export type RunJSContextContribution = (api: RunJSContextContributionApi) => void | Promise<void>;
20
+
21
+ const contributions = new Set<RunJSContextContribution>();
22
+ const appliedByVersion = new Map<RunJSVersion, Set<RunJSContextContribution>>();
23
+ const setupDoneVersions = new Set<RunJSVersion>();
24
+
25
+ async function applyContributionOnce(version: RunJSVersion, contribution: RunJSContextContribution) {
26
+ const applied = appliedByVersion.get(version) || new Set<RunJSContextContribution>();
27
+ appliedByVersion.set(version, applied);
28
+ if (applied.has(contribution)) return;
29
+
30
+ // Mark as applied before awaiting to avoid duplicate runs on concurrency.
31
+ // If it fails, remove the marker so a later setup retry can re-apply.
32
+ applied.add(contribution);
33
+ try {
34
+ await contribution({
35
+ version,
36
+ RunJSContextRegistry,
37
+ FlowRunJSContext,
38
+ });
39
+ } catch (error) {
40
+ applied.delete(contribution);
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Register a RunJS context/doc contribution.
47
+ *
48
+ * - If RunJS contexts have already been set up for a version, the contribution is applied immediately once.
49
+ * - Each contribution is executed at most once per version.
50
+ */
51
+ export function registerRunJSContextContribution(contribution: RunJSContextContribution) {
52
+ if (typeof contribution !== 'function') {
53
+ throw new Error('[flow-engine] registerRunJSContextContribution: contribution must be a function');
54
+ }
55
+ if (contributions.has(contribution)) return;
56
+ contributions.add(contribution);
57
+
58
+ // Apply immediately for already-setup versions (late registration).
59
+ for (const version of setupDoneVersions) {
60
+ void applyContributionOnce(version, contribution).catch((error) => {
61
+ // Avoid unhandled rejections in late registrations
62
+ try {
63
+ // eslint-disable-next-line no-console
64
+ console.error('[flow-engine] RunJS context contribution failed:', error);
65
+ } catch (_) {
66
+ void 0;
67
+ }
68
+ });
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Apply all registered contributions for a given version.
74
+ * Intended to be called by setupRunJSContexts().
75
+ */
76
+ export async function applyRunJSContextContributions(version: RunJSVersion) {
77
+ for (const contribution of contributions) {
78
+ await applyContributionOnce(version, contribution);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Mark setupRunJSContexts() as completed for a given version.
84
+ * Used to support late contributions that should take effect without re-running setup.
85
+ */
86
+ export function markRunJSContextsSetupDone(version: RunJSVersion) {
87
+ setupDoneVersions.add(version);
88
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { FlowContext } from '../flowContext';
11
+ import { createRunJSDeprecationProxy } from '../flowContext';
11
12
  import { JSRunner } from '../JSRunner';
12
13
  import type { JSRunnerOptions } from '../JSRunner';
13
14
  import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './registry';
@@ -36,7 +37,16 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
36
37
  throw new Error('[RunJS] No RunJSContext registered for version/model.');
37
38
  }
38
39
  const runCtx = new (Ctor as any)(ensureFlowContext(this));
39
- const globals: Record<string, any> = { ctx: runCtx, ...(options?.globals || {}) };
40
+ let doc: any = {};
41
+ try {
42
+ const locale = getLocale(this);
43
+ if ((Ctor as any)?.getDoc?.length) doc = (Ctor as any).getDoc(locale) || {};
44
+ else doc = (Ctor as any)?.getDoc?.() || {};
45
+ } catch (_) {
46
+ doc = {};
47
+ }
48
+ const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
49
+ const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
40
50
  // 对字段/区块类上下文,默认注入 window/document 以支持在沙箱中访问 DOM API
41
51
  if (modelClass === 'JSFieldModel' || modelClass === 'JSBlockModel') {
42
52
  if (typeof window !== 'undefined') globals.window = window as any;
@@ -13,6 +13,7 @@
13
13
  import { RunJSContextRegistry } from './registry';
14
14
  import { FlowRunJSContext } from '../flowContext';
15
15
  import { defineBaseContextMeta } from './contexts/base';
16
+ import { applyRunJSContextContributions, markRunJSContextsSetupDone } from './contributions';
16
17
 
17
18
  let done = false;
18
19
  export async function setupRunJSContexts() {
@@ -23,6 +24,7 @@ export async function setupRunJSContexts() {
23
24
  const [
24
25
  { JSBlockRunJSContext },
25
26
  { JSFieldRunJSContext },
27
+ { JSEditableFieldRunJSContext },
26
28
  { JSItemRunJSContext },
27
29
  { JSColumnRunJSContext },
28
30
  { FormJSFieldItemRunJSContext },
@@ -31,6 +33,7 @@ export async function setupRunJSContexts() {
31
33
  ] = await Promise.all([
32
34
  import('./contexts/JSBlockRunJSContext'),
33
35
  import('./contexts/JSFieldRunJSContext'),
36
+ import('./contexts/JSEditableFieldRunJSContext'),
34
37
  import('./contexts/JSItemRunJSContext'),
35
38
  import('./contexts/JSColumnRunJSContext'),
36
39
  import('./contexts/FormJSFieldItemRunJSContext'),
@@ -42,10 +45,13 @@ export async function setupRunJSContexts() {
42
45
  RunJSContextRegistry.register(v1, '*', FlowRunJSContext);
43
46
  RunJSContextRegistry.register(v1, 'JSBlockModel', JSBlockRunJSContext, { scenes: ['block'] });
44
47
  RunJSContextRegistry.register(v1, 'JSFieldModel', JSFieldRunJSContext, { scenes: ['detail'] });
48
+ RunJSContextRegistry.register(v1, 'JSEditableFieldModel', JSEditableFieldRunJSContext, { scenes: ['form'] });
45
49
  RunJSContextRegistry.register(v1, 'JSItemModel', JSItemRunJSContext, { scenes: ['form'] });
46
50
  RunJSContextRegistry.register(v1, 'JSColumnModel', JSColumnRunJSContext, { scenes: ['table'] });
47
51
  RunJSContextRegistry.register(v1, 'FormJSFieldItemModel', FormJSFieldItemRunJSContext, { scenes: ['form'] });
48
52
  RunJSContextRegistry.register(v1, 'JSRecordActionModel', JSRecordActionRunJSContext, { scenes: ['table'] });
49
53
  RunJSContextRegistry.register(v1, 'JSCollectionActionModel', JSCollectionActionRunJSContext, { scenes: ['table'] });
54
+ await applyRunJSContextContributions(v1);
50
55
  done = true;
56
+ markRunJSContextsSetupDone(v1);
51
57
  }
@@ -13,16 +13,16 @@ const snippet: SnippetModule = {
13
13
  contexts: ['*'],
14
14
  prefix: 'sn-api-request',
15
15
  label: 'API request template',
16
- description: 'Basic template to send HTTP requests via ctx.api.request',
16
+ description: 'Basic template to send HTTP requests via ctx.request',
17
17
  locales: {
18
18
  'zh-CN': {
19
19
  label: 'API 请求模板',
20
- description: '使用 ctx.api.request 发送 HTTP 请求的基础模板',
20
+ description: '使用 ctx.request 发送 HTTP 请求的基础模板',
21
21
  },
22
22
  },
23
23
  content: `
24
24
  // Replace url/method/params/data as needed
25
- const response = await ctx.api.request({
25
+ const response = await ctx.request({
26
26
  url: 'users:list',
27
27
  method: 'get',
28
28
  params: {
@@ -23,14 +23,13 @@ const snippet: SnippetModule = {
23
23
  content: `
24
24
  // Import an ESM module by URL
25
25
  // Works in yarn dev and yarn start
26
- const mod = await ctx.importAsync('https://cdn.jsdelivr.net/npm/lit-html@2/+esm');
26
+ const mod = await ctx.importAsync('lit-html@2');
27
27
  const { html, render } = mod;
28
28
 
29
- ctx.element.innerHTML = '';
30
29
  const container = document.createElement('div');
31
30
  container.style.padding = '8px';
32
31
  container.style.border = '1px dashed #999';
33
- ctx.element.append(container);
32
+ ctx.render(container);
34
33
 
35
34
  render(html\`<span style="color:#52c41a;">lit-html loaded and rendered</span>\`, container);
36
35
  `,
@@ -16,15 +16,20 @@ const snippet: SnippetModule = {
16
16
  contexts: [JSBlockRunJSContext, JSFieldRunJSContext, FormJSFieldItemRunJSContext],
17
17
  prefix: 'sn-query-selector',
18
18
  label: 'Query selector',
19
- description: 'Find a child element inside ctx.element using querySelector',
19
+ description: 'Find a child element inside rendered DOM using querySelector',
20
20
  locales: {
21
21
  'zh-CN': {
22
22
  label: '查询子元素',
23
- description: '使用 querySelector ctx.element 内查找子元素',
23
+ description: '使用 querySelector 在渲染的 DOM 内查找子元素',
24
24
  },
25
25
  },
26
26
  content: `
27
- const child = ctx.element.querySelector('.child-class');
27
+ const wrapper = document.createElement('div');
28
+ wrapper.innerHTML = '<div class="child-class"></div>';
29
+
30
+ ctx.render(wrapper);
31
+
32
+ const child = wrapper.querySelector('.child-class');
28
33
  if (child) {
29
34
  child.textContent = ctx.t('Hello from querySelector');
30
35
  }
@@ -22,7 +22,7 @@ const snippet: SnippetModule = {
22
22
  },
23
23
  content: `
24
24
  // Load an external library (AMD/RequireJS)
25
- const dayjs = await ctx.requireAsync('https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js');
25
+ const dayjs = await ctx.requireAsync('dayjs@1/dayjs.min.js');
26
26
  console.log('dayjs loaded:', dayjs?.default || dayjs);
27
27
  `,
28
28
  };
@@ -9,8 +9,10 @@
9
9
 
10
10
  import { RunJSContextRegistry } from '../registry';
11
11
 
12
+ export type RunJSSnippetLoader = () => Promise<any>;
13
+
12
14
  // Simple manual exports - no build-time magic needed
13
- const snippets: Record<string, () => Promise<any>> = {
15
+ const snippets: Record<string, RunJSSnippetLoader | undefined> = {
14
16
  // global
15
17
  'global/message-success': () => import('./global/message-success.snippet'),
16
18
  'global/message-error': () => import('./global/message-error.snippet'),
@@ -69,6 +71,32 @@ const snippets: Record<string, () => Promise<any>> = {
69
71
 
70
72
  export default snippets;
71
73
 
74
+ /**
75
+ * Register a RunJS snippet loader for editors/AI coding.
76
+ *
77
+ * - By default, an existing ref will NOT be overwritten (returns false).
78
+ * - Use { override: true } to overwrite an existing ref (returns true).
79
+ */
80
+ export function registerRunJSSnippet(
81
+ ref: string,
82
+ loader: RunJSSnippetLoader,
83
+ options?: {
84
+ override?: boolean;
85
+ },
86
+ ): boolean {
87
+ if (typeof ref !== 'string' || !ref.trim()) {
88
+ throw new Error('[flow-engine] registerRunJSSnippet: ref must be a non-empty string');
89
+ }
90
+ if (typeof loader !== 'function') {
91
+ throw new Error('[flow-engine] registerRunJSSnippet: loader must be a function returning a Promise');
92
+ }
93
+ const key = ref.trim();
94
+ const existed = typeof snippets[key] === 'function';
95
+ if (existed && !options?.override) return false;
96
+ snippets[key] = loader;
97
+ return true;
98
+ }
99
+
72
100
  // Cohesive snippet helpers for clients (editor, etc.)
73
101
  type EngineSnippetEntry = {
74
102
  name: string;
@@ -127,8 +155,8 @@ function resolveLocaleMeta(def: any, locale?: string) {
127
155
  }
128
156
 
129
157
  export async function getSnippetBody(ref: string): Promise<string> {
130
- const loader = (snippets as any)[ref];
131
- if (!loader) throw new Error(`[flow-engine] snippet not found: ${ref}`);
158
+ const loader = snippets[ref];
159
+ if (typeof loader !== 'function') throw new Error(`[flow-engine] snippet not found: ${ref}`);
132
160
  const mod = await loader();
133
161
  const def = mod?.default;
134
162
  // engine snippet modules export a SnippetModule as default
@@ -152,46 +180,52 @@ export async function listSnippetsForContext(
152
180
  }
153
181
  await Promise.all(
154
182
  Object.entries(snippets).map(async ([key, loader]) => {
155
- const mod = await (loader as any)();
156
- const def = mod?.default || {};
157
- const body: any = def?.content ?? mod?.content;
158
- if (typeof body !== 'string') return;
159
- let ok = true;
160
- if (Array.isArray(def?.contexts) && def.contexts.length) {
161
- const ctxNames = def.contexts.map((item: any) => {
162
- if (item === '*') return '*';
163
- if (typeof item === 'string') return item;
164
- if (typeof item === 'function') return item.name || '*';
165
- if (item && typeof item === 'object' && typeof item.name === 'string') return item.name;
166
- return String(item ?? '');
167
- });
168
- if (ctxClassName === '*') {
169
- // '*' means return all snippets without filtering by context
170
- ok = true;
171
- } else {
172
- ok = ctxNames.includes('*') || ctxNames.some((name: string) => allowedContextNames.has(name));
183
+ if (typeof loader !== 'function') return;
184
+ try {
185
+ const mod = await loader();
186
+ const def = mod?.default || {};
187
+ const body: any = def?.content ?? mod?.content;
188
+ if (typeof body !== 'string') return;
189
+ let ok = true;
190
+ if (Array.isArray(def?.contexts) && def.contexts.length) {
191
+ const ctxNames = def.contexts.map((item: any) => {
192
+ if (item === '*') return '*';
193
+ if (typeof item === 'string') return item;
194
+ if (typeof item === 'function') return item.name || '*';
195
+ if (item && typeof item === 'object' && typeof item.name === 'string') return item.name;
196
+ return String(item ?? '');
197
+ });
198
+ if (ctxClassName === '*') {
199
+ // '*' means return all snippets without filtering by context
200
+ ok = true;
201
+ } else {
202
+ ok = ctxNames.includes('*') || ctxNames.some((name: string) => allowedContextNames.has(name));
203
+ }
173
204
  }
205
+ if (ok && Array.isArray(def?.versions) && def.versions.length) {
206
+ ok = def.versions.includes('*') || def.versions.includes(version);
207
+ }
208
+ if (!ok) return;
209
+ const localeMeta = resolveLocaleMeta(def, locale);
210
+ const name = localeMeta.label || def?.label || deriveNameFromKey(key);
211
+ const description = localeMeta.description ?? def?.description;
212
+ const prefix = def?.prefix || name;
213
+ const groups = computeGroups(def, key);
214
+ const scenes = normalizeScenes(def, key);
215
+ entries.push({
216
+ name,
217
+ prefix,
218
+ description,
219
+ body,
220
+ ref: key,
221
+ group: groups[0],
222
+ groups,
223
+ scenes,
224
+ });
225
+ } catch (_) {
226
+ // fail-open: ignore broken snippet loader
227
+ return;
174
228
  }
175
- if (ok && Array.isArray(def?.versions) && def.versions.length) {
176
- ok = def.versions.includes('*') || def.versions.includes(version);
177
- }
178
- if (!ok) return;
179
- const localeMeta = resolveLocaleMeta(def, locale);
180
- const name = localeMeta.label || def?.label || deriveNameFromKey(key);
181
- const description = localeMeta.description ?? def?.description;
182
- const prefix = def?.prefix || name;
183
- const groups = computeGroups(def, key);
184
- const scenes = normalizeScenes(def, key);
185
- entries.push({
186
- name,
187
- prefix,
188
- description,
189
- body,
190
- ref: key,
191
- group: groups[0],
192
- groups,
193
- scenes,
194
- });
195
229
  }),
196
230
  );
197
231
  return entries;
@@ -21,20 +21,18 @@ const snippet: SnippetModule = {
21
21
  description: '渲染按钮并绑定点击事件处理',
22
22
  },
23
23
  },
24
- content:
25
- `
24
+ content: `
26
25
  // Render a button and bind a click handler
27
- ctx.element.innerHTML = ` +
28
- '`' +
29
- `
30
- <button id="nb-jsb-btn" style="padding:6px 12px">\${ctx.t('Click me')}</button>
31
- ` +
32
- '`' +
33
- `;
34
- const btn = document.getElementById('nb-jsb-btn');
35
- if (btn) {
36
- btn.addEventListener('click', () => ctx.message.success(ctx.t('Clicked!')));
37
- }
26
+ const button = document.createElement('button');
27
+ button.textContent = ctx.t('Click me');
28
+ button.style.padding = '6px 12px';
29
+ button.addEventListener('click', () => ctx.message.success(ctx.t('Clicked!')));
30
+
31
+ const wrapper = document.createElement('div');
32
+ wrapper.style.padding = '12px';
33
+ wrapper.appendChild(button);
34
+
35
+ ctx.render(wrapper);
38
36
  `,
39
37
  };
40
38
 
@@ -23,7 +23,7 @@ const snippet: SnippetModule = {
23
23
  },
24
24
  content: `
25
25
  // Fetch users
26
- const { data } = await ctx.api.request({
26
+ const { data } = await ctx.request({
27
27
  url: 'users:list',
28
28
  method: 'get',
29
29
  params: { pageSize: 5 },
@@ -31,14 +31,14 @@ const { data } = await ctx.api.request({
31
31
  const rows = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []);
32
32
 
33
33
  // Render as a simple HTML list
34
- ctx.element.innerHTML = [
34
+ ctx.render([
35
35
  '<div style="padding:12px">',
36
36
  '<h4 style="margin:0 0 8px">' + ctx.t('Users') + '</h4>',
37
37
  '<ul style="margin:0; padding-left:20px">',
38
38
  ...rows.map((r, i) => '<li>#' + (i + 1) + ': ' + String((r && (r.nickname ?? r.username ?? r.id)) ?? '') + '</li>'),
39
39
  '</ul>',
40
40
  '</div>'
41
- ].join('');
41
+ ].join(''));
42
42
  `,
43
43
  };
44
44
 
@@ -32,10 +32,10 @@ const canvas = document.createElement('canvas');
32
32
  canvas.width = 480;
33
33
  canvas.height = 320;
34
34
  wrapper.appendChild(canvas);
35
- ctx.element.replaceChildren(wrapper);
35
+ ctx.render(wrapper);
36
36
 
37
37
  async function renderChart() {
38
- const loaded = await ctx.requireAsync('https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js');
38
+ const loaded = await ctx.requireAsync('chart.js@4.4.0/dist/chart.umd.min.js');
39
39
  const Chart = loaded?.Chart || loaded?.default?.Chart || loaded?.default;
40
40
  if (!Chart) {
41
41
  throw new Error('Chart.js is not available');
@@ -25,8 +25,8 @@ const snippet: SnippetModule = {
25
25
  const container = document.createElement('div');
26
26
  container.style.height = '400px';
27
27
  container.style.width = '100%';
28
- ctx.element.replaceChildren(container);
29
- const echarts = await ctx.requireAsync('https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js');
28
+ ctx.render(container);
29
+ const echarts = await ctx.requireAsync('echarts@5/dist/echarts.min.js');
30
30
  if (!echarts) {
31
31
  throw new Error('ECharts library not loaded');
32
32
  }
@@ -30,8 +30,8 @@ iframe.style.width = '100%';
30
30
  iframe.style.height = '100%';
31
31
  iframe.style.border = 'none';
32
32
 
33
- // Replace existing children so the iframe is the only content
34
- ctx.element.replaceChildren(iframe);
33
+ // Render the iframe as the only content
34
+ ctx.render(iframe);
35
35
  `,
36
36
  };
37
37
 
@@ -22,7 +22,7 @@ const snippet: SnippetModule = {
22
22
  },
23
23
  },
24
24
  content: `
25
- // Render a React element into ctx.element via ReactDOM
25
+ // Render a React element into the current container
26
26
  const { Button } = ctx.libs.antd;
27
27
 
28
28
  ctx.render(
@@ -24,7 +24,7 @@ const snippet: SnippetModule = {
24
24
  content: `
25
25
  const { Card, Statistic, Row, Col } = ctx.libs.antd;
26
26
 
27
- const res = await ctx.api.request({
27
+ const res = await ctx.request({
28
28
  url: 'users:list',
29
29
  method: 'get',
30
30
  params: {
@@ -24,7 +24,7 @@ const snippet: SnippetModule = {
24
24
  content: `
25
25
  const { Timeline, Card } = ctx.libs.antd;
26
26
 
27
- const res = await ctx.api.request({
27
+ const res = await ctx.request({
28
28
  url: 'users:list',
29
29
  method: 'get',
30
30
  params: {
@@ -14,32 +14,27 @@ const snippet: SnippetModule = {
14
14
  contexts: [JSBlockRunJSContext],
15
15
  prefix: 'sn-resource-example',
16
16
  label: 'Resource example',
17
- description: 'Create a resource via ctx.createResource and render JSON output',
17
+ description: 'Create a resource via ctx.makeResource and render JSON output',
18
18
  locales: {
19
19
  'zh-CN': {
20
20
  label: '资源示例',
21
- description: '使用 ctx.useResource 加载数据并渲染 JSON 输出',
21
+ description: '使用 ctx.initResource 加载数据并渲染 JSON 输出',
22
22
  },
23
23
  },
24
- content:
25
- `
24
+ content: `
26
25
  // Create a resource and load a single record
27
- const resource = ctx.createResource('SingleRecordResource');
26
+ const resource = ctx.makeResource('SingleRecordResource');
28
27
  resource.setDataSourceKey('main');
29
28
  resource.setResourceName('users');
30
29
  // Optionally set filterByTk to target a specific record:
31
30
  // resource.setRequestOptions('params', { filterByTk: 1 });
32
31
  await resource.refresh();
33
32
 
34
- ctx.element.innerHTML = ` +
35
- '`' +
36
- `
33
+ ctx.render(\`
37
34
  <pre style="padding: 12px; background: #f5f5f5; border-radius: 6px;">
38
35
  \${JSON.stringify(resource.getData(), null, 2)}
39
36
  </pre>
40
- ` +
41
- '`' +
42
- `;
37
+ \`);
43
38
  `,
44
39
  };
45
40
 
@@ -30,12 +30,12 @@ container.style.position = 'relative';
30
30
  container.style.borderRadius = '10px';
31
31
  container.style.overflow = 'hidden';
32
32
  container.style.background = 'radial-gradient(700px 300px at 20% 25%, #172036, #0b0f19 60%), radial-gradient(600px 240px at 80% 70%, rgba(56,189,248,0.12), transparent 60%)';
33
- ctx.element.replaceChildren(container);
33
+ ctx.render(container);
34
34
 
35
35
  // 不做显式清理逻辑;如需存储信息,统一挂在 ctx.model 上
36
36
 
37
- // 使用 ctx.useResource 加载 users:list(真实数据)
38
- ctx.useResource('MultiRecordResource');
37
+ // 使用 ctx.initResource 加载 users:list(真实数据)
38
+ ctx.initResource('MultiRecordResource');
39
39
  const resource = ctx.resource;
40
40
  resource.setDataSourceKey && resource.setDataSourceKey('main');
41
41
  resource.setResourceName && resource.setResourceName('users');
@@ -44,7 +44,7 @@ try {
44
44
  await resource.refresh();
45
45
  } catch (err) {
46
46
  var msg = (err && err.message) ? err.message : 'users:list 请求失败';
47
- ctx.element.innerHTML = '<div style="color:#cbd5e1; padding: 12px; text-align:center;">' + msg + '</div>';
47
+ container.innerHTML = '<div style="color:#cbd5e1; padding: 12px; text-align:center;">' + msg + '</div>';
48
48
  throw err;
49
49
  }
50
50
 
@@ -80,7 +80,7 @@ function makeAvatarTexture(user, idx) {
80
80
  return tex;
81
81
  }
82
82
 
83
- const THREE = await ctx.importAsync('https://esm.sh/three@0.160.0');
83
+ const THREE = await ctx.importAsync('three@0.160.0');
84
84
  const { Scene, PerspectiveCamera, WebGLRenderer, Color, AmbientLight, DirectionalLight, Group, Mesh, MeshStandardMaterial, SphereGeometry, Raycaster, Vector2 } = THREE;
85
85
 
86
86
  const scene = new Scene();
@@ -186,7 +186,7 @@ async function getPrimaryKeyField() {
186
186
  if (__pkField) return __pkField;
187
187
  const name = (resource && resource.getResourceName) ? resource.getResourceName() : 'users';
188
188
  try {
189
- const meta = await ctx.api.request({ url: 'collections:get', method: 'get', params: { filterByTk: name } });
189
+ const meta = await ctx.request({ url: 'collections:get', method: 'get', params: { filterByTk: name } });
190
190
  const data = (meta && meta.data) ? meta.data : {};
191
191
  // prefer filterTargetKey, fallback to fields.primaryKey
192
192
  const ft = (data && data.filterTargetKey) ? data.filterTargetKey : (data && data.options && data.options.filterTargetKey);
@@ -29,10 +29,10 @@ mountNode.style.borderRadius = '8px';
29
29
  const target = document.createElement('div');
30
30
  target.className = 'nb-vue-counter';
31
31
  mountNode.appendChild(target);
32
- ctx.element.replaceChildren(mountNode);
32
+ ctx.render(mountNode);
33
33
 
34
34
  async function bootstrap() {
35
- const mod = await ctx.importAsync('https://esm.sh/vue@3.4.27/dist/vue.runtime.esm-browser.js');
35
+ const mod = await ctx.importAsync('vue@3.4.27/dist/vue.runtime.esm-browser.js');
36
36
  const createApp = mod?.createApp;
37
37
  const ref = mod?.ref;
38
38
  const h = mod?.h;
@@ -91,8 +91,7 @@ async function bootstrap() {
91
91
  };
92
92
 
93
93
  const app = createApp(Counter);
94
- const mountTarget = ctx.element.querySelector('.nb-vue-counter');
95
- app.mount(mountTarget || ctx.element);
94
+ app.mount(target);
96
95
  }
97
96
 
98
97
  bootstrap().catch((error) => {
@@ -26,7 +26,7 @@ const snippet: SnippetModule = {
26
26
  // Colorize based on numeric sign
27
27
  const n = Number(ctx.value ?? 0);
28
28
  const color = Number.isFinite(n) ? (n > 0 ? 'green' : n < 0 ? 'red' : '#999') : '#555';
29
- ctx.element.innerHTML = '<span style=' + JSON.stringify('color:' + color) + '>' + String(ctx.value ?? '') + '</span>';
29
+ ctx.render('<span style=' + JSON.stringify('color:' + color) + '>' + String(ctx.value ?? '') + '</span>');
30
30
  `,
31
31
  };
32
32
 
@@ -23,10 +23,22 @@ const snippet: SnippetModule = {
23
23
  },
24
24
  content: `
25
25
  const text = String(ctx.value ?? '');
26
- ctx.element.innerHTML = '<a class="nb-copy" style="cursor:pointer;color:#1677ff">' +
27
- ctx.t('Copy') + '</a>';
28
26
 
29
- ctx.element.querySelector('.nb-copy')?.addEventListener('click', async () => {
27
+ const wrapper = document.createElement('span');
28
+ wrapper.style.display = 'inline-flex';
29
+ wrapper.style.alignItems = 'center';
30
+ wrapper.style.gap = '8px';
31
+
32
+ const valueEl = document.createElement('span');
33
+ valueEl.textContent = text;
34
+ valueEl.style.color = '#666';
35
+
36
+ const copyEl = document.createElement('a');
37
+ copyEl.textContent = ctx.t('Copy');
38
+ copyEl.style.cursor = 'pointer';
39
+ copyEl.style.color = '#1677ff';
40
+
41
+ copyEl.addEventListener('click', async () => {
30
42
  if (navigator?.clipboard?.writeText) {
31
43
  await navigator.clipboard.writeText(text);
32
44
  } else {
@@ -39,6 +51,11 @@ ctx.element.querySelector('.nb-copy')?.addEventListener('click', async () => {
39
51
  }
40
52
  ctx.message.success(ctx.t('Copied'));
41
53
  });
54
+
55
+ wrapper.appendChild(valueEl);
56
+ wrapper.appendChild(copyEl);
57
+
58
+ ctx.render(wrapper);
42
59
  `,
43
60
  };
44
61