@nocobase/flow-engine 2.1.0-beta.9 → 2.1.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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
@@ -0,0 +1,147 @@
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 type { FlowModel } from '../models';
11
+
12
+ type LoadedPageOptions = {
13
+ parentId?: string;
14
+ subKey?: string;
15
+ };
16
+
17
+ type DirtyKeyOptions = {
18
+ force?: boolean;
19
+ };
20
+
21
+ type FlowSettingsContextLike = {
22
+ flowSettingsEnabled?: boolean;
23
+ };
24
+
25
+ type FlowEngineLike = {
26
+ context?: FlowSettingsContextLike;
27
+ previousEngine?: FlowEngineLike;
28
+ };
29
+
30
+ const getLoadedPageKey = (options?: LoadedPageOptions): string | undefined => {
31
+ const parentId = options?.parentId;
32
+ const subKey = options?.subKey;
33
+ if (!parentId || subKey !== 'page') {
34
+ return undefined;
35
+ }
36
+ return `${parentId}::${subKey}`;
37
+ };
38
+
39
+ const getLoadedPageKeyFromModel = (model?: FlowModel | null): string | undefined => {
40
+ let current = model;
41
+ while (current) {
42
+ if (current.subKey === 'page' && current.parent?.uid) {
43
+ return getLoadedPageKey({ parentId: current.parent.uid, subKey: current.subKey });
44
+ }
45
+ current = current.parent as FlowModel | undefined;
46
+ }
47
+ return undefined;
48
+ };
49
+
50
+ const isFlowSettingsEnabledForContext = (context?: FlowSettingsContextLike): boolean => {
51
+ try {
52
+ return !!context?.flowSettingsEnabled;
53
+ } catch (error) {
54
+ return false;
55
+ }
56
+ };
57
+
58
+ const isFlowSettingsEnabledForModel = (model?: FlowModel | null): boolean => {
59
+ if (isFlowSettingsEnabledForContext(model?.context as FlowSettingsContextLike | undefined)) {
60
+ return true;
61
+ }
62
+
63
+ const visited = new Set<FlowEngineLike>();
64
+ let engine = model?.flowEngine as FlowEngineLike | undefined;
65
+ while (engine && !visited.has(engine)) {
66
+ visited.add(engine);
67
+ if (isFlowSettingsEnabledForContext(engine.context)) {
68
+ return true;
69
+ }
70
+ engine = engine.previousEngine;
71
+ }
72
+ return false;
73
+ };
74
+
75
+ const removeLoadedModelTree = (model?: FlowModel | null): void => {
76
+ if (!model?.uid || !model.flowEngine) {
77
+ return;
78
+ }
79
+ if (model.flowEngine.getModel(model.uid) === model) {
80
+ model.flowEngine.removeModelWithSubModels(model.uid);
81
+ }
82
+ };
83
+
84
+ const mountLoadedModelToParent = <T extends FlowModel = FlowModel>(model: T | null, forceReplace = false): T | null => {
85
+ if (!model?.parent || !model.subKey) {
86
+ return model;
87
+ }
88
+
89
+ const mounted = (model.parent.subModels as any)?.[model.subKey];
90
+ const existing =
91
+ forceReplace && model.subType !== 'array' && mounted && !Array.isArray(mounted)
92
+ ? (mounted as FlowModel)
93
+ : model.parent.findSubModel(model.subKey, (m) => m.uid === model.uid);
94
+ if (existing) {
95
+ if (!forceReplace || existing === model) {
96
+ return model;
97
+ }
98
+ removeLoadedModelTree(existing);
99
+ }
100
+
101
+ if (model.subType === 'array') {
102
+ model.parent.addSubModel(model.subKey, model);
103
+ } else {
104
+ model.parent.setSubModel(model.subKey, model);
105
+ }
106
+ return model;
107
+ };
108
+
109
+ export const createLoadedPageCache = () => {
110
+ const dirtyKeys = new Set<string>();
111
+
112
+ return {
113
+ getDirtyKeyForModel(model?: FlowModel | null, options?: DirtyKeyOptions): string | undefined {
114
+ if (!options?.force && !isFlowSettingsEnabledForModel(model)) {
115
+ return undefined;
116
+ }
117
+ return getLoadedPageKeyFromModel(model);
118
+ },
119
+
120
+ markDirty(key?: string): void {
121
+ if (key) {
122
+ dirtyKeys.add(key);
123
+ }
124
+ },
125
+
126
+ shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean {
127
+ const key = getLoadedPageKey(options);
128
+ if (!key || !dirtyKeys.has(key)) {
129
+ return false;
130
+ }
131
+ try {
132
+ return !isFlowSettingsEnabled?.();
133
+ } catch (error) {
134
+ return true;
135
+ }
136
+ },
137
+
138
+ clear(options?: LoadedPageOptions): void {
139
+ const key = getLoadedPageKey(options);
140
+ if (key) {
141
+ dirtyKeys.delete(key);
142
+ }
143
+ },
144
+
145
+ mountModelToParent: mountLoadedModelToParent,
146
+ };
147
+ };
@@ -18,6 +18,35 @@ export interface ViewParam {
18
18
  sourceId?: string;
19
19
  }
