@nocobase/flow-engine 2.1.0-beta.1 → 2.1.0-beta.11

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 (63) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/JSRunner.d.ts +10 -1
  4. package/lib/JSRunner.js +50 -5
  5. package/lib/ViewScopedFlowEngine.js +5 -1
  6. package/lib/components/dnd/gridDragPlanner.js +6 -2
  7. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  8. package/lib/components/subModel/AddSubModelButton.js +15 -0
  9. package/lib/executor/FlowExecutor.js +31 -8
  10. package/lib/flowContext.js +4 -1
  11. package/lib/flowEngine.d.ts +19 -0
  12. package/lib/flowEngine.js +29 -1
  13. package/lib/runjs-context/registry.d.ts +1 -1
  14. package/lib/runjs-context/setup.js +19 -12
  15. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  16. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  17. package/lib/types.d.ts +1 -1
  18. package/lib/utils/index.d.ts +2 -2
  19. package/lib/utils/index.js +4 -0
  20. package/lib/utils/parsePathnameToViewParams.js +1 -1
  21. package/lib/utils/schema-utils.d.ts +7 -1
  22. package/lib/utils/schema-utils.js +19 -0
  23. package/lib/views/FlowView.d.ts +7 -1
  24. package/lib/views/runViewBeforeClose.d.ts +10 -0
  25. package/lib/views/runViewBeforeClose.js +45 -0
  26. package/lib/views/useDialog.d.ts +2 -1
  27. package/lib/views/useDialog.js +20 -3
  28. package/lib/views/useDrawer.d.ts +2 -1
  29. package/lib/views/useDrawer.js +20 -3
  30. package/lib/views/usePage.d.ts +2 -1
  31. package/lib/views/usePage.js +10 -3
  32. package/package.json +5 -5
  33. package/src/JSRunner.ts +68 -4
  34. package/src/ViewScopedFlowEngine.ts +4 -0
  35. package/src/__tests__/JSRunner.test.ts +27 -1
  36. package/src/__tests__/runjsContext.test.ts +13 -0
  37. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  38. package/src/components/__tests__/gridDragPlanner.test.ts +88 -0
  39. package/src/components/dnd/gridDragPlanner.ts +8 -2
  40. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  41. package/src/components/subModel/AddSubModelButton.tsx +16 -0
  42. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +50 -0
  43. package/src/executor/FlowExecutor.ts +34 -9
  44. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  45. package/src/flowContext.ts +5 -3
  46. package/src/flowEngine.ts +33 -1
  47. package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
  48. package/src/runjs-context/registry.ts +1 -1
  49. package/src/runjs-context/setup.ts +21 -12
  50. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  51. package/src/types.ts +1 -0
  52. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  53. package/src/utils/__tests__/utils.test.ts +62 -0
  54. package/src/utils/index.ts +2 -1
  55. package/src/utils/parsePathnameToViewParams.ts +2 -2
  56. package/src/utils/schema-utils.ts +30 -1
  57. package/src/views/FlowView.tsx +11 -1
  58. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  59. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  60. package/src/views/runViewBeforeClose.ts +19 -0
  61. package/src/views/useDialog.tsx +25 -3
  62. package/src/views/useDrawer.tsx +25 -3
  63. package/src/views/usePage.tsx +12 -3
@@ -19,6 +19,7 @@ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } f
19
19
  import { FlowEngineProvider } from '../provider';
20
20
  import { createViewScopedEngine } from '../ViewScopedFlowEngine';
21
21
  import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
22
+ import { runViewBeforeClose } from './runViewBeforeClose';
22
23
 
23
24
  let uuid = 0;
24
25
 
@@ -89,12 +90,18 @@ export function useDialog() {
89
90
  ctx.addDelegate(flowContext.engine.context);
90
91
  }
91
92
 
93
+ // 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
94
+ let destroyed = false;
95
+
92
96
  // 构造 currentDialog 实例
93
97
  const currentDialog = {
94
98
  type: 'dialog' as const,
95
99
  inputArgs: config.inputArgs || {},
96
100
  preventClose: !!config.preventClose,
101
+ beforeClose: undefined,
97
102
  destroy: (result?: any) => {
103
+ if (destroyed) return;
104
+ destroyed = true;
98
105
  config.onClose?.();
99
106
  dialogRef.current?.destroy();
100
107
  closeFunc?.();
@@ -107,18 +114,24 @@ export function useDialog() {
107
114
  scopedEngine.unlinkFromStack();
108
115
  },
109
116
  update: (newConfig) => dialogRef.current?.update(newConfig),
110
- close: (result?: any, force?: boolean) => {
117
+ close: async (result?: any, force?: boolean) => {
111
118
  if (config.preventClose && !force) {
112
- return;
119
+ return false;
120
+ }
121
+
122
+ const shouldClose = await runViewBeforeClose(currentDialog, { result, force });
123
+ if (!shouldClose) {
124
+ return false;
113
125
  }
114
126
 
115
127
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
116
128
  // 交由路由系统来销毁当前视图
117
129
  config.inputArgs.navigation.back();
118
- return;
130
+ return true;
119
131
  }
120
132
 
121
133
  currentDialog.destroy(result);
134
+ return true;
122
135
  },
123
136
  Footer: FooterComponent,
124
137
  Header: HeaderComponent,
@@ -140,6 +153,15 @@ export function useDialog() {
140
153
  get: () => currentDialog,
141
154
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
142
155
  });
156
+ // 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
157
+ // 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
158
+ // 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
159
+ scopedEngine.setDestroyView(() => {
160
+ if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
161
+ config.inputArgs.navigation.back();
162
+ }
163
+ currentDialog.destroy();
164
+ });
143
165
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
144
166
  registerPopupVariable(ctx, currentDialog);
