@nocobase/client-v2 2.1.0-beta.41 → 2.1.0-beta.43

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-beta.41",
3
+ "version": "2.1.0-beta.43",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.0-beta.41",
31
- "@nocobase/flow-engine": "2.1.0-beta.41",
32
- "@nocobase/sdk": "2.1.0-beta.41",
33
- "@nocobase/shared": "2.1.0-beta.41",
34
- "@nocobase/utils": "2.1.0-beta.41",
30
+ "@nocobase/evaluators": "2.1.0-beta.43",
31
+ "@nocobase/flow-engine": "2.1.0-beta.43",
32
+ "@nocobase/sdk": "2.1.0-beta.43",
33
+ "@nocobase/shared": "2.1.0-beta.43",
34
+ "@nocobase/utils": "2.1.0-beta.43",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -45,5 +45,5 @@
45
45
  "react-i18next": "^11.15.1",
46
46
  "react-router-dom": "^6.30.1"
47
47
  },
48
- "gitHead": "ea3f966811146edb13c9e8016bf4cdc81597a003"
48
+ "gitHead": "6d7750e2373bf2451d246de88cc1f62491685e18"
49
49
  }
@@ -169,6 +169,54 @@ Key props:
169
169
 
170
170
  Under the hood `VariableInput` wraps `VariableHybridInput` (inline pills), `VariableTextArea` wraps `TextAreaWithContextSelector` (textarea + variable button). Both share the same MetaTree.
171
171
 
172
+ #### TypedVariableInput
173
+
174
+ Typed-constant + variable hybrid input. Ported from v1 `Variable.Input`'s `useTypedConstant` pattern: an italic `x` button on the right triggers a Cascader switcher `[Null | Constant<types> | Variable<…namespaces>]`; the left side renders the matching editor (`Input` / `InputNumber` / `Select(True/False)` / `DatePicker`) or a pill carrying the variable path.
175
+
176
+ Reach for this when a field **accepts both** a typed literal **and** a variable reference. The canonical example is `plugin-notification-email`'s SMTP `port` and `secure` fields: users can type a numeric port / boolean flag, or pass `{{ $env.SMTP_PORT }}` to read from environment variables.
177
+
178
+ ```tsx
179
+ import { TypedVariableInput } from '@nocobase/client-v2';
180
+
181
+ // Port — numeric constant + $env variable
182
+ <Form.Item name={['options', 'port']} label={t('Port')} initialValue={465}>
183
+ <TypedVariableInput
184
+ types={[['number', { min: 1, max: 65535, step: 1 }]]}
185
+ namespaces={['$env']}
186
+ />
187
+ </Form.Item>
188
+
189
+ // Secure mode — boolean constant + $env variable
190
+ <Form.Item name={['options', 'secure']} label={t('Secure')} initialValue={true}>
191
+ <TypedVariableInput types={['boolean']} namespaces={['$env']} />
192
+ </Form.Item>
193
+ ```
194
+
195
+ Key props:
196
+
197
+ - `types`: allowed constant types. Shape mirrors v1 `useTypedConstant` — pass bare type names (`['number', 'boolean']`) or `[type, editorProps]` tuples (`[['number', { min, max, step }]]`) to forward props to the underlying antd editor. Defaults to `['string', 'number', 'boolean', 'date']`. **Even when only one type is allowed, the `Constant` entry still expands into a typed submenu** (Number / Boolean / Date / String) — matches v1 so users can see what type the constant is
198
+ - `namespaces`: restrict the variable picker to specific top-level namespaces (e.g. `['$env']`). Omit to expose every namespace registered on `flowEngine.context`
199
+ - `extraNodes`: static leaves appended after the namespace-filtered nodes
200
+ - `nullable`: whether to expose the `Null` switcher entry. Default `true`. Combined with `Form.Item.rules={[{ required: true }]}`, the user can explicitly clear the field but submission is still blocked by validation — mirrors v1's "Null + required" pairing
201
+ - `delimiters`: variable-token delimiters, default `['{{', '}}']` — same as `VariableInput`
202
+ - `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`: standard controlled-input props
203
+
204
+ Value shape:
205
+
206
+ - Constant: stored as the native type (`number` / `boolean` / `Date` / `string`)
207
+ - Variable: a string like `'{{ $env.SMTP_PORT }}'`
208
+ - Null: `null`
209
+
210
+ When **not** to use it:
211
+
212
+ - **Pure literal fields** (users will never pass a variable) → use the antd primitive directly (`InputNumber` / `Select` / `DatePicker` / `Input`) and skip the Cascader column overhead
213
+ - **Pure variable fields** (users will never pass a literal) → use `EnvVariableInput` (`$env`-only, with optional password masking) or `VariableInput` (general-purpose)
214
+
215
+ Capabilities skipped (present in v1, not yet ported to v2):
216
+
217
+ - `object` constant type (JSON editor) — v2 has no inline "JSON editor + Cascader switcher" yet; add when there's a concrete caller
218
+ - Async `loadChildren` cascading — most MetaTree namespaces are already eagerly resolved by `useFilteredMetaTree`, so this hasn't been needed
219
+
172
220
  #### FileSizeInput
