@nocobase/client-v2 2.2.0-alpha.7 → 2.2.0-beta.10

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