@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.
- package/es/BaseApplication.d.ts +1 -1
- package/es/components/form/TypedVariableInput.d.ts +5 -31
- package/es/components/form/filter/CollectionFilter.d.ts +0 -2
- package/es/components/form/filter/CollectionFilterItem.d.ts +1 -11
- package/es/components/form/filter/CollectionFilterPanel.d.ts +0 -2
- package/es/components/form/filter/index.d.ts +0 -2
- package/es/components/form/filter/useFilterActionProps.d.ts +0 -2
- package/es/components/index.d.ts +0 -1
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +0 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +0 -3
- package/es/flow/components/RunJSValueEditor.d.ts +0 -1
- package/es/flow/index.d.ts +0 -1
- package/es/index.d.ts +0 -4
- package/es/index.mjs +99 -133
- package/es/utils/index.d.ts +0 -1
- package/lib/index.js +106 -140
- package/package.json +7 -8
- package/src/BaseApplication.tsx +6 -6
- package/src/__tests__/app.test.tsx +0 -26
- package/src/__tests__/browserChecker.test.ts +0 -61
- package/src/components/README.md +1 -7
- package/src/components/README.zh-CN.md +1 -6
- package/src/components/form/JsonTextArea.tsx +0 -4
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +2 -145
- package/src/components/form/ScanInput/useCodeScanner.ts +2 -154
- package/src/components/form/TypedVariableInput.tsx +98 -445
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +9 -320
- package/src/components/form/filter/CollectionFilter.tsx +0 -4
- package/src/components/form/filter/CollectionFilterItem.tsx +7 -32
- package/src/components/form/filter/CollectionFilterPanel.tsx +0 -4
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +0 -38
- package/src/components/form/filter/index.ts +0 -2
- package/src/components/form/filter/useFilterActionProps.ts +6 -37
- package/src/components/index.ts +0 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +6 -253
- package/src/flow/components/FieldAssignExactDatePicker.tsx +11 -25
- package/src/flow/components/FieldAssignValueInput.tsx +9 -55
- package/src/flow/components/RunJSValueEditor.tsx +1 -9
- package/src/flow/index.ts +0 -1
- package/src/index.ts +0 -4
- package/src/utils/index.tsx +0 -1
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +0 -55
- package/es/components/category-tabs/index.d.ts +0 -9
- package/es/utils/getRouteRuntimeVersion.d.ts +0 -23
- package/src/__tests__/getRouteRuntimeVersion.test.ts +0 -68
- package/src/components/category-tabs/SortableCategoryTabs.tsx +0 -210
- package/src/components/category-tabs/index.ts +0 -10
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +0 -95
- 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,
|
|
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.
|
|
31
|
-
* `useTypedConstant`
|
|
32
|
-
*
|
|
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'
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
431
|
-
const
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
512
|
-
items.push(...buildContextSelectorItems(metaTree).map(fromContextItem));
|
|
513
|
-
}
|
|
297
|
+
items.push(...variableItems);
|
|
514
298
|
return items;
|
|
515
|
-
}, [
|
|
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 (
|
|
329
|
+
if (nullable) {
|
|
546
330
|
onChange?.(null);
|
|
547
331
|
return;
|
|
548
332
|
}
|
|
549
333
|
const first = normalizedTypes[0];
|
|
550
|
-
if (first)
|
|
551
|
-
|
|
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'
|
|
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
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
<
|
|
682
|
-
{
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
<
|
|
761
|
-
|
|
762
|
-
disabled={disabled}
|
|
763
|
-
type={isVariable ? 'primary' : 'default'}
|
|
377
|
+
<Tag
|
|
378
|
+
color="blue"
|
|
764
379
|
style={{
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
|