@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45

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 (209) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/FlowContextProvider.d.ts +5 -1
  4. package/lib/FlowContextProvider.js +9 -2
  5. package/lib/JSRunner.d.ts +10 -1
  6. package/lib/JSRunner.js +50 -5
  7. package/lib/ViewScopedFlowEngine.js +5 -1
  8. package/lib/components/FieldModelRenderer.js +2 -2
  9. package/lib/components/FlowModelRenderer.d.ts +3 -1
  10. package/lib/components/FlowModelRenderer.js +12 -6
  11. package/lib/components/FormItem.d.ts +6 -0
  12. package/lib/components/FormItem.js +11 -3
  13. package/lib/components/MobilePopup.js +6 -5
  14. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  15. package/lib/components/dnd/gridDragPlanner.js +613 -21
  16. package/lib/components/dnd/index.d.ts +31 -2
  17. package/lib/components/dnd/index.js +244 -23
  18. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  19. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  20. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  21. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -11
  22. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  23. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  24. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  25. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  26. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  27. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  28. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  29. package/lib/components/subModel/AddSubModelButton.js +27 -1
  30. package/lib/components/subModel/LazyDropdown.js +293 -52
  31. package/lib/components/subModel/index.d.ts +1 -0
  32. package/lib/components/subModel/index.js +19 -0
  33. package/lib/components/subModel/utils.d.ts +1 -1
  34. package/lib/components/subModel/utils.js +9 -3
  35. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  36. package/lib/components/variables/VariableHybridInput.js +499 -0
  37. package/lib/components/variables/index.d.ts +2 -0
  38. package/lib/components/variables/index.js +3 -0
  39. package/lib/data-source/index.d.ts +84 -0
  40. package/lib/data-source/index.js +259 -5
  41. package/lib/executor/FlowExecutor.js +32 -9
  42. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  43. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  44. package/lib/flow-registry/index.d.ts +1 -0
  45. package/lib/flow-registry/index.js +3 -1
  46. package/lib/flowContext.d.ts +3 -0
  47. package/lib/flowContext.js +46 -1
  48. package/lib/flowEngine.d.ts +151 -1
  49. package/lib/flowEngine.js +392 -18
  50. package/lib/flowI18n.js +2 -1
  51. package/lib/flowSettings.d.ts +14 -6
  52. package/lib/flowSettings.js +34 -6
  53. package/lib/index.d.ts +2 -0
  54. package/lib/index.js +7 -0
  55. package/lib/lazy-helper.d.ts +14 -0
  56. package/lib/lazy-helper.js +71 -0
  57. package/lib/locale/en-US.json +1 -0
  58. package/lib/locale/index.d.ts +2 -0
  59. package/lib/locale/zh-CN.json +1 -0
  60. package/lib/models/DisplayItemModel.d.ts +1 -1
  61. package/lib/models/EditableItemModel.d.ts +1 -1
  62. package/lib/models/FilterableItemModel.d.ts +1 -1
  63. package/lib/models/flowModel.d.ts +13 -10
  64. package/lib/models/flowModel.js +81 -21
  65. package/lib/provider.js +38 -23
  66. package/lib/reactive/observer.js +46 -16
  67. package/lib/runjs-context/registry.d.ts +1 -1
  68. package/lib/runjs-context/setup.js +20 -12
  69. package/lib/runjs-context/snippets/index.js +13 -2
  70. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  71. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  72. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  74. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  75. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  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/parsePathnameToViewParams.d.ts +5 -1
  82. package/lib/utils/parsePathnameToViewParams.js +29 -5
  83. package/lib/utils/randomId.d.ts +39 -0
  84. package/lib/utils/randomId.js +45 -0
  85. package/lib/utils/runjsTemplateCompat.js +1 -1
  86. package/lib/utils/runjsValue.js +41 -11
  87. package/lib/utils/schema-utils.d.ts +7 -1
  88. package/lib/utils/schema-utils.js +19 -0
  89. package/lib/views/FlowView.d.ts +7 -1
  90. package/lib/views/FlowView.js +11 -1
  91. package/lib/views/PageComponent.js +8 -6
  92. package/lib/views/ViewNavigation.d.ts +12 -2
  93. package/lib/views/ViewNavigation.js +28 -9
  94. package/lib/views/createViewMeta.js +114 -50
  95. package/lib/views/inheritLayoutContext.d.ts +10 -0
  96. package/lib/views/inheritLayoutContext.js +50 -0
  97. package/lib/views/runViewBeforeClose.d.ts +10 -0
  98. package/lib/views/runViewBeforeClose.js +45 -0
  99. package/lib/views/useDialog.d.ts +2 -1
  100. package/lib/views/useDialog.js +22 -3
  101. package/lib/views/useDrawer.d.ts +2 -1
  102. package/lib/views/useDrawer.js +22 -3
  103. package/lib/views/usePage.d.ts +5 -11
  104. package/lib/views/usePage.js +304 -144
  105. package/package.json +6 -5
  106. package/src/FlowContextProvider.tsx +9 -1
  107. package/src/JSRunner.ts +68 -4
  108. package/src/ViewScopedFlowEngine.ts +4 -0
  109. package/src/__tests__/JSRunner.test.ts +27 -1
  110. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  111. package/src/__tests__/flow-engine.test.ts +166 -0
  112. package/src/__tests__/flowContext.test.ts +82 -1
  113. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  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 +16 -0
  120. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  121. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  122. package/src/__tests__/runjsSnippets.test.ts +21 -0
  123. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  124. package/src/components/FieldModelRenderer.tsx +2 -1
  125. package/src/components/FlowModelRenderer.tsx +18 -6
  126. package/src/components/FormItem.tsx +7 -1
  127. package/src/components/MobilePopup.tsx +4 -2
  128. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  129. package/src/components/__tests__/FormItem.test.tsx +25 -0
  130. package/src/components/__tests__/dnd.test.ts +44 -0
  131. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  132. package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
  133. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  134. package/src/components/dnd/gridDragPlanner.ts +758 -19
  135. package/src/components/dnd/index.tsx +305 -28
  136. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  137. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +99 -11
  138. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  139. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +194 -5
  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 +32 -2
  145. package/src/components/subModel/LazyDropdown.tsx +332 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
  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 +7 -1
  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 +68 -1
  154. package/src/data-source/index.ts +322 -6
  155. package/src/executor/FlowExecutor.ts +35 -10
  156. package/src/executor/__tests__/flowExecutor.test.ts +85 -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 +50 -3
  161. package/src/flowEngine.ts +449 -14
  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__/dispatchEvent.when.test.ts +214 -0
  172. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  173. package/src/models/__tests__/flowModel.test.ts +80 -37
  174. package/src/models/flowModel.tsx +122 -36
  175. package/src/provider.tsx +41 -25
  176. package/src/reactive/__tests__/observer.test.tsx +82 -0
  177. package/src/reactive/observer.tsx +87 -25
  178. package/src/runjs-context/registry.ts +1 -1
  179. package/src/runjs-context/setup.ts +22 -12
  180. package/src/runjs-context/snippets/index.ts +12 -1
  181. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  182. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  183. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  184. package/src/types.ts +62 -0
  185. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  186. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +28 -0
  187. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  188. package/src/utils/__tests__/utils.test.ts +62 -0
  189. package/src/utils/createCollectionContextMeta.ts +6 -2
  190. package/src/utils/index.ts +5 -1
  191. package/src/utils/parsePathnameToViewParams.ts +47 -7
  192. package/src/utils/randomId.ts +48 -0
  193. package/src/utils/runjsTemplateCompat.ts +1 -1
  194. package/src/utils/runjsValue.ts +50 -11
  195. package/src/utils/schema-utils.ts +30 -1
  196. package/src/views/FlowView.tsx +22 -2
  197. package/src/views/PageComponent.tsx +7 -4
  198. package/src/views/ViewNavigation.ts +46 -9
  199. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  200. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  201. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  202. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  203. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  204. package/src/views/createViewMeta.ts +106 -34
  205. package/src/views/inheritLayoutContext.ts +26 -0
  206. package/src/views/runViewBeforeClose.ts +19 -0
  207. package/src/views/useDialog.tsx +27 -3
  208. package/src/views/useDrawer.tsx +27 -3
  209. package/src/views/usePage.tsx +367 -179