173
221
 
174
222
  A byte-valued size input paired with a unit selector (Byte / KB / MB / GB). The persisted value is always in bytes; the displayed number is derived from the picked unit.
@@ -169,6 +169,54 @@ import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
169
169
 
170
170
  底层共用 `VariableHybridInput`(`VariableInput`)和 `TextAreaWithContextSelector`(`VariableTextArea`),用同一套 MetaTree 数据。
171
171
 
172
+ #### TypedVariableInput
173
+
174
+ 类型化常量 + 变量混合输入器。移植 v1 `Variable.Input` 的 `useTypedConstant` 形态:右侧斜体 `x` 按钮触发 Cascader 切换 `[空值 | 常量<types> | 变量和密钥<…namespaces>]`,左侧根据当前模式渲染对应编辑器(`Input` / `InputNumber` / `Select(True/False)` / `DatePicker`)或一颗带变量路径的 pill。
175
+
176
+ 用于字段**同时接受**字面量**和**变量引用的场景。最典型的就是 `plugin-notification-email` 的 SMTP `port` 和 `secure`:可以填具体数字 / 布尔值,也可以填 `{{ $env.SMTP_PORT }}` 走环境变量。
177
+
178
+ ```tsx
179
+ import { TypedVariableInput } from '@nocobase/client-v2';
180
+
181
+ // 端口:数字常量 + $env 变量
182
+ <Form.Item name={['options', 'port']} label={t('端口')} initialValue={465}>
183
+ <TypedVariableInput
184
+ types={[['number', { min: 1, max: 65535, step: 1 }]]}
185
+ namespaces={['$env']}
186
+ />
187
+ </Form.Item>
188
+
189
+ // 安全模式:布尔常量 + $env 变量
190
+ <Form.Item name={['options', 'secure']} label={t('安全模式')} initialValue={true}>
191
+ <TypedVariableInput types={['boolean']} namespaces={['$env']} />
192
+ </Form.Item>
193
+ ```
194
+
195
+ 主要属性:
196
+
197
+ - `types`:允许的常量类型。形态对齐 v1 `useTypedConstant`,可以传裸类型名 `['number', 'boolean']`,也可以传 `[type, editorProps]` 元组 `[['number', { min, max, step }]]` 把 props 透传给底层 antd 编辑器。默认 `['string', 'number', 'boolean', 'date']`。**即使只允许一种类型,「常量」入口也会展开二级菜单**(数字 / 逻辑值 / 日期 / 字符串)——跟 v1 一致,让用户能直观看到当前常量是什么类型
198
+ - `namespaces`:限定变量 picker 可选的顶层命名空间(如 `['$env']`)。不传就用 `flowEngine.context` 里所有已注册命名空间
199
+ - `extraNodes`:在命名空间过滤后追加几条静态变量节点
200
+ - `nullable`:是否暴露「空值」入口,默认 `true`。配合 `Form.Item.rules={[{ required: true }]}` 可以让用户能手动清空、但提交时会被校验拦截——跟 v1 的「空值 + required」组合一致
201
+ - `delimiters`:变量 token 开闭分隔符,默认 `['{{', '}}']`,跟 `VariableInput` 一致
202
+ - `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`:标准受控字段属性
203
+
204
+ 值的形态:
205
+
206
+ - 常量:原生类型直接存(`number` / `boolean` / `Date` / `string`)
207
+ - 变量:字符串 `'{{ $env.SMTP_PORT }}'`
208
+ - 空值:`null`
209
+
210
+ 什么时候**不该用**:
211
+
212
+ - **纯字面量字段**(用户不会想填变量)→ 直接用 antd `InputNumber` / `Select` / `DatePicker` / `Input`,省掉 Cascader 那一格的视觉开销
213
+ - **纯变量字段**(用户不会想填字面量)→ 用 `EnvVariableInput`(`$env` 专用,带 password mask)或 `VariableInput`(更通用)
214
+
215
+ 跳过的能力(v1 有但 v2 还没补):
216
+
217
+ - `object` 类型(JSON 编辑器)——v2 还没对应的「内联 JSON 编辑器 + Cascader 切换」组件,等真有需求再补
218
+ - 异步 `loadChildren` 分支——大多数命名空间的 MetaTree 已经由 `useFilteredMetaTree` 提前展平,没遇到刚需
219
+
172
220
  #### FileSizeInput
