@nocobase/flow-engine 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (91) hide show
  1. package/lib/JSRunner.d.ts +1 -0
  2. package/lib/JSRunner.js +110 -20
  3. package/lib/components/FieldModelRenderer.js +44 -31
  4. package/lib/components/FlowContextSelector.js +27 -5
  5. package/lib/components/MobilePopup.style.js +16 -5
  6. package/lib/components/dnd/index.js +9 -2
  7. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
  8. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
  9. package/lib/components/variables/VariableHybridInput.d.ts +8 -0
  10. package/lib/components/variables/VariableHybridInput.js +128 -12
  11. package/lib/components/variables/VariableInput.js +2 -1
  12. package/lib/components/variables/types.d.ts +18 -0
  13. package/lib/flowContext.d.ts +1 -1
  14. package/lib/flowContext.js +87 -36
  15. package/lib/flowI18n.js +3 -3
  16. package/lib/flowSettings.d.ts +5 -1
  17. package/lib/flowSettings.js +70 -0
  18. package/lib/locale/en-US.json +1 -0
  19. package/lib/locale/index.d.ts +2 -0
  20. package/lib/locale/zh-CN.json +1 -0
  21. package/lib/resources/apiResource.js +2 -1
  22. package/lib/resources/baseRecordResource.js +6 -17
  23. package/lib/resources/multiRecordResource.js +13 -3
  24. package/lib/resources/singleRecordResource.js +7 -2
  25. package/lib/runjs-context/helpers.js +12 -5
  26. package/lib/types.d.ts +12 -0
  27. package/lib/utils/dataSourceDirty.d.ts +20 -0
  28. package/lib/utils/dataSourceDirty.js +139 -0
  29. package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
  30. package/lib/utils/dirtyAwareApiClient.js +378 -0
  31. package/lib/utils/index.d.ts +1 -1
  32. package/lib/utils/index.js +11 -11
  33. package/lib/utils/openViewRouteState.d.ts +28 -0
  34. package/lib/utils/openViewRouteState.js +125 -0
  35. package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
  36. package/lib/utils/parsePathnameToViewParams.js +18 -1
  37. package/lib/utils/resolveRunJSObjectValues.js +3 -2
  38. package/lib/utils/runjsModuleLoader.js +0 -30
  39. package/lib/views/ViewNavigation.js +5 -0
  40. package/package.json +4 -4
  41. package/src/JSRunner.ts +112 -25
  42. package/src/__tests__/JSRunner.test.ts +4 -5
  43. package/src/__tests__/flowContext.test.ts +131 -0
  44. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
  45. package/src/__tests__/flowI18n.test.ts +11 -0
  46. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  47. package/src/__tests__/flowSettings.test.ts +72 -0
  48. package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
  49. package/src/components/FieldModelRenderer.tsx +50 -36
  50. package/src/components/FlowContextSelector.tsx +36 -4
  51. package/src/components/MobilePopup.style.ts +22 -6
  52. package/src/components/__tests__/FieldModelRenderer.test.tsx +165 -0
  53. package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
  54. package/src/components/dnd/index.tsx +11 -2
  55. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
  56. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
  57. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
  58. package/src/components/variables/VariableHybridInput.tsx +166 -9
  59. package/src/components/variables/VariableInput.tsx +2 -1
  60. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +23 -3
  61. package/src/components/variables/__tests__/VariableHybridInput.test.tsx +178 -0
  62. package/src/components/variables/__tests__/VariableInput.test.tsx +51 -5
  63. package/src/components/variables/types.ts +18 -0
  64. package/src/flowContext.ts +100 -33
  65. package/src/flowI18n.ts +8 -3
  66. package/src/flowSettings.ts +85 -1
  67. package/src/locale/en-US.json +1 -0
  68. package/src/locale/zh-CN.json +1 -0
  69. package/src/resources/apiResource.ts +2 -1
  70. package/src/resources/baseRecordResource.ts +6 -23
  71. package/src/resources/multiRecordResource.ts +13 -3
  72. package/src/resources/singleRecordResource.ts +6 -1
  73. package/src/runjs-context/helpers.ts +12 -6
  74. package/src/types.ts +14 -0
  75. package/src/utils/__tests__/dirtyAwareApiClient.test.ts +392 -0
  76. package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
  77. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
  78. package/src/utils/dataSourceDirty.ts +126 -0
  79. package/src/utils/dirtyAwareApiClient.ts +430 -0
  80. package/src/utils/index.ts +10 -9
  81. package/src/utils/openViewRouteState.ts +107 -0
  82. package/src/utils/parsePathnameToViewParams.ts +23 -1
  83. package/src/utils/resolveRunJSObjectValues.ts +5 -2
  84. package/src/utils/runjsModuleLoader.ts +0 -32
  85. package/src/views/ViewNavigation.ts +6 -1
  86. package/src/views/__tests__/ViewNavigation.test.ts +15 -0
  87. package/lib/utils/safeGlobals.d.ts +0 -28
  88. package/lib/utils/safeGlobals.js +0 -367
  89. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
  90. package/src/utils/__tests__/safeGlobals.test.ts +0 -106
  91. package/src/utils/safeGlobals.ts +0 -406