@@ -15,6 +15,82 @@ import type { FlowView } from './FlowView';
15
15
 
16
16
  type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined;
17
17
 
18
+ function isDefined(value: any) {
19
+ return value !== undefined && value !== null;
20
+ }
21
+
22
+ function isSameViewParamValue(left: any, right: any) {
23
+ if (left === right) return true;
24
+ if (!isDefined(left) || !isDefined(right)) return false;
25
+
26
+ try {
27
+ return JSON.stringify(left) === JSON.stringify(right);
28
+ } catch (_) {
29
+ return String(left) === String(right);
30
+ }
31
+ }
32
+
33
+ function getViewStack(view?: FlowView): any[] {
34
+ const stack = view?.navigation?.viewStack;
35
+ return Array.isArray(stack) ? stack : [];
36
+ }
37
+
38
+ function getAnchoredViewStackIndex(view?: FlowView, stack = getViewStack(view)): number {
39
+ if (!stack.length) return -1;
40
+
41
+ const args = view?.inputArgs || {};
42
+ const navParams = view?.navigation?.viewParams || {};
43
+ const viewUid = args.viewUid ?? navParams.viewUid;
44
+
45
+ if (!viewUid) {
46
+ return stack.length - 1;
47
+ }
48
+
49
+ const candidates = stack.map((item, index) => ({ item, index })).filter(({ item }) => item?.viewUid === viewUid);
50
+
51
+ if (!candidates.length) {
52
+ return stack.length - 1;
53
+ }
54
+
55
+ const keys = ['filterByTk', 'sourceId', 'tabUid'];
56
+ let bestIndex = candidates[candidates.length - 1].index;
57
+ let bestScore = -1;
58
+
59
+ for (const { item, index } of candidates) {
60
+ let score = 0;
61
+ let matched = true;
62
+
63
+ for (const key of keys) {
64
+ if (!isDefined(args[key])) continue;
65
+ if (!isSameViewParamValue(item?.[key], args[key])) {
66
+ matched = false;
67
+ break;
68
+ }
69
+ score += 1;
70
+ }
71
+
72
+ if (!matched) continue;
73
+ if (score >= bestScore) {
74
+ bestIndex = index;
75
+ bestScore = score;
76
+ }
77
+ }
78
+
79
+ return bestIndex;
80
+ }
81
+
82
+ function getPopupView(ctx: FlowContext, anchorView?: FlowView) {
83
+ return anchorView ?? ctx.view;
84
+ }
85
+
86
+ function isPopupView(view?: FlowView): boolean {
87
+ if (!view) return false;
88
+ const stack = getViewStack(view);
89
+ const openerUids = view?.inputArgs?.openerUids;
90
+ const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
91
+ return getAnchoredViewStackIndex(view, stack) >= 1 || hasOpener;
92
+ }
93
+
18
94
  // 判断是否为普通对象(Plain Object),避免对类实例/代理等进行深度遍历
