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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -11,8 +11,6 @@ import { batch, define, observable, observe } from '@formily/reactive';
11
11
  import _ from 'lodash';
12
12
  import React from 'react';
13
13
  import { uid } from 'uid/secure';
14
- import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
15
- import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
16
14
  import { Emitter } from '../emitter';
17
15
  import { InstanceFlowRegistry } from '../flow-registry/InstanceFlowRegistry';
18
16
  import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
@@ -36,7 +34,9 @@ import type {
36
34
  import { IModelComponentProps, ReadonlyModelProps } from '../types';
37
35
  import { isInheritedFrom, setupRuntimeContextSteps } from '../utils';
38
36
  // import { FlowExitAllException } from '../utils/exceptions';
39
- import { Typography } from 'antd/lib';
37
+ import { Typography } from 'antd';
38
+ import type { MenuProps } from 'antd';
39
+ import { observer } from '..';
40
40
  import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
41
41
  import { buildSubModelItem } from '../components/subModel/utils';
42
42
  import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
@@ -46,8 +46,6 @@ import { FlowSettingsOpenOptions } from '../flowSettings';
46
46
  import type { ScheduleOptions } from '../scheduler/ModelOperationScheduler';
47
47
  import type { DispatchEventOptions, EventDefinition } from '../types';
48
48
  import { ForkFlowModel } from './forkFlowModel';
49
- import type { MenuProps } from 'antd';
50
- import { observer } from '..';
51
49
 
52
50
  // 使用 WeakMap 为每个类缓存一个 ModelActionRegistry 实例
53
51
  const classActionRegistries = new WeakMap<typeof FlowModel, ModelActionRegistry>();
@@ -58,20 +56,45 @@ const classEventRegistries = new WeakMap<typeof FlowModel, ModelEventRegistry>()
58
56
  // 使用WeakMap存储每个类的meta
59
57
  const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
60
58
 
59
+ type SortableModelLike = {
60
+ sortIndex?: number | null;
61
+ };
62
+
63
+ function getStableSortIndex(item: SortableModelLike, fallbackIndex: number) {
64
+ return typeof item?.sortIndex === 'number' && Number.isFinite(item.sortIndex) ? item.sortIndex : fallbackIndex + 1;
65
+ }
66
+
67
+ function sortByStableSortIndex<T extends SortableModelLike>(items: T[]) {
68
+ return items
69
+ .map((item, index) => ({
70
+ item,
71
+ index,
72
+ sortIndex: getStableSortIndex(item, index),
73
+ }))
74
+ .sort((a, b) => a.sortIndex - b.sortIndex || a.index - b.index)
75
+ .map(({ item }) => item);
76
+ }
77
+
61
78
  // 使用WeakMap存储每个类的 GlobalFlowRegistry
62
79
  const modelGlobalRegistries = new WeakMap<typeof FlowModel, GlobalFlowRegistry>();
63
80
 
64
81
  type BaseMenuItem = NonNullable<MenuProps['items']>[number];
65
- type MenuLeafItem = Exclude<BaseMenuItem, { children: MenuProps['items'] }>;
82
+ type MenuBaseItem = Omit<Exclude<BaseMenuItem, null>, 'key' | 'children'>;
66
83
 
67
- export type FlowModelExtraMenuItem = Omit<MenuLeafItem, 'key'> & {
84
+ export type FlowModelExtraMenuItem = MenuBaseItem & {
68
85
  key: React.Key;
69
86
  group?: string;
70
87
  sort?: number;
88
+ label?: React.ReactNode;
89
+ disabled?: boolean;
71
90
  onClick?: () => void;
91
+ children?: FlowModelExtraMenuItem[];
72
92
  };
73
93
 
74
- type FlowModelExtraMenuItemInput = Omit<FlowModelExtraMenuItem, 'key'> & { key?: React.Key };
94
+ type FlowModelExtraMenuItemInput = Omit<FlowModelExtraMenuItem, 'key' | 'children'> & {
95
+ key?: React.Key;
96
+ children?: FlowModelExtraMenuItemInput[];
97
+ };
75
98
 
76
99
  type ExtraMenuItemEntry = {
77
100
  group?: string;
@@ -88,6 +111,66 @@ type ExtraMenuItemEntry = {
88
111
 
89
112
  const classMenuExtensions = new WeakMap<typeof FlowModel, Set<ExtraMenuItemEntry>>();
90
113
 
114
+ const sortExtraMenuItems = (items: FlowModelExtraMenuItem[]) => {
115
+ return [...items].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
116
+ };
117
+
118
+ const isFlowModelExtraMenuItem = (item: FlowModelExtraMenuItem | null): item is FlowModelExtraMenuItem => {
119
+ return item !== null;
120
+ };
121
+
122
+ const normalizeExtraMenuItem = (
123
+ item: FlowModelExtraMenuItemInput,
124
+ {
125
+ group,
126
+ sort,
127
+ prefix,
128
+ path,
129
+ }: {
130
+ group: string;
131
+ sort: number;
132
+ prefix: string;
133
+ path: string;
134
+ },
135
+ ): FlowModelExtraMenuItem | null => {
136
+ if (!item) {
137
+ return null;
138
+ }
139
+
140
+ const normalizedGroup = item.group || group;
141
+ const normalizedSort = typeof item.sort === 'number' ? item.sort : sort;
142
+ const normalizedChildren = sortExtraMenuItems(
143
+ (item.children || [])
144
+ .map((child, index) =>
145
+ normalizeExtraMenuItem(child, {
146
+ group: normalizedGroup,
147
+ sort: normalizedSort,
148
+ prefix,
149
+ path: `${path}-${index}`,
150
+ }),
151
+ )
152
+ .filter(isFlowModelExtraMenuItem),
153
+ );
154
+
155
+ return {
156
+ ...item,
157
+ key: item.key ?? `${prefix}-${normalizedGroup}-${path}`,
158
+ group: normalizedGroup,
159
+ sort: normalizedSort,
160
+ children: normalizedChildren.length ? normalizedChildren : undefined,
161
+ };
162
+ };
163
+
164
+ async function loadOpenStepSettingsDialog() {
165
+ const mod = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
166
+ return mod.openStepSettingsDialog;
167
+ }
168
+
169
+ async function loadOpenRequiredParamsStepFormDialog() {
170
+ const mod = await import('../components/settings/wrappers/contextual/StepRequiredSettingsDialog');
171
+ return mod.openRequiredParamsStepFormDialog;
172
+ }
173
+
91
174
  export enum ModelRenderMode {
92
175
  ReactElement = 'reactElement',
93
176
  RenderFunction = 'renderFunction',
@@ -178,7 +261,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
178
261
  };
179
262
  this.stepParams = options.stepParams || {};
180
263
  this.subModels = {};
181
- this.sortIndex = options.sortIndex || 0;
264
+ this.sortIndex = getStableSortIndex({ sortIndex: options.sortIndex }, -1);
182
265
  this._options = options;
183
266
  this._title = '';
184
267
  this._extraTitle = '';
@@ -208,11 +291,14 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
208
291
  if (changed.type === 'set' && _.isEqual(changed.value, changed.oldValue)) {
209
292
  return;
210
293
  }
294
+ const hasLastAutoRun = !!this._lastAutoRunParams;
211
295
 
212
296
  if (this.flowEngine) {
213
297
  this.invalidateFlowCache('beforeRender');
214
298
  }
215
- this._rerunLastAutoRun();
299
+ if (hasLastAutoRun) {
300
+ this._rerunLastAutoRun();
301
+ }
216
302
  this.forks.forEach((fork) => {
217
303
  fork.rerender();
218
304
  });
@@ -444,11 +530,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
444
530
 
445
531
  Object.entries(mergedSubModels || {}).forEach(([key, value]) => {
446
532
  if (Array.isArray(value)) {
447
- value
448
- .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
449
- .forEach((item) => {
450
- this.addSubModel(key, item);
451
- });
533
+ sortByStableSortIndex(value).forEach((item) => {
534
+ this.addSubModel(key, item);
535
+ });
452
536
  } else {
453
537
  this.setSubModel(key, value);
454
538
  }
@@ -695,6 +779,8 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
695
779
  } else {
696
780
  this.props = { ...this.props, ...props };
697
781
  }
782
+
783
+ this._options.props = { ...this.props };
698
784
  }
699
785
 
700
786
  getProps(): ReadonlyModelProps {
@@ -709,26 +795,43 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
709
795
  stepKeyOrStepsParams?: string | Record<string, ParamObject>,
710
796
  params?: ParamObject,
711
797
  ): void {
798
+ let hasChanged = false;
799
+
712
800
  if (typeof flowKeyOrAllParams === 'string') {
713
801
  const flowKey = flowKeyOrAllParams;
714
802
  if (typeof stepKeyOrStepsParams === 'string' && params !== undefined) {
715
- if (!this.stepParams[flowKey]) {
716
- this.stepParams[flowKey] = {};
803
+ const currentStepParams = this.stepParams[flowKey]?.[stepKeyOrStepsParams] || {};
804
+ const nextStepParams = { ...currentStepParams, ...params };
805
+ if (!_.isEqual(currentStepParams, nextStepParams)) {
806
+ if (!this.stepParams[flowKey]) {
807
+ this.stepParams[flowKey] = {};
808
+ }
809
+ this.stepParams[flowKey][stepKeyOrStepsParams] = nextStepParams;
810
+ hasChanged = true;
717
811
  }
718
- this.stepParams[flowKey][stepKeyOrStepsParams] = {
719
- ...this.stepParams[flowKey][stepKeyOrStepsParams],
720
- ...params,
721
- };
722
812
  } else if (typeof stepKeyOrStepsParams === 'object' && stepKeyOrStepsParams !== null) {
723
- this.stepParams[flowKey] = { ...(this.stepParams[flowKey] || {}), ...stepKeyOrStepsParams };
813
+ const currentFlowParams = this.stepParams[flowKey] || {};
814
+ const nextFlowParams = { ...currentFlowParams, ...stepKeyOrStepsParams };
815
+ if (!_.isEqual(currentFlowParams, nextFlowParams)) {
816
+ this.stepParams[flowKey] = nextFlowParams;
817
+ hasChanged = true;
818
+ }
724
819
  }
725
820
  } else if (typeof flowKeyOrAllParams === 'object' && flowKeyOrAllParams !== null) {
726
821
  for (const fk in flowKeyOrAllParams) {
727
822
  if (Object.prototype.hasOwnProperty.call(flowKeyOrAllParams, fk)) {
728
- this.stepParams[fk] = { ...(this.stepParams[fk] || {}), ...flowKeyOrAllParams[fk] };
823
+ const currentFlowParams = this.stepParams[fk] || {};
824
+ const nextFlowParams = { ...currentFlowParams, ...flowKeyOrAllParams[fk] };
825
+ if (!_.isEqual(currentFlowParams, nextFlowParams)) {
826
+ this.stepParams[fk] = nextFlowParams;
827
+ hasChanged = true;
828
+ }
729
829
  }
730
830
  }
731
831
  }
832
+ if (!hasChanged) {
833
+ return;
834
+ }
732
835
  // 发起配置修改事件
733
836
  this.emitter.emit('onStepParamsChanged');
734
837
  }
@@ -754,7 +857,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
754
857
  }
755
858
  const isFork = (this as any).isFork === true;
756
859
  const target = this;
757
- console.log(
860
+ currentFlowEngine.logger.debug(
758
861
  `[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${
759
862
  this.cleanRun
760
863
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -774,7 +877,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
774
877
  }
775
878
  const isFork = (this as any).isFork === true;
776
879
  const target = this;
777
- console.log(
880
+ currentFlowEngine.logger.debug(
778
881
  `[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${
779
882
  this.cleanRun
780
883
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -858,6 +961,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
858
961
  }
859
962
  }, 100);
860
963
 
964
+ private resetAutoRunState(): void {
965
+ this._rerunLastAutoRun?.cancel?.();
966
+ this._lastAutoRunParams = null;
967
+ }
968
+
861
969
  /**
862
970
  * 通用事件分发钩子:开始
863
971
  * 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
@@ -951,7 +1059,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
951
1059
  }
952
1060
 
953
1061
  // 创建缓存的响应式包装器组件工厂(只创建一次)
954
- const createReactiveWrapper = (modelInstance: any) => {
1062
+ const createReactiveWrapper = (modelInstance: FlowModel) => {
955
1063
  const ReactiveWrapper = observer(() => {
956
1064
  // 触发响应式更新的关键属性访问(读取 run/渲染目标的 props)
957
1065
  const renderTarget = modelInstance;
@@ -977,6 +1085,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
977
1085
  model: renderTarget,
978
1086
  });
979
1087
  return () => {
1088
+ renderTarget.resetAutoRunState();
980
1089
  if (typeof renderTarget.onUnmount === 'function') {
981
1090
  renderTarget.onUnmount();
982
1091
  }
@@ -1131,7 +1240,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1131
1240
  if (!Array.isArray(subModels[subKey])) {
1132
1241
  subModels[subKey] = observable.shallow([]);
1133
1242
  }
1134
- const maxSortIndex = Math.max(...(subModels[subKey] as FlowModel[]).map((item) => item.sortIndex || 0), 0);
1243
+ const maxSortIndex = Math.max(
1244
+ ...(subModels[subKey] as FlowModel[]).map((item, index) => getStableSortIndex(item, index)),
1245
+ 0,
1246
+ );
1135
1247
  model.sortIndex = maxSortIndex + 1;
1136
1248
  subModels[subKey].push(model);
1137
1249
  actualParent.emitter.emit('onSubModelAdded', model);
@@ -1177,33 +1289,31 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1177
1289
  return model;
1178
1290
  }
1179
1291
 
1180
- filterSubModels<K extends keyof Structure['subModels'], R>(
1292
+ filterSubModels<K extends keyof NonNullable<Structure['subModels']>, R>(
1181
1293
  subKey: K,
1182
- callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => boolean,
1183
- ): ArrayElementType<Structure['subModels'][K]>[] {
1294
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>, index: number) => boolean,
1295
+ ): ArrayElementType<NonNullable<Structure['subModels']>[K]>[] {
1184
1296
  const model = (this.subModels as any)[subKey as string];
1185
1297
 
1186
1298
  if (!model) {
1187
1299
  return [];
1188
1300
  }
1189
1301
 
1190
- const results: ArrayElementType<Structure['subModels'][K]>[] = [];
1302
+ const results: ArrayElementType<NonNullable<Structure['subModels']>[K]>[] = [];
1191
1303
 
1192
- _.castArray(model)
1193
- .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
1194
- .forEach((item, index) => {
1195
- const result = (callback as (model: any, index: number) => boolean)(item, index);
1196
- if (result) {
1197
- results.push(item);
1198
- }
1199
- });
1304
+ sortByStableSortIndex(_.castArray(model)).forEach((item, index) => {
1305
+ const result = (callback as (model: any, index: number) => boolean)(item, index);
1306
+ if (result) {
1307
+ results.push(item);
1308
+ }
1309
+ });
1200
1310
 
1201
1311
  return results;
1202
1312
  }
1203
1313
 
1204
- mapSubModels<K extends keyof Structure['subModels'], R>(
1314
+ mapSubModels<K extends keyof NonNullable<Structure['subModels']>, R>(
1205
1315
  subKey: K,
1206
- callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => R,
1316
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>, index: number) => R,
1207
1317
  ): R[] {
1208
1318
  const model = (this.subModels as any)[subKey as string];
1209
1319
 
@@ -1213,17 +1323,15 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1213
1323
 
1214
1324
  const results: R[] = [];
1215
1325
 
1216
- _.castArray(model)
1217
- .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
1218
- .forEach((item, index) => {
1219
- const result = (callback as (model: any, index: number) => R)(item, index);
1220
- results.push(result);
1221
- });
1326
+ sortByStableSortIndex(_.castArray(model)).forEach((item, index) => {
1327
+ const result = (callback as (model: any, index: number) => R)(item, index);
1328
+ results.push(result);
1329
+ });
1222
1330
 
1223
1331
  return results;
1224
1332
  }
1225
1333
 
1226
- hasSubModel<K extends keyof Structure['subModels']>(subKey: K) {
1334
+ hasSubModel<K extends keyof NonNullable<Structure['subModels']>>(subKey: K) {
1227
1335
  const subModel = (this.subModels as any)[subKey as string];
1228
1336
  if (!subModel) {
1229
1337
  return false;
@@ -1231,10 +1339,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1231
1339
  return _.castArray(subModel).length > 0;
1232
1340
  }
1233
1341
 
1234
- findSubModel<K extends keyof Structure['subModels'], R>(
1342
+ findSubModel<K extends keyof NonNullable<Structure['subModels']>, R>(
1235
1343
  subKey: K,
1236
- callback: (model: ArrayElementType<Structure['subModels'][K]>) => R,
1237
- ): ArrayElementType<Structure['subModels'][K]> | null {
1344
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>) => R,
1345
+ ): ArrayElementType<NonNullable<Structure['subModels']>[K]> | null {
1238
1346
  const model = (this.subModels as any)[subKey as string];
1239
1347
 
1240
1348
  if (!model) {
@@ -1244,7 +1352,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1244
1352
  return (
1245
1353
  (_.castArray(model).find((item) => {
1246
1354
  return (callback as (model: any) => R)(item);
1247
- }) as ArrayElementType<Structure['subModels'][K]> | undefined) || null
1355
+ }) as ArrayElementType<NonNullable<Structure['subModels']>[K]> | undefined) || null
1248
1356
  );
1249
1357
  }
1250
1358
 
@@ -1304,7 +1412,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1304
1412
  }
1305
1413
 
1306
1414
  clearForks() {
1307
- console.log(`FlowModel ${this.uid} clearing all forks.`);
1415
+ this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
1308
1416
  // 主动使所有 fork 失效
1309
1417
  if (this.forks?.size) {
1310
1418
  this.forks.forEach((fork) => fork.dispose());
@@ -1369,7 +1477,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1369
1477
  * @param {string} stepKey 步骤的唯一标识符
1370
1478
  * @returns {void}
1371
1479
  */
1372
- openStepSettingsDialog(flowKey: string, stepKey: string) {
1480
+ async openStepSettingsDialog(flowKey: string, stepKey: string) {
1373
1481
  // 创建流程运行时上下文
1374
1482
  const flow = this.getFlow(flowKey);
1375
1483
  const step = flow?.steps?.[stepKey];
@@ -1383,7 +1491,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1383
1491
  setupRuntimeContextSteps(ctx, flow.steps, this, flowKey);
1384
1492
  ctx.defineProperty('currentStep', { value: step });
1385
1493
 
1386
- return openStepSettingsDialogFn({
1494
+ const openStepSettingsDialog = await loadOpenStepSettingsDialog();
1495
+
1496
+ return openStepSettingsDialog({
1387
1497
  model: this,
1388
1498
  flowKey,
1389
1499
  stepKey,
@@ -1399,7 +1509,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1399
1509
  * @returns {Promise<any>} 返回表单提交的值
1400
1510
  */
1401
1511
  async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string) {
1402
- return openRequiredParamsStepFormDialogFn({
1512
+ const openRequiredParamsStepFormDialog = await loadOpenRequiredParamsStepFormDialog();
1513
+
1514
+ return openRequiredParamsStepFormDialog({
1403
1515
  model: this,
1404
1516
  dialogWidth,
1405
1517
  dialogTitle,
@@ -1432,6 +1544,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1432
1544
  const data = {
1433
1545
  uid: this.uid,
1434
1546
  ..._.omit(this._options, ['flowEngine']),
1547
+ props: { ...this.props },
1435
1548
  stepParams: this.stepParams,
1436
1549
  sortIndex: this.sortIndex,
1437
1550
  flowRegistry: {},
@@ -1586,6 +1699,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1586
1699
  seen.add(Cls);
1587
1700
  const reg = classMenuExtensions.get(Cls);
1588
1701
  if (reg) {
1702
+ let entryIndex = 0;
1589
1703
  for (const entry of reg) {
1590
1704
  if (entry.matcher && !entry.matcher(model)) continue;
1591
1705
  const items =
@@ -1593,16 +1707,21 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1593
1707
  const group = entry.group || 'common-actions';
1594
1708
  const sort = entry.sort ?? 0;
1595
1709
  const prefix = entry.keyPrefix || Cls.name || 'extra';
1596
- (items || []).forEach((it, idx: number) => {
1597
- if (!it) return;
1598
- const key = it.key ?? `${prefix}-${group}-${idx}-${Math.random().toString(36).slice(2, 6)}`;
1599
- collected.push({
1600
- ...it,
1601
- key,
1602
- group: it.group || group,
1603
- sort: typeof it.sort === 'number' ? it.sort : sort,
1604
- });
1710
+ sortExtraMenuItems(
1711
+ (items || [])
1712
+ .map((it, idx: number) =>
1713
+ normalizeExtraMenuItem(it, {
1714
+ group,
1715
+ sort,
1716
+ prefix,
1717
+ path: `${entryIndex}-${idx}`,
1718
+ }),
1719
+ )
1720
+ .filter(isFlowModelExtraMenuItem),
1721
+ ).forEach((it) => {
1722
+ collected.push(it);
1605
1723
  });
1724
+ entryIndex += 1;
1606
1725
  }
1607
1726
  }
1608
1727
  const ParentClass = Object.getPrototypeOf(Cls) as typeof FlowModel;
package/src/provider.tsx CHANGED
@@ -45,34 +45,50 @@ export const FlowEngineGlobalsContextProvider: React.FC<{ children: React.ReactN
45
45
  const engine = useFlowEngine();
46
46
  const config = useContext(ConfigProvider.ConfigContext);
47
47
  const { token } = theme.useToken();
48
+ const isDarkTheme = React.useMemo(() => {
49
+ const algorithm = config?.theme?.algorithm;
50
+ if (Array.isArray(algorithm)) {
51
+ return algorithm.includes(theme.darkAlgorithm);
52
+ }
53
+ return algorithm === theme.darkAlgorithm;
54
+ }, [config]);
48
55
 
49
- useEffect(() => {
50
- const context = {
51
- antdConfig: config,
52
- // themeToken 改为可观察的 getter,在下方单独 define
53
- modal,
54
- message,
55
- notification,
56
- };
57
- engine.context.defineProperty('viewer', {
58
- cache: false,
59
- get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
- });
61
- for (const item of Object.entries(context)) {
62
- const [key, value] = item;
63
- if (value) {
64
- engine.context.defineProperty(key, { value });
65
- }
56
+ // 这些全局能力需要在 children 首次渲染前就可读,不能等到 effect 后再挂到上下文。
57
+ engine.context.defineProperty('viewer', {
58
+ cache: false,
59
+ get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
+ });
61
+ for (const item of Object.entries({
62
+ antdConfig: config,
63
+ modal,
64
+ message,
65
+ notification,
66
+ })) {
67
+ const [key, value] = item;
68
+ if (value) {
69
+ engine.context.defineProperty(key, { value });
66
70
  }
67
- // 将 themeToken 定义为 observable, 使组件能够响应主题的变更
68
- // NOTE: 必须在 antdConfig 写入后再更新 themeToken;否则会读取到旧 antdConfig 的值。
69
- engine.context.defineProperty('themeToken', {
70
- get: () => token,
71
- observable: true,
72
- cache: true,
73
- });
71
+ }
72
+ // themeToken 定义为 observable, 使组件能够响应主题的变更。
73
+ engine.context.defineProperty('themeToken', {
74
+ get: () => token,
75
+ observable: true,
76
+ cache: true,
77
+ });
78
+ // 统一把暗色模式暴露到 Flow 上下文,避免 flow 侧继续依赖 global-theme。
79
+ engine.context.defineProperty('isDarkTheme', {
80
+ get: () => isDarkTheme,
81
+ observable: true,
82
+ cache: true,
83
+ info: {
84
+ description: 'Whether current theme algorithm is dark mode.',
85
+ detail: 'boolean',
86
+ },
87
+ });
88
+
89
+ useEffect(() => {
74
90
  engine.reactView.refresh();
75
- }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed]);
91
+ }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed, isDarkTheme]);
76
92
 
77
93
  return (
78
94
  <ConfigProvider {...config} locale={engine.context.locales?.antd} popupMatchSelectWidth={false}>
@@ -208,4 +208,86 @@ describe('observer', () => {
208
208
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
209
209
  expect(screen.queryByText('Count: 1')).not.toBeInTheDocument();
210
210
  });
211
+
212
+ it('should flush pending update without TDZ error when context becomes active before timer callback runs', async () => {
213
+ vi.useFakeTimers();
214
+
215
+ try {
216
+ const model = observable({ count: 0 });
217
+ const pageActive = observable.ref(false);
218
+ const tabActive = observable.ref(true);
219
+
220
+ const context = {
221
+ pageActive,
222
+ tabActive,
223
+ };
224
+
225
+ (useFlowContext as any).mockReturnValue(context);
226
+
227
+ const Component = observer(() => <div>Count: {model.count}</div>);
228
+
229
+ render(<Component />);
230
+
231
+ expect(screen.getByText('Count: 0')).toBeInTheDocument();
232
+
233
+ act(() => {
234
+ model.count++;
235
+ pageActive.value = true;
236
+ });
237
+
238
+ await act(async () => {
239
+ await vi.runAllTimersAsync();
240
+ });
241
+
242
+ expect(screen.getByText('Count: 1')).toBeInTheDocument();
243
+ } finally {
244
+ vi.useRealTimers();
245
+ }
246
+ });
247
+
248
+ it('should cleanup pending timer and listener on unmount', async () => {
249
+ vi.useFakeTimers();
250
+
251
+ try {
252
+ const model = observable({ count: 0 });
253
+ const pageActive = observable.ref(false);
254
+ const tabActive = observable.ref(true);
255
+ const renderSpy = vi.fn();
256
+
257
+ const context = {
258
+ pageActive,
259
+ tabActive,
260
+ };
261
+
262
+ (useFlowContext as any).mockReturnValue(context);
263
+
264
+ const Component = observer(() => {
265
+ renderSpy(model.count);
266
+ return <div>Count: {model.count}</div>;
267
+ });
268
+
269
+ const { unmount } = render(<Component />);
270
+
271
+ expect(renderSpy).toHaveBeenCalledTimes(1);
272
+ expect(screen.getByText('Count: 0')).toBeInTheDocument();
273
+
274
+ act(() => {
275
+ model.count++;
276
+ });
277
+
278
+ unmount();
279
+
280
+ act(() => {
281
+ pageActive.value = true;
282
+ });
283
+
284
+ await act(async () => {
285
+ await vi.runAllTimersAsync();
286
+ });
287
+
288
+ expect(renderSpy).toHaveBeenCalledTimes(1);
289
+ } finally {
290
+ vi.useRealTimers();
291
+ }
292
+ });
211
293
  });