@nocobase/flow-engine 2.0.51 → 2.0.53

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.
@@ -58,6 +58,102 @@ var import_runViewBeforeClose = require("./runViewBeforeClose");
58
58
  let uuid = 0;
59
59
  const GLOBAL_EMBED_CONTAINER_ID = "nocobase-embed-container";
60
60
  const EMBED_REPLACING_DATA_KEY = "nocobaseEmbedReplacing";
61
+ function isPromiseLike(value) {
62
+ return !!value && typeof value.then === "function";
63
+ }
64
+ __name(isPromiseLike, "isPromiseLike");
65
+ function closeReplacingGlobalEmbed(target, activeView) {
66
+ target.dataset[EMBED_REPLACING_DATA_KEY] = "1";
67
+ try {
68
+ const closeResult = activeView.close();
69
+ if (isPromiseLike(closeResult)) {
70
+ return Promise.resolve(closeResult).finally(() => {
71
+ delete target.dataset[EMBED_REPLACING_DATA_KEY];
72
+ });
73
+ }
74
+ delete target.dataset[EMBED_REPLACING_DATA_KEY];
75
+ return closeResult;
76
+ } catch (error) {
77
+ delete target.dataset[EMBED_REPLACING_DATA_KEY];
78
+ throw error;
79
+ }
80
+ }
81
+ __name(closeReplacingGlobalEmbed, "closeReplacingGlobalEmbed");
82
+ function createPendingGlobalEmbedView(openedPromise, inputArgs, preventClose) {
83
+ let openedPage;
84
+ const pendingActions = {};
85
+ const readyPromise = openedPromise.then(({ page }) => {
86
+ openedPage = page;
87
+ if (openedPage) {
88
+ if ("beforeClose" in pendingActions) {
89
+ openedPage.beforeClose = pendingActions.beforeClose;
90
+ }
91
+ if ("update" in pendingActions) {
92
+ openedPage.update(pendingActions.update);
93
+ }
94
+ if ("footer" in pendingActions) {
95
+ openedPage.setFooter(pendingActions.footer);
96
+ }
97
+ if ("header" in pendingActions) {
98
+ openedPage.setHeader(pendingActions.header);
99
+ }
100
+ if (pendingActions.destroyed) {
101
+ openedPage.destroy(pendingActions.destroyed.result);
102
+ }
103
+ }
104
+ return { page };
105
+ });
106
+ return Object.assign(
107
+ readyPromise.then(({ page }) => page ? page : false),
108
+ {
109
+ type: "embed",
110
+ inputArgs,
111
+ preventClose,
112
+ Header: null,
113
+ Footer: null,
114
+ get beforeClose() {
115
+ return (openedPage == null ? void 0 : openedPage.beforeClose) ?? pendingActions.beforeClose;
116
+ },
117
+ set beforeClose(value) {
118
+ if (openedPage) {
119
+ openedPage.beforeClose = value;
120
+ } else {
121
+ pendingActions.beforeClose = value;
122
+ }
123
+ },
124
+ close: /* @__PURE__ */ __name((result, force) => readyPromise.then(({ page }) => page ? page.close(result, force) : false), "close"),
125
+ destroy: /* @__PURE__ */ __name((result) => {
126
+ if (openedPage) {
127
+ openedPage.destroy(result);
128
+ } else {
129
+ pendingActions.destroyed = { result };
130
+ }
131
+ }, "destroy"),
132
+ update: /* @__PURE__ */ __name((newConfig) => {
133
+ if (openedPage) {
134
+ openedPage.update(newConfig);
135
+ } else {
136
+ pendingActions.update = { ...pendingActions.update, ...newConfig };
137
+ }
138
+ }, "update"),
139
+ setFooter: /* @__PURE__ */ __name((footer) => {
140
+ if (openedPage) {
141
+ openedPage.setFooter(footer);
142
+ } else {
143
+ pendingActions.footer = footer;
144
+ }
145
+ }, "setFooter"),
146
+ setHeader: /* @__PURE__ */ __name((header) => {
147
+ if (openedPage) {
148
+ openedPage.setHeader(header);
149
+ } else {
150
+ pendingActions.header = header;
151
+ }
152
+ }, "setHeader")
153
+ }
154
+ );
155
+ }
156
+ __name(createPendingGlobalEmbedView, "createPendingGlobalEmbedView");
61
157
  const PageElementsHolder = import_react.default.memo(
62
158
  import_react.default.forwardRef((props, ref) => {
63
159
  const [elements, patchElement] = (0, import_usePatchElement.default)();
@@ -68,174 +164,229 @@ const PageElementsHolder = import_react.default.memo(
68
164
  function usePage() {
69
165
  const holderRef = import_react.default.useRef(null);
70
166
  const globalEmbedActiveRef = import_react.default.useRef(null);
167
+ const globalEmbedReplacementTokenRef = import_react.default.useRef(0);
71
168
  const open = /* @__PURE__ */ __name((config, flowContext) => {
72
- var _a, _b, _c;
73
- const parentEngine = flowContext == null ? void 0 : flowContext.engine;
74
- uuid += 1;
75
- const pageRef = import_react.default.createRef();
76
- let closeFunc;
77
- let resolvePromise;
78
- const promise = new Promise((resolve) => {
79
- resolvePromise = resolve;
80
- });
81
- const FooterComponent = /* @__PURE__ */ __name(({ children }) => {
82
- import_react.default.useEffect(() => {
83
- var _a2;
84
- (_a2 = pageRef.current) == null ? void 0 : _a2.setFooter(children);
85
- return () => {
86
- var _a3;
87
- (_a3 = pageRef.current) == null ? void 0 : _a3.setFooter(null);
88
- };
89
- }, [children]);
90
- return null;
91
- }, "FooterComponent");
92
- const HeaderComponent = /* @__PURE__ */ __name((props) => {
93
- import_react.default.useEffect(() => {
94
- var _a2;
95
- (_a2 = pageRef.current) == null ? void 0 : _a2.setHeader(props);
96
- return () => {
97
- var _a3;
98
- (_a3 = pageRef.current) == null ? void 0 : _a3.setHeader(null);
99
- };
100
- }, [props]);
101
- return null;
102
- }, "HeaderComponent");
103
169
  const {
104
170
  target,
105
171
  content,
106
172
  preventClose,
107
173
  inheritContext = true,
108
174
  inputArgs: viewInputArgs = {},
175
+ onOpenCancelled,
109
176
  ...restConfig
110
177
  } = config;
111
178
  const isGlobalEmbedContainer = target instanceof HTMLElement && target.id === GLOBAL_EMBED_CONTAINER_ID;
112
- if (isGlobalEmbedContainer && globalEmbedActiveRef.current) {
113
- try {
114
- target.dataset[EMBED_REPLACING_DATA_KEY] = "1";
115
- globalEmbedActiveRef.current.destroy();
116
- } finally {
117
- delete target.dataset[EMBED_REPLACING_DATA_KEY];
118
- globalEmbedActiveRef.current = null;
179
+ const openCurrentPage = /* @__PURE__ */ __name(() => {
180
+ var _a, _b, _c;
181
+ const parentEngine = flowContext == null ? void 0 : flowContext.engine;
182
+ uuid += 1;
183
+ const pageRef = import_react.default.createRef();
184
+ let closeFunc;
185
+ let resolvePromise;
186
+ const promise = new Promise((resolve) => {
187
+ resolvePromise = resolve;
188
+ });
189
+ const FooterComponent = /* @__PURE__ */ __name(({ children }) => {
190
+ import_react.default.useEffect(() => {
191
+ var _a2;
192
+ (_a2 = pageRef.current) == null ? void 0 : _a2.setFooter(children);
193
+ return () => {
194
+ var _a3;
195
+ (_a3 = pageRef.current) == null ? void 0 : _a3.setFooter(null);
196
+ };
197
+ }, [children]);
198
+ return null;
199
+ }, "FooterComponent");
200
+ const HeaderComponent = /* @__PURE__ */ __name((props) => {
201
+ import_react.default.useEffect(() => {
202
+ var _a2;
203
+ (_a2 = pageRef.current) == null ? void 0 : _a2.setHeader(props);
204
+ return () => {
205
+ var _a3;
206
+ (_a3 = pageRef.current) == null ? void 0 : _a3.setHeader(null);
207
+ };
208
+ }, [props]);
209
+ return null;
210
+ }, "HeaderComponent");
211
+ const ctx = new import_flowContext.FlowContext();
212
+ const scopedEngine = (0, import_ViewScopedFlowEngine.createViewScopedEngine)(flowContext.engine);
213
+ const openerEngine = (0, import_viewEvents.resolveOpenerEngine)(parentEngine, scopedEngine);
214
+ ctx.defineProperty("engine", { value: scopedEngine });
215
+ ctx.addDelegate(scopedEngine.context);
216
+ if (inheritContext) {
217
+ ctx.addDelegate(flowContext);
218
+ } else {
219
+ ctx.addDelegate(flowContext.engine.context);
119
220
  }
120
- }
121
- const ctx = new import_flowContext.FlowContext();
122
- const scopedEngine = (0, import_ViewScopedFlowEngine.createViewScopedEngine)(flowContext.engine);
123
- const openerEngine = (0, import_viewEvents.resolveOpenerEngine)(parentEngine, scopedEngine);
124
- ctx.defineProperty("engine", { value: scopedEngine });
125
- ctx.addDelegate(scopedEngine.context);
126
- if (inheritContext) {
127
- ctx.addDelegate(flowContext);
128
- } else {
129
- ctx.addDelegate(flowContext.engine.context);
130
- }
131
- const currentPage = {
132
- type: "embed",
133
- inputArgs: viewInputArgs,
134
- preventClose: !!config.preventClose,
135
- beforeClose: void 0,
136
- destroy: /* @__PURE__ */ __name((result) => {
137
- var _a2, _b2, _c2, _d, _e;
138
- (_a2 = config.onClose) == null ? void 0 : _a2.call(config);
139
- resolvePromise == null ? void 0 : resolvePromise(result);
140
- (_b2 = pageRef.current) == null ? void 0 : _b2.destroy();
141
- closeFunc == null ? void 0 : closeFunc();
142
- if (isGlobalEmbedContainer) {
143
- globalEmbedActiveRef.current = null;
144
- }
145
- const isReplacing = isGlobalEmbedContainer && target instanceof HTMLElement && ((_c2 = target.dataset) == null ? void 0 : _c2[EMBED_REPLACING_DATA_KEY]) === "1";
146
- if (!isReplacing) {
147
- const openerEmitter = openerEngine == null ? void 0 : openerEngine.emitter;
148
- (0, import_viewEvents.bumpViewActivatedVersion)(openerEmitter);
149
- (_e = openerEmitter == null ? void 0 : openerEmitter.emit) == null ? void 0 : _e.call(openerEmitter, import_viewEvents.VIEW_ACTIVATED_EVENT, { type: "embed", viewUid: (_d = currentPage == null ? void 0 : currentPage.inputArgs) == null ? void 0 : _d.viewUid });
150
- }
151
- scopedEngine.unlinkFromStack();
152
- }, "destroy"),
153
- update: /* @__PURE__ */ __name((newConfig) => {
154
- var _a2;
155
- return (_a2 = pageRef.current) == null ? void 0 : _a2.update(newConfig);
156
- }, "update"),
157
- close: /* @__PURE__ */ __name(async (result, force) => {
158
- var _a2, _b2;
159
- if (preventClose && !force) {
160
- return false;
161
- }
162
- const shouldClose = await (0, import_runViewBeforeClose.runViewBeforeClose)(currentPage, { result, force });
163
- if (!shouldClose) {
164
- return false;
221
+ let destroyed = false;
222
+ let closingPromise;
223
+ const currentPage = {
224
+ type: "embed",
225
+ inputArgs: viewInputArgs,
226
+ preventClose: !!config.preventClose,
227
+ beforeClose: void 0,
228
+ destroy: /* @__PURE__ */ __name((result) => {
229
+ var _a2, _b2, _c2, _d, _e;
230
+ if (destroyed) return;
231
+ destroyed = true;
232
+ (_a2 = config.onClose) == null ? void 0 : _a2.call(config);
233
+ resolvePromise == null ? void 0 : resolvePromise(result);
234
+ (_b2 = pageRef.current) == null ? void 0 : _b2.destroy();
235
+ closeFunc == null ? void 0 : closeFunc();
236
+ if (isGlobalEmbedContainer && globalEmbedActiveRef.current === currentPage) {
237
+ globalEmbedActiveRef.current = null;
238
+ }
239
+ const isReplacing = isGlobalEmbedContainer && target instanceof HTMLElement && ((_c2 = target.dataset) == null ? void 0 : _c2[EMBED_REPLACING_DATA_KEY]) === "1";
240
+ if (!isReplacing) {
241
+ const openerEmitter = openerEngine == null ? void 0 : openerEngine.emitter;
242
+ (0, import_viewEvents.bumpViewActivatedVersion)(openerEmitter);
243
+ (_e = openerEmitter == null ? void 0 : openerEmitter.emit) == null ? void 0 : _e.call(openerEmitter, import_viewEvents.VIEW_ACTIVATED_EVENT, { type: "embed", viewUid: (_d = currentPage == null ? void 0 : currentPage.inputArgs) == null ? void 0 : _d.viewUid });
244
+ }
245
+ scopedEngine.unlinkFromStack();
246
+ }, "destroy"),
247
+ update: /* @__PURE__ */ __name((newConfig) => {
248
+ var _a2;
249
+ return (_a2 = pageRef.current) == null ? void 0 : _a2.update(newConfig);
250
+ }, "update"),
251
+ close: /* @__PURE__ */ __name((result, force) => {
252
+ if (destroyed) {
253
+ return Promise.resolve(true);
254
+ }
255
+ if (closingPromise) {
256
+ return closingPromise;
257
+ }
258
+ closingPromise = (async () => {
259
+ var _a2, _b2;
260
+ try {
261
+ if (preventClose && !force) {
262
+ closingPromise = void 0;
263
+ return false;
264
+ }
265
+ const shouldClose = await (0, import_runViewBeforeClose.runViewBeforeClose)(currentPage, { result, force });
266
+ if (destroyed) {
267
+ return true;
268
+ }
269
+ if (!shouldClose) {
270
+ closingPromise = void 0;
271
+ return false;
272
+ }
273
+ if (config.triggerByRouter && ((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.navigation) == null ? void 0 : _b2.back)) {
274
+ config.inputArgs.navigation.back();
275
+ return true;
276
+ }
277
+ currentPage.destroy(result);
278
+ return true;
279
+ } catch (error) {
280
+ if (!destroyed) {
281
+ closingPromise = void 0;
282
+ }
283
+ throw error;
284
+ }
285
+ })();
286
+ return closingPromise;
287
+ }, "close"),
288
+ Header: HeaderComponent,
289
+ Footer: FooterComponent,
290
+ setFooter: /* @__PURE__ */ __name((footer) => {
291
+ var _a2;
292
+ (_a2 = pageRef.current) == null ? void 0 : _a2.setFooter(footer);
293
+ }, "setFooter"),
294
+ setHeader: /* @__PURE__ */ __name((header) => {
295
+ var _a2;
296
+ (_a2 = pageRef.current) == null ? void 0 : _a2.setHeader(header);
297
+ }, "setHeader"),
298
+ navigation: (_a = config.inputArgs) == null ? void 0 : _a.navigation,
299
+ get record() {
300
+ return (0, import_variablesParams.getViewRecordFromParent)(flowContext, ctx);
165
301
  }
166
- if (config.triggerByRouter && ((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.navigation) == null ? void 0 : _b2.back)) {
167
- config.inputArgs.navigation.back();
168
- return true;
302
+ };
303
+ ctx.defineProperty("view", {
304
+ get: /* @__PURE__ */ __name(() => currentPage, "get"),
305
+ // 仅当访问关联字段或前端无本地记录数据时,才交给服务端解析
306
+ resolveOnServer: (0, import_variablesParams.createViewRecordResolveOnServer)(ctx, () => (0, import_variablesParams.getViewRecordFromParent)(flowContext, ctx))
307
+ });
308
+ (0, import_createViewMeta.registerPopupVariable)(ctx, currentPage);
309
+ const PageWithContext = (0, import__.observer)(
310
+ () => {
311
+ var _a2, _b2, _c2, _d;
312
+ const mountedRef = import_react.default.useRef(false);
313
+ const pageContent = import_react.default.useMemo(
314
+ () => typeof content === "function" ? content(currentPage, ctx) : content,
315
+ []
316
+ );
317
+ void ctx.themeToken;
318
+ import_react.default.useEffect(() => {
319
+ var _a3;
320
+ (_a3 = config.onOpen) == null ? void 0 : _a3.call(config, currentPage, ctx);
321
+ }, []);
322
+ if (((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.hidden) == null ? void 0 : _b2.value) && !mountedRef.current) {
323
+ return null;
324
+ }
325
+ mountedRef.current = true;
326
+ return /* @__PURE__ */ import_react.default.createElement(
327
+ import_PageComponent.PageComponent,
328
+ {
329
+ ref: pageRef,
330
+ hidden: (_d = (_c2 = config.inputArgs) == null ? void 0 : _c2.hidden) == null ? void 0 : _d.value,
331
+ ...restConfig,
332
+ onClose: () => {
333
+ return currentPage.close(config.result);
334
+ }
335
+ },
336
+ pageContent
337
+ );
338
+ },
339
+ {
340
+ displayName: "PageWithContext"
169
341
  }
170
- currentPage.destroy(result);
171
- return true;
172
- }, "close"),
173
- Header: HeaderComponent,
174
- Footer: FooterComponent,
175
- setFooter: /* @__PURE__ */ __name((footer) => {
176
- var _a2;
177
- (_a2 = pageRef.current) == null ? void 0 : _a2.setFooter(footer);
178
- }, "setFooter"),
179
- setHeader: /* @__PURE__ */ __name((header) => {
180
- var _a2;
181
- (_a2 = pageRef.current) == null ? void 0 : _a2.setHeader(header);
182
- }, "setHeader"),
183
- navigation: (_a = config.inputArgs) == null ? void 0 : _a.navigation,
184
- get record() {
185
- return (0, import_variablesParams.getViewRecordFromParent)(flowContext, ctx);
342
+ );
343
+ const key = (viewInputArgs == null ? void 0 : viewInputArgs.viewUid) || `page-${uuid}`;
344
+ const page = /* @__PURE__ */ import_react.default.createElement(import_provider.FlowEngineProvider, { key, engine: scopedEngine }, /* @__PURE__ */ import_react.default.createElement(import_FlowContextProvider.FlowViewContextProvider, { context: ctx }, /* @__PURE__ */ import_react.default.createElement(PageWithContext, null)));
345
+ if (target && target instanceof HTMLElement) {
346
+ closeFunc = (_b = holderRef.current) == null ? void 0 : _b.patchElement(import_react_dom.default.createPortal(page, target, key));
347
+ } else {
348
+ closeFunc = (_c = holderRef.current) == null ? void 0 : _c.patchElement(page);
186
349
  }
187
- };
188
- ctx.defineProperty("view", {
189
- get: /* @__PURE__ */ __name(() => currentPage, "get"),
190
- // 仅当访问关联字段或前端无本地记录数据时,才交给服务端解析
191
- resolveOnServer: (0, import_variablesParams.createViewRecordResolveOnServer)(ctx, () => (0, import_variablesParams.getViewRecordFromParent)(flowContext, ctx))
192
- });
193
- (0, import_createViewMeta.registerPopupVariable)(ctx, currentPage);
194
- const PageWithContext = (0, import__.observer)(
195
- () => {
196
- var _a2, _b2, _c2, _d;
197
- const mountedRef = import_react.default.useRef(false);
198
- const pageContent = import_react.default.useMemo(
199
- () => typeof content === "function" ? content(currentPage, ctx) : content,
200
- []
201
- );
202
- void ctx.themeToken;
203
- import_react.default.useEffect(() => {
204
- var _a3;
205
- (_a3 = config.onOpen) == null ? void 0 : _a3.call(config, currentPage, ctx);
206
- }, []);
207
- if (((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.hidden) == null ? void 0 : _b2.value) && !mountedRef.current) {
208
- return null;
209
- }
210
- mountedRef.current = true;
211
- return /* @__PURE__ */ import_react.default.createElement(
212
- import_PageComponent.PageComponent,
213
- {
214
- ref: pageRef,
215
- hidden: (_d = (_c2 = config.inputArgs) == null ? void 0 : _c2.hidden) == null ? void 0 : _d.value,
216
- ...restConfig,
217
- onClose: () => {
218
- currentPage.close(config.result);
350
+ if (isGlobalEmbedContainer) {
351
+ globalEmbedActiveRef.current = currentPage;
352
+ }
353
+ return Object.assign(promise, currentPage);
354
+ }, "openCurrentPage");
355
+ if (isGlobalEmbedContainer && globalEmbedActiveRef.current) {
356
+ const replacementToken = globalEmbedReplacementTokenRef.current += 1;
357
+ const cancelOpen = /* @__PURE__ */ __name(() => onOpenCancelled == null ? void 0 : onOpenCancelled(), "cancelOpen");
358
+ let closeResult;
359
+ try {
360
+ closeResult = closeReplacingGlobalEmbed(target, globalEmbedActiveRef.current);
361
+ } catch (error) {
362
+ cancelOpen();
363
+ throw error;
364
+ }
365
+ if (isPromiseLike(closeResult)) {
366
+ return createPendingGlobalEmbedView(
367
+ Promise.resolve(closeResult).then(
368
+ (closed) => {
369
+ if (closed === false || replacementToken !== globalEmbedReplacementTokenRef.current) {
370
+ cancelOpen();
371
+ return { page: null };
372
+ }
373
+ return { page: openCurrentPage() };
374
+ },
375
+ (error) => {
376
+ cancelOpen();
377
+ throw error;
219
378
  }
220
- },
221
- pageContent
379
+ ),
380
+ viewInputArgs,
381
+ !!config.preventClose
222
382
  );
223
- },
224
- {
225
- displayName: "PageWithContext"
226
383
  }
227
- );
228
- const key = (viewInputArgs == null ? void 0 : viewInputArgs.viewUid) || `page-${uuid}`;
229
- const page = /* @__PURE__ */ import_react.default.createElement(import_provider.FlowEngineProvider, { key, engine: scopedEngine }, /* @__PURE__ */ import_react.default.createElement(import_FlowContextProvider.FlowViewContextProvider, { context: ctx }, /* @__PURE__ */ import_react.default.createElement(PageWithContext, null)));
230
- if (target && target instanceof HTMLElement) {
231
- closeFunc = (_b = holderRef.current) == null ? void 0 : _b.patchElement(import_react_dom.default.createPortal(page, target, key));
232
- } else {
233
- closeFunc = (_c = holderRef.current) == null ? void 0 : _c.patchElement(page);
234
- }
235
- if (isGlobalEmbedContainer) {
236
- globalEmbedActiveRef.current = { destroy: currentPage.destroy };
384
+ if (closeResult === false) {
385
+ cancelOpen();
386
+ return createPendingGlobalEmbedView(Promise.resolve({ page: null }), viewInputArgs, !!config.preventClose);
387
+ }
237
388
  }
238
- return Object.assign(promise, currentPage);
389
+ return openCurrentPage();
239
390
  }, "open");
240
391
  const api = import_react.default.useMemo(() => ({ open }), []);
241
392
  return [api, /* @__PURE__ */ import_react.default.createElement(PageElementsHolder, { key: "page-holder", ref: holderRef })];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.51",
3
+ "version": "2.0.53",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.0.51",
12
- "@nocobase/shared": "2.0.51",
11
+ "@nocobase/sdk": "2.0.53",
12
+ "@nocobase/shared": "2.0.53",
13
13
  "ahooks": "^3.7.2",
14
14
  "axios": "^1.7.0",
15
15
  "dayjs": "^1.11.9",
@@ -37,5 +37,5 @@
37
37
  ],
38
38
  "author": "NocoBase Team",
39
39
  "license": "Apache-2.0",
40
- "gitHead": "f77260e6a5471835f9dac39c55fe1aebd11b65f1"
40
+ "gitHead": "652f01597d2293faea886f803c3dfefbecfc654f"
41
41
  }
@@ -160,6 +160,23 @@ describe('FlowContext properties and methods', () => {
160
160
  expect(ctx.shared).toBe('from delegate');
161
161
  });
162
162
 
163
+ it('should expose current language as a top-level variable', async () => {
164
+ const engine = new FlowEngine();
165
+ const ctx = engine.context;
166
+ ctx.defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
167
+ ctx.defineProperty('i18n', { value: { language: 'en-US' } });
168
+
169
+ expect(ctx.locale).toBe('zh-CN');
170
+ await expect(ctx.resolveJsonTemplate('{{ ctx.locale }}')).resolves.toBe('zh-CN');
171
+
172
+ const localeNode = ctx.getPropertyMetaTree().find((node) => node.name === 'locale');
173
+ expect(localeNode).toMatchObject({
174
+ name: 'locale',
175
+ title: '{{t("Current language")}}',
176
+ paths: ['locale'],
177
+ });
178
+ });
179
+
163
180
  it('should throw sync error in get', () => {
164
181
  const ctx = new FlowContext();
165
182
  ctx.defineProperty('error', {
@@ -1142,6 +1142,7 @@ describe('AddSubModelButton toggleable behavior', () => {
1142
1142
  },
1143
1143
  { timeout: 3000 },
1144
1144
  );
1145
+ await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
1145
1146
 
1146
1147
  // dropdown should remain open and children should still be visible (no flicker / reload)
1147
1148
  expect(screen.getByText('Async Group')).toBeInTheDocument();
@@ -1158,6 +1159,7 @@ describe('AddSubModelButton toggleable behavior', () => {
1158
1159
 
1159
1160
  // ensure destroy has been called (avoid flakiness on exact call counts)
1160
1161
  await waitFor(() => {
1162
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false');
1161
1163
  expect(repo.destroy).toHaveBeenCalled();
1162
1164
  });
1163
1165
  });
@@ -0,0 +1,46 @@
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 _ from 'lodash';
11
+ import { FlowDefinition } from '../FlowDefinition';
12
+ import { FlowDefinitionOptions } from '../types';
13
+ import { BaseFlowRegistry, IFlowRepository } from './BaseFlowRegistry';
14
+
15
+ export type FlowRegistryData = Record<string, Omit<FlowDefinitionOptions, 'key'> & { key?: string }>;
16
+
17
+ export class DetachedFlowRegistry extends BaseFlowRegistry {
18
+ constructor(flows: FlowRegistryData = {}) {
19
+ super();
20
+ this.addFlows(_.cloneDeep(flows));
21
+ }
22
+
23
+ saveFlow(_flow: FlowDefinition): void {}
24
+
25
+ destroyFlow(flowKey: string): void {
26
+ this.removeFlow(flowKey);
27
+ }
28
+ }
29
+
30
+ export function serializeFlowRegistry(registry: Pick<IFlowRepository, 'getFlows'>): FlowRegistryData {
31
+ const flows: FlowRegistryData = {};
32
+ for (const [key, flow] of registry.getFlows()) {
33
+ flows[key] = _.cloneDeep(flow.toData());
34
+ }
35
+ return flows;
36
+ }
37
+
38
+ export function replaceFlowRegistry(
39
+ registry: Pick<IFlowRepository, 'getFlows' | 'removeFlow' | 'addFlows'>,
40
+ flows: FlowRegistryData,
41
+ ) {
42
+ for (const key of Array.from(registry.getFlows().keys())) {
43
+ registry.removeFlow(key);
44
+ }
45
+ registry.addFlows(_.cloneDeep(flows));
46
+ }
@@ -0,0 +1,47 @@
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 { describe, expect, test } from 'vitest';
11
+ import { DetachedFlowRegistry, replaceFlowRegistry, serializeFlowRegistry } from '../DetachedFlowRegistry';
12
+
13
+ describe('DetachedFlowRegistry', () => {
14
+ test('keeps flow edits detached and can replace another registry', () => {
15
+ const source = {
16
+ flow1: {
17
+ title: 'Flow 1',
18
+ steps: {
19
+ step1: { title: 'Step 1' } as any,
20
+ },
21
+ },
22
+ };
23
+ const registry = new DetachedFlowRegistry(source);
24
+
25
+ source.flow1.title = 'Changed outside';
26
+ expect(registry.getFlow('flow1')?.title).toBe('Flow 1');
27
+
28
+ const flow = registry.getFlow('flow1');
29
+ expect(flow).toBeDefined();
30
+ if (!flow) {
31
+ throw new Error('flow1 should exist');
32
+ }
33
+ flow.title = 'Draft title';
34
+ const serialized = serializeFlowRegistry(registry);
35
+ serialized.flow1.title = 'Changed serialized';
36
+ expect(registry.getFlow('flow1')?.title).toBe('Draft title');
37
+
38
+ const target = new DetachedFlowRegistry({ stale: { title: 'Stale', steps: {} } });
39
+ replaceFlowRegistry(target, serializeFlowRegistry(registry));
40
+
41
+ expect(target.hasFlow('stale')).toBe(false);
42
+ expect(target.getFlow('flow1')?.title).toBe('Draft title');
43
+
44
+ target.destroyFlow('flow1');
45
+ expect(target.hasFlow('flow1')).toBe(false);
46
+ });
47
+ });
@@ -10,3 +10,4 @@
10
10
  export * from './BaseFlowRegistry';
11
11
  export * from './InstanceFlowRegistry';
12
12
  export * from './GlobalFlowRegistry';
13
+ export * from './DetachedFlowRegistry';