@tai42/studio-sdk 30.0.0 → 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/combo-draft-editor.d.ts +31 -0
- package/dist/components/combo-draft-editor.d.ts.map +1 -0
- package/dist/components/combo-draft-editor.js +16 -0
- package/dist/components/combo-draft-editor.js.map +1 -0
- package/dist/components/committed-combo-list.d.ts +17 -0
- package/dist/components/committed-combo-list.d.ts.map +1 -0
- package/dist/components/{extension-combo-parts.js → committed-combo-list.js} +2 -22
- package/dist/components/committed-combo-list.js.map +1 -0
- 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-combo-builder.d.ts +4 -4
- package/dist/components/extension-combo-builder.js +3 -2
- package/dist/components/extension-combo-builder.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/selected-tool-badges.d.ts +13 -0
- package/dist/components/selected-tool-badges.d.ts.map +1 -0
- package/dist/components/selected-tool-badges.js +14 -0
- package/dist/components/selected-tool-badges.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/tag-filter-field.d.ts +11 -0
- package/dist/components/tag-filter-field.d.ts.map +1 -0
- package/dist/components/tag-filter-field.js +15 -0
- package/dist/components/tag-filter-field.js.map +1 -0
- 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 +24 -19
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +55 -35
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/tool-select.d.ts +26 -0
- package/dist/components/tool-select.d.ts.map +1 -0
- package/dist/components/tool-select.js +82 -0
- package/dist/components/tool-select.js.map +1 -0
- package/dist/extension-combos.d.ts +6 -1
- package/dist/extension-combos.d.ts.map +1 -1
- package/dist/extension-combos.js +8 -0
- package/dist/extension-combos.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/combo-draft-editor.tsx +96 -0
- package/src/components/{extension-combo-parts.tsx → committed-combo-list.tsx} +4 -105
- 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-combo-builder.tsx +10 -10
- 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/selected-tool-badges.tsx +41 -0
- package/src/components/subject-section.tsx +3 -2
- package/src/components/table.tsx +12 -2
- package/src/components/tag-filter-field.tsx +37 -0
- package/src/components/tool-meta-access.tsx +101 -0
- package/src/components/tool-name-entry.tsx +77 -0
- package/src/components/tool-picker.tsx +87 -52
- package/src/components/tool-select.tsx +136 -0
- package/src/extension-combos.ts +15 -1
- 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
- package/dist/components/extension-combo-parts.d.ts +0 -46
- package/dist/components/extension-combo-parts.d.ts.map +0 -1
- package/dist/components/extension-combo-parts.js.map +0 -1
- package/dist/components/tool-picker-parts.d.ts +0 -43
- package/dist/components/tool-picker-parts.d.ts.map +0 -1
- package/dist/components/tool-picker-parts.js +0 -89
- package/dist/components/tool-picker-parts.js.map +0 -1
- package/src/components/tool-picker-parts.tsx +0 -195
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* opted into.
|
|
13
13
|
*
|
|
14
14
|
* `skeletonValueForSchema` is the sibling entry for programmatic reseeds (a
|
|
15
|
-
* union variant switch): same walk, but required
|
|
16
|
-
*
|
|
15
|
+
* union variant switch): same walk, but every required field with a known empty
|
|
16
|
+
* value is minted, so the committed seed has no missing required field. A minted
|
|
17
|
+
* empty value may still fail its own value check (see the sibling's docstring).
|
|
17
18
|
*/
|
|
18
19
|
import { classifySchema } from './classify';
|
|
19
20
|
import type { FieldModel } from './field-model';
|
|
@@ -37,22 +38,56 @@ function explicitDefault(
|
|
|
37
38
|
* whole-tool-schema call).
|
|
38
39
|
*/
|
|
39
40
|
export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
|
|
40
|
-
return seedValue(schema, root, false);
|
|
41
|
+
return finiteSeed(seedValue(schema, root, false, new Set()));
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* A SELF-CONSISTENT skeleton for a schema — the seed a variant switch (or any
|
|
45
46
|
* other programmatic reseed) commits as the field's new value. Where
|
|
46
47
|
* {@link defaultValueForSchema} deliberately leaves a required scalar absent so
|
|
47
|
-
* a fresh form flags it, a committed skeleton
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* the `[]` / `{}` /
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
* a fresh form flags it, a committed skeleton carries no missing required field,
|
|
49
|
+
* so here EVERY required field with a known empty value is minted: required
|
|
50
|
+
* strings seed `''`, numbers `0`, booleans `false`, a templated text an empty
|
|
51
|
+
* inline source `{ content: '' }` — consistently with the `[]` / `{}` /
|
|
52
|
+
* nested-object seeds the default path already mints. A minted empty value is
|
|
53
|
+
* not promised to pass its own value check: an empty templated text fails it
|
|
54
|
+
* ("needs inline content or a stored template"), as does `''` under a
|
|
55
|
+
* `minLength` or a `pattern`, and validation reports that error like any
|
|
56
|
+
* other. Kinds with no empty value to name (enum,
|
|
57
|
+
* union, free-form JSON) stay absent either way: inventing a choice would be a
|
|
58
|
+
* lie the user never made.
|
|
53
59
|
*/
|
|
54
60
|
export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
|
|
55
|
-
return seedValue(schema, root, true);
|
|
61
|
+
return finiteSeed(seedValue(schema, root, true, new Set()));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The walk's answer when an object's required structure leads back into an object
|
|
66
|
+
* shape it is already inside with no `null` accepted anywhere on the way: that
|
|
67
|
+
* nesting never ends, so the object has no finite value.
|
|
68
|
+
*/
|
|
69
|
+
const NO_FINITE_VALUE: unique symbol = Symbol('no finite value');
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Thrown by {@link defaultValueForSchema} and {@link skeletonValueForSchema} when the
|
|
73
|
+
* schema admits no finite value: a chain of required properties refers back to its
|
|
74
|
+
* own definition with no `null` accepted on the way. A caller that seeds on a user
|
|
75
|
+
* action catches exactly this type to show the failure on its field; any other error
|
|
76
|
+
* is not a seeding outcome and propagates.
|
|
77
|
+
*/
|
|
78
|
+
export class NoFiniteValueError extends Error {
|
|
79
|
+
constructor() {
|
|
80
|
+
super(
|
|
81
|
+
'SchemaForm: the schema admits no finite value: its required properties refer back to their own definition with no null accepted on the way',
|
|
82
|
+
);
|
|
83
|
+
this.name = 'NoFiniteValueError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The entry points' result: the seeded value, or {@link NoFiniteValueError} when none is finite. */
|
|
88
|
+
function finiteSeed(seed: unknown): unknown {
|
|
89
|
+
if (seed === NO_FINITE_VALUE) throw new NoFiniteValueError();
|
|
90
|
+
return seed;
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
/**
|
|
@@ -60,16 +95,44 @@ export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = sc
|
|
|
60
95
|
* carries an explicit default. A required property recurses; an optional one is
|
|
61
96
|
* materialized only when it declares a `default`, so its own required children
|
|
62
97
|
* never raise errors on a field the user did not opt into.
|
|
98
|
+
*
|
|
99
|
+
* `expanding` holds the object shapes being seeded on the way down. Reaching one of
|
|
100
|
+
* them again (a self-referencing definition) answers {@link NO_FINITE_VALUE}, and
|
|
101
|
+
* so does an object with any required property that answers it. A `nullable` object
|
|
102
|
+
* turns that answer into `null`, the value that ends the nesting there. So the
|
|
103
|
+
* answer climbs to the nearest nullable object above the point where the cycle
|
|
104
|
+
* closes, and reaches the entry point only when no object on the way accepts
|
|
105
|
+
* `null`: then no finite value exists.
|
|
63
106
|
*/
|
|
64
107
|
function seedObject(
|
|
108
|
+
model: ModelOf<'object'>,
|
|
109
|
+
nullable: boolean,
|
|
110
|
+
root: JsonSchema,
|
|
111
|
+
mintScalars: boolean,
|
|
112
|
+
expanding: ReadonlySet<object>,
|
|
113
|
+
): unknown {
|
|
114
|
+
const seeded = seedProperties(model, root, mintScalars, expanding);
|
|
115
|
+
return seeded === NO_FINITE_VALUE && nullable ? null : seeded;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* An object's own seed (see {@link seedObject}): its properties' seeds, or
|
|
120
|
+
* {@link NO_FINITE_VALUE} when its shape is already being seeded above or a required
|
|
121
|
+
* property has no finite value.
|
|
122
|
+
*/
|
|
123
|
+
function seedProperties(
|
|
65
124
|
model: ModelOf<'object'>,
|
|
66
125
|
root: JsonSchema,
|
|
67
126
|
mintScalars: boolean,
|
|
68
|
-
|
|
127
|
+
expanding: ReadonlySet<object>,
|
|
128
|
+
): unknown {
|
|
129
|
+
if (expanding.has(model.shape)) return NO_FINITE_VALUE;
|
|
130
|
+
const inside = new Set(expanding).add(model.shape);
|
|
69
131
|
const entries: [string, unknown][] = [];
|
|
70
132
|
for (const [name, propSchema] of model.properties) {
|
|
71
133
|
if (model.required.has(name)) {
|
|
72
|
-
const seeded = seedValue(propSchema, root, mintScalars);
|
|
134
|
+
const seeded = seedValue(propSchema, root, mintScalars, inside);
|
|
135
|
+
if (seeded === NO_FINITE_VALUE) return NO_FINITE_VALUE;
|
|
73
136
|
if (seeded !== undefined) entries.push([name, seeded]);
|
|
74
137
|
continue;
|
|
75
138
|
}
|
|
@@ -101,9 +164,16 @@ function seedScalar(kind: 'string' | 'number' | 'boolean', mintScalars: boolean)
|
|
|
101
164
|
/**
|
|
102
165
|
* The shared seeding walk. `mintScalars` distinguishes the two entry points
|
|
103
166
|
* above: `false` leaves value-less required scalars absent; `true` mints their
|
|
104
|
-
* type's empty value.
|
|
167
|
+
* type's empty value. `expanding` holds the object shapes being seeded on the way
|
|
168
|
+
* down; a nullable object whose own seed has no finite value seeds `null` (see
|
|
169
|
+
* {@link seedObject}).
|
|
105
170
|
*/
|
|
106
|
-
function seedValue(
|
|
171
|
+
function seedValue(
|
|
172
|
+
schema: JsonSchema,
|
|
173
|
+
root: JsonSchema,
|
|
174
|
+
mintScalars: boolean,
|
|
175
|
+
expanding: ReadonlySet<object>,
|
|
176
|
+
): unknown {
|
|
107
177
|
const classified = classifySchema(schema, root);
|
|
108
178
|
const explicit = explicitDefault(schema, classified.schema);
|
|
109
179
|
if (explicit.present) return explicit.value;
|
|
@@ -113,7 +183,7 @@ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean):
|
|
|
113
183
|
case 'const':
|
|
114
184
|
return model.value;
|
|
115
185
|
case 'object':
|
|
116
|
-
return seedObject(model, root, mintScalars);
|
|
186
|
+
return seedObject(model, classified.nullable, root, mintScalars, expanding);
|
|
117
187
|
case 'array':
|
|
118
188
|
case 'multiselect':
|
|
119
189
|
return [];
|
|
@@ -123,6 +193,10 @@ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean):
|
|
|
123
193
|
case 'number':
|
|
124
194
|
case 'boolean':
|
|
125
195
|
return seedScalar(model.kind, mintScalars);
|
|
196
|
+
case 'templated-text':
|
|
197
|
+
// Inline is the templated text's own starting mode, so its empty value is an
|
|
198
|
+
// empty inline source; the default path leaves it absent like a scalar.
|
|
199
|
+
return mintScalars ? { content: '' } : undefined;
|
|
126
200
|
case 'enum':
|
|
127
201
|
case 'union':
|
|
128
202
|
case 'json':
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The injected expression door as a schema-form field: an `x-tai42-expression`
|
|
3
|
+
* annotation mapped onto the door's input-shape descriptor, and the door mounted
|
|
4
|
+
* inside a Suspense boundary that paints a resting shell while a lazily-injected
|
|
5
|
+
* door resolves. A jq-annotated string draws it as the whole field; a jq-annotated
|
|
6
|
+
* templated-text field draws it as its inline editor.
|
|
7
|
+
*
|
|
8
|
+
* NO JQ EDGE: the door arrives from the host (see `ExpressionFieldContext`);
|
|
9
|
+
* this module names no jq type.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
import { Suspense, useMemo } from 'react';
|
|
13
|
+
|
|
14
|
+
import { Field } from '../components/field';
|
|
15
|
+
import { Textarea } from '../components/inputs';
|
|
16
|
+
import type { ExpressionFieldComponent, ExpressionInputShape } from './context';
|
|
17
|
+
import type { ExpressionAnnotation } from './field-model';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An expression-annotated field, rendered through the injected door: a
|
|
21
|
+
* multiline resting control with the door's own visual-editor button. The door
|
|
22
|
+
* brings its own label/description/error chrome (a11y-linked slots), so it is NOT
|
|
23
|
+
* wrapped in the form's `Field` component — the annotation's descriptor fields map
|
|
24
|
+
* onto the input-shape descriptor, and the form's per-field error feeds `error`.
|
|
25
|
+
*
|
|
26
|
+
* The door is rendered as an ELEMENT, never called as a function, so its hooks and
|
|
27
|
+
* state (editor open, worker handle) belong to it and survive re-renders of this
|
|
28
|
+
* field.
|
|
29
|
+
*
|
|
30
|
+
* It mounts inside a `Suspense` boundary because a host is free to inject a `lazy`
|
|
31
|
+
* door to keep the jq subgraph in a split chunk of ITS OWN bundle. The fallback
|
|
32
|
+
* paints the resting shell — label/description/error chrome around the current
|
|
33
|
+
* value in a read-only multiline control — so the field holds its layout and keeps
|
|
34
|
+
* the value visible while that chunk resolves, and the swap does not flash. A door
|
|
35
|
+
* injected eagerly never suspends and renders straight through.
|
|
36
|
+
*
|
|
37
|
+
* The form passes only the props the field owns; a jq door's `serverValidate`,
|
|
38
|
+
* `onEditorOpenChange`, and `compact` stay unwired, because no author-time
|
|
39
|
+
* validation endpoint applies to a schema-declared field, the hosts that render
|
|
40
|
+
* `SchemaForm` register no global shortcuts to mute, and every sibling field shows
|
|
41
|
+
* a visible label (the compact variant is for dense host rows).
|
|
42
|
+
*/
|
|
43
|
+
export function ExpressionField({
|
|
44
|
+
component: Door,
|
|
45
|
+
heading,
|
|
46
|
+
description,
|
|
47
|
+
error,
|
|
48
|
+
expression,
|
|
49
|
+
argName,
|
|
50
|
+
value,
|
|
51
|
+
onChange,
|
|
52
|
+
}: {
|
|
53
|
+
component: ExpressionFieldComponent;
|
|
54
|
+
heading: string;
|
|
55
|
+
description: string | undefined;
|
|
56
|
+
error: string | undefined;
|
|
57
|
+
expression: ExpressionAnnotation;
|
|
58
|
+
argName: string;
|
|
59
|
+
value: string;
|
|
60
|
+
onChange: (value: string) => void;
|
|
61
|
+
}): ReactNode {
|
|
62
|
+
// Memoised: the descriptor's identity feeds the door's own memoisation, so a
|
|
63
|
+
// fresh object each keystroke would defeat it.
|
|
64
|
+
const shape = useMemo(() => expressionShape(expression, argName), [expression, argName]);
|
|
65
|
+
return (
|
|
66
|
+
<Suspense
|
|
67
|
+
fallback={
|
|
68
|
+
<ExpressionFieldSkeleton
|
|
69
|
+
heading={heading}
|
|
70
|
+
description={description}
|
|
71
|
+
error={error}
|
|
72
|
+
value={value}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
<Door
|
|
77
|
+
label={heading}
|
|
78
|
+
description={description}
|
|
79
|
+
error={error}
|
|
80
|
+
shape={shape}
|
|
81
|
+
multiline
|
|
82
|
+
value={value}
|
|
83
|
+
onChange={onChange}
|
|
84
|
+
/>
|
|
85
|
+
</Suspense>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The resting shell shown while a lazily-injected door resolves. It mirrors the
|
|
91
|
+
* live door's footprint — the same label/description/error chrome wrapping a
|
|
92
|
+
* multiline control seeded with the current value — but the control is
|
|
93
|
+
* non-interactive (read-only, `aria-busy`) because there is nothing to edit yet. Matching the
|
|
94
|
+
* footprint is what keeps the Suspense swap from shifting layout or flashing.
|
|
95
|
+
*/
|
|
96
|
+
function ExpressionFieldSkeleton({
|
|
97
|
+
heading,
|
|
98
|
+
description,
|
|
99
|
+
error,
|
|
100
|
+
value,
|
|
101
|
+
}: {
|
|
102
|
+
heading: string;
|
|
103
|
+
description: string | undefined;
|
|
104
|
+
error: string | undefined;
|
|
105
|
+
value: string;
|
|
106
|
+
}): ReactNode {
|
|
107
|
+
return (
|
|
108
|
+
<Field label={heading} description={description} error={error}>
|
|
109
|
+
<Textarea value={value} readOnly aria-busy="true" />
|
|
110
|
+
</Field>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Map a classified expression annotation onto the door's input-shape descriptor.
|
|
116
|
+
* A bare annotation (`language` only) maps to NO descriptor — an undeclared shape
|
|
117
|
+
* is the honest rendering when the server said nothing about `.`. When any
|
|
118
|
+
* descriptor field is present, the required descriptor members the annotation
|
|
119
|
+
* omits fall back to neutral values rather than invented copy.
|
|
120
|
+
*/
|
|
121
|
+
function expressionShape(
|
|
122
|
+
expression: ExpressionAnnotation,
|
|
123
|
+
argName: string,
|
|
124
|
+
): ExpressionInputShape | undefined {
|
|
125
|
+
const declared =
|
|
126
|
+
[
|
|
127
|
+
expression.label,
|
|
128
|
+
expression.blurb,
|
|
129
|
+
expression.keys,
|
|
130
|
+
expression.variables,
|
|
131
|
+
expression.returns,
|
|
132
|
+
expression.caveats,
|
|
133
|
+
].some((field) => field !== undefined) || expression.hasSample;
|
|
134
|
+
if (!declared) return undefined;
|
|
135
|
+
return {
|
|
136
|
+
// Opaque, host-namespaced, stable per field (memoisation/telemetry only).
|
|
137
|
+
id: argName === '' ? 'tai42.schema-form' : `tai42.schema-form.${argName}`,
|
|
138
|
+
label: expression.label ?? 'input',
|
|
139
|
+
blurb: expression.blurb ?? '',
|
|
140
|
+
keys: expression.keys ?? [],
|
|
141
|
+
returns: expression.returns ?? '',
|
|
142
|
+
...(expression.caveats === undefined ? {} : { caveats: expression.caveats }),
|
|
143
|
+
...(expression.hasSample ? { sample: expression.sample } : {}),
|
|
144
|
+
...(expression.variables === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { variables: expression.variables.map(expressionVariable) }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Map one classified annotation variable onto the door's variable descriptor. */
|
|
151
|
+
function expressionVariable(
|
|
152
|
+
variable: NonNullable<ExpressionAnnotation['variables']>[number],
|
|
153
|
+
): NonNullable<ExpressionInputShape['variables']>[number] {
|
|
154
|
+
return {
|
|
155
|
+
name: variable.name,
|
|
156
|
+
blurb: variable.blurb,
|
|
157
|
+
// The annotation glosses no sub-keys of a variable's value, so the door gets
|
|
158
|
+
// the variable's shape as untyped (empty keys).
|
|
159
|
+
keys: [],
|
|
160
|
+
...(variable.hasSample ? { sample: variable.sample } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -70,10 +70,11 @@ export interface ExpressionAnnotationVariable {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* The `x-tai42-expression` annotation on a string
|
|
74
|
-
* that the field's value is authored in a pipeline expression
|
|
75
|
-
* today) and optionally describes what `.` is for that expression.
|
|
76
|
-
* uses it to open the visual-editor door instead of a plain text box
|
|
73
|
+
* The `x-tai42-expression` annotation on a string or templated-text property: the
|
|
74
|
+
* server declares that the field's value is authored in a pipeline expression
|
|
75
|
+
* language (only jq today) and optionally describes what `.` is for that expression.
|
|
76
|
+
* The renderer uses it to open the visual-editor door instead of a plain text box
|
|
77
|
+
* (on a templated-text field, as its inline editor). The shape
|
|
77
78
|
* mirrors jq-studio's input-shape descriptor WITHOUT importing it — classification
|
|
78
79
|
* stays a pure schema concern; the renderer maps this onto a jq field declaration.
|
|
79
80
|
*/
|
|
@@ -165,6 +166,13 @@ export type FieldModel =
|
|
|
165
166
|
readonly kind: 'object';
|
|
166
167
|
readonly properties: readonly (readonly [string, JsonSchema])[];
|
|
167
168
|
readonly required: ReadonlySet<string>;
|
|
169
|
+
/**
|
|
170
|
+
* The schema's own `properties` map, by identity. Every classification of one
|
|
171
|
+
* definition — through a `$ref`, a null-union or single-`allOf` wrapper, or an
|
|
172
|
+
* annotated property site — carries this same map, so a walker recognises a
|
|
173
|
+
* definition nested inside itself (a self-referencing schema) by it.
|
|
174
|
+
*/
|
|
175
|
+
readonly shape: Readonly<Record<string, JsonSchema>>;
|
|
168
176
|
}
|
|
169
177
|
| {
|
|
170
178
|
// A string-keyed map typed by `additionalProperties` (no fixed
|
|
@@ -174,10 +182,25 @@ export type FieldModel =
|
|
|
174
182
|
readonly kind: 'record';
|
|
175
183
|
readonly values: JsonSchema;
|
|
176
184
|
}
|
|
185
|
+
| {
|
|
186
|
+
// A property typed as the platform's templated text (its schema carries
|
|
187
|
+
// `x-tai42-templated-text`): inline `content` or a stored template `id`, plus
|
|
188
|
+
// the render `kwargs`. The value is that object, authored as one field.
|
|
189
|
+
readonly kind: 'templated-text';
|
|
190
|
+
/** Present when the property's `x-tai42-expression` annotation declares its
|
|
191
|
+
* inline content an expression: the inline editor is the expression door. */
|
|
192
|
+
readonly expression: ExpressionAnnotation | undefined;
|
|
193
|
+
}
|
|
177
194
|
| {
|
|
178
195
|
readonly kind: 'union';
|
|
179
196
|
readonly variants: readonly UnionVariant[];
|
|
180
197
|
readonly discriminator: string | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* The schema's own `anyOf`/`oneOf` member list, by identity: every
|
|
200
|
+
* classification of one union carries this same list, so a field can tell
|
|
201
|
+
* whether it still draws the union it drew before.
|
|
202
|
+
*/
|
|
203
|
+
readonly members: readonly JsonSchema[];
|
|
181
204
|
}
|
|
182
205
|
| {
|
|
183
206
|
// A shape with no structured editor to build — a property-less object, a
|
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* inline; every other kind delegates to a dedicated field component). Object,
|
|
5
5
|
* array, and union renderers recurse back through here for their children.
|
|
6
6
|
*/
|
|
7
|
-
import { memo, type ReactNode } from 'react';
|
|
7
|
+
import { memo, type ReactNode, useContext } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Checkbox } from '../components/checkbox';
|
|
10
10
|
import { Field } from '../components/field';
|
|
11
11
|
import { NumberInput, TextInput } from '../components/inputs';
|
|
12
|
-
import { ownValue } from '../guards';
|
|
12
|
+
import { isRecord, ownValue } from '../guards';
|
|
13
13
|
import { ArrayField } from './array-field';
|
|
14
14
|
import { classifySchema } from './classify';
|
|
15
|
+
import { CollapsedObjectField } from './collapsed-object-field';
|
|
16
|
+
import { ExpandedShapesContext } from './context';
|
|
15
17
|
import { EnumField } from './enum-field';
|
|
16
18
|
import { FieldGroup } from './field-group';
|
|
17
19
|
import type { FieldModel } from './field-model';
|
|
@@ -21,6 +23,7 @@ import { ObjectFields } from './object-fields';
|
|
|
21
23
|
import { RecordField } from './record-field';
|
|
22
24
|
import { scalarLabel } from './resolve';
|
|
23
25
|
import { StringField } from './string-field';
|
|
26
|
+
import { TemplatedField } from './templated-field';
|
|
24
27
|
import type { FieldWords, JsonSchema, SchemaFormErrors } from './types';
|
|
25
28
|
import { UnionField } from './union-field';
|
|
26
29
|
|
|
@@ -67,12 +70,15 @@ function labelText(explicit: string | undefined, label: string | undefined): str
|
|
|
67
70
|
*/
|
|
68
71
|
function FieldNodeImpl(props: FieldNodeProps): ReactNode {
|
|
69
72
|
const { root, value, onChange, path, required, idPrefix, errors } = props;
|
|
73
|
+
const expanded = useContext(ExpandedShapesContext);
|
|
70
74
|
const classified = classifySchema(props.schema, root);
|
|
71
75
|
const base = labelText(classified.title, props.label);
|
|
72
76
|
const heading = props.markRequired === true && required ? `${base} *` : base;
|
|
73
77
|
const { model, description } = classified;
|
|
74
78
|
return renderField({
|
|
75
79
|
model,
|
|
80
|
+
definition: classified.schema,
|
|
81
|
+
expanded,
|
|
76
82
|
nullable: classified.nullable,
|
|
77
83
|
heading,
|
|
78
84
|
description,
|
|
@@ -93,6 +99,10 @@ function FieldNodeImpl(props: FieldNodeProps): ReactNode {
|
|
|
93
99
|
/** The props the per-kind renderer needs, computed once by {@link FieldNodeImpl}. */
|
|
94
100
|
interface RenderFieldArgs {
|
|
95
101
|
readonly model: FieldModel;
|
|
102
|
+
/** The ref-resolved schema the model was classified from. */
|
|
103
|
+
readonly definition: JsonSchema;
|
|
104
|
+
/** The object shapes the fields above this one are drawing. */
|
|
105
|
+
readonly expanded: ReadonlySet<object>;
|
|
96
106
|
readonly nullable: boolean;
|
|
97
107
|
readonly heading: string;
|
|
98
108
|
readonly description: string | undefined;
|
|
@@ -187,6 +197,19 @@ function renderField(args: RenderFieldArgs): ReactNode {
|
|
|
187
197
|
onChange={onChange}
|
|
188
198
|
/>
|
|
189
199
|
);
|
|
200
|
+
case 'templated-text':
|
|
201
|
+
return (
|
|
202
|
+
<TemplatedField
|
|
203
|
+
heading={heading}
|
|
204
|
+
description={description}
|
|
205
|
+
error={error}
|
|
206
|
+
expression={model.expression}
|
|
207
|
+
argName={path}
|
|
208
|
+
required={required}
|
|
209
|
+
value={value}
|
|
210
|
+
onChange={onChange}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
190
213
|
case 'multiselect':
|
|
191
214
|
return (
|
|
192
215
|
<MultiSelectField
|
|
@@ -228,10 +251,26 @@ function renderCompositeField(
|
|
|
228
251
|
/>
|
|
229
252
|
);
|
|
230
253
|
case 'object':
|
|
254
|
+
// A definition nested inside itself draws its fields only for a level the
|
|
255
|
+
// value holds; an absent level is one Add button, so the nesting stops at the data.
|
|
256
|
+
if (args.expanded.has(model.shape) && !isRecord(value)) {
|
|
257
|
+
return (
|
|
258
|
+
<CollapsedObjectField
|
|
259
|
+
heading={heading}
|
|
260
|
+
description={description}
|
|
261
|
+
error={error}
|
|
262
|
+
definition={args.definition}
|
|
263
|
+
shape={model.shape}
|
|
264
|
+
root={root}
|
|
265
|
+
onChange={onChange}
|
|
266
|
+
/>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
231
269
|
return (
|
|
232
270
|
<FieldGroup heading={heading} description={description} error={error} atRoot={path === ''}>
|
|
233
271
|
<ObjectFields
|
|
234
272
|
properties={model.properties}
|
|
273
|
+
shape={model.shape}
|
|
235
274
|
requiredKeys={model.required}
|
|
236
275
|
root={root}
|
|
237
276
|
value={value}
|
package/src/schema-form/index.ts
CHANGED
|
@@ -35,12 +35,13 @@ export type {
|
|
|
35
35
|
ExpressionFieldProps,
|
|
36
36
|
ExpressionInputKey,
|
|
37
37
|
ExpressionInputShape,
|
|
38
|
+
UseTemplatedTextCatalog,
|
|
38
39
|
} from './context';
|
|
39
40
|
export { RecordEntryRendererContext } from './context';
|
|
40
|
-
export { ExpressionFieldContext } from './context';
|
|
41
|
+
export { ExpressionFieldContext, TemplatedTextCatalogContext } from './context';
|
|
41
42
|
export type { DateRangePair } from './date-range-field';
|
|
42
43
|
export { dateRangePairs, fieldDrawingOrder } from './date-range-field';
|
|
43
|
-
export { defaultValueForSchema } from './default-value';
|
|
44
|
+
export { defaultValueForSchema, NoFiniteValueError } from './default-value';
|
|
44
45
|
export { resolveRef } from './resolve';
|
|
45
46
|
export type { CompletionProvider, SchemaFormProps } from './SchemaForm';
|
|
46
47
|
export { SchemaForm } from './SchemaForm';
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Renders the property fields of an object schema, one {@link FieldNode} per
|
|
3
|
-
* property
|
|
3
|
+
* property, and records the object's `shape` as expanded for every field below it
|
|
4
|
+
* (see {@link ExpandedShapesContext}). Each child's edits are folded back into an immutable copy of the
|
|
4
5
|
* object value; an optional `skip` omits a property (used for a union's
|
|
5
6
|
* discriminator tag, which the variant picker owns).
|
|
6
7
|
*/
|
|
7
|
-
import type
|
|
8
|
+
import { type ReactNode, useContext, useMemo } from 'react';
|
|
8
9
|
|
|
9
10
|
import { ownValue } from '../guards';
|
|
11
|
+
import { ExpandedShapesContext } from './context';
|
|
10
12
|
import { DateRangeField, dateRangePairs, fieldDrawingOrder } from './date-range-field';
|
|
11
13
|
import { FieldNode } from './field-node';
|
|
12
14
|
import { useKeyedCallbacks } from './keyed-callbacks';
|
|
@@ -19,6 +21,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
19
21
|
|
|
20
22
|
export function ObjectFields({
|
|
21
23
|
properties,
|
|
24
|
+
shape,
|
|
22
25
|
requiredKeys,
|
|
23
26
|
root,
|
|
24
27
|
value,
|
|
@@ -31,6 +34,8 @@ export function ObjectFields({
|
|
|
31
34
|
fieldWording,
|
|
32
35
|
}: {
|
|
33
36
|
properties: readonly (readonly [string, JsonSchema])[];
|
|
37
|
+
/** The object model's `shape`, recorded as expanded for the fields below. */
|
|
38
|
+
shape: object;
|
|
34
39
|
requiredKeys: ReadonlySet<string>;
|
|
35
40
|
root: JsonSchema;
|
|
36
41
|
value: unknown;
|
|
@@ -50,6 +55,8 @@ export function ObjectFields({
|
|
|
50
55
|
fieldWording?: Readonly<Record<string, FieldWords>>;
|
|
51
56
|
}): ReactNode {
|
|
52
57
|
const obj = isPlainObject(value) ? value : {};
|
|
58
|
+
const above = useContext(ExpandedShapesContext);
|
|
59
|
+
const expanded = useMemo(() => new Set(above).add(shape), [above, shape]);
|
|
53
60
|
const changeFor = useKeyedCallbacks<string, unknown>(
|
|
54
61
|
(name, next) => {
|
|
55
62
|
onChange(setKey(obj, name, next));
|
|
@@ -96,7 +103,7 @@ export function ObjectFields({
|
|
|
96
103
|
const timeOf = (name: string | undefined, propSchema: JsonSchema | undefined): ReactNode =>
|
|
97
104
|
name === undefined || propSchema === undefined ? null : fieldOf(name, propSchema);
|
|
98
105
|
return (
|
|
99
|
-
|
|
106
|
+
<ExpandedShapesContext.Provider value={expanded}>
|
|
100
107
|
{drawn.map((name) => {
|
|
101
108
|
const propSchema = schemaByName.get(name);
|
|
102
109
|
if (propSchema === undefined) return null;
|
|
@@ -127,7 +134,7 @@ export function ObjectFields({
|
|
|
127
134
|
}
|
|
128
135
|
return fieldOf(name, propSchema);
|
|
129
136
|
})}
|
|
130
|
-
|
|
137
|
+
</ExpandedShapesContext.Provider>
|
|
131
138
|
);
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -23,6 +23,7 @@ import { defaultValueForSchema } from './default-value';
|
|
|
23
23
|
import { FieldGroup } from './field-group';
|
|
24
24
|
import { FieldNode } from './field-node';
|
|
25
25
|
import { useKeyedCallbacks } from './keyed-callbacks';
|
|
26
|
+
import { useSeedingAction } from './seeding-action';
|
|
26
27
|
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
27
28
|
|
|
28
29
|
interface Row {
|
|
@@ -126,8 +127,10 @@ export function RecordField({
|
|
|
126
127
|
setValue,
|
|
127
128
|
rows.map((row) => row.id),
|
|
128
129
|
);
|
|
130
|
+
const add = useSeedingAction(values);
|
|
129
131
|
const addRow = (): void => {
|
|
130
|
-
|
|
132
|
+
const entry = add.seed(() => newEntryValue(values, root));
|
|
133
|
+
if (entry.ok) commit([...rows, { id: nextId.current++, key: '', value: entry.value }]);
|
|
131
134
|
};
|
|
132
135
|
const removeRow = (id: number): void => {
|
|
133
136
|
commit(rows.filter((row) => row.id !== id));
|
|
@@ -137,7 +140,12 @@ export function RecordField({
|
|
|
137
140
|
const summary = keyFaultSummary(blankRowIds.size, duplicateRowIds.size);
|
|
138
141
|
|
|
139
142
|
return (
|
|
140
|
-
<FieldGroup
|
|
143
|
+
<FieldGroup
|
|
144
|
+
heading={heading}
|
|
145
|
+
description={description}
|
|
146
|
+
error={add.failure ?? error}
|
|
147
|
+
atRoot={false}
|
|
148
|
+
>
|
|
141
149
|
{rows.length === 0 ? <span className="tai-field-hint">No entries</span> : null}
|
|
142
150
|
{summary !== undefined ? (
|
|
143
151
|
<span role="alert" className="tai-field-error">
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state behind a control that seeds a fresh value on a user action: a list's
|
|
3
|
+
* "Add item", a map's "Add entry", a nested level's "Add", and the union variant
|
|
4
|
+
* picker's switch. Seeding throws {@link NoFiniteValueError} when the schema admits no
|
|
5
|
+
* finite value, and a throw inside an event handler never reaches an error boundary,
|
|
6
|
+
* so that failure is logged and kept as the field's error message. Only the seed runs
|
|
7
|
+
* inside the catch, and only that error type is caught: the caller commits the value
|
|
8
|
+
* (its `onChange`) outside it, so every other error propagates as thrown.
|
|
9
|
+
*
|
|
10
|
+
* A failure belongs to the `subject` it was raised for (the schema identity the
|
|
11
|
+
* caller seeds from): when the same field later draws a different subject, the
|
|
12
|
+
* failure is no longer reported. The next successful seed clears it.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
|
|
16
|
+
import { NoFiniteValueError } from './default-value';
|
|
17
|
+
|
|
18
|
+
/** A seed attempt's outcome: the seeded value, or a failure now shown on the field. */
|
|
19
|
+
export type SeedOutcome = { readonly ok: true; readonly value: unknown } | { readonly ok: false };
|
|
20
|
+
|
|
21
|
+
export interface SeedingAction {
|
|
22
|
+
/** The failure message for the current subject, or `undefined` when there is none. */
|
|
23
|
+
readonly failure: string | undefined;
|
|
24
|
+
/** Run `make`, the seeding step; a {@link NoFiniteValueError} becomes the failure. */
|
|
25
|
+
readonly seed: (make: () => unknown) => SeedOutcome;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useSeedingAction(subject: object): SeedingAction {
|
|
29
|
+
const [failed, setFailed] = useState<{ subject: object; message: string } | undefined>(undefined);
|
|
30
|
+
const seed = (make: () => unknown): SeedOutcome => {
|
|
31
|
+
let value: unknown;
|
|
32
|
+
try {
|
|
33
|
+
value = make();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!(error instanceof NoFiniteValueError)) throw error;
|
|
36
|
+
console.error('SchemaForm: seeding a new value failed', error);
|
|
37
|
+
setFailed({ subject, message: error.message });
|
|
38
|
+
return { ok: false };
|
|
39
|
+
}
|
|
40
|
+
setFailed(undefined);
|
|
41
|
+
return { ok: true, value };
|
|
42
|
+
};
|
|
43
|
+
return { failure: failed?.subject === subject ? failed.message : undefined, seed };
|
|
44
|
+
}
|