@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -10,13 +10,15 @@
10
10
  import { CloseCircleFilled } from '@ant-design/icons';
11
11
  import {
12
12
  buildContextSelectorItems,
13
+ loadMetaTreeChildren,
13
14
  useFlowContext,
14
15
  type ContextSelectorItem,
15
16
  type MetaTreeNode,
16
17
  } from '@nocobase/flow-engine';
17
18
  import { Button, Cascader, DatePicker, Input, InputNumber, Select, Space, Tag, theme, type CascaderProps } from 'antd';
18
19
  import dayjs from 'dayjs';
19
- import React, { useCallback, useMemo } from 'react';
20
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
21
+ import { css } from '@emotion/css';
20
22
  import {
21
23
  makeFormatVariablePath,
22
24
  makeParseVariablePath,
@@ -25,11 +27,11 @@ import {
25
27
  } from './VariableInput';
26
28
 
27
29
  /**
28
- * Constant types this input can edit. Subset of v1 `Variable.Input`
29
- * `useTypedConstant` drops `'object'` (no v2 JSON editor yet) and `'null'`
30
- * (handled by the dedicated `nullable` prop).
30
+ * Constant types this input can edit. Matches v1 `Variable.Input`
31
+ * `useTypedConstant` minus `'null'` (handled by the dedicated `nullable` prop).
32
+ * `'object'` renders an inline JSON editor (textarea).
31
33
  */
32
- export type TypedConstantType = 'string' | 'number' | 'boolean' | 'date';
34
+ export type TypedConstantType = 'string' | 'number' | 'boolean' | 'date' | 'object';
33
35
 
34
36
  /**
35
37
  * One allowed constant type. Either a bare type name (`'number'`) or a
@@ -50,25 +52,51 @@ export interface TypedVariableInputProps {
50
52
  * Allowed constant types. The `Constant` switcher entry always exposes a
51
53
  * typed submenu (matching v1 `Variable.Input`) so users can see what type
52
54
  * the constant is, even when only one type is permitted. Default: all four
53
- * supported types.
55
+ * supported types. Passing `[]` switches the component into a variable-only
56
+ * mode: constants/null are hidden, the empty state becomes a readonly
57
+ * placeholder, and clearing a selected variable resets to `null`.
54
58
  */
55
59
  types?: TypedConstantSpec[];
56
60
  /**
57
61
  * Restrict the variable picker to specific top-level meta-tree namespaces
58
62
  * (e.g. `['$env']`). When omitted, every registered top-level property is
59
63
  * exposed — matching `VariableInput`'s default behaviour.
64
+ *
65
+ * Ignored when `metaTree` is supplied (an explicit tree wins).
60
66
  */
61
67
  namespaces?: string[];
62
68
  /** Additional leaves appended to the picker after the namespace-filtered nodes. */
63
69
  extraNodes?: MetaTreeNode[];
70
+ /**
71
+ * Provide the variable tree explicitly instead of reading the global
72
+ * FlowContext meta tree. When set, `namespaces`/`extraNodes` are ignored and
73
+ * this tree is used verbatim — use for context-scoped variable sources that
74
+ * are not part of the global registry (e.g. a workflow node's upstream
75
+ * outputs). Lazy `children` thunks are resolved on demand as the user
76
+ * expands the cascader.
77
+ */
78
+ metaTree?: MetaTreeNode[];
64
79
  /**
65
80
  * When true (default), the switcher exposes a `Null` option that resets the
66
81
  * value to `null`. When false, the value is constrained to one of the
67
82
  * allowed types or a variable reference.
68
83
  */
69
84
  nullable?: boolean;
85
+ /**
86
+ * Opt-in: when the incoming `value` is `undefined`, initialize the editor to
87
+ * the first allowed constant type instead of rendering the null-mode
88
+ * placeholder. `null` still keeps its explicit null semantics; only
89
+ * `undefined` triggers this path.
90
+ */
91
+ defaultToFirstConstantTypeWhenUndefined?: boolean;
70
92
  /** Variable-token delimiters. Default `['{{', '}}']` — see `VariableInput`. */
71
93
  delimiters?: VariableDelimiters;
94
+ /**
95
+ * Hide variable choices from the switcher. In variable-only mode this hides
96
+ * the selector button entirely; in mixed constant/variable mode it keeps the
97
+ * null/constant choices but removes variable entries.
98
+ */
99
+ hideVariable?: boolean;
72
100
  disabled?: boolean;
73
101
  placeholder?: string;
74
102
  style?: React.CSSProperties;
@@ -82,6 +110,7 @@ const TYPE_LABEL_KEYS: Record<TypedConstantType, string> = {
82
110
  number: 'Number',
83
111
  boolean: 'Boolean',
84
112
  date: 'Date',
113
+ object: 'JSON',
85
114
  };
86
115
 
87
116
  type NormalizedType = { type: TypedConstantType; props: Record<string, unknown> };
@@ -92,18 +121,23 @@ function normalizeTypes(types: TypedConstantSpec[]): NormalizedType[] {
92
121
  );
93
122
  }