19
95
  function isPlainObject(val: any) {
20
96
  if (val === null || typeof val !== 'object') return false;
@@ -79,19 +155,11 @@ function makeMetaFromValue(value: any, title?: string, seen?: WeakSet<any>): any
79
155
  export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): PropertyMetaFactory {
80
156
  const t = (k: string) => ctx.t(k);
81
157
 
82
- const isPopupView = (view?: FlowView): boolean => {
83
- if (!view) return false;
84
- const stack = Array.isArray(view.navigation?.viewStack) ? view.navigation.viewStack : [];
85
- const openerUids = view?.inputArgs?.openerUids;
86
- const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
87
- return stack.length >= 2 || hasOpener;
88
- };
89
-
90
- const hasPopupNow = (): boolean => isPopupView(anchorView ?? ctx.view);
158
+ const hasPopupNow = (flowCtx: FlowContext = ctx): boolean => isPopupView(getPopupView(flowCtx, anchorView));
91
159
 
92
160
  // 统一解析锚定视图下的 RecordRef,避免在设置弹窗等二级视图中被误导
93
161
  const resolveRecordRef = async (flowCtx: FlowContext): Promise<RecordRef | undefined> => {
94
- const view = anchorView ?? flowCtx.view;
162
+ const view = getPopupView(flowCtx, anchorView);
95
163
  if (!view || !isPopupView(view)) return undefined;
96
164
 
97
165
  const base = await buildPopupRuntime(flowCtx, view);
@@ -119,11 +187,12 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
119
187
  const getParentRecordRef = async (level: number, flowCtx?: FlowContext): Promise<RecordRef | undefined> => {
120
188
  try {
121
189
  const useCtx = flowCtx || ctx;
122
- const nav = useCtx.view?.navigation;
123
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
124
- if (stack.length < 2 || level < 1) return undefined;
125
- const idx = stack.length - 1 - level;
126
- if (idx < 0) return undefined;
190
+ const view = getPopupView(useCtx, anchorView);
191
+ const stack = getViewStack(view);
192
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
193
+ if (currentIndex < 1 || level < 1) return undefined;
194
+ const idx = currentIndex - level;
195
+ if (idx < 1) return undefined;
127
196
  const parent = stack[idx];
128
197
  if (!parent?.viewUid) return undefined;
129
198
 
@@ -156,9 +225,10 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
156
225
 
157
226
  const hasParentNow = (level: number): boolean => {
158
227
  try {
159
- const nav = (anchorView ?? ctx.view)?.navigation;
160
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
161
- return stack.length >= level + 1; // level=1 需要至少2层
228
+ const view = getPopupView(ctx, anchorView);
229
+ const stack = getViewStack(view);
230
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
231
+ return currentIndex - level >= 1; // level=1 需要至少一个上级弹窗
162
232
  } catch (_) {
163
233
  return false;
164
234
  }
@@ -231,9 +301,10 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
231
301
  disabled: () => !hasPopupNow(),
232
302
  hidden: () => !hasPopupNow(),
233
303
  buildVariablesParams: async (c) => {
234
- if (!hasPopupNow()) return undefined;
304
+ if (!hasPopupNow(c)) return undefined;
235
305
  const ref = await resolveRecordRef(c);
236
- const inputArgs = c.view?.inputArgs;
306
+ const view = getPopupView(c, anchorView);
307
+ const inputArgs = view?.inputArgs;
237
308
  type PopupVariableParams = {
238
309
  record?: RecordRef;
239
310
  sourceRecord?: RecordRef;
@@ -253,9 +324,9 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
253
324
 
254
325
  // 构建 parent 链(用于服务端解析 ctx.popup.parent[.parent...].record.*)
255
326
  try {
256
- const nav = c.view?.navigation;
257
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
258
- if (stack.length >= 2) {
327
+ const stack = getViewStack(view);
328
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
329
+ if (currentIndex >= 2) {
259
330
  let cur: Record<string, any> = params;
260
331
  let level = 1;
261
332
  let parentRef = await getParentRecordRef(level, c);
@@ -315,20 +386,21 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
315
386
  }
316
387
  // 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断)
317
388
  try {
318
- const inputArgs = ctx.view?.inputArgs;
389
+ const view = getPopupView(ctx, anchorView);
390
+ const inputArgs = view?.inputArgs;
319
391
  const srcId = inputArgs?.sourceId;
320
392
  let assoc: string | undefined = inputArgs?.associationName;
321
393
  let dsKey: string = inputArgs?.dataSourceKey || 'main';
322
394
 
323
395
  // 兜底:若 associationName 缺失或不含“.”,尝试从当前视图模型的 openView 参数推断
324
396
  if (!assoc || typeof assoc !== 'string' || !assoc.includes('.')) {
325
- const nav = ctx.view?.navigation;
326
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
327
- const last = stack?.[stack.length - 1];
328
- if (last?.viewUid) {
329
- let model = ctx?.engine?.getModel(last.viewUid, true) as PopupModelLike;
397
+ const stack = getViewStack(view);
398
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
399
+ const current = currentIndex >= 0 ? stack?.[currentIndex] : undefined;
400
+ if (current?.viewUid) {
401
+ let model = ctx?.engine?.getModel(current.viewUid, true) as PopupModelLike;
330
402
  if (!model) {
331
- model = (await ctx.engine.loadModel({ uid: last.viewUid })) as PopupModelLike;
403
+ model = (await ctx.engine.loadModel({ uid: current.viewUid })) as PopupModelLike;
332
404
  }
333
405
  const p = model?.getStepParams?.('popupSettings', 'openView') || {};
334
406
  assoc = p?.associationName || assoc;
@@ -406,12 +478,12 @@ interface PopupNode {
406
478
  }
407
479
 
408
480
  export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
409
- const nav = view?.navigation;
410
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
481
+ const stack = getViewStack(view);
482
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
411
483
 
412
484
  const openerUids = view?.inputArgs?.openerUids;
413
485
  const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
414
- const hasStackPopup = stack.length >= 2;
486
+ const hasStackPopup = currentIndex >= 1;
415
487
  const isPopup = hasStackPopup || hasOpener;
416
488
  if (!isPopup) return undefined;
417
489
 
@@ -457,7 +529,7 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
457
529
  if (parentNode) node.parent = parentNode;
458
530
  return node;
459
531
  };
460
- const currentNode = await buildNode((view?.navigation?.viewStack?.length || 1) - 1);
532
+ const currentNode = await buildNode(currentIndex);
461
533
  return currentNode;
462
534
  }
463
535
 
@@ -0,0 +1,26 @@
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 { FlowContext } from '../flowContext';
11
+
12
+ export function inheritLayoutContextForDetachedView(viewContext: FlowContext, sourceContext: FlowContext) {
13
+ const layoutContext = sourceContext?.layoutContext;
14
+ const engineContext = sourceContext?.engine?.context;
15
+
16
+ if (!(layoutContext instanceof FlowContext)) {
17
+ return;
18
+ }
19
+
20
+ if (layoutContext === sourceContext || layoutContext === engineContext) {
21
+ return;
22
+ }
23
+
24
+ // inheritContext=false 只隔离业务上下文,Layout 运行时上下文仍需要传给弹窗/嵌入视图。
25
+ viewContext.addDelegate(layoutContext);
26
+ }
@@ -0,0 +1,19 @@
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 { FlowView, FlowViewBeforeClosePayload } from './FlowView';
11
+
12
+ export async function runViewBeforeClose(view: FlowView, payload: FlowViewBeforeClosePayload): Promise<boolean> {
13
+ if (payload.force) {
14
+ return true;
15
+ }
16
+
17
+ const result = await view.beforeClose?.(payload);
18
+ return result !== false;
19
+ }
@@ -19,6 +19,8 @@ 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';
23
+ import { inheritLayoutContextForDetachedView } from './inheritLayoutContext';
22
24
 
23
25
  let uuid = 0;
24
26
 
@@ -87,14 +89,21 @@ export function useDialog() {
87
89
  ctx.addDelegate(flowContext);
88
90
  } else {
89
91
  ctx.addDelegate(flowContext.engine.context);
92
+ inheritLayoutContextForDetachedView(ctx, flowContext);
90
93
  }
91
94
 
95
+ // 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
96
+ let destroyed = false;
97
+
92
98
  // 构造 currentDialog 实例
93
99
  const currentDialog = {
94
100
  type: 'dialog' as const,
95
101
  inputArgs: config.inputArgs || {},
96
102
  preventClose: !!config.preventClose,
103
+ beforeClose: undefined,
97
104
  destroy: (result?: any) => {
105
+ if (destroyed) return;
106
+ destroyed = true;
98
107
  config.onClose?.();
99
108
  dialogRef.current?.destroy();
100
109
  closeFunc?.();
@@ -107,18 +116,24 @@ export function useDialog() {
107
116
  scopedEngine.unlinkFromStack();
108
117
  },
109
118
  update: (newConfig) => dialogRef.current?.update(newConfig),
110
- close: (result?: any, force?: boolean) => {
119
+ close: async (result?: any, force?: boolean) => {
111
120
  if (config.preventClose && !force) {
112
- return;
121
+ return false;
122
+ }
123
+
124
+ const shouldClose = await runViewBeforeClose(currentDialog, { result, force });
125
+ if (!shouldClose) {
126
+ return false;
113
127
  }
114
128
 
115
129
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
116
130
  // 交由路由系统来销毁当前视图
117
131
  config.inputArgs.navigation.back();
118
- return;
132
+ return true;
119
133
  }
120
134
 
121
135
  currentDialog.destroy(result);
136
+ return true;
122
137
  },
123
138
  Footer: FooterComponent,
124
139
  Header: HeaderComponent,
@@ -140,6 +155,15 @@ export function useDialog() {
140
155
  get: () => currentDialog,
141
156
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
142
157
  });
158
+ // 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
159
+ // 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
160
+ // 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
161
+ scopedEngine.setDestroyView(() => {
162
+ if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
163
+ config.inputArgs.navigation.back();
164
+ }
165
+ currentDialog.destroy();
166
+ });
143
167
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
144
168
  registerPopupVariable(ctx, currentDialog);
145
169
  // 内部组件,在 Provider 内部计算 content
@@ -19,6 +19,8 @@ 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';
23
+ import { inheritLayoutContextForDetachedView } from './inheritLayoutContext';
22
24
 
23
25
  export function useDrawer() {
24
26
  const holderRef = React.useRef(null);
@@ -116,14 +118,21 @@ export function useDrawer() {
116
118
  ctx.addDelegate(flowContext);
117
119
  } else {
118
120
  ctx.addDelegate(flowContext.engine.context);
121
+ inheritLayoutContextForDetachedView(ctx, flowContext);
119
122
  }
120
123
 
124
+ // 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
125
+ let destroyed = false;
126
+
121
127
  // 构造 currentDrawer 实例
122
128
  const currentDrawer = {
123
129
  type: 'drawer' as const,
124
130
  inputArgs: config.inputArgs || {},
125
131
  preventClose: !!config.preventClose,
132
+ beforeClose: undefined,
126
133
  destroy: (result?: any) => {
134
+ if (destroyed) return;
135
+ destroyed = true;
127
136
  config.onClose?.();
128
137
  drawerRef.current?.destroy();
129
138
  closeFunc?.();
@@ -136,18 +145,24 @@ export function useDrawer() {
136
145
  scopedEngine.unlinkFromStack();
137
146
  },
138
147
  update: (newConfig) => drawerRef.current?.update(newConfig),
139
- close: (result?: any, force?: boolean) => {
148
+ close: async (result?: any, force?: boolean) => {
140
149
  if (config.preventClose && !force) {
141
- return;
150
+ return false;
151
+ }
152
+
153
+ const shouldClose = await runViewBeforeClose(currentDrawer, { result, force });
154
+ if (!shouldClose) {
155
+ return false;
142
156
  }
143
157
 
144
158
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
145
159
  // 交由路由系统来销毁当前视图
146
160
  config.inputArgs.navigation.back();
147
- return;
161
+ return true;
148
162
  }
149
163
 
150
164
  currentDrawer.destroy(result);
165
+ return true;
151
166
  },
152
167
  Footer: FooterComponent,
153
168
  Header: HeaderComponent,
@@ -169,6 +184,15 @@ export function useDrawer() {
169
184
  get: () => currentDrawer,
170
185
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
171
186
  });
187
+ // 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
188
+ // 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
189
+ // 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
190
+ scopedEngine.setDestroyView(() => {
191
+ if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
192
+ config.inputArgs.navigation.back();
193
+ }
194
+ currentDrawer.destroy();
195
+ });
172
196
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
173
197
  registerPopupVariable(ctx, currentDrawer);
174
198