@nocobase/flow-engine 2.0.0-beta.2 → 2.0.0-beta.20

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 (124) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/JSRunner.d.ts +6 -0
  3. package/lib/JSRunner.js +2 -1
  4. package/lib/ViewScopedFlowEngine.js +3 -0
  5. package/lib/acl/Acl.js +13 -3
  6. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  7. package/lib/components/dnd/gridDragPlanner.js +53 -1
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +11 -3
  10. package/lib/components/variables/VariableInput.js +8 -2
  11. package/lib/data-source/index.js +6 -0
  12. package/lib/executor/FlowExecutor.d.ts +2 -1
  13. package/lib/executor/FlowExecutor.js +156 -22
  14. package/lib/flowContext.d.ts +4 -1
  15. package/lib/flowContext.js +176 -107
  16. package/lib/flowEngine.d.ts +21 -0
  17. package/lib/flowEngine.js +38 -0
  18. package/lib/flowSettings.js +12 -10
  19. package/lib/index.d.ts +3 -0
  20. package/lib/index.js +16 -0
  21. package/lib/models/CollectionFieldModel.d.ts +1 -0
  22. package/lib/models/CollectionFieldModel.js +3 -2
  23. package/lib/models/flowModel.d.ts +7 -0
  24. package/lib/models/flowModel.js +66 -1
  25. package/lib/provider.js +7 -6
  26. package/lib/resources/baseRecordResource.d.ts +5 -0
  27. package/lib/resources/baseRecordResource.js +24 -0
  28. package/lib/resources/multiRecordResource.d.ts +1 -0
  29. package/lib/resources/multiRecordResource.js +11 -4
  30. package/lib/resources/singleRecordResource.js +2 -0
  31. package/lib/resources/sqlResource.d.ts +1 -0
  32. package/lib/resources/sqlResource.js +8 -3
  33. package/lib/runjs-context/contexts/base.js +10 -4
  34. package/lib/runjsLibs.d.ts +28 -0
  35. package/lib/runjsLibs.js +532 -0
  36. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  37. package/lib/scheduler/ModelOperationScheduler.js +21 -21
  38. package/lib/types.d.ts +15 -0
  39. package/lib/utils/createCollectionContextMeta.js +1 -0
  40. package/lib/utils/index.d.ts +2 -0
  41. package/lib/utils/index.js +10 -0
  42. package/lib/utils/params-resolvers.js +16 -9
  43. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  44. package/lib/utils/resolveModuleUrl.js +65 -0
  45. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  46. package/lib/utils/runjsModuleLoader.js +422 -0
  47. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  48. package/lib/utils/runjsTemplateCompat.js +743 -0
  49. package/lib/utils/safeGlobals.d.ts +5 -9
  50. package/lib/utils/safeGlobals.js +129 -17
  51. package/lib/views/createViewMeta.d.ts +0 -7
  52. package/lib/views/createViewMeta.js +19 -70
  53. package/lib/views/index.d.ts +1 -2
  54. package/lib/views/index.js +4 -3
  55. package/lib/views/useDialog.js +8 -3
  56. package/lib/views/useDrawer.js +7 -2
  57. package/lib/views/usePage.d.ts +4 -0
  58. package/lib/views/usePage.js +43 -6
  59. package/lib/views/usePopover.js +4 -1
  60. package/lib/views/viewEvents.d.ts +17 -0
  61. package/lib/views/viewEvents.js +90 -0
  62. package/package.json +4 -4
  63. package/src/BlockScopedFlowEngine.ts +2 -5
  64. package/src/JSRunner.ts +8 -1
  65. package/src/ViewScopedFlowEngine.ts +4 -0
  66. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  67. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  68. package/src/__tests__/flowSettings.open.test.tsx +69 -15
  69. package/src/__tests__/provider.test.tsx +0 -5
  70. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  71. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  72. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  73. package/src/acl/Acl.tsx +3 -3
  74. package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
  75. package/src/components/dnd/gridDragPlanner.ts +60 -0
  76. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  77. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  78. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +11 -3
  79. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +63 -4
  80. package/src/components/variables/VariableInput.tsx +8 -2
  81. package/src/data-source/index.ts +6 -0
  82. package/src/executor/FlowExecutor.ts +193 -23
  83. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  84. package/src/flowContext.ts +234 -118
  85. package/src/flowEngine.ts +41 -0
  86. package/src/flowSettings.ts +12 -11
  87. package/src/index.ts +10 -0
  88. package/src/models/CollectionFieldModel.tsx +3 -1
  89. package/src/models/__tests__/dispatchEvent.when.test.ts +356 -0
  90. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  91. package/src/models/__tests__/flowModel.test.ts +16 -0
  92. package/src/models/flowModel.tsx +94 -1
  93. package/src/provider.tsx +9 -7
  94. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  95. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  96. package/src/resources/baseRecordResource.ts +31 -0
  97. package/src/resources/multiRecordResource.ts +11 -4
  98. package/src/resources/singleRecordResource.ts +3 -0
  99. package/src/resources/sqlResource.ts +8 -3
  100. package/src/runjs-context/contexts/base.ts +9 -2
  101. package/src/runjsLibs.ts +622 -0
  102. package/src/scheduler/ModelOperationScheduler.ts +23 -21
  103. package/src/types.ts +26 -1
  104. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  105. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  106. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  107. package/src/utils/__tests__/safeGlobals.test.ts +49 -2
  108. package/src/utils/createCollectionContextMeta.ts +1 -0
  109. package/src/utils/index.ts +6 -0
  110. package/src/utils/params-resolvers.ts +23 -9
  111. package/src/utils/resolveModuleUrl.ts +91 -0
  112. package/src/utils/runjsModuleLoader.ts +553 -0
  113. package/src/utils/runjsTemplateCompat.ts +828 -0
  114. package/src/utils/safeGlobals.ts +133 -16
  115. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  116. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  117. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  118. package/src/views/createViewMeta.ts +22 -75
  119. package/src/views/index.tsx +1 -2
  120. package/src/views/useDialog.tsx +9 -2
  121. package/src/views/useDrawer.tsx +8 -1
  122. package/src/views/usePage.tsx +51 -5
  123. package/src/views/usePopover.tsx +4 -1
  124. package/src/views/viewEvents.ts +55 -0