20
20
 
21
+ export interface ParsePathnameToViewParamsOptions {
22
+ rootPrefix?: string;
23
+ basePath?: string;
24
+ }
25
+
26
+ const normalizePathname = (pathname: string) => {
27
+ if (!pathname || pathname === '/') {
28
+ return '/';
29
+ }
30
+ return `/${pathname.replace(/^\/+/, '').replace(/\/+$/, '')}`;
31
+ };
32
+
33
+ const normalizeBasePath = (basePath: string) => `/${basePath.replace(/^\/+/, '').replace(/\/+$/, '')}`;
34
+
35
+ const stripBasePath = (pathname: string, basePath: string) => {
36
+ const normalizedPathname = normalizePathname(pathname);
37
+ const normalizedBasePath = normalizeBasePath(basePath);
38
+
39
+ if (normalizedPathname === normalizedBasePath) {
40
+ return '';
41
+ }
42
+
43
+ if (normalizedPathname.startsWith(`${normalizedBasePath}/`)) {
44
+ return normalizedPathname.slice(normalizedBasePath.length + 1);
45
+ }
46
+
47
+ return '';
48
+ };
49
+
21
50
  /**
22
51
  * 解析路径名为视图参数数组
23
52
  *
@@ -33,15 +62,21 @@ export interface ViewParam {
33
62
  * parsePathnameToViewParams('/admin/xxx/view/yyy') // [{ viewUid: 'xxx' }, { viewUid: 'yyy' }]
34
63
  * ```
35
64
  */
