@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
@@ -81,6 +81,34 @@ describe('FlowExecutor', () => {
81
81
  expect(result.step2).toBe('step2-ok');
82
82
  });
83
83
 
84
+ it('runFlow warns and skips steps without use or handler', async () => {
85
+ const flows = {
86
+ referenceSettings: {
87
+ steps: {
88
+ target: {},
89
+ },
90
+ },
91
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
92
+ const model = createModelWithFlows('m-empty-step', flows);
93
+ const loggerChildSpy = vi.spyOn(engine.logger, 'child').mockReturnValue(engine.logger);
94
+ const loggerWarnSpy = vi.spyOn(engine.logger, 'warn').mockImplementation(() => {});
95
+ const loggerErrorSpy = vi.spyOn(engine.logger, 'error').mockImplementation(() => {});
96
+
97
+ try {
98
+ const result = await engine.executor.runFlow(model, 'referenceSettings');
99
+
100
+ expect(result).toEqual({});
101
+ expect(loggerWarnSpy).toHaveBeenCalledWith(
102
+ "BaseModel.applyFlow: Step 'target' in flow 'referenceSettings' has neither 'use' nor 'handler'. Skipping.",
103
+ );
104
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
105
+ } finally {
106
+ loggerChildSpy.mockRestore();
107
+ loggerWarnSpy.mockRestore();
108
+ loggerErrorSpy.mockRestore();
109
+ }
110
+ });
111
+
84
112
  it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => {
85
113
  const calls: string[] = [];
86
114
  const mkFlow = (key: string, sort: number) => ({
@@ -232,6 +260,37 @@ describe('FlowExecutor', () => {
232
260
  expect(calls.sort()).toEqual(['a', 'b']);
233
261
  });
234
262
 
263
+ it('dispatchEvent sequential exposes abortedByExitAll metadata on result array', async () => {
264
+ const flows = {
265
+ stopClose: {
266
+ on: { eventName: 'close' },
267
+ steps: {
268
+ only: {
269
+ handler: vi.fn().mockImplementation((ctx) => {
270
+ ctx.exit();
271
+ }),
272
+ },
273
+ },
274
+ },
275
+ afterClose: {
276
+ on: { eventName: 'close', phase: 'afterAllFlows' },
277
+ steps: {
278
+ only: {
279
+ handler: vi.fn(),
280
+ },
281
+ },
282
+ },
283
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
284
+
285
+ const model = createModelWithFlows('m-close-meta', flows);
286
+
287
+ const result = await engine.executor.dispatchEvent(model, 'close', {}, { sequential: true });
288
+
289
+ expect(Array.isArray(result)).toBe(true);
290
+ expect((result as any).__abortedByExitAll).toBe(true);
291
+ expect(flows.afterClose.steps.only.handler).not.toHaveBeenCalled();
292
+ });
293
+
235
294
  it('dispatchEvent sequential respects sort order and stops on errors', async () => {
236
295
  const calls: string[] = [];
237
296
  const mkFlow = (key: string, sort: number, opts?: { throw?: boolean }) => ({
@@ -288,6 +347,32 @@ describe('FlowExecutor', () => {
288
347
  expect(handler).toHaveBeenCalledTimes(2); // 每个 flow 各 1 次,共 2 次
289
348
  });
290
349
 
350
+ it("dispatchEvent('beforeRender') keeps aborted flag on end event when cache hits", async () => {
351
+ const handler = vi.fn().mockImplementation((ctx) => {
352
+ ctx.exitAll();
353
+ });
354
+ const flows = {
355
+ abortFlow: { steps: { s: { handler } } },
356
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
357
+ const model = createModelWithFlows('m-br-cache-aborted', flows);
358
+
359
+ const endEvents: any[] = [];
360
+ const onEnd = (payload: any) => {
361
+ endEvents.push(payload);
362
+ };
363
+ engine.emitter.on('model:event:beforeRender:end', onEnd);
364
+
365
+ await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
366
+ await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
367
+
368
+ engine.emitter.off('model:event:beforeRender:end', onEnd);
369
+
370
+ expect(handler).toHaveBeenCalledTimes(1);
371
+ expect(endEvents).toHaveLength(2);
372
+ expect(endEvents[0]?.aborted).toBe(true);
373
+ expect(endEvents[1]?.aborted).toBe(true);
374
+ });
375
+
291
376
  it('dispatchEvent supports sequential execution order and exitAll break', async () => {
292
377
  const calls: string[] = [];
293
378
  const mkFlow = (key: string, sort: number, opts?: { exitAll?: boolean }) => ({
@@ -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';
@@ -11,6 +11,7 @@ import { ISchema } from '@formily/json-schema';
11
11
  import { observable } from '@formily/reactive';
12
12
  import { APIClient, RequestOptions } from '@nocobase/sdk';
13
13
  import type { Router } from '@remix-run/router';
14
+ import axios from 'axios';
14
15
  import { MessageInstance } from 'antd/es/message/interface';
15
16
  import * as antd from 'antd';
16
17
  import type { HookAPI } from 'antd/es/modal/useModal';
@@ -27,7 +28,7 @@ import { ContextPathProxy } from './ContextPathProxy';
27
28
  import { DataSource, DataSourceManager } from './data-source';
28
29
  import { FlowEngine } from './flowEngine';
29
30
  import { FlowI18n } from './flowI18n';
30
- import { JSRunner, JSRunnerOptions } from './JSRunner';
31
+ import { JSRunner, JSRunnerOptions, shouldPreprocessRunJSTemplates } from './JSRunner';
31
32
  import type { FlowModel } from './models/flowModel';
32
33
  import type { ForkFlowModel } from './models/forkFlowModel';
33
34
  import { FlowResource, FlowSQLRepository } from './resources';
@@ -58,6 +59,31 @@ import dayjs from 'dayjs';
58
59
  import { externalReactRender, setupRunJSLibs } from './runjsLibs';
59
60
  import { runjsImportAsync, runjsImportModule, runjsRequireAsync } from './utils/runjsModuleLoader';
60
61
 
62
+ function normalizePathname(pathname: string) {
63
+ return pathname.endsWith('/') ? pathname : `${pathname}/`;
64
+ }
65
+
66
+ function shouldBypassApiClient(url: string, app?: { getApiUrl?: (pathname?: string) => string }) {
67
+ try {
68
+ const requestUrl = new URL(url);
69
+ if (!['http:', 'https:'].includes(requestUrl.protocol)) {
70
+ return false;
71
+ }
72
+
73
+ if (!app?.getApiUrl) {
74
+ return true;
75
+ }
76
+
77
+ const apiUrl = new URL(app.getApiUrl());
78
+ const apiPath = normalizePathname(apiUrl.pathname);
79
+ const requestPath = normalizePathname(requestUrl.pathname);
80
+
81
+ return requestUrl.origin !== apiUrl.origin || !requestPath.startsWith(apiPath);
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
61
87
  // Helper: detect a RecordRef-like object
62
88
  function isRecordRefLike(val: any): boolean {
63
89
  return !!(val && typeof val === 'object' && 'collection' in val && 'filterByTk' in val);
@@ -2980,8 +3006,10 @@ export class FlowContext {
2980
3006
  }
2981
3007
 
2982
3008
  class BaseFlowEngineContext extends FlowContext {
3009
+ declare t: (key: any, options?: any) => string;
2983
3010
  declare router: Router;
2984
3011
  declare dataSourceManager: DataSourceManager;
3012
+ declare isDarkTheme: boolean;
2985
3013
  declare requireAsync: (url: string) => Promise<any>;
2986
3014
  declare importAsync: (url: string) => Promise<any>;
2987
3015
  declare createJSRunner: (options?: JSRunnerOptions) => Promise<JSRunner>;
@@ -3008,6 +3036,7 @@ class BaseFlowEngineContext extends FlowContext {
3008
3036
  declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
3009
3037
  declare engine: FlowEngine;
3010
3038
  declare api: APIClient;
3039
+ declare locale: string;
3011
3040
  declare viewer: FlowViewer;
3012
3041
  declare view: FlowView;
3013
3042
  declare modal: HookAPI;
@@ -3024,6 +3053,10 @@ class BaseFlowEngineContext extends FlowContext {
3024
3053
  return this.engine.getModel(modelName, searchInPreviousEngines);
3025
3054
  });
3026
3055
  this.defineMethod('request', (options: RequestOptions) => {
3056
+ const app = this.app as { getApiUrl?: (pathname?: string) => string } | undefined;
3057
+ if (typeof options?.url === 'string' && shouldBypassApiClient(options.url, app)) {
3058
+ return axios.request(options);
3059
+ }
3027
3060
  return this.api.request(options);
3028
3061
  });
3029
3062
  this.defineMethod(
@@ -3035,8 +3068,10 @@ class BaseFlowEngineContext extends FlowContext {
3035
3068
  ...(runnerOptions || {}),
3036
3069
  globals: mergedGlobals,
3037
3070
  });
3038
- // Enable by default; use `preprocessTemplates: false` to explicitly disable.
3039
- const shouldPreprocessTemplates = preprocessTemplates !== false;
3071
+ const shouldPreprocessTemplates = shouldPreprocessRunJSTemplates({
3072
+ version: runnerOptions?.version,
3073
+ preprocessTemplates,
3074
+ });
3040
3075
  const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
3041
3076
  return runner.run(jsCode);
3042
3077
  },
@@ -3112,6 +3147,15 @@ export class FlowEngineContext extends BaseFlowEngineContext {
3112
3147
  this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
3113
3148
  return i18n.translate(keyOrTemplate, options);
3114
3149
  });
3150
+ this.defineProperty('locale', {
3151
+ get: () => this.api?.auth?.locale || this.i18n?.language,
3152
+ cache: false,
3153
+ meta: Object.assign(() => ({ type: 'string', title: this.t('Current language'), sort: 970 }), {
3154
+ title: escapeT('Current language'),
3155
+ sort: 970,
3156
+ hasChildren: false,
3157
+ }),
3158
+ });
3115
3159
  this.defineMethod('renderJson', function (template: any) {
3116
3160
  return this.resolveJsonTemplate(template);
3117
3161
  });
@@ -3537,6 +3581,9 @@ export class FlowEngineContext extends BaseFlowEngineContext {
3537
3581
  },
3538
3582
  });
3539
3583
  this.defineMethod('aclCheck', function (params) {
3584
+ if (this.skipAclCheck) {
3585
+ return true;
3586
+ }
3540
3587
  return this.acl.aclCheck(params);
3541
3588
  });
3542
3589
  this.defineMethod('createResource', function (this: BaseFlowEngineContext, resourceType) {