@@ -15,26 +15,33 @@ import { FlowViewContextProvider } from '../FlowContextProvider';
15
15
  import { registerPopupVariable } from './createViewMeta';
16
16
  import { PageComponent } from './PageComponent';
17
17
  import usePatchElement from './usePatchElement';
18
+ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } from './viewEvents';
18
19
  import { FlowEngineProvider } from '../provider';
19
20
  import { createViewScopedEngine } from '../ViewScopedFlowEngine';
20
21
  import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
21
22
 
22
23
  let uuid = 0;
23
24
 
25
+ /** Global embed container element ID */
26
+ export const GLOBAL_EMBED_CONTAINER_ID = 'nocobase-embed-container';
27
+ /** Dataset key used to signal embed replacement in progress (skip style reset on close) */
28
+ export const EMBED_REPLACING_DATA_KEY = 'nocobaseEmbedReplacing';
29
+
24
30
  // 稳定的 Holder 组件,避免在父组件重渲染时更换组件类型导致卸载, 否则切换主题时会丢失所有页面内容
25
31
  const PageElementsHolder = React.memo(
26
32
  React.forwardRef((props: any, ref: any) => {
27
33
  const [elements, patchElement] = usePatchElement();
28
34
  React.useImperativeHandle(ref, () => ({ patchElement }), [patchElement]);
29
- console.log('[NocoBase] Rendering PageElementsHolder with elements count:', elements.length);
30
35
  return <>{elements}</>;
31
36
  }),
32
37
  );