36
- export const parsePathnameToViewParams = (pathname: string): ViewParam[] => {
65
+ export const parsePathnameToViewParams = (
66
+ pathname: string,
67
+ options: ParsePathnameToViewParamsOptions = {},
68
+ ): ViewParam[] => {
37
69
  if (!pathname || pathname === '/') {
38
70
  return [];
39
71
  }
40
72
 
73
+ const rootPrefix = options.rootPrefix || 'admin';
74
+ const relativePath = options.basePath ? stripBasePath(pathname, options.basePath) : '';
75
+
41
76
  // 移除开头的斜杠并分割路径
42
- const segments = pathname.replace(/^\/+/, '').split('/').filter(Boolean);
77
+ const segments = (options.basePath ? relativePath : pathname).replace(/^\/+/, '').split('/').filter(Boolean);
43
78
 
44
- if (segments.length < 2) {
79
+ if (segments.length < (options.basePath ? 1 : 2)) {
45
80
  return [];
46
81
  }
47
82
 
@@ -49,11 +84,16 @@ export const parsePathnameToViewParams = (pathname: string): ViewParam[] => {
49
84
  let currentView: ViewParam | null = null;
50
85
  let i = 0;
51
86
 
87
+ if (options.basePath) {
88
+ currentView = { viewUid: segments[0] };
89
+ i = 1;
90
+ }
91
+
52
92
  while (i < segments.length) {
53
93
  const segment = segments[i];
54
94
 
55
- // 处理 admin 或 view 关键字
56
- if (segment === 'admin' || segment === 'view') {
95
+ // 处理布局根前缀或 view 关键字
96
+ if (segment === rootPrefix || segment === 'view') {
57
97
  // 如果有当前视图,先保存到结果中
58
98
  if (currentView) {
59
99
  result.push(currentView);
@@ -0,0 +1,48 @@
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
+ const CHARSET = '0123456789abcdefghijklmnopqrstuvwxyz';
11
+
12
+ /**
13
+ * Generate a random base36 identifier with an optional semantic prefix.
14
+ *
15
+ * Equivalent in shape to v1's `uid()` from `@formily/shared` (11 chars
16
+ * of `[0-9a-z]`), with an opt-in prefix appended at the front. v2 forbids
17
+ * direct `@formily/*` imports in `src/client-v2/`, so this helper is the
18
+ * single substitute the rest of the codebase should reach for.
19
+ *
20
+ * Common semantic prefixes observed across the codebase — pass the one
21
+ * that matches your domain rather than relying on a default, so the
22
+ * intent is explicit at the call site:
23
+ *
24
+ * - `s_` — service / settings record (authenticators, channels, …)
25
+ * - `v_` — verifier / variable / LLM service
26
+ * - `f_` — field
27
+ * - `t_` — through table
28
+ *
29
+ * Example:
30
+ *
31
+ * ```ts
32
+ * import { randomId } from '@nocobase/flow-engine';
33
+ *
34
+ * name: randomId('s_'), // → 's_keeoaui1ubi'
35
+ * name: randomId('v_'), // → 'v_a8f3kp2x9qm'
36
+ * name: randomId(), // → 'a8f3kp2x9qm'
37
+ * ```
38
+ *
39
+ * Not cryptographically secure — uses `Math.random()`. Good enough for
40
+ * unique form names / schema keys, NOT for security tokens.
41
+ */
42
+ export function randomId(prefix = '', length = 11): string {
43
+ let id = '';
44
+ for (let i = 0; i < length; i++) {
45
+ id += CHARSET[(Math.random() * CHARSET.length) | 0];
46
+ }
47
+ return `${prefix}${id}`;
48
+ }
@@ -553,8 +553,8 @@ function extractUsedCtxLibKeys(code: string): string[] {
553
553
  }
554
554
 
555
555
  function injectEnsureLibsPreamble(code: string): string {
556
- if (!CTX_LIBS_MARKER_RE.test(code)) return code;
557
556
  if (ENSURE_LIBS_MARKER_RE.test(code)) return code;
557
+ if (!CTX_LIBS_MARKER_RE.test(code)) return code;
558
558
  const keys = extractUsedCtxLibKeys(code);
559
559
  if (!keys.length) return code;
560
560
  return `/* __runjs_ensure_libs */\nawait ctx.__ensureLibs(${JSON.stringify(keys)});\n${code}`;
@@ -236,6 +236,37 @@ function normalizeSubPath(raw: string): { subPath: string; wildcard: boolean } {
236
236
  return { subPath: s, wildcard: false };
237
237
  }
238
238
 
239
+ function extractCtxRootUsage(expr: string): { varName: string; subPath: string; wildcard: boolean } | null {
240
+ const raw = String(expr || '').trim();
241
+ if (!raw || raw === 'ctx') return null;
242
+
243
+ const dotMatch = raw.match(/^ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*)([\s\S]*)$/);
244
+ if (dotMatch) {
245
+ const varName = dotMatch[1] || '';
246
+ const rest = dotMatch[2] || '';
247
+ const normalized = normalizeSubPath(rest);
248
+ return {
249
+ varName,
250
+ subPath: normalized.subPath,
251
+ wildcard: normalized.wildcard,
252
+ };
253
+ }
254
+
255
+ const bracketMatch = raw.match(/^ctx\s*\[\s*(['"])([a-zA-Z_$][a-zA-Z0-9_$]*)\1\s*\]([\s\S]*)$/);
256
+ if (bracketMatch) {
257
+ const varName = bracketMatch[2] || '';
258
+ const rest = bracketMatch[3] || '';
259
+ const normalized = normalizeSubPath(rest);
260
+ return {
261
+ varName,
262
+ subPath: normalized.subPath,
263
+ wildcard: normalized.wildcard,
264
+ };
265
+ }
266
+
267
+ return null;
268
+ }
269
+
239
270
  /**
240
271
  * Heuristic extraction of ctx variable usage from RunJS code.
241
272
  *
@@ -256,27 +287,35 @@ export function extractUsedVariablePathsFromRunJS(code: string): Record<string,
256
287
  usage.set(varName, set);
257
288
  };
258
289
 
290
+ const addCtxUsage = (expr: string) => {
291
+ const hit = extractCtxRootUsage(expr);
292
+ if (!hit?.varName) return;
293
+ add(hit.varName, hit.wildcard ? '' : hit.subPath);
294
+ };
295
+
259
296
  // dot form: ctx.foo.bar / ctx.foo[0].bar (excluding ctx.method(...))
260
297
  const dotRe = /ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*(?:(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\[[^\]]+\]))*)(?!\s*\()/g;
261
298
  let match: RegExpExecArray | null;
262
299
  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);
300
+ addCtxUsage(`ctx.${match[1] || ''}`);
270
301
  }
271
302
 
272
303
  // bracket root: ctx['foo'].bar / ctx["foo"][0] (excluding ctx['method'](...))
273
304
  const bracketRootRe =
274
305
  /ctx\s*\[\s*(['"])([a-zA-Z_$][a-zA-Z0-9_$]*)\1\s*\]((?:(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\[[^\]]+\]))*)(?!\s*\()/g;
275
306
  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);
307
+ addCtxUsage(`ctx['${match[2] || ''}']${match[3] || ''}`);
308
+ }
309
+
310
+ // async-safe helper form: await ctx.getVar('ctx.foo.bar')
311
+ const getVarRe = /ctx\.getVar\s*\(\s*(['"])((?:\\.|(?!\1)[\s\S])*)\1\s*\)/g;
312
+ while ((match = getVarRe.exec(srcWithStrings))) {
313
+ const expr = String(match[2] || '')
314
+ .replace(/\\'/g, "'")
315
+ .replace(/\\"/g, '"')
316
+ .trim();
317
+ if (!expr.startsWith('ctx')) continue;
318
+ addCtxUsage(expr);
280
319
  }
281
320
 
282
321
  const out: Record<string, string[]> = {};
@@ -11,7 +11,7 @@ import type { ISchema } from '@formily/json-schema';
11
11
  import { Schema } from '@formily/json-schema';
12
12
  import type { FlowModel } from '../models';
13
13
  import { FlowRuntimeContext } from '../flowContext';
14
- import type { StepDefinition, StepUIMode } from '../types';
14
+ import type { EventDefinition, StepDefinition, StepUIMode } from '../types';
15
15
  import { setupRuntimeContextSteps } from './setupRuntimeContextSteps';
16
16
 
17
17
  /**
@@ -242,6 +242,35 @@ export async function resolveStepUiSchema<TModel extends FlowModel = FlowModel>(
242
242
  return resolvedStepUiSchema;
243
243
  }
244
244
 
245
+ /**
246
+ * 判断事件在设置菜单中是否应被隐藏。
247
+ * - 支持 EventDefinition.hideInSettings。
248
+ * - hideInSettings 可为布尔值或函数(接收 FlowRuntimeContext)。
249
+ */
250
+ export async function shouldHideEventInSettings<TModel extends FlowModel = FlowModel>(
251
+ model: TModel,
252
+ flow: any,
253
+ event: EventDefinition<TModel> | undefined,
254
+ ): Promise<boolean> {
255
+ if (!event) return true;
256
+
257
+ const { hideInSettings } = event;
258
+
259
+ if (typeof hideInSettings === 'function') {
260
+ try {
261
+ const ctx = new FlowRuntimeContext(model, flow.key, 'settings');
262
+ setupRuntimeContextSteps(ctx, flow.steps || {}, model, flow.key);
263
+ const result = await hideInSettings(ctx as any);
264
+ return !!result;
265
+ } catch (error) {
266
+ console.warn(`Error evaluating hideInSettings for event '${event.name || ''}' in flow '${flow.key}':`, error);
267
+ return false;
268
+ }
269
+ }
270
+
271
+ return !!hideInSettings;
272
+ }
273
+
245
274
  /**
246
275
  * 判断步骤在设置菜单中是否应被隐藏。
247
276
  * - 支持 StepDefinition.hideInSettings 与 ActionDefinition.hideInSettings(step 优先)。
@@ -11,13 +11,23 @@ import { PopoverProps as AntdPopoverProps } from 'antd';
11
11
  import { FlowContext } from '../flowContext';
12
12
  import { ViewNavigation } from './ViewNavigation';
13
13
 
14
+ export type FlowViewBeforeClosePayload = {
15
+ result?: any;
16
+ force?: boolean;
17
+ };
18
+
19
+ export type FlowViewBeforeCloseHandler = (
20
+ payload: FlowViewBeforeClosePayload,
21
+ ) => Promise<boolean | void> | boolean | void;
22
+
14
23
  export type FlowView = {
15
24
  type: 'drawer' | 'popover' | 'dialog' | 'embed';
16
25
  inputArgs: any;
17
26
  Header: React.FC<{ title?: React.ReactNode; extra?: React.ReactNode }> | null;
18
27
  Footer: React.FC<{ children?: React.ReactNode }> | null;
19
- close: (result?: any, force?: boolean) => void;
28
+ close: (result?: any, force?: boolean) => Promise<boolean | void> | boolean | void;
20
29
  update: (newConfig: any) => void;
30
+ beforeClose?: FlowViewBeforeCloseHandler;
21
31
  navigation?: ViewNavigation;
22
32
  /** 页面的销毁方法 */
23
33
  destroy?: () => void;
@@ -74,11 +84,21 @@ export class FlowViewer {
74
84
  if (this.types[type]) {
75
85
  zIndex += 1;
76
86
  const onClose = others.onClose;
87
+ let zIndexReleased = false;
88
+ const releaseZIndex = () => {
89
+ if (!zIndexReleased) {
90
+ zIndexReleased = true;
91
+ zIndex -= 1;
92
+ }
93
+ };
77
94
  const _zIndex = others.zIndex;
78
95
  others.onClose = (...args) => {
79
96
  onClose?.(...args);
80
- zIndex -= 1;
97
+ releaseZIndex();
81
98
  };
99
+ if (type === 'embed') {
100
+ others.onOpenCancelled = releaseZIndex;
101
+ }
82
102
  // embed 不能设置过高的 zIndex,会遮挡菜单的折叠按钮图表
83
103
  if (type !== 'embed') {
84
104
  others.zIndex = _zIndex ?? this.getNextZIndex();
@@ -24,6 +24,7 @@ export const PageComponent = forwardRef((props: any, ref) => {
24
24
  title: _title,
25
25
  styles = {},
26
26
  zIndex = 4, // 这个默认值是为了防止表格的阴影显示到子页面上面
27
+ onClose,
27
28
  } = mergedProps;
28
29
  const closedRef = useRef(false);
29
30
  const flowEngine = useFlowEngine();
@@ -86,10 +87,12 @@ export const PageComponent = forwardRef((props: any, ref) => {
86
87
  type="text"
87
88
  size="small"
88
89
  icon={<CloseOutlined />}
89
- onClick={() => {
90
+ onClick={async () => {
90
91
  if (!closedRef.current) {
91
- closedRef.current = true;
92
- props.onClose?.();
92
+ const closed = await onClose?.();
93
+ if (closed !== false) {
94
+ closedRef.current = true;
95
+ }
93
96
  }
94
97
  }}
95
98
  style={{
@@ -111,7 +114,7 @@ export const PageComponent = forwardRef((props: any, ref) => {
111
114
  {extra && <div>{extra}</div>}
112
115
  </div>
113
116
  );
114
- }, [header, _title, flowEngine.context.themeToken, styles.header, props.onClose]);
117
+ }, [header, _title, flowEngine.context.themeToken, styles.header, onClose]);
115
118
 
116
119
  // Footer 组件
117
120
  const FooterComponent = useMemo(() => {
@@ -13,6 +13,16 @@ import { ViewParam as SharedViewParam } from '../utils';
13
13
 
14
14
  type ViewParams = Omit<SharedViewParam, 'viewUid'> & { viewUid?: string };
15
15
 
16
+ export interface GeneratePathnameFromViewParamsOptions {
17
+ prefix?: string;
18
+ basePath?: string;
19
+ }
20
+
21
+ export interface ViewNavigationOptions {
22
+ basePath?: string;
23
+ layoutBasePath?: string;
24
+ }
25
+
16
26
  function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
17
27
  if (val === undefined || val === null) return '';
18
28
  // 1.x 兼容:对象按 key1=v1&key2=v2 拼接后整体 encodeURIComponent
@@ -26,6 +36,15 @@ function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
26
36
  return encodeURIComponent(String(val));
27
37
  }
28
38
 
39
+ function hasUsableSourceId(sourceId: unknown): sourceId is string | number {
40
+ return sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
41
+ }
42
+
43
+ function normalizeBasePath(basePath?: string) {
44
+ const value = basePath || '/admin';
45
+ return `/${value.replace(/^\/+/, '').replace(/\/+$/, '')}`;
46
+ }
47
+
29
48
  /**
30
49
  * 将 ViewParam 数组转换为 pathname
31
50
  *
@@ -39,12 +58,17 @@ function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
39
58
  * generatePathnameFromViewParams([{ viewUid: 'xxx' }, { viewUid: 'yyy' }]) // '/admin/xxx/view/yyy'
40
59
  * ```
41
60
  */
42
- export function generatePathnameFromViewParams(viewParams: ViewParams[]): string {
61
+ export function generatePathnameFromViewParams(
62
+ viewParams: ViewParams[],
63
+ options: GeneratePathnameFromViewParamsOptions = {},
64
+ ): string {
65
+ const basePath = normalizeBasePath(options.basePath || options.prefix);
66
+
43
67
  if (!viewParams || viewParams.length === 0) {
44
- return '/admin';
68
+ return basePath;
45
69
  }
46
70
 
47
- const segments = ['admin'];
71
+ const segments = basePath.replace(/^\/+/, '').split('/').filter(Boolean);
48
72
 
49
73
  viewParams.forEach((viewParam, index) => {
50
74
  // 如果不是第一个视图,添加 'view' 关键字
@@ -65,8 +89,8 @@ export function generatePathnameFromViewParams(viewParams: ViewParams[]): string
65
89
  segments.push('filterbytk', encoded);
66
90
  }
67
91
  }
68
- if (viewParam.sourceId) {
69
- segments.push('sourceid', viewParam.sourceId);
92
+ if (hasUsableSourceId(viewParam.sourceId)) {
93
+ segments.push('sourceid', String(viewParam.sourceId));
70
94
  }
71
95
  });
72
96
 
@@ -77,10 +101,12 @@ export class ViewNavigation {
77
101
  viewStack: ReadonlyArray<ViewParams>; // 只能通过 setViewStack 修改
78
102
  ctx: FlowEngineContext;
79
103
  viewParams: ViewParams;
104
+ private readonly basePath?: string;
80
105
 
81
- constructor(ctx: FlowEngineContext, viewParams: ViewParams[]) {
106
+ constructor(ctx: FlowEngineContext, viewParams: ViewParams[], options: ViewNavigationOptions = {}) {
82
107
  this.setViewStack(viewParams);
83
108
  this.ctx = ctx;
109
+ this.basePath = options.basePath || options.layoutBasePath;
84
110
 
85
111
  define(this, {
86
112
  viewParams: observable,
@@ -102,7 +128,7 @@ export class ViewNavigation {
102
128
  });
103
129
 
104
130
  // 2. 根据 viewStack 生成新的 pathname
105
- const newPathname = generatePathnameFromViewParams(newViewStack);
131
+ const newPathname = generatePathnameFromViewParams(newViewStack, { basePath: this.getLayoutBasePath() });
106
132
 
107
133
  // 3. 触发一次跳转。使用 replace 的方式
108
134
  this.ctx.router.navigate(newPathname, { replace: true });
@@ -111,7 +137,9 @@ export class ViewNavigation {
111
137
  navigateTo(viewParam: ViewParams, opts?: { replace?: boolean; state?: any }) {
112
138
  // 1. 基于当前 viewStack 生成一个 pathname
113
139
  // 2. 将当前传入的参数转为 path string
114
- const newViewPathname = generatePathnameFromViewParams([...this.viewStack, viewParam]);
140
+ const newViewPathname = generatePathnameFromViewParams([...this.viewStack, viewParam], {
141
+ basePath: this.getLayoutBasePath(),
142
+ });
115
143
 
116
144
  // 3. 与 pathname 拼接成新的 pathname(这里直接使用新生成的 pathname)
117
145
  const newPathname = newViewPathname;
@@ -122,7 +150,16 @@ export class ViewNavigation {
122
150
 
123
151
  back() {
124
152
  const prevStack = this.viewStack.slice(0, -1);
125
- const prevPath = generatePathnameFromViewParams(prevStack);
153
+ const prevPath = generatePathnameFromViewParams(prevStack, { basePath: this.getLayoutBasePath() });
126
154
  this.ctx.router.navigate(prevPath, { replace: true });
127
155
  }
156
+
157
+ private getLayoutBasePath() {
158
+ const routePath = (this.ctx as any).layout?.routePath;
159
+ return (
160
+ this.basePath ||
161
+ (this.ctx as any).layoutRoute?.basePathname ||
162
+ (routePath?.startsWith('/') ? routePath : '/admin')
163
+ );
164
+ }
128
165
  }