@tai42/studio-sdk 14.0.0 → 15.0.0
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/dist/components/components.css +19 -0
- package/dist/components/templated-text-field.d.ts +98 -0
- package/dist/components/templated-text-field.d.ts.map +1 -0
- package/dist/components/templated-text-field.js +217 -0
- package/dist/components/templated-text-field.js.map +1 -0
- package/dist/hooks/useApi.d.ts +310 -83
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/state-binding/BindingJqField.d.ts +8 -1
- package/dist/state-binding/BindingJqField.d.ts.map +1 -1
- package/dist/state-binding/BindingJqField.js +5 -2
- package/dist/state-binding/BindingJqField.js.map +1 -1
- package/dist/state-binding/BindingTemplatedJqField.d.ts +24 -0
- package/dist/state-binding/BindingTemplatedJqField.d.ts.map +1 -0
- package/dist/state-binding/BindingTemplatedJqField.js +7 -0
- package/dist/state-binding/BindingTemplatedJqField.js.map +1 -0
- package/dist/state-binding/InputInjectionRow.d.ts +4 -3
- package/dist/state-binding/InputInjectionRow.d.ts.map +1 -1
- package/dist/state-binding/InputInjectionRow.js +9 -9
- package/dist/state-binding/InputInjectionRow.js.map +1 -1
- package/dist/state-binding/StateAttachRow.d.ts +6 -4
- package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
- package/dist/state-binding/StateAttachRow.js +13 -12
- package/dist/state-binding/StateAttachRow.js.map +1 -1
- package/dist/state-binding/StateBindingEditor.d.ts +4 -2
- package/dist/state-binding/StateBindingEditor.d.ts.map +1 -1
- package/dist/state-binding/StateBindingEditor.js +10 -8
- package/dist/state-binding/StateBindingEditor.js.map +1 -1
- package/dist/state-binding/SubjectScopeFields.d.ts +12 -9
- package/dist/state-binding/SubjectScopeFields.d.ts.map +1 -1
- package/dist/state-binding/SubjectScopeFields.js +5 -5
- package/dist/state-binding/SubjectScopeFields.js.map +1 -1
- package/dist/state-binding/UpdateRow.d.ts +4 -3
- package/dist/state-binding/UpdateRow.d.ts.map +1 -1
- package/dist/state-binding/UpdateRow.js +29 -16
- package/dist/state-binding/UpdateRow.js.map +1 -1
- package/dist/state-binding/index.d.ts +5 -1
- package/dist/state-binding/index.d.ts.map +1 -1
- package/dist/state-binding/index.js +2 -0
- package/dist/state-binding/index.js.map +1 -1
- package/dist/state-binding/templated-text-catalog.d.ts +21 -0
- package/dist/state-binding/templated-text-catalog.d.ts.map +1 -0
- package/dist/state-binding/templated-text-catalog.js +48 -0
- package/dist/state-binding/templated-text-catalog.js.map +1 -0
- package/dist/state-binding/types.d.ts +23 -2
- package/dist/state-binding/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/components.css +19 -0
- package/src/components/templated-text-field.tsx +508 -0
- package/src/index.ts +13 -0
- package/src/state-binding/BindingJqField.tsx +12 -0
- package/src/state-binding/BindingTemplatedJqField.tsx +73 -0
- package/src/state-binding/InputInjectionRow.tsx +16 -7
- package/src/state-binding/StateAttachRow.tsx +19 -11
- package/src/state-binding/StateBindingEditor.tsx +14 -6
- package/src/state-binding/SubjectScopeFields.tsx +23 -15
- package/src/state-binding/UpdateRow.tsx +57 -24
- package/src/state-binding/index.ts +6 -0
- package/src/state-binding/templated-text-catalog.ts +69 -0
- package/src/state-binding/types.ts +31 -2
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TemplatedTextField` — the ONE control that authors a {@link TemplatedText}
|
|
3
|
+
* value: an authored-text field that is either inline `content` (text written in
|
|
4
|
+
* place) or a stored template `id` (a template resource fetched and rendered at
|
|
5
|
+
* run time), with the render `kwargs` that apply in BOTH cases.
|
|
6
|
+
*
|
|
7
|
+
* The two sources are mutually exclusive by CONSTRUCTION: the mode toggle only ever
|
|
8
|
+
* writes `content` XOR `id`, so a value can never carry both. "Neither" is a `null`
|
|
9
|
+
* value, allowed only when the field is optional; a `required` field emits its
|
|
10
|
+
* empty source instead so the schema's non-empty guard (and the field error) refuse
|
|
11
|
+
* the save. The kwargs editor is shared across both modes.
|
|
12
|
+
*
|
|
13
|
+
* It seeds its editing state from `value` ONCE and drives itself thereafter,
|
|
14
|
+
* emitting a normalized value on every change (a caller resets it by remounting
|
|
15
|
+
* under a fresh `key`). The inline editor is a plain multiline textarea by default;
|
|
16
|
+
* a caller authoring jq or another rich field passes `renderInline` to supply its
|
|
17
|
+
* own editor. The stored-template list, its loading/error state, the verbatim
|
|
18
|
+
* server message for an id that no longer resolves, and whether storage is present
|
|
19
|
+
* are passed in by the caller — this control holds no data edge of its own.
|
|
20
|
+
*
|
|
21
|
+
* When storage is absent (a supported configuration in which stored templates
|
|
22
|
+
* cannot be fetched or resolved) the stored source is not offered: an inline or
|
|
23
|
+
* empty value edits inline; a value already carrying an `id` is shown read-only with
|
|
24
|
+
* its kwargs, never converted or dropped. See `storageAbsent` /
|
|
25
|
+
* `storagePresenceLoading`.
|
|
26
|
+
*/
|
|
27
|
+
import { useState, type ReactNode } from 'react';
|
|
28
|
+
|
|
29
|
+
import type { TemplatedText } from '@tai42/api-client';
|
|
30
|
+
|
|
31
|
+
import { Button, ErrorState, Skeleton } from './primitives';
|
|
32
|
+
import { Field } from './field';
|
|
33
|
+
import { RadioGroup } from './radio-group';
|
|
34
|
+
import { Select } from './select';
|
|
35
|
+
import { Textarea, TextInput } from './inputs';
|
|
36
|
+
import { CloseIcon } from './icons';
|
|
37
|
+
|
|
38
|
+
/** One stored template the id picker offers. */
|
|
39
|
+
export interface TemplatedTextTemplateOption {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
/** The label shown for the id; defaults to the id itself. */
|
|
42
|
+
readonly label?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The props `renderInline` receives to draw the inline-content editor. */
|
|
46
|
+
export interface TemplatedTextInlineProps {
|
|
47
|
+
readonly label: string;
|
|
48
|
+
readonly value: string;
|
|
49
|
+
readonly onChange: (value: string) => void;
|
|
50
|
+
readonly error?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The field renders its own group header (label + description) above the source
|
|
53
|
+
* toggle, so the inline editor's OWN visible label is redundant: an editor honours
|
|
54
|
+
* this by rendering that label visually hidden while keeping it as its accessible
|
|
55
|
+
* name (its `<label for>` association stays intact). Always `true` — the control is
|
|
56
|
+
* always grouped — so an editor that draws its own label must respect it.
|
|
57
|
+
*/
|
|
58
|
+
readonly hideLabel: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TemplatedTextFieldProps {
|
|
62
|
+
/** The field's accessible name, shown on the active editor. */
|
|
63
|
+
readonly label: string;
|
|
64
|
+
readonly value: TemplatedText | null;
|
|
65
|
+
readonly onChange: (value: TemplatedText | null) => void;
|
|
66
|
+
/** Group-level helper shown under the label, above the source toggle. */
|
|
67
|
+
readonly description?: string;
|
|
68
|
+
/** A field-level error surfaced beneath the control. */
|
|
69
|
+
readonly error?: string;
|
|
70
|
+
/**
|
|
71
|
+
* When true the field must carry a source: clearing the inline text or leaving
|
|
72
|
+
* the template unchosen emits an EMPTY source (not `null`), so the schema guard
|
|
73
|
+
* and the field error refuse the save rather than silently dropping the field.
|
|
74
|
+
*/
|
|
75
|
+
readonly required?: boolean;
|
|
76
|
+
/** The stored templates the id picker offers. */
|
|
77
|
+
readonly templates?: readonly TemplatedTextTemplateOption[];
|
|
78
|
+
/** The template list fetch is in flight. */
|
|
79
|
+
readonly templatesLoading?: boolean;
|
|
80
|
+
/** The template list fetch failed — surfaced with a retry. */
|
|
81
|
+
readonly templatesError?: string;
|
|
82
|
+
readonly onTemplatesRetry?: () => void;
|
|
83
|
+
/** The verbatim server message when the stored `id` no longer resolves. */
|
|
84
|
+
readonly resolveError?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Storage is registered ABSENT (`GET /api/storage` → `present: false`) — a fully
|
|
87
|
+
* supported configuration in which stored templates can be neither browsed nor
|
|
88
|
+
* resolved. The stored source is therefore not offered: an inline or empty value
|
|
89
|
+
* shows the inline editor alone (no toggle, no catalog); a value that already
|
|
90
|
+
* carries an `id` is shown READ-ONLY with its kwargs and a note that stored
|
|
91
|
+
* templates are unavailable here — the authored value is never converted to inline,
|
|
92
|
+
* dropped, or blanked.
|
|
93
|
+
*/
|
|
94
|
+
readonly storageAbsent?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Storage presence is not yet known. A placeholder stands in for the source region
|
|
97
|
+
* so the toggle is not shown and then removed once presence resolves.
|
|
98
|
+
*/
|
|
99
|
+
readonly storagePresenceLoading?: boolean;
|
|
100
|
+
/** Supply a custom inline-content editor (jq, rich text); defaults to a textarea. */
|
|
101
|
+
readonly renderInline?: (props: TemplatedTextInlineProps) => ReactNode;
|
|
102
|
+
readonly disabled?: boolean;
|
|
103
|
+
/** Placeholder for the default textarea editor (ignored when `renderInline` is given). */
|
|
104
|
+
readonly placeholder?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A short one-line rendering of a templated-text value, for read-only summaries. */
|
|
108
|
+
export function templatedTextSummary(value: TemplatedText | null): string {
|
|
109
|
+
if (value === null) return '(unset)';
|
|
110
|
+
if (value.id !== undefined) return `template: ${value.id}`;
|
|
111
|
+
return value.content ?? '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type Mode = 'inline' | 'stored';
|
|
115
|
+
|
|
116
|
+
interface KwargRow {
|
|
117
|
+
readonly key: string;
|
|
118
|
+
readonly value: string;
|
|
119
|
+
/**
|
|
120
|
+
* The stored value this row was seeded from, kept so an UNTOUCHED row re-emits it
|
|
121
|
+
* BYTE-FOR-BYTE: the row form cannot tell a stored string `"7"` from the number `7`,
|
|
122
|
+
* so re-parsing an untouched row would silently coerce the stored type. `undefined`
|
|
123
|
+
* for a row the author added.
|
|
124
|
+
*/
|
|
125
|
+
readonly original?: unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The row's canonical string form, matched against `value` to detect an untouched row. */
|
|
129
|
+
function stringifyRowValue(raw: unknown): string {
|
|
130
|
+
return typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Parse a cell as JSON when it can be (numbers/bools/objects), else keep the string. */
|
|
134
|
+
function parseCellValue(raw: string): unknown {
|
|
135
|
+
const trimmed = raw.trim();
|
|
136
|
+
if (trimmed.length === 0) return '';
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(trimmed);
|
|
139
|
+
} catch {
|
|
140
|
+
return raw;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Collapse the kwargs rows into an object, dropping rows with a blank key. */
|
|
145
|
+
function rowsToObject(rows: readonly KwargRow[]): Record<string, unknown> {
|
|
146
|
+
const out: Record<string, unknown> = {};
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const key = row.key.trim();
|
|
149
|
+
if (key.length === 0) continue;
|
|
150
|
+
// An untouched seeded row re-emits its stored value verbatim (no JSON-type coerce);
|
|
151
|
+
// an edited or added row is parsed from the cell text.
|
|
152
|
+
out[key] =
|
|
153
|
+
row.original !== undefined && stringifyRowValue(row.original) === row.value
|
|
154
|
+
? row.original
|
|
155
|
+
: parseCellValue(row.value);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Seed rows from a stored kwargs object (stringifying non-string values). */
|
|
161
|
+
function objectToRows(kwargs: Record<string, unknown> | undefined): KwargRow[] {
|
|
162
|
+
if (kwargs === undefined) return [];
|
|
163
|
+
return Object.entries(kwargs).map(([key, raw]) => ({
|
|
164
|
+
key,
|
|
165
|
+
value: stringifyRowValue(raw),
|
|
166
|
+
original: raw,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The add/remove kwargs editor, revealed on demand and open when kwargs already exist. */
|
|
171
|
+
function KwargsEditor({
|
|
172
|
+
label,
|
|
173
|
+
rows,
|
|
174
|
+
disabled,
|
|
175
|
+
onChange,
|
|
176
|
+
}: {
|
|
177
|
+
readonly label: string;
|
|
178
|
+
readonly rows: readonly KwargRow[];
|
|
179
|
+
readonly disabled: boolean;
|
|
180
|
+
readonly onChange: (rows: KwargRow[]) => void;
|
|
181
|
+
}): ReactNode {
|
|
182
|
+
const [open, setOpen] = useState(rows.length > 0);
|
|
183
|
+
const groupLabel = `${label} render parameters`;
|
|
184
|
+
|
|
185
|
+
if (!open) {
|
|
186
|
+
// Wrapped so the ghost button keeps its content width and stays left-aligned
|
|
187
|
+
// in the field's column, marking it as this field's own disclosure rather than
|
|
188
|
+
// a full-width control floating between two fields.
|
|
189
|
+
return (
|
|
190
|
+
<div>
|
|
191
|
+
<Button
|
|
192
|
+
type="button"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
disabled={disabled}
|
|
195
|
+
onClick={() => {
|
|
196
|
+
setOpen(true);
|
|
197
|
+
onChange([...rows, { key: '', value: '' }]);
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
Add render parameters
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
role="group"
|
|
209
|
+
aria-label={groupLabel}
|
|
210
|
+
style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}
|
|
211
|
+
>
|
|
212
|
+
<span style={{ fontSize: 'var(--tai-text-sm)', color: 'var(--tai-color-text-muted)' }}>
|
|
213
|
+
Render parameters
|
|
214
|
+
</span>
|
|
215
|
+
{rows.map((row, index) => (
|
|
216
|
+
<div
|
|
217
|
+
key={index}
|
|
218
|
+
style={{ display: 'flex', gap: 'var(--tai-space-2)', alignItems: 'center' }}
|
|
219
|
+
>
|
|
220
|
+
<TextInput
|
|
221
|
+
aria-label={`${groupLabel} key ${String(index + 1)}`}
|
|
222
|
+
placeholder="name"
|
|
223
|
+
value={row.key}
|
|
224
|
+
autoComplete="off"
|
|
225
|
+
disabled={disabled}
|
|
226
|
+
onChange={(event) => {
|
|
227
|
+
const next = [...rows];
|
|
228
|
+
next[index] = { ...row, key: event.target.value };
|
|
229
|
+
onChange(next);
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
<TextInput
|
|
233
|
+
aria-label={`${groupLabel} value ${String(index + 1)}`}
|
|
234
|
+
placeholder="value"
|
|
235
|
+
value={row.value}
|
|
236
|
+
autoComplete="off"
|
|
237
|
+
disabled={disabled}
|
|
238
|
+
onChange={(event) => {
|
|
239
|
+
const next = [...rows];
|
|
240
|
+
next[index] = { ...row, value: event.target.value };
|
|
241
|
+
onChange(next);
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
<Button
|
|
245
|
+
type="button"
|
|
246
|
+
variant="ghost"
|
|
247
|
+
aria-label={`Remove ${groupLabel} ${String(index + 1)}`}
|
|
248
|
+
disabled={disabled}
|
|
249
|
+
onClick={() => {
|
|
250
|
+
onChange(rows.filter((_, position) => position !== index));
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<CloseIcon aria-hidden="true" />
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
<div>
|
|
258
|
+
<Button
|
|
259
|
+
type="button"
|
|
260
|
+
variant="ghost"
|
|
261
|
+
disabled={disabled}
|
|
262
|
+
onClick={() => {
|
|
263
|
+
onChange([...rows, { key: '', value: '' }]);
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
Add parameter
|
|
267
|
+
</Button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The initial mode: stored when the seed carries an `id`, inline otherwise. */
|
|
274
|
+
function initialMode(value: TemplatedText | null): Mode {
|
|
275
|
+
return value?.id !== undefined ? 'stored' : 'inline';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function TemplatedTextField({
|
|
279
|
+
label,
|
|
280
|
+
value,
|
|
281
|
+
onChange,
|
|
282
|
+
description,
|
|
283
|
+
error,
|
|
284
|
+
required = false,
|
|
285
|
+
templates = [],
|
|
286
|
+
templatesLoading = false,
|
|
287
|
+
templatesError,
|
|
288
|
+
onTemplatesRetry,
|
|
289
|
+
resolveError,
|
|
290
|
+
storageAbsent = false,
|
|
291
|
+
storagePresenceLoading = false,
|
|
292
|
+
renderInline,
|
|
293
|
+
disabled = false,
|
|
294
|
+
placeholder,
|
|
295
|
+
}: TemplatedTextFieldProps): ReactNode {
|
|
296
|
+
const [mode, setMode] = useState<Mode>(() => initialMode(value));
|
|
297
|
+
const [inlineDraft, setInlineDraft] = useState(() => value?.content ?? '');
|
|
298
|
+
const [storedDraft, setStoredDraft] = useState(() => value?.id ?? '');
|
|
299
|
+
const [kwargsRows, setKwargsRows] = useState<KwargRow[]>(() => objectToRows(value?.kwargs));
|
|
300
|
+
|
|
301
|
+
const emit = (
|
|
302
|
+
nextMode: Mode,
|
|
303
|
+
nextInline: string,
|
|
304
|
+
nextStored: string,
|
|
305
|
+
nextKwargs: readonly KwargRow[],
|
|
306
|
+
): void => {
|
|
307
|
+
const kwargs = rowsToObject(nextKwargs);
|
|
308
|
+
const kw = Object.keys(kwargs).length > 0 ? { kwargs } : {};
|
|
309
|
+
if (nextMode === 'stored') {
|
|
310
|
+
const id = nextStored.trim();
|
|
311
|
+
if (id === '') {
|
|
312
|
+
onChange(required ? { id: '', ...kw } : null);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
onChange({ id, ...kw });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (nextInline.trim() === '') {
|
|
319
|
+
onChange(required ? { content: '', ...kw } : null);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
onChange({ content: nextInline, ...kw });
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const changeMode = (next: Mode): void => {
|
|
326
|
+
setMode(next);
|
|
327
|
+
emit(next, inlineDraft, storedDraft, kwargsRows);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const changeInline = (next: string): void => {
|
|
331
|
+
setInlineDraft(next);
|
|
332
|
+
emit('inline', next, storedDraft, kwargsRows);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const changeStored = (next: string): void => {
|
|
336
|
+
setStoredDraft(next);
|
|
337
|
+
emit('stored', inlineDraft, next, kwargsRows);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const changeKwargs = (next: KwargRow[]): void => {
|
|
341
|
+
setKwargsRows(next);
|
|
342
|
+
emit(mode, inlineDraft, storedDraft, next);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const templateOptions = templates.map((template) => ({
|
|
346
|
+
value: template.id,
|
|
347
|
+
label: template.label ?? template.id,
|
|
348
|
+
}));
|
|
349
|
+
// A stored id the catalog does not offer cannot be picked from the list; it is
|
|
350
|
+
// shown as an unresolved note rather than silently blanked.
|
|
351
|
+
const unresolvedId =
|
|
352
|
+
mode === 'stored' &&
|
|
353
|
+
storedDraft.trim() !== '' &&
|
|
354
|
+
!templatesLoading &&
|
|
355
|
+
templatesError === undefined &&
|
|
356
|
+
!templates.some((template) => template.id === storedDraft.trim())
|
|
357
|
+
? storedDraft.trim()
|
|
358
|
+
: undefined;
|
|
359
|
+
|
|
360
|
+
// The inline editor, shared by the present-storage inline mode and the
|
|
361
|
+
// absent-storage inline-only rendering: the caller's rich editor when supplied,
|
|
362
|
+
// else a plain textarea.
|
|
363
|
+
const inlineEditor =
|
|
364
|
+
renderInline !== undefined ? (
|
|
365
|
+
renderInline({ label, value: inlineDraft, onChange: changeInline, error, hideLabel: true })
|
|
366
|
+
) : (
|
|
367
|
+
<Field label={label} hideLabel error={error}>
|
|
368
|
+
<Textarea
|
|
369
|
+
value={inlineDraft}
|
|
370
|
+
placeholder={placeholder}
|
|
371
|
+
disabled={disabled}
|
|
372
|
+
onChange={(event) => {
|
|
373
|
+
changeInline(event.target.value);
|
|
374
|
+
}}
|
|
375
|
+
/>
|
|
376
|
+
</Field>
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// A stored `id` already authored on the value: with storage absent it is shown
|
|
380
|
+
// read-only rather than resolved, browsed, converted, or dropped.
|
|
381
|
+
const authoredStoredId = value?.id;
|
|
382
|
+
const showsReadOnlyStored = storageAbsent && authoredStoredId !== undefined;
|
|
383
|
+
|
|
384
|
+
// The source region. While presence is unknown a placeholder stands in, so the
|
|
385
|
+
// toggle is never shown and then removed. With storage absent the stored source
|
|
386
|
+
// cannot work and is not offered: an inline or empty value edits inline; a value
|
|
387
|
+
// carrying an `id` is shown read-only with an unavailability note. With storage
|
|
388
|
+
// present the full inline/stored toggle and picker render.
|
|
389
|
+
let source: ReactNode;
|
|
390
|
+
if (storagePresenceLoading) {
|
|
391
|
+
source = <Skeleton height={36} />;
|
|
392
|
+
} else if (storageAbsent) {
|
|
393
|
+
source = showsReadOnlyStored ? (
|
|
394
|
+
<>
|
|
395
|
+
{/* A labeled read-only reference, NOT an editable field: no input box, a
|
|
396
|
+
"Stored template" caption, and the id in monospace — so it reads as a stored
|
|
397
|
+
reference the way the state-template screen shows one, never as literal text a
|
|
398
|
+
reader could mistake for the condition itself. A long id wraps rather than
|
|
399
|
+
overflowing its row. */}
|
|
400
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-1)' }}>
|
|
401
|
+
<span style={{ fontSize: 'var(--tai-text-sm)', color: 'var(--tai-color-text-muted)' }}>
|
|
402
|
+
Stored template
|
|
403
|
+
</span>
|
|
404
|
+
<code style={{ fontFamily: 'var(--tai-font-mono)', wordBreak: 'break-all' }}>
|
|
405
|
+
{authoredStoredId}
|
|
406
|
+
</code>
|
|
407
|
+
</div>
|
|
408
|
+
<p
|
|
409
|
+
style={{
|
|
410
|
+
margin: 0,
|
|
411
|
+
fontSize: 'var(--tai-text-sm)',
|
|
412
|
+
color: 'var(--tai-color-text-muted)',
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
Stored templates are unavailable — this deployment has no storage backend.
|
|
416
|
+
</p>
|
|
417
|
+
</>
|
|
418
|
+
) : (
|
|
419
|
+
inlineEditor
|
|
420
|
+
);
|
|
421
|
+
} else {
|
|
422
|
+
source = (
|
|
423
|
+
<>
|
|
424
|
+
<RadioGroup
|
|
425
|
+
aria-label={`${label} source`}
|
|
426
|
+
variant="segmented"
|
|
427
|
+
value={mode}
|
|
428
|
+
disabled={disabled}
|
|
429
|
+
options={[
|
|
430
|
+
{ value: 'inline', label: 'Inline text' },
|
|
431
|
+
{ value: 'stored', label: 'Stored template' },
|
|
432
|
+
]}
|
|
433
|
+
onValueChange={(next) => {
|
|
434
|
+
changeMode(next as Mode);
|
|
435
|
+
}}
|
|
436
|
+
/>
|
|
437
|
+
|
|
438
|
+
{mode === 'inline' ? (
|
|
439
|
+
inlineEditor
|
|
440
|
+
) : templatesError !== undefined ? (
|
|
441
|
+
<ErrorState message={templatesError} onRetry={onTemplatesRetry} />
|
|
442
|
+
) : templatesLoading ? (
|
|
443
|
+
<Skeleton height={36} />
|
|
444
|
+
) : (
|
|
445
|
+
<Field label={label} hideLabel error={error}>
|
|
446
|
+
<Select
|
|
447
|
+
placeholder={templates.length === 0 ? 'No templates available' : 'Select a template'}
|
|
448
|
+
value={storedDraft}
|
|
449
|
+
disabled={disabled || templates.length === 0}
|
|
450
|
+
options={templateOptions}
|
|
451
|
+
onValueChange={changeStored}
|
|
452
|
+
/>
|
|
453
|
+
</Field>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{unresolvedId !== undefined ? (
|
|
457
|
+
<p role="alert" style={{ margin: 0, color: 'var(--tai-color-err-text)' }}>
|
|
458
|
+
{resolveError ?? `The stored template "${unresolvedId}" is not available.`}
|
|
459
|
+
</p>
|
|
460
|
+
) : null}
|
|
461
|
+
</>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ONE coherent group per field: the label + description form the group header, and
|
|
466
|
+
// the source region and the render-parameters disclosure sit under it — so the
|
|
467
|
+
// controls unambiguously belong to THIS field, read label → description → source →
|
|
468
|
+
// parameters like every sibling field. The header names the `role="group"`
|
|
469
|
+
// container; the active editor is named by the field's own name through its
|
|
470
|
+
// visually-hidden `<label for>` (see `hideLabel`). The container-name redundancy
|
|
471
|
+
// `Field` accepts by design (a redundant container is audible, an unnamed group is
|
|
472
|
+
// silent).
|
|
473
|
+
//
|
|
474
|
+
// A read-only stored value shows its render parameters as read-only text — the values
|
|
475
|
+
// in normal weight, NOT the muted disabled inputs a reader could mistake for empty
|
|
476
|
+
// placeholders — matching the read-only treatment of the id above; every editable
|
|
477
|
+
// rendering keeps the full add/remove editor.
|
|
478
|
+
return (
|
|
479
|
+
<Field label={label} description={description} group>
|
|
480
|
+
{source}
|
|
481
|
+
{showsReadOnlyStored ? (
|
|
482
|
+
kwargsRows.length > 0 ? (
|
|
483
|
+
<div
|
|
484
|
+
role="group"
|
|
485
|
+
aria-label={`${label} render parameters`}
|
|
486
|
+
style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-1)' }}
|
|
487
|
+
>
|
|
488
|
+
<span style={{ fontSize: 'var(--tai-text-sm)', color: 'var(--tai-color-text-muted)' }}>
|
|
489
|
+
Render parameters
|
|
490
|
+
</span>
|
|
491
|
+
{kwargsRows.map((row) => (
|
|
492
|
+
<div
|
|
493
|
+
key={row.key}
|
|
494
|
+
style={{ fontFamily: 'var(--tai-font-mono)', wordBreak: 'break-all' }}
|
|
495
|
+
>
|
|
496
|
+
<code>{row.key}</code>
|
|
497
|
+
<span style={{ color: 'var(--tai-color-text-muted)' }}>: </span>
|
|
498
|
+
<code>{row.value}</code>
|
|
499
|
+
</div>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
) : null
|
|
503
|
+
) : (
|
|
504
|
+
<KwargsEditor label={label} rows={kwargsRows} disabled={disabled} onChange={changeKwargs} />
|
|
505
|
+
)}
|
|
506
|
+
</Field>
|
|
507
|
+
);
|
|
508
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -190,6 +190,12 @@ export { Checkbox } from './components/checkbox';
|
|
|
190
190
|
export type { CheckboxProps } from './components/checkbox';
|
|
191
191
|
export { RadioGroup } from './components/radio-group';
|
|
192
192
|
export type { RadioGroupProps, RadioOption } from './components/radio-group';
|
|
193
|
+
export { TemplatedTextField, templatedTextSummary } from './components/templated-text-field';
|
|
194
|
+
export type {
|
|
195
|
+
TemplatedTextFieldProps,
|
|
196
|
+
TemplatedTextInlineProps,
|
|
197
|
+
TemplatedTextTemplateOption,
|
|
198
|
+
} from './components/templated-text-field';
|
|
193
199
|
export { Badge } from './components/badge';
|
|
194
200
|
export type { BadgeProps } from './components/badge';
|
|
195
201
|
export { Tabs } from './components/tabs';
|
|
@@ -418,6 +424,7 @@ export {
|
|
|
418
424
|
AdapterMapping,
|
|
419
425
|
SubjectScopeFields,
|
|
420
426
|
BindingJqField,
|
|
427
|
+
BindingTemplatedJqField,
|
|
421
428
|
appendTjq,
|
|
422
429
|
resolveTemplateJq,
|
|
423
430
|
findByRef,
|
|
@@ -435,8 +442,11 @@ export {
|
|
|
435
442
|
generateTemplateCall,
|
|
436
443
|
parseTemplateCall,
|
|
437
444
|
encodeTemplateSegment,
|
|
445
|
+
templatedTextCatalog,
|
|
438
446
|
} from './state-binding';
|
|
439
447
|
export type {
|
|
448
|
+
StoragePresenceQueryLike,
|
|
449
|
+
TemplateNamesQueryLike,
|
|
440
450
|
StateBindingEditorProps,
|
|
441
451
|
StateBindingSectionProps,
|
|
442
452
|
StateAttachRowProps,
|
|
@@ -446,6 +456,7 @@ export type {
|
|
|
446
456
|
AdapterMappingProps,
|
|
447
457
|
SubjectScopeFieldsProps,
|
|
448
458
|
BindingJqFieldProps,
|
|
459
|
+
BindingTemplatedJqFieldProps,
|
|
449
460
|
TemplateJqSuggestion,
|
|
450
461
|
ResolvedTemplateJq,
|
|
451
462
|
MappingRow,
|
|
@@ -460,6 +471,8 @@ export type {
|
|
|
460
471
|
BindingTemplateJqOption,
|
|
461
472
|
BindingSourceSchemas,
|
|
462
473
|
SchemaFieldPath,
|
|
474
|
+
TemplatedText,
|
|
475
|
+
TemplatedTextCatalog,
|
|
463
476
|
} from './state-binding';
|
|
464
477
|
|
|
465
478
|
// -- MCP context widgets (elicitation / progress / completions / output) -----
|
|
@@ -38,6 +38,13 @@ export interface BindingJqFieldProps {
|
|
|
38
38
|
readonly placeholder?: string;
|
|
39
39
|
/** Template jq offered as `tjq_<name>({…})` inserts under the field. */
|
|
40
40
|
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
41
|
+
/**
|
|
42
|
+
* Render the field's own visible label visually hidden while keeping it as the
|
|
43
|
+
* control's accessible name — for a field mounted under a group header that
|
|
44
|
+
* already shows the label (a `TemplatedTextField` slot). Off by default, so a
|
|
45
|
+
* bare use (a mapping row's jq) keeps its visible label.
|
|
46
|
+
*/
|
|
47
|
+
readonly hideLabel?: boolean;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/** Append a `tjq_<name>({…})` template call to the current expression (space-separated when non-empty). */
|
|
@@ -55,6 +62,7 @@ export function BindingJqField({
|
|
|
55
62
|
error,
|
|
56
63
|
placeholder,
|
|
57
64
|
suggestions = [],
|
|
65
|
+
hideLabel = false,
|
|
58
66
|
}: BindingJqFieldProps): ReactNode {
|
|
59
67
|
const Door = useContext(ExpressionFieldContext);
|
|
60
68
|
// A stable hook for e2e: the injected jq editor owns its own DOM, so the wrapper
|
|
@@ -67,6 +75,9 @@ export function BindingJqField({
|
|
|
67
75
|
return (
|
|
68
76
|
<div
|
|
69
77
|
data-testid={testId}
|
|
78
|
+
// The label-hide is scoped to a direct-child jq editor: the injected door is a
|
|
79
|
+
// direct child here, so a group header above it becomes the single visible label.
|
|
80
|
+
className={hideLabel ? 'tai-templated-inline--grouped' : undefined}
|
|
70
81
|
style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-1)' }}
|
|
71
82
|
>
|
|
72
83
|
{Door !== undefined ? (
|
|
@@ -81,6 +92,7 @@ export function BindingJqField({
|
|
|
81
92
|
) : (
|
|
82
93
|
<Field
|
|
83
94
|
label={label}
|
|
95
|
+
hideLabel={hideLabel}
|
|
84
96
|
description={typeof description === 'string' ? description : undefined}
|
|
85
97
|
error={error}
|
|
86
98
|
>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One state-binding jq slot authored as a {@link TemplatedText}: a
|
|
3
|
+
* `TemplatedTextField` whose INLINE editor is the jq field (the host's visual jq
|
|
4
|
+
* door, else a textarea) carrying the attached templates' template-jq inserts, and
|
|
5
|
+
* whose STORED mode picks a stored template that renders to the jq at run time.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { TemplatedTextField } from '../components/templated-text-field';
|
|
10
|
+
import { BindingJqField, type TemplateJqSuggestion } from './BindingJqField';
|
|
11
|
+
import type { TemplatedText, TemplatedTextCatalog } from './types';
|
|
12
|
+
|
|
13
|
+
export interface BindingTemplatedJqFieldProps {
|
|
14
|
+
readonly label: string;
|
|
15
|
+
readonly value: TemplatedText | null;
|
|
16
|
+
readonly onChange: (value: TemplatedText | null) => void;
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
readonly error?: string;
|
|
19
|
+
/** The subject/adapter slots require a source; the rest are optional (`null` clears). */
|
|
20
|
+
readonly required?: boolean;
|
|
21
|
+
readonly placeholder?: string;
|
|
22
|
+
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
23
|
+
readonly templates?: TemplatedTextCatalog;
|
|
24
|
+
readonly disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function BindingTemplatedJqField({
|
|
28
|
+
label,
|
|
29
|
+
value,
|
|
30
|
+
onChange,
|
|
31
|
+
description,
|
|
32
|
+
error,
|
|
33
|
+
required,
|
|
34
|
+
placeholder,
|
|
35
|
+
suggestions,
|
|
36
|
+
templates,
|
|
37
|
+
disabled,
|
|
38
|
+
}: BindingTemplatedJqFieldProps): ReactNode {
|
|
39
|
+
return (
|
|
40
|
+
<TemplatedTextField
|
|
41
|
+
label={label}
|
|
42
|
+
value={value}
|
|
43
|
+
onChange={onChange}
|
|
44
|
+
description={description}
|
|
45
|
+
error={error}
|
|
46
|
+
required={required}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
templates={templates?.templates}
|
|
49
|
+
templatesLoading={templates?.loading}
|
|
50
|
+
templatesError={templates?.error}
|
|
51
|
+
onTemplatesRetry={templates?.onRetry}
|
|
52
|
+
storageAbsent={templates?.storageAbsent}
|
|
53
|
+
storagePresenceLoading={templates?.storagePresenceLoading}
|
|
54
|
+
renderInline={({
|
|
55
|
+
label: inlineLabel,
|
|
56
|
+
value: inlineValue,
|
|
57
|
+
onChange: onInlineChange,
|
|
58
|
+
error: inlineError,
|
|
59
|
+
hideLabel: inlineHideLabel,
|
|
60
|
+
}) => (
|
|
61
|
+
<BindingJqField
|
|
62
|
+
label={inlineLabel}
|
|
63
|
+
value={inlineValue}
|
|
64
|
+
onChange={onInlineChange}
|
|
65
|
+
error={inlineError}
|
|
66
|
+
placeholder={placeholder}
|
|
67
|
+
suggestions={suggestions}
|
|
68
|
+
hideLabel={inlineHideLabel}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|