173
221
 
174
222
  文件大小输入器。值统一存字节数,UI 上配一个单位选择器(Byte / KB / MB / GB)。
@@ -0,0 +1,441 @@
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 { CloseCircleFilled } from '@ant-design/icons';
11
+ import {
12
+ buildContextSelectorItems,
13
+ useFlowContext,
14
+ type ContextSelectorItem,
15
+ type MetaTreeNode,
16
+ } from '@nocobase/flow-engine';
17
+ import { Button, Cascader, DatePicker, Input, InputNumber, Select, Space, Tag, theme, type CascaderProps } from 'antd';
18
+ import dayjs from 'dayjs';
19
+ import React, { useCallback, useMemo } from 'react';
20
+ import {
21
+ makeFormatVariablePath,
22
+ makeParseVariablePath,
23
+ useFilteredMetaTree,
24
+ type VariableDelimiters,
25
+ } from './VariableInput';
26
+
27
+ /**
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).
31
+ */
32
+ export type TypedConstantType = 'string' | 'number' | 'boolean' | 'date';
33
+
34
+ /**
35
+ * One allowed constant type. Either a bare type name (`'number'`) or a
36
+ * `[type, editorProps]` pair (`['number', { min: 1, max: 65535 }]`) where
37
+ * `editorProps` is forwarded to the antd editor for that type — same
38
+ * shape as v1 `useTypedConstant`.
39
+ */
40
+ export type TypedConstantSpec = TypedConstantType | [TypedConstantType, Record<string, unknown>];
41
+
42
+ export interface TypedVariableInputProps {
43
+ /**
44
+ * Stored value. A `{{ ... }}` string is treated as a variable reference;
45
+ * anything else is a constant of the inferred type.
46
+ */
47
+ value?: unknown;
48
+ onChange?: (next: unknown) => void;
49
+ /**
50
+ * Allowed constant types. The `Constant` switcher entry always exposes a
51
+ * typed submenu (matching v1 `Variable.Input`) so users can see what type
52
+ * the constant is, even when only one type is permitted. Default: all four
53
+ * supported types.
54
+ */
55
+ types?: TypedConstantSpec[];
56
+ /**
57
+ * Restrict the variable picker to specific top-level meta-tree namespaces
58
+ * (e.g. `['$env']`). When omitted, every registered top-level property is
59
+ * exposed — matching `VariableInput`'s default behaviour.
60
+ */
61
+ namespaces?: string[];
62
+ /** Additional leaves appended to the picker after the namespace-filtered nodes. */
63
+ extraNodes?: MetaTreeNode[];
64
+ /**
65
+ * When true (default), the switcher exposes a `Null` option that resets the
66
+ * value to `null`. When false, the value is constrained to one of the
67
+ * allowed types or a variable reference.
68
+ */
69
+ nullable?: boolean;
70
+ /** Variable-token delimiters. Default `['{{', '}}']` — see `VariableInput`. */
71
+ delimiters?: VariableDelimiters;
72
+ disabled?: boolean;
73
+ placeholder?: string;
74
+ style?: React.CSSProperties;
75
+ className?: string;
76
+ }
77
+
78
+ const DEFAULT_TYPES: TypedConstantSpec[] = ['string', 'number', 'boolean', 'date'];
79
+
80
+ const TYPE_LABEL_KEYS: Record<TypedConstantType, string> = {
81
+ string: 'String',
82
+ number: 'Number',
83
+ boolean: 'Boolean',
84
+ date: 'Date',
85
+ };
86
+
87
+ type NormalizedType = { type: TypedConstantType; props: Record<string, unknown> };
88
+
89
+ function normalizeTypes(types: TypedConstantSpec[]): NormalizedType[] {
90
+ return types.map((spec) =>
91
+ Array.isArray(spec) ? { type: spec[0], props: spec[1] ?? {} } : { type: spec, props: {} },
92
+ );
93
+ }
94
+
95
+ function defaultValueFor(type: TypedConstantType): unknown {
96
+ switch (type) {
97
+ case 'string':
98
+ return '';
99
+ case 'number':
100
+ return 0;
101
+ case 'boolean':
102
+ return false;
103
+ case 'date': {
104
+ const now = new Date();
105
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
106
+ }
107
+ default:
108
+ return null;
109
+ }
110
+ }
111
+
112
+ type DetectedMode = { mode: 'null' | 'variable' | TypedConstantType; variablePath?: string[] };
113
+
114
+ function detectMode(value: unknown, parseVariablePath: (v?: string) => string[] | undefined): DetectedMode {
115
+ if (value == null) return { mode: 'null' };
116
+ if (typeof value === 'string') {
117
+ const path = parseVariablePath(value);
118
+ if (path && path.length > 0) return { mode: 'variable', variablePath: path };
119
+ return { mode: 'string' };
120
+ }
121
+ if (typeof value === 'number') return { mode: 'number' };
122
+ if (typeof value === 'boolean') return { mode: 'boolean' };
123
+ if (value instanceof Date) return { mode: 'date' };
124
+ return { mode: 'string' };
125
+ }
126
+
127
+ const NULL_KEY = '__null__';
128
+ const CONST_KEY = '__const__';
129
+
130
+ interface SwitcherOption {
131
+ value: string;
132
+ label: React.ReactNode;
133
+ isLeaf?: boolean;
134
+ children?: SwitcherOption[];
135
+ meta?: MetaTreeNode;
136
+ paths?: string[];
137
+ disabled?: boolean;
138
+ }
139
+
140
+ function fromContextItem(item: ContextSelectorItem): SwitcherOption {
141
+ return {
142
+ value: item.value,
143
+ label: item.label,
144
+ isLeaf: item.isLeaf,
145
+ meta: item.meta,
146
+ paths: item.paths,
147
+ disabled: item.disabled,
148
+ children: item.children?.map(fromContextItem),
149
+ };
150
+ }
151
+
152
+ function resolveVariableLabels(path: string[], metaTree: MetaTreeNode[]): string[] {
153
+ const labels: string[] = [];
154
+ let nodes: MetaTreeNode[] | undefined = metaTree;
155
+ for (const segment of path) {
156
+ if (!nodes) break;
157
+ const matched: MetaTreeNode | undefined = nodes.find((node) => node.name === segment);
158
+ if (!matched) {
159
+ labels.push(segment);
160
+ nodes = undefined;
161
+ continue;
162
+ }
163
+ labels.push(typeof matched.title === 'string' && matched.title ? matched.title : matched.name);
164
+ nodes = Array.isArray(matched.children) ? matched.children : undefined;
165
+ }
166
+ return labels;
167
+ }
168
+
169
+ function renderConstantEditor(
170
+ type: TypedConstantType,
171
+ value: unknown,
172
+ onChange: ((next: unknown) => void) | undefined,
173
+ options: {
174
+ typedProps: Record<string, unknown>;
175
+ disabled?: boolean;
176
+ placeholder?: string;
177
+ t: (text: string) => string;
178
+ },
179
+ ): React.ReactNode {
180
+ const { typedProps, disabled, placeholder, t } = options;
181
+ switch (type) {
182
+ case 'string':
183
+ return (
184
+ <Input
185
+ value={typeof value === 'string' ? value : ''}
186
+ onChange={(ev) => onChange?.(ev.target.value)}
187
+ disabled={disabled}
188
+ placeholder={placeholder}
189
+ {...typedProps}
190
+ />
191
+ );
192
+ case 'number':
193
+ return (
194
+ <InputNumber
195
+ value={typeof value === 'number' ? value : null}
196
+ onChange={(next) => onChange?.(next)}
197
+ disabled={disabled}
198
+ placeholder={placeholder}
199
+ style={{ width: '100%' }}
200
+ {...typedProps}
201
+ />
202
+ );
203
+ case 'boolean':
204
+ return (
205
+ <Select
206
+ value={typeof value === 'boolean' ? value : undefined}
207
+ onChange={(next) => onChange?.(next)}
208
+ disabled={disabled}
209
+ placeholder={placeholder ?? t('Select')}
210
+ options={[
211
+ { value: true, label: t('True') },
212
+ { value: false, label: t('False') },
213
+ ]}
214
+ style={{ width: '100%' }}
215
+ {...typedProps}
216
+ />
217
+ );
218
+ case 'date': {
219
+ const parsed = value instanceof Date ? dayjs(value) : null;
220
+ return (
221
+ <DatePicker
222
+ value={parsed}
223
+ onChange={(next) => onChange?.(next ? next.toDate() : null)}
224
+ disabled={disabled}
225
+ showTime
226
+ allowClear={false}
227
+ style={{ width: '100%' }}
228
+ {...typedProps}
229
+ />
230
+ );
231
+ }
232
+ default:
233
+ return null;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Constant-or-variable input. Port of v1 `Variable.Input` typed-constant
239
+ * Cascader pattern (the `[Null | Constant<type> | Variable<…namespaces>]`
240
+ * switcher). Use this when a field can accept either a typed literal
241
+ * (number, boolean, date, string) or a variable reference like
242
+ * `{{ $env.SMTP_PORT }}` — see `plugin-notification-email`'s port / secure
243
+ * fields for the canonical example.
244
+ *
245
+ * Pure literal fields should keep using the antd primitive
246
+ * (`InputNumber`, `Select`, `DatePicker`). Pure variable fields should keep
247
+ * using `EnvVariableInput` / `VariableInput`. This component carries the
248
+ * Cascader switcher overhead, so reach for it only when the field
249
+ * genuinely accepts both shapes.
250
+ */
251
+ export function TypedVariableInput(props: TypedVariableInputProps) {
252
+ const {
253
+ value,
254
+ onChange,
255
+ types = DEFAULT_TYPES,
256
+ namespaces,
257
+ extraNodes,
258
+ nullable = true,
259
+ delimiters,
260
+ disabled,
261
+ placeholder,
262
+ style,
263
+ className,
264
+ } = props;
265
+ const ctx = useFlowContext();
266
+ const t = useCallback((text: string): string => (typeof ctx?.t === 'function' ? ctx.t(text) : text), [ctx]);
267
+ const { token } = theme.useToken();
268
+
269
+ const metaTree = useFilteredMetaTree({ namespaces, extraNodes });
270
+
271
+ const parseVariablePath = useMemo(() => makeParseVariablePath(delimiters), [delimiters]);
272
+ const formatVariablePath = useMemo(() => makeFormatVariablePath(delimiters), [delimiters]);
273
+
274
+ const normalizedTypes = useMemo(() => normalizeTypes(types), [types]);
275
+ const detected = useMemo(() => detectMode(value, parseVariablePath), [value, parseVariablePath]);
276
+ const variableItems = useMemo(() => buildContextSelectorItems(metaTree).map(fromContextItem), [metaTree]);
277
+
278
+ const switcherOptions = useMemo<SwitcherOption[]>(() => {
279
+ const items: SwitcherOption[] = [];
280
+ if (nullable) {
281
+ items.push({ value: NULL_KEY, label: t('Null'), isLeaf: true });
282
+ }
283
+ // Always render Constant with a typed submenu — even when only one type is
284
+ // allowed. Matches v1 `Variable.Input`, where clicking 常量 reveals 数字 /
285
+ // 逻辑值 / etc. so the user can see what type the constant actually is.
286
+ if (normalizedTypes.length > 0) {
287
+ items.push({
288
+ value: CONST_KEY,
289
+ label: t('Constant'),
290
+ children: normalizedTypes.map(({ type }) => ({
291
+ value: type,
292
+ label: t(TYPE_LABEL_KEYS[type]),
293
+ isLeaf: true,
294
+ })),
295
+ });
296
+ }
297
+ items.push(...variableItems);
298
+ return items;
299
+ }, [nullable, normalizedTypes, variableItems, t]);
300
+
301
+ const onSwitcherChange = useCallback<NonNullable<CascaderProps<SwitcherOption>['onChange']>>(
302
+ (path, selectedOptions) => {
303
+ const head = path?.[0];
304
+ if (head === NULL_KEY) {
305
+ onChange?.(null);
306
+ return;
307
+ }
308
+ if (head === CONST_KEY) {
309
+ // Cascader always emits the second segment now that we render typed
310
+ // children even for single-type configs. Fall back to the first
311
+ // allowed type so picking only the parent still does something
312
+ // sensible (`changeOnSelect` lets this fire).
313
+ const targetType = (path[1] as TypedConstantType | undefined) ?? normalizedTypes[0]?.type;
314
+ if (!targetType) return;
315
+ if (detected.mode === targetType) return;
316
+ onChange?.(defaultValueFor(targetType));
317
+ return;
318
+ }
319
+ const leaf = selectedOptions?.[selectedOptions.length - 1] as SwitcherOption | undefined;
320
+ const meta = leaf?.meta;
321
+ if (!meta) return;
322
+ const formatted = formatVariablePath(meta);
323
+ if (formatted != null) onChange?.(formatted);
324
+ },
325
+ [onChange, normalizedTypes, detected.mode, formatVariablePath],
326
+ );
327
+
328
+ const onClearVariable = useCallback(() => {
329
+ if (nullable) {
330
+ onChange?.(null);
331
+ return;
332
+ }
333
+ const first = normalizedTypes[0];
334
+ if (first) onChange?.(defaultValueFor(first.type));
335
+ }, [nullable, normalizedTypes, onChange]);
336
+
337
+ const constantTypeForRendering: TypedConstantType = useMemo(() => {
338
+ const m = detected.mode;
339
+ if (m === 'string' || m === 'number' || m === 'boolean' || m === 'date') return m;
340
+ return normalizedTypes[0]?.type ?? 'string';
341
+ }, [detected.mode, normalizedTypes]);
342
+
343
+ const typedProps = useMemo(
344
+ () => normalizedTypes.find((nt) => nt.type === constantTypeForRendering)?.props ?? {},
345
+ [normalizedTypes, constantTypeForRendering],
346
+ );
347
+
348
+ const isVariable = detected.mode === 'variable';
349
+ const isNull = detected.mode === 'null';
350
+
351
+ const variableLabels = useMemo(
352
+ () => (isVariable && detected.variablePath ? resolveVariableLabels(detected.variablePath, metaTree) : []),
353
+ [isVariable, detected.variablePath, metaTree],
354
+ );
355
+
356
+ 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"
379
+ style={{
380
+ marginInlineEnd: 0,
381
+ maxWidth: '100%',
382
+ overflow: 'hidden',
383
+ textOverflow: 'ellipsis',
384
+ whiteSpace: 'nowrap',
385
+ }}
386
+ >
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>
438
+ );
439
+ }
440
+
441
+ export default TypedVariableInput;