33
38
 
34
39
  export function usePage() {
35
40
  const holderRef = React.useRef(null);
41
+ const globalEmbedActiveRef = React.useRef<null | { destroy: () => void }>(null);
36
42
 
37
43
  const open = (config, flowContext) => {
44
+ const parentEngine = flowContext?.engine;
38
45
  uuid += 1;
39
46
  const pageRef = React.createRef<{
40
47
  destroy: () => void;
@@ -75,11 +82,33 @@ export function usePage() {
75
82
  return null; // Header 组件本身不渲染内容
76
83
  };
77
84
 
78
- const { target, content, preventClose, inheritContext = true, inputArgs, ...restConfig } = config;
85
+ const {
86
+ target,
87
+ content,
88
+ preventClose,
89
+ inheritContext = true,
90
+ inputArgs: viewInputArgs = {},
91
+ ...restConfig
92
+ } = config;
93
+ const isGlobalEmbedContainer = target instanceof HTMLElement && target.id === GLOBAL_EMBED_CONTAINER_ID;
94
+
95
+ // Global embed container uses "replace" behavior: opening a new view destroys the previous one.
96
+ if (isGlobalEmbedContainer && globalEmbedActiveRef.current) {
97
+ try {
98
+ // Avoid style "reset flicker" when replacing: tell embed wrappers to skip resetting container styles.
99
+ target.dataset[EMBED_REPLACING_DATA_KEY] = '1';
100
+ globalEmbedActiveRef.current.destroy();
101
+ } finally {
102
+ delete target.dataset[EMBED_REPLACING_DATA_KEY];
103
+ globalEmbedActiveRef.current = null;
104
+ }
105
+ }
79
106
 
80
107
  const ctx = new FlowContext();
81
108
  // 为当前视图创建作用域引擎(隔离实例与缓存)
82
109
  const scopedEngine = createViewScopedEngine(flowContext.engine);
110
+ const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
111
+
83
112
  ctx.defineProperty('engine', { value: scopedEngine });
84
113
  ctx.addDelegate(scopedEngine.context);
85
114
  if (inheritContext) {
@@ -91,13 +120,27 @@ export function usePage() {
91
120
  // 构造 currentPage 实例
92
121
  const currentPage = {
93
122
  type: 'embed' as const,
94
- inputArgs: config.inputArgs || {},
123
+ inputArgs: viewInputArgs,
95
124
  preventClose: !!config.preventClose,
96
125
  destroy: (result?: any) => {
97
126
  config.onClose?.();
98
127
  resolvePromise?.(result);
99
128
  pageRef.current?.destroy();
100
129
  closeFunc?.();
130
+
131
+ if (isGlobalEmbedContainer) {
132
+ globalEmbedActiveRef.current = null;
133
+ }
134
+
135
+ // Notify opener view that it becomes active again.
136
+ const isReplacing =
137
+ isGlobalEmbedContainer && target instanceof HTMLElement && target.dataset?.[EMBED_REPLACING_DATA_KEY] === '1';
138
+ if (!isReplacing) {
139
+ const openerEmitter = openerEngine?.emitter;
140
+ bumpViewActivatedVersion(openerEmitter);
141
+ openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'embed', viewUid: currentPage?.inputArgs?.viewUid });
142
+ }
143
+
101
144
  // 关闭时修正 previous/next 指针
102
145
  scopedEngine.unlinkFromStack();
103
146
  },
@@ -132,7 +175,6 @@ export function usePage() {
132
175
 
133
176
  ctx.defineProperty('view', {
134
177
  get: () => currentPage,
135
- // meta: createViewMeta(ctx),
136
178
  // 仅当访问关联字段或前端无本地记录数据时,才交给服务端解析
137
179
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
138
180
  });
@@ -180,7 +222,7 @@ export function usePage() {
180
222
  },
181
223
  );
182
224
 
183
- const key = inputArgs?.viewUid || `page-${uuid}`;
225
+ const key = viewInputArgs?.viewUid || `page-${uuid}`;
184
226
  const page = (
185
227
  <FlowEngineProvider key={key} engine={scopedEngine}>
186
228
  <FlowViewContextProvider context={ctx}>
@@ -195,6 +237,10 @@ export function usePage() {
195
237
  closeFunc = holderRef.current?.patchElement(page);
196
238
  }
197
239
 
240
+ if (isGlobalEmbedContainer) {
241
+ globalEmbedActiveRef.current = { destroy: currentPage.destroy };
242
+ }
243
+
198
244
  return Object.assign(promise, currentPage);
199
245
  };
200
246
 
@@ -40,8 +40,11 @@ const PopoverComponent = React.forwardRef<any, any>(({ afterClose, content, plac
40
40
  destroyTooltipOnHide
41
41
  content={config.content}
42
42
  placement={config.placement}
43
- getPopupContainer={() => document.body}
43
+ getPopupContainer={() => document.querySelector('#nocobase-app-container') || document.body}
44
44
  onOpenChange={(nextOpen) => {
45
+ if (!nextOpen && config.preventClose) {
46
+ return;
47
+ }
45
48
  setVisible(nextOpen);
46
49
  if (!nextOpen) {
47
50
  afterClose?.();
@@ -0,0 +1,55 @@
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 { FlowEngine } from '../flowEngine';
11
+
12
+ export const VIEW_ACTIVATED_VERSION = Symbol.for('__NOCOBASE_VIEW_ACTIVATED_VERSION__');
13
+
14
+ export const VIEW_ACTIVATED_EVENT = 'view:activated' as const;
15
+ export const DATA_SOURCE_DIRTY_EVENT = 'dataSource:dirty' as const;
16
+
17
+ export const ENGINE_SCOPE_KEY = '__NOCOBASE_ENGINE_SCOPE__' as const;
18
+ export const VIEW_ENGINE_SCOPE = 'view' as const;
19
+
20
+ export function getEmitterViewActivatedVersion(emitter): number {
21
+ const raw = Reflect.get(emitter, VIEW_ACTIVATED_VERSION);
22
+ const num = typeof raw === 'number' ? raw : Number(raw);
23
+ return Number.isFinite(num) && num > 0 ? num : 0;
24
+ }
25
+
26
+ export function bumpViewActivatedVersion(emitter): number {
27
+ const current = getEmitterViewActivatedVersion(emitter);
28
+ if (!Object.isExtensible(emitter)) return current;
29
+ const next = current + 1;
30
+ Reflect.set(emitter, VIEW_ACTIVATED_VERSION, next);
31
+ return next;
32
+ }
33
+
34
+ function isViewEngine(engine: FlowEngine): boolean {
35
+ return Reflect.get(engine, ENGINE_SCOPE_KEY) === VIEW_ENGINE_SCOPE;
36
+ }
37
+
38
+ function findNearestViewEngine(engine: FlowEngine | undefined): FlowEngine | undefined {
39
+ let cur: FlowEngine | undefined = engine;
40
+ let guard = 0;
41
+ while (cur && guard++ < 50) {
42
+ if (isViewEngine(cur)) return cur;
43
+ cur = (cur as { previousEngine?: FlowEngine | undefined }).previousEngine;
44
+ }
45
+ }
46
+
47
+ export function resolveOpenerEngine(parentEngine: FlowEngine, scopedEngine: FlowEngine): FlowEngine | undefined {
48
+ if (!parentEngine) return undefined;
49
+ const parentViewEngine = findNearestViewEngine(parentEngine);
50
+ if (parentViewEngine) return parentViewEngine;
51
+
52
+ // Fallback: resolve from previous engine in the stack (historical behavior).
53
+ const previousEngine = scopedEngine?.previousEngine;
54
+ return findNearestViewEngine(previousEngine) || parentEngine;
55
+ }