@tai42/studio-sdk 30.0.1 → 31.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/README.md +98 -10
- package/dist/components/agent-picker.d.ts +1 -1
- package/dist/components/agent-picker.d.ts.map +1 -1
- package/dist/components/agent-picker.js +1 -0
- package/dist/components/agent-picker.js.map +1 -1
- package/dist/components/channel-picker.d.ts +1 -1
- package/dist/components/channel-picker.d.ts.map +1 -1
- package/dist/components/channel-picker.js +1 -0
- package/dist/components/channel-picker.js.map +1 -1
- package/dist/components/components.css +53 -0
- package/dist/components/explorer-body.d.ts +1 -0
- package/dist/components/explorer-body.d.ts.map +1 -1
- package/dist/components/explorer-body.js +6 -4
- package/dist/components/explorer-body.js.map +1 -1
- package/dist/components/explorer-view.d.ts +16 -7
- package/dist/components/explorer-view.d.ts.map +1 -1
- package/dist/components/explorer-view.js +27 -13
- package/dist/components/explorer-view.js.map +1 -1
- package/dist/components/extension-config-editor.js.map +1 -1
- package/dist/components/field.d.ts +6 -0
- package/dist/components/field.d.ts.map +1 -1
- package/dist/components/field.js +15 -2
- package/dist/components/field.js.map +1 -1
- package/dist/components/folder-actions.d.ts +51 -0
- package/dist/components/folder-actions.d.ts.map +1 -0
- package/dist/components/folder-actions.js +173 -0
- package/dist/components/folder-actions.js.map +1 -0
- package/dist/components/folder-filing.d.ts +32 -0
- package/dist/components/folder-filing.d.ts.map +1 -0
- package/dist/components/folder-filing.js +51 -0
- package/dist/components/folder-filing.js.map +1 -0
- package/dist/components/folder-nav.d.ts +9 -4
- package/dist/components/folder-nav.d.ts.map +1 -1
- package/dist/components/folder-nav.js +18 -22
- package/dist/components/folder-nav.js.map +1 -1
- package/dist/components/folder-occupancy.d.ts +27 -0
- package/dist/components/folder-occupancy.d.ts.map +1 -0
- package/dist/components/folder-occupancy.js +54 -0
- package/dist/components/folder-occupancy.js.map +1 -0
- package/dist/components/named-list-picker.d.ts +5 -3
- package/dist/components/named-list-picker.d.ts.map +1 -1
- package/dist/components/named-list-picker.js +7 -7
- package/dist/components/named-list-picker.js.map +1 -1
- package/dist/components/primitives.d.ts +2 -1
- package/dist/components/primitives.d.ts.map +1 -1
- package/dist/components/primitives.js +2 -1
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/searchable-select-model.d.ts +40 -0
- package/dist/components/searchable-select-model.d.ts.map +1 -0
- package/dist/components/searchable-select-model.js +69 -0
- package/dist/components/searchable-select-model.js.map +1 -0
- package/dist/components/searchable-select.d.ts +38 -0
- package/dist/components/searchable-select.d.ts.map +1 -0
- package/dist/components/searchable-select.js +200 -0
- package/dist/components/searchable-select.js.map +1 -0
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +2 -2
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/table.d.ts +7 -1
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +2 -2
- package/dist/components/table.js.map +1 -1
- package/dist/components/tool-meta-access.d.ts +28 -0
- package/dist/components/tool-meta-access.d.ts.map +1 -0
- package/dist/components/tool-meta-access.js +65 -0
- package/dist/components/tool-meta-access.js.map +1 -0
- package/dist/components/tool-name-entry.d.ts +26 -0
- package/dist/components/tool-name-entry.d.ts.map +1 -0
- package/dist/components/tool-name-entry.js +43 -0
- package/dist/components/tool-name-entry.js.map +1 -0
- package/dist/components/tool-picker.d.ts +23 -18
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +51 -33
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/tool-select.d.ts +6 -5
- package/dist/components/tool-select.d.ts.map +1 -1
- package/dist/components/tool-select.js +31 -14
- package/dist/components/tool-select.js.map +1 -1
- package/dist/hooks/useApi.d.ts +1 -0
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useSystemKinds.d.ts +27 -12
- package/dist/hooks/useSystemKinds.d.ts.map +1 -1
- package/dist/hooks/useSystemKinds.js +56 -16
- package/dist/hooks/useSystemKinds.js.map +1 -1
- package/dist/index.d.ts +51 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -26
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/use-mount-install.d.ts +6 -6
- package/dist/plugin/version.d.ts +1 -1
- package/dist/plugin/version.js +1 -1
- package/dist/schema-editor/SchemaEditor.d.ts +7 -2
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
- package/dist/schema-editor/SchemaEditor.js +1 -1
- package/dist/schema-editor/SchemaEditor.js.map +1 -1
- package/dist/schema-editor/index.d.ts +2 -2
- package/dist/schema-editor/index.d.ts.map +1 -1
- package/dist/schema-form/array-field.d.ts.map +1 -1
- package/dist/schema-form/array-field.js +6 -2
- package/dist/schema-form/array-field.js.map +1 -1
- package/dist/schema-form/classify.d.ts +4 -1
- package/dist/schema-form/classify.d.ts.map +1 -1
- package/dist/schema-form/classify.js +36 -15
- package/dist/schema-form/classify.js.map +1 -1
- package/dist/schema-form/collapsed-object-field.d.ts +20 -0
- package/dist/schema-form/collapsed-object-field.d.ts.map +1 -0
- package/dist/schema-form/collapsed-object-field.js +14 -0
- package/dist/schema-form/collapsed-object-field.js.map +1 -0
- package/dist/schema-form/context.d.ts +27 -1
- package/dist/schema-form/context.d.ts.map +1 -1
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -1
- package/dist/schema-form/default-value.d.ts +21 -6
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +74 -15
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/expression-field.d.ts +48 -0
- package/dist/schema-form/expression-field.d.ts.map +1 -0
- package/dist/schema-form/expression-field.js +88 -0
- package/dist/schema-form/expression-field.js.map +1 -0
- package/dist/schema-form/field-model.d.ts +23 -4
- package/dist/schema-form/field-model.d.ts.map +1 -1
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +16 -3
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/index.d.ts +3 -3
- package/dist/schema-form/index.d.ts.map +1 -1
- package/dist/schema-form/index.js +2 -2
- package/dist/schema-form/index.js.map +1 -1
- package/dist/schema-form/object-fields.d.ts +6 -3
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +14 -3
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/record-field.d.ts.map +1 -1
- package/dist/schema-form/record-field.js +6 -2
- package/dist/schema-form/record-field.js.map +1 -1
- package/dist/schema-form/seeding-action.d.ts +15 -0
- package/dist/schema-form/seeding-action.d.ts.map +1 -0
- package/dist/schema-form/seeding-action.js +35 -0
- package/dist/schema-form/seeding-action.js.map +1 -0
- package/dist/schema-form/string-annotations.d.ts +22 -8
- package/dist/schema-form/string-annotations.d.ts.map +1 -1
- package/dist/schema-form/string-annotations.js +39 -14
- package/dist/schema-form/string-annotations.js.map +1 -1
- package/dist/schema-form/string-field.js +2 -84
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/schema-form/templated-field.d.ts +13 -0
- package/dist/schema-form/templated-field.d.ts.map +1 -0
- package/dist/schema-form/templated-field.js +43 -0
- package/dist/schema-form/templated-field.js.map +1 -0
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js +6 -2
- package/dist/schema-form/union-field.js.map +1 -1
- package/dist/schema-form/union.d.ts +4 -4
- package/dist/schema-form/union.d.ts.map +1 -1
- package/dist/schema-form/union.js +8 -1
- package/dist/schema-form/union.js.map +1 -1
- package/dist/schema-form/validate.d.ts.map +1 -1
- package/dist/schema-form/validate.js +28 -0
- package/dist/schema-form/validate.js.map +1 -1
- package/dist/search-match.d.ts +29 -0
- package/dist/search-match.d.ts.map +1 -0
- package/dist/search-match.js +38 -0
- package/dist/search-match.js.map +1 -0
- package/dist/tool-organization.d.ts +15 -0
- package/dist/tool-organization.d.ts.map +1 -0
- package/dist/tool-organization.js +6 -0
- package/dist/tool-organization.js.map +1 -0
- package/package.json +3 -2
- package/src/components/agent-picker.tsx +2 -1
- package/src/components/channel-picker.tsx +2 -1
- package/src/components/components.css +53 -0
- package/src/components/explorer-body.tsx +29 -5
- package/src/components/explorer-view.tsx +48 -17
- package/src/components/extension-config-editor.tsx +3 -3
- package/src/components/field.tsx +18 -2
- package/src/components/folder-actions.tsx +341 -0
- package/src/components/folder-filing.tsx +109 -0
- package/src/components/folder-nav.tsx +26 -10
- package/src/components/folder-occupancy.ts +83 -0
- package/src/components/named-list-picker.tsx +11 -7
- package/src/components/primitives.tsx +2 -1
- package/src/components/searchable-select-model.ts +105 -0
- package/src/components/searchable-select.tsx +432 -0
- package/src/components/subject-section.tsx +3 -2
- package/src/components/table.tsx +12 -2
- package/src/components/tool-meta-access.tsx +101 -0
- package/src/components/tool-name-entry.tsx +77 -0
- package/src/components/tool-picker.tsx +83 -50
- package/src/components/tool-select.tsx +49 -38
- package/src/hooks/useSystemKinds.tsx +77 -17
- package/src/index.ts +98 -94
- package/src/plugin/version.ts +1 -1
- package/src/schema-editor/SchemaEditor.tsx +9 -3
- package/src/schema-editor/index.ts +2 -2
- package/src/schema-form/array-field.tsx +10 -2
- package/src/schema-form/classify.ts +41 -15
- package/src/schema-form/collapsed-object-field.tsx +56 -0
- package/src/schema-form/context.ts +33 -1
- package/src/schema-form/default-value.ts +89 -15
- package/src/schema-form/expression-field.tsx +162 -0
- package/src/schema-form/field-model.ts +27 -4
- package/src/schema-form/field-node.tsx +41 -2
- package/src/schema-form/index.ts +3 -2
- package/src/schema-form/object-fields.tsx +11 -4
- package/src/schema-form/record-field.tsx +10 -2
- package/src/schema-form/seeding-action.ts +44 -0
- package/src/schema-form/string-annotations.ts +40 -14
- package/src/schema-form/string-field.tsx +2 -147
- package/src/schema-form/templated-field.tsx +124 -0
- package/src/schema-form/union-field.tsx +11 -2
- package/src/schema-form/union.ts +12 -5
- package/src/schema-form/validate.ts +28 -0
- package/src/search-match.ts +41 -0
- package/src/tool-organization.ts +34 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Detect the annotations that opt a
|
|
3
|
-
*
|
|
4
|
-
* `format: "data-url"`)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Detect the vendor and media annotations that opt a property out of its plain
|
|
3
|
+
* editor: the media-upload annotations (`contentEncoding`/`contentMediaType`/
|
|
4
|
+
* `format: "data-url"`) on a string, the `x-tai42-expression` expression-editor
|
|
5
|
+
* annotation on a string or templated-text property (or on a union of them, where
|
|
6
|
+
* it governs every member), and the `x-tai42-templated-text` mark the platform's
|
|
7
|
+
* templated-text type carries. The first two degrade to
|
|
8
|
+
* `undefined` on anything malformed, so an annotated field never renders differently
|
|
9
|
+
* from the plain field it falls back to.
|
|
7
10
|
*/
|
|
8
11
|
import { isRecord } from '../guards';
|
|
9
12
|
import type {
|
|
@@ -15,6 +18,29 @@ import type {
|
|
|
15
18
|
import type { JsonSchema } from './types';
|
|
16
19
|
|
|
17
20
|
const EXPRESSION_KEYWORD = 'x-tai42-expression';
|
|
21
|
+
const TEMPLATED_TEXT_KEYWORD = 'x-tai42-templated-text';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Is this (ref-resolved) schema the platform's templated-text type? The type stamps
|
|
25
|
+
* `x-tai42-templated-text` on its own schema, so every property of the type is
|
|
26
|
+
* recognised by the mark, never by a field name or a property layout.
|
|
27
|
+
*/
|
|
28
|
+
export function isTemplatedText(schema: JsonSchema): boolean {
|
|
29
|
+
return isRecord(schema[TEMPLATED_TEXT_KEYWORD]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A schema node as its property site declares it: the `x-tai42-expression`
|
|
34
|
+
* annotation sits on the property site (a union wrapper, or the node holding a
|
|
35
|
+
* `$ref`), and it governs the node the value is checked against (the union member
|
|
36
|
+
* the value takes, or the referenced definition), so a node that carries none of its
|
|
37
|
+
* own gets the site's. The node is returned unchanged when there is nothing to carry.
|
|
38
|
+
*/
|
|
39
|
+
export function withSiteExpression(member: JsonSchema, site: JsonSchema): JsonSchema {
|
|
40
|
+
const annotation: unknown = site[EXPRESSION_KEYWORD];
|
|
41
|
+
if (annotation === undefined || member[EXPRESSION_KEYWORD] !== undefined) return member;
|
|
42
|
+
return { ...member, [EXPRESSION_KEYWORD]: annotation };
|
|
43
|
+
}
|
|
18
44
|
|
|
19
45
|
/**
|
|
20
46
|
* Detect the media-upload annotations on a (ref-resolved) string schema. A field
|
|
@@ -99,20 +125,20 @@ function parseCaveatList(value: unknown): readonly string[] | undefined | null {
|
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
/**
|
|
102
|
-
* Detect the `x-tai42-expression` annotation on a (
|
|
103
|
-
* mirroring {@link mediaUpload}: a
|
|
104
|
-
*
|
|
105
|
-
* any wrongly-typed member — yields
|
|
106
|
-
* the
|
|
107
|
-
* annotation is a server-side authoring bug, and
|
|
108
|
-
*
|
|
109
|
-
* rendering.
|
|
128
|
+
* Detect the `x-tai42-expression` annotation on a property schema (a string, or the
|
|
129
|
+
* property site of a templated-text field), mirroring {@link mediaUpload}: a
|
|
130
|
+
* well-formed annotation opts the field into the jq expression editor; anything
|
|
131
|
+
* else — absent, non-object, unknown language, or any wrongly-typed member — yields
|
|
132
|
+
* `undefined`, so the field renders EXACTLY as its unannotated editor. Malformed
|
|
133
|
+
* payloads NEVER throw: a bad annotation is a server-side authoring bug, and
|
|
134
|
+
* degrading to the ordinary editor keeps the form usable while staying
|
|
135
|
+
* byte-identical to the unannotated rendering.
|
|
110
136
|
*/
|
|
111
137
|
export function expressionAnnotation(schema: JsonSchema): ExpressionAnnotation | undefined {
|
|
112
138
|
const raw: unknown = schema[EXPRESSION_KEYWORD];
|
|
113
139
|
if (!isRecord(raw)) return undefined;
|
|
114
140
|
// The language gate doubles as the forward-compatibility valve: a future
|
|
115
|
-
// language this client does not understand degrades to
|
|
141
|
+
// language this client does not understand degrades to the unannotated field.
|
|
116
142
|
if (raw.language !== 'jq') return undefined;
|
|
117
143
|
|
|
118
144
|
const label = optionalString(raw.label);
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
* annotated field with no injected door falls back to the plain string input.
|
|
20
20
|
*/
|
|
21
21
|
import type { ReactNode } from 'react';
|
|
22
|
-
import {
|
|
22
|
+
import { useCallback, useContext, useState } from 'react';
|
|
23
23
|
|
|
24
24
|
import { CompletionInput } from '../components/completion-input';
|
|
25
25
|
import { Field } from '../components/field';
|
|
26
26
|
import { AlertTriangleIcon } from '../components/icons';
|
|
27
27
|
import { Textarea, TextInput } from '../components/inputs';
|
|
28
28
|
import { errorMessage } from '../errors';
|
|
29
|
-
import type { ExpressionFieldComponent, ExpressionInputShape } from './context';
|
|
30
29
|
import { CompletionProviderContext, ExpressionFieldContext } from './context';
|
|
30
|
+
import { ExpressionField } from './expression-field';
|
|
31
31
|
import type { DateConstraints, ExpressionAnnotation, MediaUpload } from './field-model';
|
|
32
32
|
import { MediaField } from './media-field';
|
|
33
33
|
import type { CompletionProvider } from './SchemaForm';
|
|
@@ -240,151 +240,6 @@ function CompletionField({
|
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/**
|
|
244
|
-
* An expression-annotated string field, rendered through the injected door: a
|
|
245
|
-
* multiline resting control with the door's own visual-editor button. The door
|
|
246
|
-
* brings its own label/description/error chrome (a11y-linked slots), so it is NOT
|
|
247
|
-
* wrapped in the form's `Field` component — the annotation's descriptor fields map
|
|
248
|
-
* onto the input-shape descriptor, and the form's per-field error feeds `error`.
|
|
249
|
-
*
|
|
250
|
-
* The door is rendered as an ELEMENT, never called as a function, so its hooks and
|
|
251
|
-
* state (editor open, worker handle) belong to it and survive re-renders of this
|
|
252
|
-
* field.
|
|
253
|
-
*
|
|
254
|
-
* It mounts inside a `Suspense` boundary because a host is free to inject a `lazy`
|
|
255
|
-
* door to keep the jq subgraph in a split chunk of ITS OWN bundle. The fallback
|
|
256
|
-
* paints the resting shell — label/description/error chrome around the current
|
|
257
|
-
* value in a read-only multiline control — so the field holds its layout and keeps
|
|
258
|
-
* the value visible while that chunk resolves, and the swap does not flash. A door
|
|
259
|
-
* injected eagerly never suspends and renders straight through.
|
|
260
|
-
*
|
|
261
|
-
* The form passes only the props the field owns; a jq door's `serverValidate`,
|
|
262
|
-
* `onEditorOpenChange`, and `compact` stay unwired, because no author-time
|
|
263
|
-
* validation endpoint applies to a schema-declared field, the hosts that render
|
|
264
|
-
* `SchemaForm` register no global shortcuts to mute, and every sibling field shows
|
|
265
|
-
* a visible label (the compact variant is for dense host rows).
|
|
266
|
-
*/
|
|
267
|
-
function ExpressionField({
|
|
268
|
-
component: Door,
|
|
269
|
-
heading,
|
|
270
|
-
description,
|
|
271
|
-
error,
|
|
272
|
-
expression,
|
|
273
|
-
argName,
|
|
274
|
-
value,
|
|
275
|
-
onChange,
|
|
276
|
-
}: {
|
|
277
|
-
component: ExpressionFieldComponent;
|
|
278
|
-
heading: string;
|
|
279
|
-
description: string | undefined;
|
|
280
|
-
error: string | undefined;
|
|
281
|
-
expression: ExpressionAnnotation;
|
|
282
|
-
argName: string;
|
|
283
|
-
value: string;
|
|
284
|
-
onChange: (value: string) => void;
|
|
285
|
-
}): ReactNode {
|
|
286
|
-
// Memoised: the descriptor's identity feeds the door's own memoisation, so a
|
|
287
|
-
// fresh object each keystroke would defeat it.
|
|
288
|
-
const shape = useMemo(() => expressionShape(expression, argName), [expression, argName]);
|
|
289
|
-
return (
|
|
290
|
-
<Suspense
|
|
291
|
-
fallback={
|
|
292
|
-
<ExpressionFieldSkeleton
|
|
293
|
-
heading={heading}
|
|
294
|
-
description={description}
|
|
295
|
-
error={error}
|
|
296
|
-
value={value}
|
|
297
|
-
/>
|
|
298
|
-
}
|
|
299
|
-
>
|
|
300
|
-
<Door
|
|
301
|
-
label={heading}
|
|
302
|
-
description={description}
|
|
303
|
-
error={error}
|
|
304
|
-
shape={shape}
|
|
305
|
-
multiline
|
|
306
|
-
value={value}
|
|
307
|
-
onChange={onChange}
|
|
308
|
-
/>
|
|
309
|
-
</Suspense>
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* The resting shell shown while a lazily-injected door resolves. It mirrors the
|
|
315
|
-
* live door's footprint — the same label/description/error chrome wrapping a
|
|
316
|
-
* multiline control seeded with the current value — but the control is
|
|
317
|
-
* non-interactive (read-only, `aria-busy`) because there is nothing to edit yet. Matching the
|
|
318
|
-
* footprint is what keeps the Suspense swap from shifting layout or flashing.
|
|
319
|
-
*/
|
|
320
|
-
function ExpressionFieldSkeleton({
|
|
321
|
-
heading,
|
|
322
|
-
description,
|
|
323
|
-
error,
|
|
324
|
-
value,
|
|
325
|
-
}: {
|
|
326
|
-
heading: string;
|
|
327
|
-
description: string | undefined;
|
|
328
|
-
error: string | undefined;
|
|
329
|
-
value: string;
|
|
330
|
-
}): ReactNode {
|
|
331
|
-
return (
|
|
332
|
-
<Field label={heading} description={description} error={error}>
|
|
333
|
-
<Textarea value={value} readOnly aria-busy="true" />
|
|
334
|
-
</Field>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Map a classified expression annotation onto the door's input-shape descriptor.
|
|
340
|
-
* A bare annotation (`language` only) maps to NO descriptor — an undeclared shape
|
|
341
|
-
* is the honest rendering when the server said nothing about `.`. When any
|
|
342
|
-
* descriptor field is present, the required descriptor members the annotation
|
|
343
|
-
* omits fall back to neutral values rather than invented copy.
|
|
344
|
-
*/
|
|
345
|
-
function expressionShape(
|
|
346
|
-
expression: ExpressionAnnotation,
|
|
347
|
-
argName: string,
|
|
348
|
-
): ExpressionInputShape | undefined {
|
|
349
|
-
const declared =
|
|
350
|
-
[
|
|
351
|
-
expression.label,
|
|
352
|
-
expression.blurb,
|
|
353
|
-
expression.keys,
|
|
354
|
-
expression.variables,
|
|
355
|
-
expression.returns,
|
|
356
|
-
expression.caveats,
|
|
357
|
-
].some((field) => field !== undefined) || expression.hasSample;
|
|
358
|
-
if (!declared) return undefined;
|
|
359
|
-
return {
|
|
360
|
-
// Opaque, host-namespaced, stable per field (memoisation/telemetry only).
|
|
361
|
-
id: argName === '' ? 'tai42.schema-form' : `tai42.schema-form.${argName}`,
|
|
362
|
-
label: expression.label ?? 'input',
|
|
363
|
-
blurb: expression.blurb ?? '',
|
|
364
|
-
keys: expression.keys ?? [],
|
|
365
|
-
returns: expression.returns ?? '',
|
|
366
|
-
...(expression.caveats === undefined ? {} : { caveats: expression.caveats }),
|
|
367
|
-
...(expression.hasSample ? { sample: expression.sample } : {}),
|
|
368
|
-
...(expression.variables === undefined
|
|
369
|
-
? {}
|
|
370
|
-
: { variables: expression.variables.map(expressionVariable) }),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** Map one classified annotation variable onto the door's variable descriptor. */
|
|
375
|
-
function expressionVariable(
|
|
376
|
-
variable: NonNullable<ExpressionAnnotation['variables']>[number],
|
|
377
|
-
): NonNullable<ExpressionInputShape['variables']>[number] {
|
|
378
|
-
return {
|
|
379
|
-
name: variable.name,
|
|
380
|
-
blurb: variable.blurb,
|
|
381
|
-
// The annotation glosses no sub-keys of a variable's value, so the door gets
|
|
382
|
-
// the variable's shape as untyped (empty keys).
|
|
383
|
-
keys: [],
|
|
384
|
-
...(variable.hasSample ? { sample: variable.sample } : {}),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
243
|
// The native control per JSON-Schema `format`. `date` and `time` post the exact
|
|
389
244
|
// wire shapes their formats expect (`YYYY-MM-DD`; `HH:mm` or `HH:mm:ss`), so they
|
|
390
245
|
// render natively. `date-time` is deliberately absent: the browser's
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The templated-text field renderer: a property typed as the platform's templated
|
|
3
|
+
* text is authored through the shared {@link TemplatedTextField} — inline content or
|
|
4
|
+
* a stored template id, plus the render kwargs — exactly as a hand-built templated
|
|
5
|
+
* field is. When the property carries an `x-tai42-expression` annotation and the host
|
|
6
|
+
* injected an expression door, the inline editor IS that door (the resting control
|
|
7
|
+
* plus its visual-editor button); otherwise it is the field's plain textarea.
|
|
8
|
+
*
|
|
9
|
+
* The stored-template catalog comes from the host's {@link TemplatedTextCatalogContext}
|
|
10
|
+
* hook, called only here, so only a form that draws a templated-text field reads it.
|
|
11
|
+
*/
|
|
12
|
+
import type { TemplatedText } from '@tai42/api-client';
|
|
13
|
+
import { type ReactNode, useContext, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
TemplatedTextField,
|
|
17
|
+
type TemplatedTextInlineProps,
|
|
18
|
+
} from '../components/templated-text-field';
|
|
19
|
+
import { isRecord } from '../guards';
|
|
20
|
+
import { ExpressionFieldContext, TemplatedTextCatalogContext } from './context';
|
|
21
|
+
import { ExpressionField } from './expression-field';
|
|
22
|
+
import type { ExpressionAnnotation } from './field-model';
|
|
23
|
+
|
|
24
|
+
/** The form value read as a templated text: its string source(s) and its kwargs. */
|
|
25
|
+
function templatedTextOf(value: unknown): TemplatedText | null {
|
|
26
|
+
if (!isRecord(value)) return null;
|
|
27
|
+
const { content, id, kwargs } = value;
|
|
28
|
+
return {
|
|
29
|
+
...(typeof content === 'string' ? { content } : {}),
|
|
30
|
+
...(typeof id === 'string' ? { id } : {}),
|
|
31
|
+
...(isRecord(kwargs) ? { kwargs } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which mount of the field is current. The field seeds its editing state from the
|
|
37
|
+
* value once, so a value the caller REPLACES (not one this field emitted) mounts a
|
|
38
|
+
* fresh field seeded from it.
|
|
39
|
+
*/
|
|
40
|
+
interface Mount {
|
|
41
|
+
readonly value: unknown;
|
|
42
|
+
readonly emitted: unknown;
|
|
43
|
+
readonly generation: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function TemplatedField({
|
|
47
|
+
heading,
|
|
48
|
+
description,
|
|
49
|
+
error,
|
|
50
|
+
expression,
|
|
51
|
+
argName,
|
|
52
|
+
required,
|
|
53
|
+
value,
|
|
54
|
+
onChange,
|
|
55
|
+
}: {
|
|
56
|
+
heading: string;
|
|
57
|
+
description: string | undefined;
|
|
58
|
+
error: string | undefined;
|
|
59
|
+
expression: ExpressionAnnotation | undefined;
|
|
60
|
+
argName: string;
|
|
61
|
+
required: boolean;
|
|
62
|
+
value: unknown;
|
|
63
|
+
onChange: (value: unknown) => void;
|
|
64
|
+
}): ReactNode {
|
|
65
|
+
const useCatalog = useContext(TemplatedTextCatalogContext);
|
|
66
|
+
const catalog = useCatalog();
|
|
67
|
+
const door = useContext(ExpressionFieldContext);
|
|
68
|
+
const [mount, setMount] = useState<Mount>({ value, emitted: value, generation: 0 });
|
|
69
|
+
if (mount.value !== value) {
|
|
70
|
+
setMount({
|
|
71
|
+
value,
|
|
72
|
+
emitted: mount.emitted,
|
|
73
|
+
generation: value === mount.emitted ? mount.generation : mount.generation + 1,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// An emptied optional field drops its key; a required one keeps its empty source
|
|
78
|
+
// (the shared field emits it), so validation names the missing source and a union
|
|
79
|
+
// member stays the chosen one.
|
|
80
|
+
const emit = (next: TemplatedText | null): void => {
|
|
81
|
+
const formValue = next ?? undefined;
|
|
82
|
+
setMount((current) => ({ ...current, emitted: formValue }));
|
|
83
|
+
onChange(formValue);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const renderInline =
|
|
87
|
+
expression !== undefined && door !== undefined
|
|
88
|
+
? (inline: TemplatedTextInlineProps): ReactNode => (
|
|
89
|
+
// The field's group header is the single visible label; the door keeps
|
|
90
|
+
// its own label element as the control's accessible name.
|
|
91
|
+
<div className={inline.hideLabel ? 'tai-templated-inline--grouped' : undefined}>
|
|
92
|
+
<ExpressionField
|
|
93
|
+
component={door}
|
|
94
|
+
heading={inline.label}
|
|
95
|
+
description={undefined}
|
|
96
|
+
error={inline.error}
|
|
97
|
+
expression={expression}
|
|
98
|
+
argName={argName}
|
|
99
|
+
value={inline.value}
|
|
100
|
+
onChange={inline.onChange}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<TemplatedTextField
|
|
108
|
+
key={mount.generation}
|
|
109
|
+
label={heading}
|
|
110
|
+
description={description}
|
|
111
|
+
error={error}
|
|
112
|
+
value={templatedTextOf(value)}
|
|
113
|
+
onChange={emit}
|
|
114
|
+
required={required}
|
|
115
|
+
templates={catalog?.templates}
|
|
116
|
+
templatesLoading={catalog?.loading}
|
|
117
|
+
templatesError={catalog?.error}
|
|
118
|
+
onTemplatesRetry={catalog?.onRetry}
|
|
119
|
+
storageAbsent={catalog?.storageAbsent}
|
|
120
|
+
storagePresenceLoading={catalog?.storagePresenceLoading}
|
|
121
|
+
renderInline={renderInline}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -18,6 +18,7 @@ import type { FieldModel, UnionVariant } from './field-model';
|
|
|
18
18
|
import { FieldNode } from './field-node';
|
|
19
19
|
import { ObjectFields } from './object-fields';
|
|
20
20
|
import { resolveRef } from './resolve';
|
|
21
|
+
import { useSeedingAction } from './seeding-action';
|
|
21
22
|
import { groupClass } from './styles';
|
|
22
23
|
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
23
24
|
import { activeVariantIndex } from './union';
|
|
@@ -132,11 +133,15 @@ export function UnionField({
|
|
|
132
133
|
value: String(position),
|
|
133
134
|
label: variant.label,
|
|
134
135
|
}));
|
|
136
|
+
const switching = useSeedingAction(model.members);
|
|
135
137
|
const selectVariant = (key: string): void => {
|
|
136
138
|
const position = Number(key);
|
|
137
139
|
const variant = model.variants[position];
|
|
138
140
|
if (variant === undefined) return;
|
|
139
|
-
|
|
141
|
+
const switched = switching.seed(() =>
|
|
142
|
+
switchVariantValue(value, variant, model.discriminator, root),
|
|
143
|
+
);
|
|
144
|
+
if (switched.ok) onChange(switched.value);
|
|
140
145
|
};
|
|
141
146
|
|
|
142
147
|
const active = index === -1 ? undefined : model.variants[index];
|
|
@@ -147,7 +152,10 @@ export function UnionField({
|
|
|
147
152
|
// and describes — a synthetic heading there is noise, so it is suppressed
|
|
148
153
|
// exactly as a root object's is; nested unions keep theirs.
|
|
149
154
|
<FieldGroup heading={heading} description={description} error={error} atRoot={path === ''}>
|
|
150
|
-
<Field
|
|
155
|
+
<Field
|
|
156
|
+
label={pickerLabel(model.discriminator, model.variants, root)}
|
|
157
|
+
error={switching.failure}
|
|
158
|
+
>
|
|
151
159
|
<Select
|
|
152
160
|
options={options}
|
|
153
161
|
value={index === -1 ? '' : String(index)}
|
|
@@ -159,6 +167,7 @@ export function UnionField({
|
|
|
159
167
|
<div className={groupClass}>
|
|
160
168
|
<ObjectFields
|
|
161
169
|
properties={activeClassified.model.properties}
|
|
170
|
+
shape={activeClassified.model.shape}
|
|
162
171
|
requiredKeys={activeClassified.model.required}
|
|
163
172
|
root={root}
|
|
164
173
|
value={value}
|
package/src/schema-form/union.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which variant of a union a given value currently inhabits. Discriminated
|
|
3
|
-
* unions read the tag property directly; plain unions score each
|
|
4
|
-
* by how many of its required properties the value carries (a
|
|
5
|
-
* when Pydantic emitted no discriminator). Returns `-1` when no
|
|
6
|
-
* active (an unset/`null` value, or nothing matches).
|
|
3
|
+
* unions read the tag property directly; plain unions score each variant that can
|
|
4
|
+
* hold an object by how many of its required properties the value carries (a
|
|
5
|
+
* best-effort match when Pydantic emitted no discriminator). Returns `-1` when no
|
|
6
|
+
* variant is active (an unset/`null` value, or nothing matches).
|
|
7
7
|
*/
|
|
8
8
|
import type { UnionVariant } from './field-model';
|
|
9
|
-
import { resolveRef } from './resolve';
|
|
9
|
+
import { resolveRef, typeList } from './resolve';
|
|
10
10
|
import type { JsonSchema } from './types';
|
|
11
11
|
|
|
12
12
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
13
13
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Whether a resolved schema can hold an object: it is typed `object`, or untyped. */
|
|
17
|
+
function holdsObject(schema: JsonSchema): boolean {
|
|
18
|
+
const types = typeList(schema);
|
|
19
|
+
return types.length === 0 || types.includes('object');
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
/** Whether a resolved scalar schema accepts the runtime type of `value`. */
|
|
17
23
|
function scalarSchemaAccepts(schema: JsonSchema, value: unknown): boolean {
|
|
18
24
|
if (schema.const !== undefined) return schema.const === value;
|
|
@@ -64,6 +70,7 @@ export function activeVariantIndex(
|
|
|
64
70
|
let bestScore = -1;
|
|
65
71
|
variants.forEach((variant, index) => {
|
|
66
72
|
const resolved = resolveRef(variant.schema, root);
|
|
73
|
+
if (!holdsObject(resolved)) return;
|
|
67
74
|
const required = resolved.required ?? [];
|
|
68
75
|
let score = 0;
|
|
69
76
|
for (const key of required) {
|
|
@@ -508,6 +508,31 @@ function validateRecord(
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/**
|
|
512
|
+
* A templated-text value: an object carrying exactly one non-empty source — inline
|
|
513
|
+
* `content` or a stored template `id` — and, when present, a `kwargs` object.
|
|
514
|
+
*/
|
|
515
|
+
function validateTemplatedText(value: unknown, path: string, ctx: ValidateCtx): void {
|
|
516
|
+
if (!isRecord(value)) {
|
|
517
|
+
shapeError(ctx, path, 'must be an object');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const content = ownValue(value, 'content');
|
|
521
|
+
const id = ownValue(value, 'id');
|
|
522
|
+
const hasContent = typeof content === 'string';
|
|
523
|
+
const hasId = typeof id === 'string';
|
|
524
|
+
if (hasContent === hasId) {
|
|
525
|
+
shapeError(ctx, path, 'must set exactly one of inline content or a stored template id');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if ((hasContent ? content : id) === '') {
|
|
529
|
+
shapeError(ctx, path, 'needs inline content or a stored template');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const kwargs = ownValue(value, 'kwargs');
|
|
533
|
+
if (kwargs !== undefined && !isRecord(kwargs)) shapeError(ctx, path, 'kwargs must be an object');
|
|
534
|
+
}
|
|
535
|
+
|
|
511
536
|
function validateUnion(
|
|
512
537
|
model: ModelOf<'union'>,
|
|
513
538
|
value: unknown,
|
|
@@ -569,6 +594,9 @@ function dispatchKind(model: FieldModel, value: unknown, path: string, ctx: Vali
|
|
|
569
594
|
case 'record':
|
|
570
595
|
validateRecord(model, value, path, ctx);
|
|
571
596
|
return;
|
|
597
|
+
case 'templated-text':
|
|
598
|
+
validateTemplatedText(value, path, ctx);
|
|
599
|
+
return;
|
|
572
600
|
case 'union':
|
|
573
601
|
validateUnion(model, value, path, ctx);
|
|
574
602
|
return;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one text-search rule every Studio list applies: a case-insensitive substring
|
|
3
|
+
* match of the trimmed query against the texts the reader sees. A blank query matches
|
|
4
|
+
* everything.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The query in the form a match compares: trimmed and lower-cased. A list that tests
|
|
9
|
+
* many texts against one query computes this once and hands it to {@link textHasNeedle}.
|
|
10
|
+
*/
|
|
11
|
+
export function searchNeedle(query: string): string {
|
|
12
|
+
return query.trim().toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** True when `needle` (from {@link searchNeedle}) is blank or `loweredText` contains it. */
|
|
16
|
+
export function textHasNeedle(loweredText: string, needle: string): boolean {
|
|
17
|
+
return needle === '' || loweredText.includes(needle);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* True when `query` holds nothing but blanks, so it matches every text: a list that
|
|
22
|
+
* draws itself differently while no search is active asks this.
|
|
23
|
+
*/
|
|
24
|
+
export function isBlankSearch(query: string): boolean {
|
|
25
|
+
return searchNeedle(query) === '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True when any one of `texts` contains `query`, ignoring case and the query's
|
|
30
|
+
* surrounding blanks; a blank query matches every row. Each text is tested on its own,
|
|
31
|
+
* so a row searched by several fields (a name and a display name) never matches across
|
|
32
|
+
* the seam between two of them.
|
|
33
|
+
*
|
|
34
|
+
* @param query - what the reader typed.
|
|
35
|
+
* @param texts - the texts the row is searched by (for a row of the connector's tool
|
|
36
|
+
* list, its `toolDisplayLabel`, which carries both the display name and the raw name).
|
|
37
|
+
*/
|
|
38
|
+
export function matchesSearch(query: string, ...texts: readonly string[]): boolean {
|
|
39
|
+
const needle = searchNeedle(query);
|
|
40
|
+
return needle === '' || texts.some((text) => textHasNeedle(text.toLowerCase(), needle));
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool-organization folder tree the package publishes: the folder model and its
|
|
3
|
+
* navigation, which folders a screen draws, the folder writes, filing one tool, and
|
|
4
|
+
* who may make each overlay write. Every screen that shows the tree composes these.
|
|
5
|
+
*/
|
|
6
|
+
export type {
|
|
7
|
+
CreateFolder,
|
|
8
|
+
FolderActionsMenuProps,
|
|
9
|
+
FoldersChanged,
|
|
10
|
+
} from './components/folder-actions';
|
|
11
|
+
export { FolderActionsMenu, useCreateFolder } from './components/folder-actions';
|
|
12
|
+
export type { MoveToFolderDialogProps } from './components/folder-filing';
|
|
13
|
+
export { MoveToFolderDialog } from './components/folder-filing';
|
|
14
|
+
export type {
|
|
15
|
+
Folder,
|
|
16
|
+
FolderBreadcrumbProps,
|
|
17
|
+
FolderPickerProps,
|
|
18
|
+
FolderRowProps,
|
|
19
|
+
} from './components/folder-nav';
|
|
20
|
+
export {
|
|
21
|
+
childFolders,
|
|
22
|
+
FolderBreadcrumb,
|
|
23
|
+
folderPathTo,
|
|
24
|
+
FolderPicker,
|
|
25
|
+
FolderRow,
|
|
26
|
+
toFolders,
|
|
27
|
+
} from './components/folder-nav';
|
|
28
|
+
export { foldersForView } from './components/folder-occupancy';
|
|
29
|
+
export type { ToolMetaWriteAccess } from './components/tool-meta-access';
|
|
30
|
+
export {
|
|
31
|
+
ToolMetaWriteError,
|
|
32
|
+
useReportToolMetaRefusal,
|
|
33
|
+
useToolMetaWriteAccess,
|
|
34
|
+
} from './components/tool-meta-access';
|