@@ -42,6 +42,7 @@ __export(VariableHybridInput_exports, {
42
42
  module.exports = __toCommonJS(VariableHybridInput_exports);
43
43
  var import_css = require("@emotion/css");
44
44
  var import_antd = require("antd");
45
+ var import_context = require("antd/es/form/context");
45
46
  var import_react = __toESM(require("react"));
46
47
  var import_FlowContextProvider = require("../../FlowContextProvider");
47
48
  var import_FlowContextSelector = require("../FlowContextSelector");
@@ -83,14 +84,28 @@ function normalizeVariableKey(value) {
83
84
  return value.replace(/^\{\{\s*/, "").replace(/\s*\}\}$/, "").trim();
84
85
  }
85
86
  __name(normalizeVariableKey, "normalizeVariableKey");
86
- function renderHTML(value, labelMap, regExp) {
87
+ function renderHTML(value, regExp, resolveLabel) {
87
88
  const re = new RegExp(regExp.source, regExp.flags.includes("g") ? regExp.flags : `${regExp.flags}g`);
88
89
  return escapeHtml(value || "").replace(re, (matched) => {
89
- const label = labelMap.get(normalizeVariableKey(matched)) || matched;
90
+ const label = resolveLabel(matched) || matched;
90
91
  return createTagHTML(matched, label);
91
92
  });
92
93
  }
93
94
  __name(renderHTML, "renderHTML");
95
+ function resolveTitlesByPath(roots, path, ctxT) {
96
+ if (!roots || !path || !path.length) return void 0;
97
+ const titles = [];
98
+ let nodes = roots;
99
+ for (const segment of path) {
100
+ if (!nodes) return void 0;
101
+ const matched = nodes.find((node) => node.name === segment);
102
+ if (!matched) return void 0;
103
+ titles.push(reactNodeToPlainText(matched.title || matched.name));
104
+ nodes = Array.isArray(matched.children) ? matched.children : void 0;
105
+ }
106
+ return titles.map(ctxT).join("/");
107
+ }
108
+ __name(resolveTitlesByPath, "resolveTitlesByPath");
94
109
  function buildLabelMap(nodes, ctxT, converters) {
95
110
  const map = /* @__PURE__ */ new Map();
96
111
  function walk(items = [], parentTitles = []) {
@@ -112,6 +127,34 @@ function buildLabelMap(nodes, ctxT, converters) {
112
127
  return map;
113
128
  }
114
129
  __name(buildLabelMap, "buildLabelMap");
130
+ function collectReferencePaths(value, regExp, parseValueToPath) {
131
+ const re = new RegExp(regExp.source, regExp.flags.includes("g") ? regExp.flags : `${regExp.flags}g`);
132
+ const paths = [];
133
+ for (const matched of value.match(re) ?? []) {
134
+ const path = parseValueToPath(matched);
135
+ if (path && path.length) {
136
+ paths.push(path);
137
+ }
138
+ }
139
+ return paths;
140
+ }
141
+ __name(collectReferencePaths, "collectReferencePaths");
142
+ async function preloadReferencePath(path, roots) {
143
+ let nodes = roots;
144
+ let didLoad = false;
145
+ for (const segment of path) {
146
+ if (!nodes) break;
147
+ const matched = nodes.find((node) => node.name === segment);
148
+ if (!matched) break;
149
+ if (typeof matched.children === "function") {
150
+ matched.children = await (0, import_utils.loadMetaTreeChildren)(matched);
151
+ didLoad = true;
152
+ }
153
+ nodes = Array.isArray(matched.children) ? matched.children : void 0;
154
+ }
155
+ return didLoad;
156
+ }
157
+ __name(preloadReferencePath, "preloadReferencePath");
115
158
  function pasteHTML(container, html, indexes) {
116
159
  var _a;
117
160
  const selection = (_a = window.getSelection) == null ? void 0 : _a.call(window);
@@ -213,21 +256,64 @@ function getCurrentRange(element) {
213
256
  }
214
257
  __name(getCurrentRange, "getCurrentRange");
215
258
  const VariableHybridInputComponent = /* @__PURE__ */ __name((props) => {
259
+ var _a;
216
260
  const { addonBefore, className, converters, disabled, metaTree, onChange, placeholder, style } = props;
217
261
  const { token } = import_antd.theme.useToken();
218
262
  const ctx = (0, import_FlowContextProvider.useFlowContext)();
219
263
  const { resolvedMetaTree } = (0, import_useResolvedMetaTree.useResolvedMetaTree)(metaTree);
264
+ const formItemStatus = (_a = (0, import_react.useContext)(import_context.FormItemInputContext)) == null ? void 0 : _a.status;
265
+ const effectiveStatus = props.status ?? formItemStatus;
220
266
  const inputRef = (0, import_react.useRef)(null);
221
267
  const [isComposing, setIsComposing] = (0, import_react.useState)(false);
222
268
  const [changed, setChanged] = (0, import_react.useState)(false);
223
269
  const [range, setRange] = (0, import_react.useState)([-1, 0, -1, 0]);
224
270
  const value = typeof props.value === "string" ? props.value : props.value == null ? "" : String(props.value);
225
271
  const variableRegExp = (converters == null ? void 0 : converters.variableRegExp) ?? DEFAULT_VARIABLE_REGEXP;
272
+ const parseValueToPath = (converters == null ? void 0 : converters.parseValueToPath) ?? import_utils.parseValueToPath;
273
+ const [loadedFlag, setLoadedFlag] = (0, import_react.useState)(0);
274
+ (0, import_react.useEffect)(() => {
275
+ if (!value || !Array.isArray(resolvedMetaTree) || !resolvedMetaTree.length) {
276
+ return;
277
+ }
278
+ const paths = collectReferencePaths(value, variableRegExp, parseValueToPath);
279
+ if (!paths.length) {
280
+ return;
281
+ }
282
+ let cancelled = false;
283
+ const run = /* @__PURE__ */ __name(async () => {
284
+ let didLoad = false;
285
+ for (const path of paths) {
286
+ const loaded = await preloadReferencePath(path, resolvedMetaTree);
287
+ if (cancelled) return;
288
+ didLoad = didLoad || loaded;
289
+ }
290
+ if (didLoad && !cancelled) {
291
+ setLoadedFlag((prev) => prev + 1);
292
+ }
293
+ }, "run");
294
+ run();
295
+ return () => {
296
+ cancelled = true;
297
+ };
298
+ }, [value, resolvedMetaTree, variableRegExp, parseValueToPath]);
226
299
  const labelMap = (0, import_react.useMemo)(
227
300
  () => buildLabelMap(resolvedMetaTree, ctx.t, converters),
228
- [resolvedMetaTree, ctx, converters]
301
+ // `loadedFlag` is read so the map recomputes after a lazy level resolves.
302
+ // eslint-disable-next-line react-hooks/exhaustive-deps
303
+ [resolvedMetaTree, ctx, converters, loadedFlag]
304
+ );
305
+ const resolveLabel = (0, import_react.useCallback)(
306
+ (matched) => {
307
+ const mapped = labelMap.get(normalizeVariableKey(matched));
308
+ if (mapped) return mapped;
309
+ const path = parseValueToPath(matched);
310
+ return resolveTitlesByPath(resolvedMetaTree, path, ctx.t);
311
+ },
312
+ // `loadedFlag` is read so a resolved lazy level re-creates this callback and re-renders the tags. `ctx` carries the translation fn.
313
+ // eslint-disable-next-line react-hooks/exhaustive-deps
314
+ [labelMap, parseValueToPath, resolvedMetaTree, ctx, loadedFlag]
229
315
  );
230
- const [html, setHtml] = (0, import_react.useState)(() => renderHTML(value, labelMap, variableRegExp));
316
+ const [html, setHtml] = (0, import_react.useState)(() => renderHTML(value, variableRegExp, resolveLabel));
231
317
  const emitChange = (0, import_react.useCallback)(
232
318
  (target) => {
233
319
  onChange == null ? void 0 : onChange(getDomValue(target).trim());
@@ -235,20 +321,20 @@ const VariableHybridInputComponent = /* @__PURE__ */ __name((props) => {
235
321
  [onChange]
236
322
  );
237
323
  (0, import_react.useEffect)(() => {
238
- setHtml(renderHTML(value, labelMap, variableRegExp));
324
+ setHtml(renderHTML(value, variableRegExp, resolveLabel));
239
325
  if (!changed) {
240
326
  setRange([-1, 0, -1, 0]);
241
327
  }
242
- }, [value, labelMap]);
328
+ }, [value, resolveLabel]);
243
329
  (0, import_react.useEffect)(() => {
244
- var _a;
330
+ var _a2;
245
331
  const element = inputRef.current;
246
332
  if (!element) return;
247
333
  if (document.activeElement !== element) return;
248
334
  const nextRange = new Range();
249
335
  if (changed) {
250
336
  if (range.join() === "-1,0,-1,0") return;
251
- const selection = (_a = window.getSelection) == null ? void 0 : _a.call(window);
337
+ const selection = (_a2 = window.getSelection) == null ? void 0 : _a2.call(window);
252
338
  if (!selection) return;
253
339
  try {
254
340
  const children = Array.from(element.childNodes);
@@ -457,6 +543,34 @@ const VariableHybridInputComponent = /* @__PURE__ */ __name((props) => {
457
543
  border-color: ${token.colorBorder};
458
544
  }
459
545
  }
546
+
547
+ &.is-error {
548
+ border-color: ${token.colorError};
549
+
550
+ &:hover {
551
+ border-color: ${token.colorErrorBorderHover};
552
+ }
553
+
554
+ &:focus,
555
+ &:focus-visible {
556
+ border-color: ${token.colorError};
557
+ box-shadow: 0 0 0 ${token.controlOutlineWidth}px ${token.colorErrorOutline};
558
+ }
559
+ }
560
+
561
+ &.is-warning {
562
+ border-color: ${token.colorWarning};
563
+
564
+ &:hover {
565
+ border-color: ${token.colorWarningBorderHover};
566
+ }
567
+
568
+ &:focus,
569
+ &:focus-visible {
570
+ border-color: ${token.colorWarning};
571
+ box-shadow: 0 0 0 ${token.controlOutlineWidth}px ${token.colorWarningOutline};
572
+ }
573
+ }
460
574
  `;
461
575
  }, [token, addonBefore]);
462
576
  return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_antd.Space.Compact, { className: (0, import_css.cx)("nb-variable-hybrid-input", wrapperClassName, className), style }, addonBefore != null && /* @__PURE__ */ import_react.default.createElement("span", { className: addonClassName }, addonBefore), /* @__PURE__ */ import_react.default.createElement(
@@ -466,7 +580,9 @@ const VariableHybridInputComponent = /* @__PURE__ */ __name((props) => {
466
580
  role: "textbox",
467
581
  "aria-label": "textbox",
468
582
  className: (0, import_css.cx)(editorClassName, {
469
- "is-disabled": disabled
583
+ "is-disabled": disabled,
584
+ "is-error": effectiveStatus === "error",
585
+ "is-warning": effectiveStatus === "warning"
470
586
  }),
471
587
  contentEditable: !disabled,
472
588
  "data-placeholder": placeholder,
@@ -483,10 +599,10 @@ const VariableHybridInputComponent = /* @__PURE__ */ __name((props) => {
483
599
  {
484
600
  metaTree,
485
601
  disabled,
486
- parseValueToPath: (converters == null ? void 0 : converters.parseValueToPath) ?? import_utils.parseValueToPath,
602
+ parseValueToPath,
487
603
  formatPathToValue: (item) => {
488
- var _a;
489
- return ((_a = converters == null ? void 0 : converters.formatPathToValue) == null ? void 0 : _a.call(converters, item)) || (0, import_utils.formatPathToValue)(item);
604
+ var _a2;
605
+ return ((_a2 = converters == null ? void 0 : converters.formatPathToValue) == null ? void 0 : _a2.call(converters, item)) || (0, import_utils.formatPathToValue)(item);
490
606
  },
491
607
  onChange: handleSelectorChange
492
608
  }
@@ -188,7 +188,7 @@ const VariableInputComponent = /* @__PURE__ */ __name(({
188
188
  }
189
189
  }, "restoreFromValue");
190
190
  restoreFromValue();
191
- }, [resolvedMetaTree, innerValue, resolvePathFromValue, currentMetaTreeNode]);
191
+ }, [resolvedMetaTree, innerValue, resolvePathFromValue, currentMetaTreeNode, value]);
192
192
  const ValueComponent = (0, import_react.useMemo)(() => {
193
193
  const Component = renderInputComponent == null ? void 0 : renderInputComponent(resolvedMetaTreeNode);
194
194
  const CustomComponent = resolvedMetaTreeNode == null ? void 0 : resolvedMetaTreeNode.render;
@@ -325,6 +325,7 @@ const VariableInputComponent = /* @__PURE__ */ __name(({
325
325
  {
326
326
  metaTree: resolvedMetaTree,
327
327
  value: innerValue,
328
+ active: (0, import_utils.isVariableValue)(innerValue),
328
329
  onChange: handleVariableSelect,
329
330
  parseValueToPath: resolvePathFromValue,
330
331
  formatPathToValue: resolveValueFromPath,
@@ -13,12 +13,30 @@ export interface FlowContextSelectorProps extends Omit<CascaderProps<ContextSele
13
13
  value?: string;
14
14
  onChange?: (value: string, metaTreeNode?: MetaTreeNode) => void;
15
15
  children?: CascaderProps<ContextSelectorItem>['children'];
16
+ /**
17
+ * Controls whether the default `x` trigger button is rendered as active
18
+ * (`type="primary"`). When omitted, the selector falls back to its parsed
19
+ * `value` path (`true` iff a valid variable path is currently selected).
20
+ *
21
+ * Use this when callers intentionally feed synthetic paths such as
22
+ * `['constant']` / `['null']` into the cascader to keep menu state aligned,
23
+ * but only want real variable references to show the blue active button.
24
+ */
25
+ active?: boolean;
16
26
  metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
17
27
  parseValueToPath?: (value: string) => string[] | undefined;
18
28
  formatPathToValue?: (item: MetaTreeNode) => string;
19
29
  open?: boolean;
20
30
  onlyLeafSelectable?: boolean;
21
31
  ignoreFieldNames?: string[];
32
+ /**
33
+ * Footer rendered at the bottom of the dropdown. Defaults to a muted
34
+ * "Double click to choose entire object" hint when non-leaf selection is
35
+ * allowed (`onlyLeafSelectable` is false) — since double-clicking a non-leaf
36
+ * node selects the whole object. Pass an explicit node to override, or `null`
37
+ * to hide it.
38
+ */
39
+ dropdownFooter?: React.ReactNode;
22
40
  }
23
41
  export interface ContextSelectorItem {
24
42
  label: React.ReactNode;
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { ISchema } from '@formily/json-schema';
10
- import { APIClient, RequestOptions } from '@nocobase/sdk';
10
+ import type { APIClient, RequestOptions } from '@nocobase/sdk';
11
11
  import type { Router } from '@remix-run/router';
12
12
  import { MessageInstance } from 'antd/es/message/interface';
13
13
  import type { HookAPI } from 'antd/es/modal/useModal';
@@ -75,6 +75,7 @@ var import_utils = require("./utils");
75
75
  var import_exceptions = require("./utils/exceptions");
76
76
  var import_params_resolvers = require("./utils/params-resolvers");
77
77
  var import_serverContextParams = require("./utils/serverContextParams");
78
+ var import_dirtyAwareApiClient = require("./utils/dirtyAwareApiClient");
78
79
  var import_variablesParams = require("./utils/variablesParams");
79
80
  var import_registry = require("./runjs-context/registry");
80
81
  var import_createEphemeralContext = require("./utils/createEphemeralContext");
@@ -1899,15 +1900,16 @@ const _FlowContext = class _FlowContext {
1899
1900
  const options = this._props[key];
1900
1901
  if (!options) return void 0;
1901
1902
  if ("value" in options) {
1902
- return options.value;
1903
+ return key === "api" ? (0, import_dirtyAwareApiClient.getDirtyAwareApiClient)(options.value, currentContext) : options.value;
1903
1904
  }
1904
1905
  if (options.get) {
1905
1906
  if (options.cache === false) {
1906
- return options.get(currentContext);
1907
+ const value = options.get(currentContext);
1908
+ return key === "api" ? (0, import_dirtyAwareApiClient.getDirtyAwareApiClient)(value, currentContext) : value;
1907
1909
  }
1908
1910
  const cacheKey = options.observable ? "_observableCache" : "_cache";
1909
1911
  if (key in this[cacheKey]) {
1910
- return this[cacheKey][key];
1912
+ return key === "api" ? (0, import_dirtyAwareApiClient.getDirtyAwareApiClient)(this[cacheKey][key], currentContext) : this[cacheKey][key];
1911
1913
  }
1912
1914
  if (this._pending[key]) return this._pending[key];
1913
1915
  const result = options.get(this.createProxy());
@@ -1917,7 +1919,7 @@ const _FlowContext = class _FlowContext {
1917
1919
  (v) => {
1918
1920
  this[cacheKey][key] = v;
1919
1921
  delete this._pending[key];
1920
- return v;
1922
+ return key === "api" ? (0, import_dirtyAwareApiClient.getDirtyAwareApiClient)(v, currentContext) : v;
1921
1923
  },
1922
1924
  (err) => {
1923
1925
  delete this._pending[key];
@@ -1927,7 +1929,7 @@ const _FlowContext = class _FlowContext {
1927
1929
  return this._pending[key];
1928
1930
  }
1929
1931
  this[cacheKey][key] = result;
1930
- return result;
1932
+ return key === "api" ? (0, import_dirtyAwareApiClient.getDirtyAwareApiClient)(result, currentContext) : result;
1931
1933
  }
1932
1934
  return void 0;
1933
1935
  }
@@ -2251,7 +2253,7 @@ const _BaseFlowEngineContext = class _BaseFlowEngineContext extends FlowContext
2251
2253
  this.defineMethod("getModel", (modelName, searchInPreviousEngines) => {
2252
2254
  return this.engine.getModel(modelName, searchInPreviousEngines);
2253
2255
  });
2254
- this.defineMethod("request", (options) => {
2256
+ this.defineMethod("request", function(options) {
2255
2257
  const app = this.app;
2256
2258
  if (typeof (options == null ? void 0 : options.url) === "string" && shouldBypassApiClient(options.url, app)) {
2257
2259
  return import_axios.default.request(options);
@@ -2294,6 +2296,36 @@ const _BaseFlowModelContext = class _BaseFlowModelContext extends BaseFlowEngine
2294
2296
  };
2295
2297
  __name(_BaseFlowModelContext, "BaseFlowModelContext");
2296
2298
  let BaseFlowModelContext = _BaseFlowModelContext;
2299
+ const OPEN_VIEW_INHERITED_INPUT_ARG_KEYS = [
2300
+ "dataSourceKey",
2301
+ "collectionName",
2302
+ "associationName",
2303
+ "filterByTk",
2304
+ "sourceId",
2305
+ "tabUid"
2306
+ ];
2307
+ function pickDefinedKeys(source, keys) {
2308
+ const res = {};
2309
+ for (const key of keys) {
2310
+ if (typeof (source == null ? void 0 : source[key]) !== "undefined") {
2311
+ res[key] = source[key];
2312
+ }
2313
+ }
2314
+ return res;
2315
+ }
2316
+ __name(pickDefinedKeys, "pickDefinedKeys");
2317
+ function pickDefinedOpenViewInputArgs(source) {
2318
+ return pickDefinedKeys(source, OPEN_VIEW_INHERITED_INPUT_ARG_KEYS);
2319
+ }
2320
+ __name(pickDefinedOpenViewInputArgs, "pickDefinedOpenViewInputArgs");
2321
+ function applyDefinedDefaults(target, defaults) {
2322
+ for (const [key, value] of Object.entries(defaults)) {
2323
+ if (typeof target[key] === "undefined") {
2324
+ target[key] = value;
2325
+ }
2326
+ }
2327
+ }
2328
+ __name(applyDefinedDefaults, "applyDefinedDefaults");
2297
2329
  const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContext {
2298
2330
  // public dataSourceManager: DataSourceManager;
2299
2331
  constructor(engine) {
@@ -2525,8 +2557,16 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2525
2557
  });
2526
2558
  this.defineProperty("role", {
2527
2559
  get: /* @__PURE__ */ __name(() => {
2528
- var _a, _b;
2529
- return (_b = (_a = this.api) == null ? void 0 : _a.auth) == null ? void 0 : _b.role;
2560
+ var _a, _b, _c;
2561
+ const currentRole = (_b = (_a = this.api) == null ? void 0 : _a.auth) == null ? void 0 : _b.role;
2562
+ if (currentRole !== "__union__") {
2563
+ return currentRole;
2564
+ }
2565
+ const roles = (_c = this.user) == null ? void 0 : _c.roles;
2566
+ if (!Array.isArray(roles)) {
2567
+ return [];
2568
+ }
2569
+ return roles.map((role) => role == null ? void 0 : role.name).filter((name) => !!name);
2530
2570
  }, "get"),
2531
2571
  cache: false,
2532
2572
  // 注意:使用惰性 meta 工厂,避免在 i18n 尚未注入时提前求值导致无法翻译
@@ -2680,7 +2720,17 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2680
2720
  doc = {};
2681
2721
  }
2682
2722
  const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
2683
- const globals = { ctx: deprecatedCtx, ...(options == null ? void 0 : options.globals) || {} };
2723
+ const browserGlobals = {};
2724
+ if (typeof window !== "undefined") {
2725
+ browserGlobals.window = window;
2726
+ if (typeof navigator !== "undefined") {
2727
+ browserGlobals.navigator = navigator;
2728
+ }
2729
+ }
2730
+ if (typeof document !== "undefined") {
2731
+ browserGlobals.document = document;
2732
+ }
2733
+ const globals = { ctx: deprecatedCtx, ...browserGlobals, ...(options == null ? void 0 : options.globals) || {} };
2684
2734
  const { timeoutMs } = options || {};
2685
2735
  return new import_JSRunner.JSRunner({ globals, timeoutMs });
2686
2736
  });
@@ -2791,23 +2841,19 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
2791
2841
  }
2792
2842
  });
2793
2843
  this.defineMethod("openView", async function(uid, options) {
2794
- var _a, _b, _c, _d, _e;
2795
- const opts = { ...options };
2844
+ var _a, _b, _c, _d;
2845
+ const inheritedInputArgs = {
2846
+ ...typeof ((_a = this.model) == null ? void 0 : _a["getInputArgs"]) === "function" ? pickDefinedOpenViewInputArgs(this.model["getInputArgs"]()) : {},
2847
+ ...pickDefinedOpenViewInputArgs(this.inputArgs)
2848
+ };
2849
+ const opts = { ...options || {} };
2850
+ applyDefinedDefaults(opts, inheritedInputArgs);
2796
2851
  if (opts.defineProperties || opts.defineMethods) {
2797
2852
  opts.navigation = false;
2798
2853
  }
2799
2854
  let model2 = null;
2800
2855
  model2 = await this.engine.loadModel({ uid });
2801
2856
  if (!model2) {
2802
- const pickDefined = /* @__PURE__ */ __name((src, keys) => {
2803
- const res = {};
2804
- for (const k of keys) {
2805
- if (typeof (src == null ? void 0 : src[k]) !== "undefined") {
2806
- res[k] = src[k];
2807
- }
2808
- }
2809
- return res;
2810
- }, "pickDefined");
2811
2857
  model2 = this.engine.createModel({
2812
2858
  uid,
2813
2859
  // 注意: 新建的 model 应该使用 ${parentModel.uid}-xxx 形式的 uid
@@ -2819,7 +2865,7 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
2819
2865
  popupSettings: {
2820
2866
  openView: {
2821
2867
  // 仅在创建时持久化一份默认配置;运行时以本次 opts 为准,避免多个 opener 互相覆盖。
2822
- ...pickDefined(opts, ["dataSourceKey", "collectionName", "associationName", "mode", "size"])
2868
+ ...pickDefinedKeys(opts, ["dataSourceKey", "collectionName", "associationName", "mode", "size"])
2823
2869
  }
2824
2870
  }
2825
2871
  }
@@ -2827,12 +2873,10 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
2827
2873
  await model2.save();
2828
2874
  }
2829
2875
  model2.setParent(this.model);
2830
- const viewUid = (opts == null ? void 0 : opts.routeViewUid) ?? (opts == null ? void 0 : opts.viewUid) ?? (((_b = (_a = model2.stepParams) == null ? void 0 : _a.popupSettings) == null ? void 0 : _b.openView) ? model2.uid : this.model.uid);
2876
+ const viewUid = (opts == null ? void 0 : opts.routeViewUid) ?? (opts == null ? void 0 : opts.viewUid) ?? (((_c = (_b = model2.stepParams) == null ? void 0 : _b.popupSettings) == null ? void 0 : _c.openView) ? model2.uid : this.model.uid);
2831
2877
  const parentView = this.view;
2832
2878
  const pendingType = (opts == null ? void 0 : opts.isMobileLayout) ? "embed" : (opts == null ? void 0 : opts.mode) || "drawer";
2833
2879
  const pendingInputArgs = { ...opts, viewUid, navigation: opts.navigation };
2834
- pendingInputArgs.filterByTk = pendingInputArgs.filterByTk || ((_c = this.inputArgs) == null ? void 0 : _c.filterByTk);
2835
- pendingInputArgs.sourceId = pendingInputArgs.sourceId || ((_d = this.inputArgs) == null ? void 0 : _d.sourceId);
2836
2880
  const pendingView = {
2837
2881
  type: pendingType,
2838
2882
  inputArgs: pendingInputArgs,
@@ -2841,7 +2885,7 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
2841
2885
  engineCtx: this.engine.context
2842
2886
  };
2843
2887
  model2.context.defineProperty("view", { value: pendingView });
2844
- const popupFlow = (_e = model2.getFlow) == null ? void 0 : _e.call(model2, "popupSettings");
2888
+ const popupFlow = (_d = model2.getFlow) == null ? void 0 : _d.call(model2, "popupSettings");
2845
2889
  const on = popupFlow == null ? void 0 : popupFlow.on;
2846
2890
  let openEventName = "click";
2847
2891
  if (typeof on === "string" && on) {
@@ -2849,17 +2893,10 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
2849
2893
  } else if (on && typeof on === "object" && typeof on.eventName === "string" && on.eventName) {
2850
2894
  openEventName = on.eventName;
2851
2895
  }
2852
- await model2.dispatchEvent(
2853
- openEventName,
2854
- {
2855
- // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
2856
- // ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
2857
- ...opts
2858
- },
2859
- {
2860
- debounce: true
2861
- }
2862
- );
2896
+ await model2.dispatchEvent(openEventName, {
2897
+ // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
2898
+ ...opts
2899
+ });
2863
2900
  });
2864
2901
  this.defineMethod("getEvents", function() {
2865
2902
  return this.model.getEvents();
@@ -3475,6 +3512,20 @@ const _FlowRunJSContext = class _FlowRunJSContext extends FlowContext {
3475
3512
  ReactDOMShim.__nbRunjsInternalShim = true;
3476
3513
  this.defineProperty("ReactDOM", { value: ReactDOMShim });
3477
3514
  (0, import_runjsLibs.setupRunJSLibs)(this);
3515
+ this.defineMethod("openView", async function(uid, options) {
3516
+ const delegateOpenView = delegate.openView;
3517
+ if (typeof delegateOpenView !== "function") {
3518
+ throw new Error("ctx.openView is not available in current context.");
3519
+ }
3520
+ const routeState = (0, import_utils.createOpenViewRouteState)(options);
3521
+ if (!routeState) {
3522
+ return delegateOpenView(uid, options);
3523
+ }
3524
+ return delegateOpenView(uid, {
3525
+ ...options || {},
3526
+ [import_utils.RUNJS_OPEN_VIEW_ROUTE_STATE]: routeState
3527
+ });
3528
+ });
3478
3529
  this.defineMethod(
3479
3530
  "render",
3480
3531
  function(vnode, container) {
package/lib/flowI18n.js CHANGED
@@ -81,7 +81,7 @@ const _FlowI18n = class _FlowI18n {
81
81
  * @private
82
82
  */
83
83
  isTemplate(str) {
84
- return /\{\{\s*t\s*\(\s*["'`].*?["'`]\s*(?:,\s*.*?)?\s*\)\s*\}\}/g.test(str);
84
+ return /\{\{\s*t\s*\(\s*(["'`])(?:\\.|(?!\1).)*?\1\s*(?:,\s*.*?)?\s*\)\s*\}\}/.test(str);
85
85
  }
86
86
  /**
87
87
  * 编译模板字符串
@@ -89,8 +89,8 @@ const _FlowI18n = class _FlowI18n {
89
89
  */
90
90
  compileTemplate(template) {
91
91
  return template.replace(
92
- /\{\{\s*t\s*\(\s*["'`](.*?)["'`]\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
93
- (match, key, optionsStr) => {
92
+ /\{\{\s*t\s*\(\s*(["'`])((?:\\.|(?!\1).)*?)\1\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
93
+ (match, _quote, key, optionsStr) => {
94
94
  try {
95
95
  let templateOptions = {};
96
96
  if (optionsStr) {
@@ -9,7 +9,7 @@
9
9
  import React from 'react';
10
10
  import { FlowEngine } from '.';
11
11
  import type { FlowModel } from './models';
12
- import { StepSettingsDialogProps, ToolbarItemConfig } from './types';
12
+ import { DynamicFlowSource, DynamicFlowSourceProvider, StepSettingsDialogProps, ToolbarItemConfig } from './types';
13
13
  /**
14
14
  * 打开流程设置的参数接口
15
15
  */
@@ -93,6 +93,7 @@ export declare class FlowSettings {
93
93
  enabled: boolean;
94
94
  private engine;
95
95
  toolbarItems: ToolbarItemConfig[];
96
+ private dynamicFlowSourceProviders;
96
97
  constructor(engine: FlowEngine);
97
98
  on(event: 'beforeOpen', callback: (...args: any[]) => void): void;
98
99
  off(event: 'beforeOpen', callback: (...args: any[]) => void): void;
@@ -206,6 +207,9 @@ export declare class FlowSettings {
206
207
  * @returns {ToolbarItemConfig[]} 所有项目配置
207
208
  */
208
209
  getToolbarItems(): ToolbarItemConfig[];
210
+ registerDynamicFlowSourceProvider(provider: DynamicFlowSourceProvider): () => void;
211
+ hasDynamicFlowSourceProvider(model: FlowModel): boolean;
212
+ getDynamicFlowSources(model: FlowModel): Promise<DynamicFlowSource[]>;
209
213
  /**
210
214
  * 清空所有工具栏项目
211
215
  * @example
@@ -78,6 +78,7 @@ const _FlowSettings = class _FlowSettings {
78
78
  __privateAdd(this, _forceEnabled, false);
79
79
  // 强制启用状态,主要用于设计模式下的强制启用
80
80
  __publicField(this, "toolbarItems", []);
81
+ __publicField(this, "dynamicFlowSourceProviders", []);
81
82
  __privateAdd(this, _emitter, new import_emitter.Emitter());
82
83
  this.engine = engine;
83
84
  this.enabled = false;
@@ -369,6 +370,75 @@ const _FlowSettings = class _FlowSettings {
369
370
  getToolbarItems() {
370
371
  return [...this.toolbarItems];
371
372
  }
373
+ registerDynamicFlowSourceProvider(provider) {
374
+ const existingIndex = this.dynamicFlowSourceProviders.findIndex((item) => item.key === provider.key);
375
+ if (existingIndex !== -1) {
376
+ console.warn(
377
+ `FlowSettings: Dynamic flow source provider with key '${provider.key}' already exists and will be replaced.`
378
+ );
379
+ this.dynamicFlowSourceProviders[existingIndex] = provider;
380
+ } else {
381
+ this.dynamicFlowSourceProviders.push(provider);
382
+ }
383
+ this.dynamicFlowSourceProviders.sort((a, b) => (a.sort || 0) - (b.sort || 0));
384
+ return () => {
385
+ const index = this.dynamicFlowSourceProviders.indexOf(provider);
386
+ if (index !== -1) {
387
+ this.dynamicFlowSourceProviders.splice(index, 1);
388
+ }
389
+ };
390
+ }
391
+ hasDynamicFlowSourceProvider(model) {
392
+ return this.dynamicFlowSourceProviders.some((provider) => {
393
+ try {
394
+ return provider.visible ? provider.visible(model) : true;
395
+ } catch (error) {
396
+ console.warn(`FlowSettings: Dynamic flow source provider '${provider.key}' visibility check failed.`, error);
397
+ return false;
398
+ }
399
+ });
400
+ }
401
+ async getDynamicFlowSources(model) {
402
+ const t = (0, import_utils.getT)(model);
403
+ const selfSource = {
404
+ key: "self",
405
+ label: t("Current block"),
406
+ model,
407
+ sort: -1e3
408
+ };
409
+ const sources = [];
410
+ const seenKeys = /* @__PURE__ */ new Set(["self"]);
411
+ const seenModelUids = /* @__PURE__ */ new Set([model.uid]);
412
+ for (const provider of this.dynamicFlowSourceProviders) {
413
+ try {
414
+ if (provider.visible && !provider.visible(model)) {
415
+ continue;
416
+ }
417
+ const providerSources = await provider.getSources(model);
418
+ for (const source of providerSources || []) {
419
+ if (!(source == null ? void 0 : source.key) || !source.model) {
420
+ continue;
421
+ }
422
+ const key = String(source.key);
423
+ const uid = source.model.uid;
424
+ if (seenKeys.has(key) || seenModelUids.has(uid)) {
425
+ continue;
426
+ }
427
+ seenKeys.add(key);
428
+ seenModelUids.add(uid);
429
+ sources.push({
430
+ ...source,
431
+ key,
432
+ label: source.label || key,
433
+ sort: source.sort || 0
434
+ });
435
+ }
436
+ } catch (error) {
437
+ console.warn(`FlowSettings: Dynamic flow source provider '${provider.key}' failed.`, error);
438
+ }
439
+ }
440
+ return [selfSource, ...sources.sort((a, b) => (a.sort || 0) - (b.sort || 0))];
441
+ }
372
442
  /**
373
443
  * 清空所有工具栏项目
374
444
  * @example
@@ -56,6 +56,7 @@
56
56
  "Replace current block with template?": "Replace current block with template?",
57
57
  "Replaced with template block": "Replaced with template block",
58
58
  "Render failed": "Render failed",
59
+ "Response record": "Response record",
59
60
  "Step configuration": "Step configuration",
60
61
  "Step parameter configuration": "Step parameter configuration",
61
62
  "Step with key {{stepKey}} not found": "Step with key {{stepKey}} not found",
@@ -65,6 +65,7 @@ export declare const locales: {
65
65
  "Replace current block with template?": string;
66
66
  "Replaced with template block": string;
67
67
  "Render failed": string;
68
+ "Response record": string;
68
69
  "Step configuration": string;
69
70
  "Step parameter configuration": string;
70
71
  "Step with key {{stepKey}} not found": string;
@@ -150,6 +151,7 @@ export declare const locales: {
150
151
  "Other blocks": string;
151
152
  "Previous step": string;
152
153
  "Render failed": string;
154
+ "Response record": string;
153
155
  "Step configuration": string;
154
156
  "Step parameter configuration": string;
155
157
  "Step with key {{stepKey}} not found": string;