94
123
 
95
- function defaultValueFor(type: TypedConstantType): unknown {
124
+ function defaultValueFor(type: TypedConstantType, typedProps: Record<string, unknown> = {}): unknown {
96
125
  switch (type) {
97
126
  case 'string':
98
127
  return '';
99
- case 'number':
128
+ case 'number': {
129
+ const min = typeof typedProps.min === 'number' ? typedProps.min : undefined;
130
+ if (min !== undefined && min > 0) return min;
100
131
  return 0;
132
+ }
101
133
  case 'boolean':
102
134
  return false;
103
135
  case 'date': {
104
136
  const now = new Date();
105
137
  return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
106
138
  }
139
+ case 'object':
140
+ return {};
107
141
  default:
108
142
  return null;
109
143
  }
@@ -121,6 +155,7 @@ function detectMode(value: unknown, parseVariablePath: (v?: string) => string[]
121
155
  if (typeof value === 'number') return { mode: 'number' };
122
156
  if (typeof value === 'boolean') return { mode: 'boolean' };
123
157
  if (value instanceof Date) return { mode: 'date' };
158
+ if (typeof value === 'object') return { mode: 'object' };
124
159
  return { mode: 'string' };
125
160
  }
126
161
 
@@ -131,6 +166,7 @@ interface SwitcherOption {
131
166
  value: string;
132
167
  label: React.ReactNode;
133
168
  isLeaf?: boolean;
169
+ loading?: boolean;
134
170
  children?: SwitcherOption[];
135
171
  meta?: MetaTreeNode;
136
172
  paths?: string[];
@@ -175,9 +211,10 @@ function renderConstantEditor(
175
211
  disabled?: boolean;
176
212
  placeholder?: string;
177
213
  t: (text: string) => string;
214
+ onJsonError?: (message: string | null) => void;
178
215
  },
179
216
  ): React.ReactNode {
180
- const { typedProps, disabled, placeholder, t } = options;
217
+ const { typedProps, disabled, placeholder, t, onJsonError } = options;
181
218
  switch (type) {
182
219
  case 'string':
183
220
  return (
@@ -229,11 +266,126 @@ function renderConstantEditor(
229
266
  />
230
267
  );
231
268
  }
269
+ case 'object':
270
+ return (
271
+ <JsonConstantEditor
272
+ value={value}
273
+ onChange={onChange}
274
+ onError={onJsonError}
275
+ disabled={disabled}
276
+ typedProps={typedProps}
277
+ />
278
+ );
232
279
  default:
233
280
  return null;
234
281
  }
235
282
  }
236
283
 
284
+ const jsonEditorClassName = css`
285
+ font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
286
+ font-size: 80%;
287
+ `;
288
+
289
+ /**
290
+ * Inline JSON editor for the `'object'` constant type. Formily-free port of v1's
291
+ * `Json` input (which used a Formily field for error feedback): keeps the raw
292
+ * text as local draft state, parses on blur, and writes the parsed object up via
293
+ * `onChange`. A parse error is reported up through `onError` (the raw
294
+ * `JSON.parse` message, matching v1) so the parent can render it on its own row
295
+ * below the input — keeping the textarea + switcher button row intact.
296
+ */
297
+ function JsonConstantEditor({
298
+ value,
299
+ onChange,
300
+ onError,
301
+ disabled,
302
+ typedProps,
303
+ }: {
304
+ value: unknown;
305
+ onChange?: (next: unknown) => void;
306
+ onError?: (message: string | null) => void;
307
+ disabled?: boolean;
308
+ typedProps: Record<string, unknown>;
309
+ }) {
310
+ const stringify = useCallback((v: unknown) => {
311
+ if (v == null) return '';
312
+ if (typeof v === 'string') return v;
313
+ try {
314
+ return JSON.stringify(v, null, 2);
315
+ } catch {
316
+ return '';
317
+ }
318
+ }, []);
319
+
320
+ const [text, setText] = useState<string>(() => stringify(value));
321
+ const [hasError, setHasError] = useState(false);
322
+
323
+ // Re-sync the draft when the external value changes (e.g. switching type).
324
+ useEffect(() => {
325
+ setText(stringify(value));
326
+ setHasError(false);
327
+ onError?.(null);
328
+ // `onError` intentionally omitted — only resync on external value change.
329
+ // eslint-disable-next-line react-hooks/exhaustive-deps
330
+ }, [value, stringify]);
331
+
332
+ // Validate as the user types (live red border + error row), mirroring v1's `Json` which calls `setFeedback` on every
333
+ // change — not just on blur.
334
+ const handleChange = useCallback(
335
+ (raw: string) => {
336
+ setText(raw);
337
+ const trimmed = raw.trim();
338
+ if (trimmed === '') {
339
+ setHasError(false);
340
+ onError?.(null);
341
+ return;
342
+ }
343
+ try {
344
+ JSON.parse(trimmed);
345
+ setHasError(false);
346
+ onError?.(null);
347
+ } catch (err) {
348
+ setHasError(true);
349
+ onError?.((err as Error).message);
350
+ }
351
+ },
352
+ [onError],
353
+ );
354
+
355
+ // Commit the parsed object up on blur (v1 emits the value on blur). A still-invalid value keeps its error and is not
356
+ // emitted.
357
+ const commit = useCallback(
358
+ (raw: string) => {
359
+ const trimmed = raw.trim();
360
+ if (trimmed === '') {
361
+ onChange?.(null);
362
+ return;
363
+ }
364
+ try {
365
+ onChange?.(JSON.parse(trimmed));
366
+ } catch {
367
+ /* error already shown live via handleChange */
368
+ }
369
+ },
370
+ [onChange],
371
+ );
372
+
373
+ return (
374
+ // No `autoSize` — it would disable the resize grip. Default to 2 rows (mirrors v1's `Json`) and let the user
375
+ // drag-resize from the corner.
376
+ <Input.TextArea
377
+ className={jsonEditorClassName}
378
+ value={text}
379
+ onChange={(ev) => handleChange(ev.target.value)}
380
+ onBlur={(ev) => commit(ev.target.value)}
381
+ disabled={disabled}
382
+ rows={2}
383
+ status={hasError ? 'error' : undefined}
384
+ {...typedProps}
385
+ />
386
+ );
387
+ }
388
+
237
389
  /**
238
390
  * Constant-or-variable input. Port of v1 `Variable.Input` typed-constant
239
391
  * Cascader pattern (the `[Null | Constant<type> | Variable<…namespaces>]`
@@ -255,8 +407,11 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
255
407
  types = DEFAULT_TYPES,
256
408
  namespaces,
257
409
  extraNodes,
410
+ metaTree: metaTreeProp,
258
411
  nullable = true,
412
+ defaultToFirstConstantTypeWhenUndefined = true,
259
413
  delimiters,
414
+ hideVariable = false,
260
415
  disabled,
261
416
  placeholder,
262
417
  style,
@@ -266,24 +421,86 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
266
421
  const t = useCallback((text: string): string => (typeof ctx?.t === 'function' ? ctx.t(text) : text), [ctx]);
267
422
  const { token } = theme.useToken();
268
423
 
269
- const metaTree = useFilteredMetaTree({ namespaces, extraNodes });
424
+ // An explicit `metaTree` wins over the global FlowContext tree. The hook is still called unconditionally (Rules of
425
+ // Hooks); its result is simply unused when a tree is injected.
426
+ const filteredMetaTree = useFilteredMetaTree({ namespaces, extraNodes });
427
+ const metaTree = metaTreeProp ?? filteredMetaTree;
270
428
 
271
429
  const parseVariablePath = useMemo(() => makeParseVariablePath(delimiters), [delimiters]);
272
430
  const formatVariablePath = useMemo(() => makeFormatVariablePath(delimiters), [delimiters]);
273
431
 
274
432
  const normalizedTypes = useMemo(() => normalizeTypes(types), [types]);
275
- const detected = useMemo(() => detectMode(value, parseVariablePath), [value, parseVariablePath]);
276
- const variableItems = useMemo(() => buildContextSelectorItems(metaTree).map(fromContextItem), [metaTree]);
433
+ const variableOnly = normalizedTypes.length === 0;
434
+ const defaultedValue = useMemo(() => {
435
+ if (variableOnly || !defaultToFirstConstantTypeWhenUndefined || value !== undefined) {
436
+ return undefined;
437
+ }
438
+ const firstType = normalizedTypes[0];
439
+ return firstType ? defaultValueFor(firstType.type, firstType.props) : undefined;
440
+ }, [defaultToFirstConstantTypeWhenUndefined, normalizedTypes, value, variableOnly]);
441
+ const effectiveValue = value === undefined && defaultedValue !== undefined ? defaultedValue : value;
442
+ const detected = useMemo(() => detectMode(effectiveValue, parseVariablePath), [effectiveValue, parseVariablePath]);
443
+
444
+ useEffect(() => {
445
+ if (value === undefined && defaultedValue !== undefined) {
446
+ onChange?.(defaultedValue);
447
+ }
448
+ }, [defaultedValue, onChange, value]);
449
+
450
+ // rc-cascader caches its options by *reference* (useEntities), so lazily filling a node's children in place is
451
+ // invisible to it. We mirror `FlowContextSelector`: lazy `loadData` mutates the **meta tree** in place, bumps
452
+ // `updateFlag`, and the options are rebuilt from scratch (fresh references) on every bump — forcing rc-cascader to
453
+ // re-index the new column.
454
+ const [updateFlag, setUpdateFlag] = useState(0);
455
+ const triggerUpdate = useCallback(() => setUpdateFlag((prev) => prev + 1), []);
456
+
457
+ const loadData = useCallback(
458
+ (selectedOptions: SwitcherOption[]) => {
459
+ const target = selectedOptions[selectedOptions.length - 1];
460
+ // Only variable nodes lazy-load; the Null / Constant switcher entries never do.
461
+ if (!target || target.value === NULL_KEY || target.value === CONST_KEY) {
462
+ return;
463
+ }
464
+ const meta = target.meta;
465
+ if (!meta) {
466
+ return;
467
+ }
468
+ // Already-resolved children (array): nothing to fetch.
469
+ if (Array.isArray(meta.children)) {
470
+ return;
471
+ }
472
+ if (typeof meta.children !== 'function') {
473
+ return;
474
+ }
475
+ target.loading = true;
476
+ triggerUpdate();
477
+ // `loadMetaTreeChildren` already swallows its own errors (returns []), so a bare chain is safe; `return`
478
+ // satisfies the promise/catch-or-return rule.
479
+ return loadMetaTreeChildren(meta)
480
+ .then((childMetas) => {
481
+ // Cache resolved children on the meta node; the options tree is then rebuilt from the mutated meta tree on
482
+ // the next `updateFlag` bump.
483
+ meta.children = childMetas;
484
+ })
485
+ .finally(() => {
486
+ target.loading = false;
487
+ triggerUpdate();
488
+ });
489
+ },
490
+ [triggerUpdate],
491
+ );
277
492
 
278
493
  const switcherOptions = useMemo<SwitcherOption[]>(() => {
494
+ // `updateFlag` is read so this recomputes (with fresh option references) after a lazy load mutates the meta tree.
495
+ void updateFlag;
279
496
  const items: SwitcherOption[] = [];
280
- if (nullable) {
497
+ if (!variableOnly && nullable) {
281
498
  items.push({ value: NULL_KEY, label: t('Null'), isLeaf: true });
282
499
  }
283
500
  // Always render Constant with a typed submenu — even when only one type is
284
501
  // allowed. Matches v1 `Variable.Input`, where clicking 常量 reveals 数字 /
285
502
  // 逻辑值 / etc. so the user can see what type the constant actually is.
286
- if (normalizedTypes.length > 0) {
503
+ if (!variableOnly && normalizedTypes.length > 0) {
287
504
  items.push({
288
505
  value: CONST_KEY,
289
506
  label: t('Constant'),
@@ -294,9 +511,11 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
294
511
  })),
295
512
  });
296
513
  }
297
- items.push(...variableItems);
514
+ if (!hideVariable) {
515
+ items.push(...buildContextSelectorItems(metaTree).map(fromContextItem));
516
+ }
298
517
  return items;
299
- }, [nullable, normalizedTypes, variableItems, t]);
518
+ }, [hideVariable, metaTree, normalizedTypes, nullable, t, updateFlag, variableOnly]);
300
519
 
301
520
  const onSwitcherChange = useCallback<NonNullable<CascaderProps<SwitcherOption>['onChange']>>(
302
521
  (path, selectedOptions) => {
@@ -313,7 +532,8 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
313
532
  const targetType = (path[1] as TypedConstantType | undefined) ?? normalizedTypes[0]?.type;
314
533
  if (!targetType) return;
315
534
  if (detected.mode === targetType) return;
316
- onChange?.(defaultValueFor(targetType));
535
+ const target = normalizedTypes.find(({ type }) => type === targetType);
536
+ onChange?.(defaultValueFor(targetType, target?.props));
317
537
  return;
318
538
  }
319
539
  const leaf = selectedOptions?.[selectedOptions.length - 1] as SwitcherOption | undefined;
@@ -326,17 +546,23 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
326
546
  );
327
547
 
328
548
  const onClearVariable = useCallback(() => {
329
- if (nullable) {
549
+ if (variableOnly) {
330
550
  onChange?.(null);
331
551
  return;
332
552
  }
333
553
  const first = normalizedTypes[0];
334
- if (first) onChange?.(defaultValueFor(first.type));
335
- }, [nullable, normalizedTypes, onChange]);
554
+ if (first) {
555
+ onChange?.(defaultValueFor(first.type, first.props));
556
+ return;
557
+ }
558
+ if (nullable) {
559
+ onChange?.(null);
560
+ }
561
+ }, [nullable, normalizedTypes, onChange, variableOnly]);
336
562
 
337
563
  const constantTypeForRendering: TypedConstantType = useMemo(() => {
338
564
  const m = detected.mode;
339
- if (m === 'string' || m === 'number' || m === 'boolean' || m === 'date') return m;
565
+ if (m === 'string' || m === 'number' || m === 'boolean' || m === 'date' || m === 'object') return m;
340
566
  return normalizedTypes[0]?.type ?? 'string';
341
567
  }, [detected.mode, normalizedTypes]);
342
568
 
@@ -347,94 +573,219 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
347
573
 
348
574
  const isVariable = detected.mode === 'variable';
349
575
  const isNull = detected.mode === 'null';
576
+ const showSwitcher = switcherOptions.length > 0 && !(hideVariable && isVariable);
577
+
578
+ const variableLabels = useMemo(() => {
579
+ // `updateFlag` is read so this recomputes after the preload effect below resolves a lazy level in the tree (same
580
+ // pattern as `switcherOptions`).
581
+ void updateFlag;
582
+ return isVariable && detected.variablePath ? resolveVariableLabels(detected.variablePath, metaTree) : [];
583
+ }, [isVariable, detected.variablePath, metaTree, updateFlag]);
350
584
 
351
- const variableLabels = useMemo(
352
- () => (isVariable && detected.variablePath ? resolveVariableLabels(detected.variablePath, metaTree) : []),
353
- [isVariable, detected.variablePath, metaTree],
585
+ const switcherValue = useMemo(() => {
586
+ if (isVariable && detected.variablePath?.length) {
587
+ return detected.variablePath;
588
+ }
589
+ if (variableOnly) {
590
+ return undefined;
591
+ }
592
+ if (isNull && nullable) {
593
+ return [NULL_KEY];
594
+ }
595
+ return [CONST_KEY, constantTypeForRendering];
596
+ }, [constantTypeForRendering, detected.variablePath, isNull, isVariable, nullable, variableOnly]);
597
+
598
+ // Preload a saved variable's label path across lazy levels. `resolveVariableLabels` can only read already-loaded
599
+ // `children`; when a saved reference points below a node whose children are still a lazy thunk (e.g. a relation field
600
+ // that hasn't been expanded), the deep segments render as raw names. Walk the saved path on mount / value change,
601
+ // resolving each lazy level in place (then bump `updateFlag` so the labels recompute). Mirrors v1 `Variable.Input`'s
602
+ // preload effect.
603
+ useEffect(() => {
604
+ if (!isVariable || !detected.variablePath?.length) {
605
+ return;
606
+ }
607
+ let cancelled = false;
608
+ const run = async () => {
609
+ let nodes: MetaTreeNode[] | undefined = metaTree;
610
+ let didLoad = false;
611
+ for (const segment of detected.variablePath as string[]) {
612
+ if (!nodes) {
613
+ break;
614
+ }
615
+ const matched: MetaTreeNode | undefined = nodes.find((node) => node.name === segment);
616
+ if (!matched) {
617
+ break;
618
+ }
619
+ if (typeof matched.children === 'function') {
620
+ const resolved = await loadMetaTreeChildren(matched);
621
+ if (cancelled) {
622
+ return;
623
+ }
624
+ matched.children = resolved;
625
+ didLoad = true;
626
+ }
627
+ nodes = Array.isArray(matched.children) ? matched.children : undefined;
628
+ }
629
+ if (didLoad && !cancelled) {
630
+ triggerUpdate();
631
+ }
632
+ };
633
+ run();
634
+ return () => {
635
+ cancelled = true;
636
+ };
637
+ // `metaTree` identity is stable per structural change (see consumers); re-run when the saved path or the tree
638
+ // changes.
639
+ }, [isVariable, detected.variablePath, metaTree, triggerUpdate]);
640
+
641
+ // JSON parse error from the object editor, rendered on its own row below the input (not squeezed into the compact
642
+ // row). Cleared whenever the value is no longer an object literal.
643
+ const [jsonError, setJsonError] = useState<string | null>(null);
644
+ useEffect(() => {
645
+ if (constantTypeForRendering !== 'object') setJsonError(null);
646
+ }, [constantTypeForRendering]);
647
+
648
+ const variableValueClassName = useMemo(
649
+ () => css`
650
+ &:hover .clear-button,
651
+ &:focus-within .clear-button {
652
+ visibility: visible;
653
+ pointer-events: auto;
654
+ opacity: 1;
655
+ }
656
+
657
+ .clear-button {
658
+ display: inline-flex;
659
+ align-items: center;
660
+ justify-content: center;
661
+ width: ${token.fontSizeIcon}px;
662
+ height: ${token.fontSizeIcon}px;
663
+ padding: 0;
664
+ color: ${token.colorTextQuaternary};
665
+ background: transparent;
666
+ cursor: pointer;
667
+ visibility: hidden;
668
+ pointer-events: none;
669
+ opacity: 0;
670
+ transition:
671
+ visibility ${token.motionDurationMid} ease,
672
+ color ${token.motionDurationMid} ease,
673
+ opacity ${token.motionDurationSlow} ease;
674
+
675
+ &:hover {
676
+ color: ${token.colorTextTertiary};
677
+ background: transparent;
678
+ }
679
+ }
680
+ `,
681
+ [token],
354
682
  );
355
683
 
356
684
  return (
357
- <Space.Compact style={{ display: 'flex', width: '100%', ...style }} className={className}>
358
- <div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
359
- {isVariable ? (
360
- <div
361
- role="button"
362
- aria-label="variable-tag"
363
- style={{
364
- display: 'flex',
365
- alignItems: 'center',
366
- gap: token.marginXXS,
367
- padding: `0 ${token.paddingSM}px`,
368
- minHeight: token.controlHeight,
369
- border: `1px solid ${token.colorBorder}`,
370
- borderRadius: token.borderRadius,
371
- borderTopRightRadius: 0,
372
- borderBottomRightRadius: 0,
373
- background: disabled ? token.colorBgContainerDisabled : token.colorBgContainer,
374
- overflow: 'hidden',
375
- }}
376
- >
377
- <Tag
378
- color="blue"
685
+ <div style={{ width: '100%' }}>
686
+ {/* Default `Space.Compact` (align-items: stretch) so the switcher button
687
+ grows to the value component's height — its `height: auto` (below) lets
688
+ a multi-line value (the JSON textarea) stretch the button to match,
689
+ joined like v1 (which sets `.ant-btn { height: auto }` likewise). */}
690
+ <Space.Compact style={{ display: 'flex', width: '100%', ...style }} className={className}>
691
+ <div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
692
+ {isVariable ? (
693
+ <div
694
+ className={variableValueClassName}
695
+ role="button"
696
+ aria-label="variable-tag"
379
697
  style={{
380
- marginInlineEnd: 0,
381
- maxWidth: '100%',
698
+ display: 'flex',
699
+ alignItems: 'center',
700
+ gap: token.marginXXS,
701
+ padding: `0 ${token.paddingSM}px`,
702
+ minHeight: token.controlHeight,
703
+ border: `1px solid ${token.colorBorder}`,
704
+ borderRadius: token.borderRadius,
705
+ borderTopRightRadius: showSwitcher ? 0 : token.borderRadius,
706
+ borderBottomRightRadius: showSwitcher ? 0 : token.borderRadius,
707
+ background: disabled ? token.colorBgContainerDisabled : token.colorBgContainer,
382
708
  overflow: 'hidden',
383
- textOverflow: 'ellipsis',
384
- whiteSpace: 'nowrap',
385
709
  }}
386
710
  >
387
- {variableLabels.map((label, index) => (
388
- <React.Fragment key={`${label}-${index}`}>
389
- {index ? ' / ' : ''}
390
- {label}
391
- </React.Fragment>
392
- ))}
393
- </Tag>
394
- {!disabled ? (
395
- <Button
396
- type="text"
397
- size="small"
398
- aria-label="icon-close"
399
- onClick={onClearVariable}
400
- icon={<CloseCircleFilled style={{ color: token.colorTextTertiary }} />}
401
- />
402
- ) : null}
403
- </div>
404
- ) : isNull ? (
405
- // v1 used the `placeholder` slot (not `value`) so the antd default
406
- // placeholder colour applies — keeps the field looking visibly
407
- // empty/inactive rather than holding a real text value.
408
- <Input placeholder={`<${t('Null')}>`} readOnly disabled={disabled} style={{ width: '100%' }} />
409
- ) : (
410
- renderConstantEditor(constantTypeForRendering, value, onChange, {
411
- typedProps,
412
- disabled,
413
- placeholder,
414
- t,
415
- })
416
- )}
417
- </div>
418
- <Cascader<SwitcherOption>
419
- options={switcherOptions}
420
- onChange={onSwitcherChange}
421
- disabled={disabled}
422
- changeOnSelect
423
- >
424
- <Button
425
- aria-label="variable-switcher"
426
- disabled={disabled}
427
- type={isVariable ? 'primary' : 'default'}
428
- style={{
429
- flexShrink: 0,
430
- fontStyle: 'italic',
431
- fontFamily: '"New York", "Times New Roman", Times, serif',
432
- }}
433
- >
434
- x
435
- </Button>
436
- </Cascader>
437
- </Space.Compact>
711
+ <Tag
712
+ color="blue"
713
+ style={{
714
+ marginInlineEnd: 0,
715
+ maxWidth: '100%',
716
+ overflow: 'hidden',
717
+ textOverflow: 'ellipsis',
718
+ whiteSpace: 'nowrap',
719
+ }}
720
+ >
721
+ {variableLabels.map((label, index) => (
722
+ <React.Fragment key={`${label}-${index}`}>
723
+ {index ? ' / ' : ''}
724
+ {label}
725
+ </React.Fragment>
726
+ ))}
727
+ </Tag>
728
+ {!disabled ? (
729
+ <Button
730
+ type="text"
731
+ size="small"
732
+ className="clear-button"
733
+ aria-label="icon-close"
734
+ onClick={onClearVariable}
735
+ icon={<CloseCircleFilled />}
736
+ />
737
+ ) : null}
738
+ </div>
739
+ ) : variableOnly ? (
740
+ <Input placeholder={placeholder} readOnly disabled={disabled} style={{ width: '100%' }} />
741
+ ) : isNull && nullable ? (
742
+ // v1 used the `placeholder` slot (not `value`) so the antd default placeholder colour applies — keeps the
743
+ // field looking visibly empty/inactive rather than holding a real text value.
744
+ <Input placeholder={`<${t('Null')}>`} readOnly disabled={disabled} style={{ width: '100%' }} />
745
+ ) : (
746
+ renderConstantEditor(constantTypeForRendering, effectiveValue, onChange, {
747
+ typedProps,
748
+ disabled,
749
+ placeholder,
750
+ t,
751
+ onJsonError: setJsonError,
752
+ })
753
+ )}
754
+ </div>
755
+ {showSwitcher ? (
756
+ <Cascader<SwitcherOption>
757
+ options={switcherOptions}
758
+ value={switcherValue}
759
+ onChange={onSwitcherChange}
760
+ loadData={loadData}
761
+ disabled={disabled}
762
+ changeOnSelect
763
+ >
764
+ <Button
765
+ aria-label="variable-switcher"
766
+ disabled={disabled}
767
+ type={isVariable ? 'primary' : 'default'}
768
+ style={{
769
+ flexShrink: 0,
770
+ // `height: auto` (instead of antd's fixed control height) lets the button stretch to the value
771
+ // component's height under the compact row's default `align-items: stretch` — so it stays joined to a
772
+ // tall JSON textarea. Mirrors v1's `.ant-btn { height: auto }`.
773
+ height: 'auto',
774
+ fontStyle: 'italic',
775
+ fontFamily: '"New York", "Times New Roman", Times, serif',
776
+ }}
777
+ >
778
+ x
779
+ </Button>
780
+ </Cascader>
781
+ ) : null}
782
+ </Space.Compact>
783
+ {jsonError ? (
784
+ <div style={{ marginTop: token.marginXXS, color: token.colorError, fontSize: token.fontSizeSM }}>
785
+ {jsonError}
786
+ </div>
787
+ ) : null}
788
+ </div>
438
789
  );
439
790
  }
440
791