145
167
  // 内部组件,在 Provider 内部计算 content
@@ -19,6 +19,7 @@ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } f
19
19
  import { FlowEngineProvider } from '../provider';
20
20
  import { createViewScopedEngine } from '../ViewScopedFlowEngine';
21
21
  import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
22
+ import { runViewBeforeClose } from './runViewBeforeClose';
22
23
 
23
24
  export function useDrawer() {
24
25
  const holderRef = React.useRef(null);
@@ -118,12 +119,18 @@ export function useDrawer() {
118
119
  ctx.addDelegate(flowContext.engine.context);
119
120
  }
120
121
 
122
+ // 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
123
+ let destroyed = false;
124
+
121
125
  // 构造 currentDrawer 实例
122
126
  const currentDrawer = {
123
127
  type: 'drawer' as const,
124
128
  inputArgs: config.inputArgs || {},
125
129
  preventClose: !!config.preventClose,
130
+ beforeClose: undefined,
126
131
  destroy: (result?: any) => {
132
+ if (destroyed) return;
133
+ destroyed = true;
127
134
  config.onClose?.();
128
135
  drawerRef.current?.destroy();
129
136
  closeFunc?.();
@@ -136,18 +143,24 @@ export function useDrawer() {
136
143
  scopedEngine.unlinkFromStack();
137
144
  },
138
145
  update: (newConfig) => drawerRef.current?.update(newConfig),
139
- close: (result?: any, force?: boolean) => {
146
+ close: async (result?: any, force?: boolean) => {
140
147
  if (config.preventClose && !force) {
141
- return;
148
+ return false;
149
+ }
150
+
151
+ const shouldClose = await runViewBeforeClose(currentDrawer, { result, force });
152
+ if (!shouldClose) {
153
+ return false;
142
154
  }
143
155
 
144
156
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
145
157
  // 交由路由系统来销毁当前视图
146
158
  config.inputArgs.navigation.back();
147
- return;
159
+ return true;
148
160
  }
149
161
 
150
162
  currentDrawer.destroy(result);
163
+ return true;
151
164
  },
152
165
  Footer: FooterComponent,
153
166
  Header: HeaderComponent,
@@ -169,6 +182,15 @@ export function useDrawer() {
169
182
  get: () => currentDrawer,
170
183
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
171
184
  });
185
+ // 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
186
+ // 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
187
+ // 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
188
+ scopedEngine.setDestroyView(() => {
189
+ if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
190
+ config.inputArgs.navigation.back();
191
+ }
192
+ currentDrawer.destroy();
193
+ });
172
194
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
173
195
  registerPopupVariable(ctx, currentDrawer);
174
196
 
@@ -19,6 +19,7 @@ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } f
19
19
  import { FlowEngineProvider } from '../provider';
20
20
  import { createViewScopedEngine } from '../ViewScopedFlowEngine';
21
21
  import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
22
+ import { runViewBeforeClose } from './runViewBeforeClose';
22
23
 
23
24
  let uuid = 0;
24
25
 
@@ -122,6 +123,7 @@ export function usePage() {
122
123
  type: 'embed' as const,
123
124
  inputArgs: viewInputArgs,
124
125
  preventClose: !!config.preventClose,
126
+ beforeClose: undefined,
125
127
  destroy: (result?: any) => {
126
128
  config.onClose?.();
127
129
  resolvePromise?.(result);
@@ -145,18 +147,24 @@ export function usePage() {
145
147
  scopedEngine.unlinkFromStack();
146
148
  },
147
149
  update: (newConfig) => pageRef.current?.update(newConfig),
148
- close: (result?: any, force?: boolean) => {
150
+ close: async (result?: any, force?: boolean) => {
149
151
  if (preventClose && !force) {
150
- return;
152
+ return false;
153
+ }
154
+
155
+ const shouldClose = await runViewBeforeClose(currentPage, { result, force });
156
+ if (!shouldClose) {
157
+ return false;
151
158
  }
152
159
 
153
160
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
154
161
  // 交由路由系统来销毁当前视图
155
162
  config.inputArgs.navigation.back();
156
- return;
163
+ return true;
157
164
  }
158
165
 
159
166
  currentPage.destroy(result);
167
+ return true;
160
168
  },
161
169
  Header: HeaderComponent,
162
170
  Footer: FooterComponent,
@@ -178,6 +186,7 @@ export function usePage() {
178
186
  // 仅当访问关联字段或前端无本地记录数据时,才交给服务端解析
179
187
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
180
188
  });
189
+ // embed 视图不注册 destroyView,afterSuccess 关闭弹窗时自然跳过
181
190
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
182
191
  registerPopupVariable(ctx, currentPage);
183
192