@tai42/studio-sdk 16.0.0 → 16.0.1
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/explorer-body.d.ts +49 -0
- package/dist/components/explorer-body.d.ts.map +1 -0
- package/dist/components/explorer-body.js +67 -0
- package/dist/components/explorer-body.js.map +1 -0
- package/dist/components/explorer-page-size.d.ts +4 -0
- package/dist/components/explorer-page-size.d.ts.map +1 -0
- package/dist/components/explorer-page-size.js +36 -0
- package/dist/components/explorer-page-size.js.map +1 -0
- package/dist/components/explorer-tag-filter.d.ts +17 -0
- package/dist/components/explorer-tag-filter.d.ts.map +1 -0
- package/dist/components/explorer-tag-filter.js +29 -0
- package/dist/components/explorer-tag-filter.js.map +1 -0
- package/dist/components/explorer-tags.d.ts +22 -0
- package/dist/components/explorer-tags.d.ts.map +1 -0
- package/dist/components/explorer-tags.js +51 -0
- package/dist/components/explorer-tags.js.map +1 -0
- package/dist/components/explorer-view.d.ts +3 -17
- package/dist/components/explorer-view.d.ts.map +1 -1
- package/dist/components/explorer-view.js +53 -170
- package/dist/components/explorer-view.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +0 -5
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +4 -42
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +44 -0
- package/dist/components/extension-combo-parts.d.ts.map +1 -0
- package/dist/components/extension-combo-parts.js +57 -0
- package/dist/components/extension-combo-parts.js.map +1 -0
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +51 -37
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/icons/chrome-marks.d.ts +50 -0
- package/dist/components/icons/chrome-marks.d.ts.map +1 -0
- package/dist/components/icons/chrome-marks.js +51 -0
- package/dist/components/icons/chrome-marks.js.map +1 -0
- package/dist/components/icons/icon-frame.d.ts +29 -0
- package/dist/components/icons/icon-frame.d.ts.map +1 -0
- package/dist/components/icons/icon-frame.js +19 -0
- package/dist/components/icons/icon-frame.js.map +1 -0
- package/dist/components/icons/index.d.ts +40 -0
- package/dist/components/icons/index.d.ts.map +1 -0
- package/dist/components/icons/index.js +6 -0
- package/dist/components/icons/index.js.map +1 -0
- package/dist/components/icons/nav-map.d.ts +14 -0
- package/dist/components/icons/nav-map.d.ts.map +1 -0
- package/dist/components/icons/nav-map.js +28 -0
- package/dist/components/icons/nav-map.js.map +1 -0
- package/dist/components/icons/nav-marks.d.ts +44 -0
- package/dist/components/icons/nav-marks.d.ts.map +1 -0
- package/dist/components/icons/nav-marks.js +45 -0
- package/dist/components/icons/nav-marks.js.map +1 -0
- package/dist/components/icons/status-marks.d.ts +21 -0
- package/dist/components/icons/status-marks.d.ts.map +1 -0
- package/dist/components/icons/status-marks.js +22 -0
- package/dist/components/icons/status-marks.js.map +1 -0
- package/dist/components/icons/theme-marks.d.ts +9 -0
- package/dist/components/icons/theme-marks.d.ts.map +1 -0
- package/dist/components/icons/theme-marks.js +10 -0
- package/dist/components/icons/theme-marks.js.map +1 -0
- package/dist/components/json-diff.d.ts.map +1 -1
- package/dist/components/json-diff.js +1 -3
- package/dist/components/json-diff.js.map +1 -1
- package/dist/components/json-tree-context.d.ts +13 -0
- package/dist/components/json-tree-context.d.ts.map +1 -0
- package/dist/components/json-tree-context.js +15 -0
- package/dist/components/json-tree-context.js.map +1 -0
- package/dist/components/json-tree-model.d.ts +56 -0
- package/dist/components/json-tree-model.d.ts.map +1 -0
- package/dist/components/json-tree-model.js +125 -0
- package/dist/components/json-tree-model.js.map +1 -0
- package/dist/components/json-tree-nodes.d.ts +30 -0
- package/dist/components/json-tree-nodes.d.ts.map +1 -0
- package/dist/components/json-tree-nodes.js +118 -0
- package/dist/components/json-tree-nodes.js.map +1 -0
- package/dist/components/json-tree.d.ts.map +1 -1
- package/dist/components/json-tree.js +7 -295
- package/dist/components/json-tree.js.map +1 -1
- package/dist/components/json-value-format.d.ts +20 -0
- package/dist/components/json-value-format.d.ts.map +1 -0
- package/dist/components/json-value-format.js +63 -0
- package/dist/components/json-value-format.js.map +1 -0
- package/dist/components/markdown-blocks.d.ts +38 -0
- package/dist/components/markdown-blocks.d.ts.map +1 -0
- package/dist/components/markdown-blocks.js +168 -0
- package/dist/components/markdown-blocks.js.map +1 -0
- package/dist/components/markdown-inline.d.ts +24 -0
- package/dist/components/markdown-inline.d.ts.map +1 -0
- package/dist/components/markdown-inline.js +257 -0
- package/dist/components/markdown-inline.js.map +1 -0
- package/dist/components/markdown.d.ts +3 -52
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +4 -376
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/observer-registry.d.ts +38 -0
- package/dist/components/observer-registry.d.ts.map +1 -0
- package/dist/components/observer-registry.js +131 -0
- package/dist/components/observer-registry.js.map +1 -0
- package/dist/components/overflow-measure.d.ts +38 -0
- package/dist/components/overflow-measure.d.ts.map +1 -0
- package/dist/components/overflow-measure.js +54 -0
- package/dist/components/overflow-measure.js.map +1 -0
- package/dist/components/prose-regions.d.ts +81 -0
- package/dist/components/prose-regions.d.ts.map +1 -0
- package/dist/components/prose-regions.js +128 -0
- package/dist/components/prose-regions.js.map +1 -0
- package/dist/components/scroll-region.d.ts.map +1 -1
- package/dist/components/scroll-region.js +3 -298
- package/dist/components/scroll-region.js.map +1 -1
- package/dist/components/tab-stop-parse.d.ts +53 -0
- package/dist/components/tab-stop-parse.d.ts.map +1 -0
- package/dist/components/tab-stop-parse.js +202 -0
- package/dist/components/tab-stop-parse.js.map +1 -0
- package/dist/components/templated-text-field.d.ts.map +1 -1
- package/dist/components/templated-text-field.js +10 -148
- package/dist/components/templated-text-field.js.map +1 -1
- package/dist/components/templated-text-kwargs-editor.d.ts +24 -0
- package/dist/components/templated-text-kwargs-editor.d.ts.map +1 -0
- package/dist/components/templated-text-kwargs-editor.js +48 -0
- package/dist/components/templated-text-kwargs-editor.js.map +1 -0
- package/dist/components/templated-text-kwargs.d.ts +37 -0
- package/dist/components/templated-text-kwargs.d.ts.map +1 -0
- package/dist/components/templated-text-kwargs.js +66 -0
- package/dist/components/templated-text-kwargs.js.map +1 -0
- package/dist/components/templated-text-source.d.ts +33 -0
- package/dist/components/templated-text-source.d.ts.map +1 -0
- package/dist/components/templated-text-source.js +53 -0
- package/dist/components/templated-text-source.js.map +1 -0
- package/dist/components/tool-picker-parts.d.ts +39 -0
- package/dist/components/tool-picker-parts.d.ts.map +1 -0
- package/dist/components/tool-picker-parts.js +87 -0
- package/dist/components/tool-picker-parts.js.map +1 -0
- package/dist/components/tool-picker.d.ts +1 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +4 -66
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +7 -34
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +48 -0
- package/dist/components/version-history-rows.d.ts.map +1 -0
- package/dist/components/version-history-rows.js +51 -0
- package/dist/components/version-history-rows.js.map +1 -0
- package/dist/guards.d.ts +6 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +8 -0
- package/dist/guards.js.map +1 -0
- package/dist/hooks/interactions-connection.d.ts +43 -0
- package/dist/hooks/interactions-connection.d.ts.map +1 -0
- package/dist/hooks/interactions-connection.js +171 -0
- package/dist/hooks/interactions-connection.js.map +1 -0
- package/dist/hooks/interactions-stream.d.ts +80 -0
- package/dist/hooks/interactions-stream.d.ts.map +1 -0
- package/dist/hooks/interactions-stream.js +233 -0
- package/dist/hooks/interactions-stream.js.map +1 -0
- package/dist/hooks/useApi.d.ts +2192 -2192
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useSse.d.ts +3 -6
- package/dist/hooks/useSse.d.ts.map +1 -1
- package/dist/hooks/useSse.js +38 -451
- package/dist/hooks/useSse.js.map +1 -1
- package/dist/navigation/app-link.d.ts +25 -0
- package/dist/navigation/app-link.d.ts.map +1 -0
- package/dist/navigation/app-link.js +33 -0
- package/dist/navigation/app-link.js.map +1 -0
- package/dist/navigation/context.d.ts +3 -20
- package/dist/navigation/context.d.ts.map +1 -1
- package/dist/navigation/context.js +7 -267
- package/dist/navigation/context.js.map +1 -1
- package/dist/navigation/entry-state.d.ts +23 -0
- package/dist/navigation/entry-state.d.ts.map +1 -0
- package/dist/navigation/entry-state.js +39 -0
- package/dist/navigation/entry-state.js.map +1 -0
- package/dist/navigation/guard-registry.d.ts +30 -0
- package/dist/navigation/guard-registry.d.ts.map +1 -0
- package/dist/navigation/guard-registry.js +46 -0
- package/dist/navigation/guard-registry.js.map +1 -0
- package/dist/navigation/guarded-navigation.d.ts +24 -0
- package/dist/navigation/guarded-navigation.d.ts.map +1 -0
- package/dist/navigation/guarded-navigation.js +102 -0
- package/dist/navigation/guarded-navigation.js.map +1 -0
- package/dist/navigation/index.d.ts +3 -2
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +2 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +8 -11
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/navigation/use-back-forward-guard.d.ts +5 -0
- package/dist/navigation/use-back-forward-guard.d.ts.map +1 -0
- package/dist/navigation/use-back-forward-guard.js +82 -0
- package/dist/navigation/use-back-forward-guard.js.map +1 -0
- package/dist/plugin/registry.d.ts +6 -15
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +83 -75
- package/dist/plugin/registry.js.map +1 -1
- package/dist/schema-form/SecretRefField.d.ts +2 -3
- package/dist/schema-form/SecretRefField.d.ts.map +1 -1
- package/dist/schema-form/SecretRefField.js +23 -12
- package/dist/schema-form/SecretRefField.js.map +1 -1
- package/dist/schema-form/classify.d.ts +1 -122
- package/dist/schema-form/classify.d.ts.map +1 -1
- package/dist/schema-form/classify.js +174 -237
- package/dist/schema-form/classify.js.map +1 -1
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +42 -19
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/enum-field.d.ts +1 -1
- package/dist/schema-form/enum-field.d.ts.map +1 -1
- package/dist/schema-form/field-model.d.ts +121 -0
- package/dist/schema-form/field-model.d.ts.map +1 -0
- package/dist/schema-form/field-model.js +2 -0
- package/dist/schema-form/field-model.js.map +1 -0
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +25 -14
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/media-field.d.ts +1 -1
- package/dist/schema-form/media-field.d.ts.map +1 -1
- package/dist/schema-form/media-field.js +47 -28
- package/dist/schema-form/media-field.js.map +1 -1
- package/dist/schema-form/string-annotations.d.ts +20 -0
- package/dist/schema-form/string-annotations.d.ts.map +1 -0
- package/dist/schema-form/string-annotations.js +105 -0
- package/dist/schema-form/string-annotations.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +1 -1
- package/dist/schema-form/string-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.d.ts +1 -1
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js.map +1 -1
- package/dist/schema-form/union.d.ts +1 -1
- package/dist/schema-form/union.d.ts.map +1 -1
- package/dist/schema-form/validate.d.ts.map +1 -1
- package/dist/schema-form/validate.js +126 -105
- package/dist/schema-form/validate.js.map +1 -1
- package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
- package/dist/state-binding/AdapterMapping.js +0 -0
- package/dist/state-binding/AdapterMapping.js.map +1 -1
- package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
- package/dist/state-binding/StateAttachRow.js +52 -36
- package/dist/state-binding/StateAttachRow.js.map +1 -1
- package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
- package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
- package/dist/state-binding/adapter-mapping-row.js +87 -0
- package/dist/state-binding/adapter-mapping-row.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
- package/dist/structured-output/StructuredOutput.js +1 -3
- package/dist/structured-output/StructuredOutput.js.map +1 -1
- package/package.json +2 -2
- package/src/components/explorer-body.tsx +239 -0
- package/src/components/explorer-page-size.ts +39 -0
- package/src/components/explorer-tag-filter.tsx +80 -0
- package/src/components/explorer-tags.ts +64 -0
- package/src/components/explorer-view.tsx +114 -366
- package/src/components/extension-combo-builder.tsx +35 -152
- package/src/components/extension-combo-parts.tsx +237 -0
- package/src/components/fleet-report.tsx +97 -54
- package/src/components/icons/chrome-marks.tsx +195 -0
- package/src/components/icons/icon-frame.tsx +50 -0
- package/src/components/icons/index.ts +84 -0
- package/src/components/icons/nav-map.ts +55 -0
- package/src/components/icons/nav-marks.tsx +205 -0
- package/src/components/icons/status-marks.tsx +46 -0
- package/src/components/icons/theme-marks.tsx +33 -0
- package/src/components/json-diff.tsx +1 -4
- package/src/components/json-tree-context.ts +26 -0
- package/src/components/json-tree-model.ts +139 -0
- package/src/components/json-tree-nodes.tsx +224 -0
- package/src/components/json-tree.tsx +15 -434
- package/src/components/json-value-format.ts +64 -0
- package/src/components/markdown-blocks.ts +198 -0
- package/src/components/markdown-inline.tsx +365 -0
- package/src/components/markdown.tsx +5 -471
- package/src/components/observer-registry.ts +139 -0
- package/src/components/overflow-measure.ts +69 -0
- package/src/components/prose-regions.ts +153 -0
- package/src/components/scroll-region.tsx +20 -360
- package/src/components/tab-stop-parse.ts +217 -0
- package/src/components/templated-text-field.tsx +38 -330
- package/src/components/templated-text-kwargs-editor.tsx +147 -0
- package/src/components/templated-text-kwargs.ts +92 -0
- package/src/components/templated-text-source.tsx +226 -0
- package/src/components/tool-picker-parts.tsx +184 -0
- package/src/components/tool-picker.tsx +22 -114
- package/src/components/version-history-panel.tsx +38 -142
- package/src/components/version-history-rows.tsx +220 -0
- package/src/guards.ts +7 -0
- package/src/hooks/interactions-connection.ts +227 -0
- package/src/hooks/interactions-stream.ts +274 -0
- package/src/hooks/useSse.ts +50 -477
- package/src/navigation/app-link.tsx +70 -0
- package/src/navigation/context.tsx +11 -330
- package/src/navigation/entry-state.ts +42 -0
- package/src/navigation/guard-registry.ts +65 -0
- package/src/navigation/guarded-navigation.ts +147 -0
- package/src/navigation/index.ts +2 -2
- package/src/navigation/types.ts +8 -11
- package/src/navigation/use-back-forward-guard.ts +90 -0
- package/src/plugin/registry.ts +111 -90
- package/src/schema-form/SecretRefField.tsx +121 -71
- package/src/schema-form/classify.ts +244 -395
- package/src/schema-form/default-value.ts +48 -17
- package/src/schema-form/enum-field.tsx +1 -1
- package/src/schema-form/field-model.ts +133 -0
- package/src/schema-form/field-node.tsx +94 -38
- package/src/schema-form/media-field.tsx +134 -65
- package/src/schema-form/string-annotations.ts +111 -0
- package/src/schema-form/string-field.tsx +1 -1
- package/src/schema-form/union-field.tsx +2 -1
- package/src/schema-form/union.ts +1 -1
- package/src/schema-form/validate.ts +184 -103
- package/src/state-binding/AdapterMapping.tsx +0 -0
- package/src/state-binding/StateAttachRow.tsx +222 -147
- package/src/state-binding/adapter-mapping-row.tsx +232 -0
- package/src/structured-output/StructuredOutput.tsx +1 -4
- package/dist/components/icons.d.ts +0 -160
- package/dist/components/icons.d.ts.map +0 -1
- package/dist/components/icons.js +0 -160
- package/dist/components/icons.js.map +0 -1
- package/src/components/icons.tsx +0 -586
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
* empty value so the committed seed validates against its own schema.
|
|
17
17
|
*/
|
|
18
18
|
import { classifySchema } from './classify';
|
|
19
|
+
import type { FieldModel } from './field-model';
|
|
19
20
|
import type { JsonSchema } from './types';
|
|
20
21
|
|
|
22
|
+
type ModelOf<K extends FieldModel['kind']> = Extract<FieldModel, { kind: K }>;
|
|
23
|
+
|
|
21
24
|
/** The explicit `default` (wrapper takes precedence over the resolved node), if any. */
|
|
22
25
|
function explicitDefault(
|
|
23
26
|
raw: JsonSchema,
|
|
@@ -52,6 +55,48 @@ export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = sc
|
|
|
52
55
|
return seedValue(schema, root, true);
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Seed an object's required nested structure and any optional property that
|
|
60
|
+
* carries an explicit default. A required property recurses; an optional one is
|
|
61
|
+
* materialized only when it declares a `default`, so its own required children
|
|
62
|
+
* never raise errors on a field the user did not opt into.
|
|
63
|
+
*/
|
|
64
|
+
function seedObject(
|
|
65
|
+
model: ModelOf<'object'>,
|
|
66
|
+
root: JsonSchema,
|
|
67
|
+
mintScalars: boolean,
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
const result: Record<string, unknown> = {};
|
|
70
|
+
for (const [name, propSchema] of model.properties) {
|
|
71
|
+
if (model.required.has(name)) {
|
|
72
|
+
const seeded = seedValue(propSchema, root, mintScalars);
|
|
73
|
+
if (seeded !== undefined) result[name] = seeded;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const propClassified = classifySchema(propSchema, root);
|
|
77
|
+
const propDefault = explicitDefault(propSchema, propClassified.schema);
|
|
78
|
+
if (propDefault.present) result[name] = propDefault.value;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The empty value a required scalar mints in a committed skeleton: strings `''`,
|
|
85
|
+
* numbers `0`, booleans `false`. Absent (left value-less) on the default path,
|
|
86
|
+
* where a fresh form should flag the missing required scalar instead.
|
|
87
|
+
*/
|
|
88
|
+
function seedScalar(kind: 'string' | 'number' | 'boolean', mintScalars: boolean): unknown {
|
|
89
|
+
if (!mintScalars) return undefined;
|
|
90
|
+
switch (kind) {
|
|
91
|
+
case 'string':
|
|
92
|
+
return '';
|
|
93
|
+
case 'number':
|
|
94
|
+
return 0;
|
|
95
|
+
case 'boolean':
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/**
|
|
56
101
|
* The shared seeding walk. `mintScalars` distinguishes the two entry points
|
|
57
102
|
* above: `false` leaves value-less required scalars absent; `true` mints their
|
|
@@ -66,30 +111,16 @@ function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean):
|
|
|
66
111
|
switch (model.kind) {
|
|
67
112
|
case 'const':
|
|
68
113
|
return model.value;
|
|
69
|
-
case 'object':
|
|
70
|
-
|
|
71
|
-
for (const [name, propSchema] of model.properties) {
|
|
72
|
-
if (model.required.has(name)) {
|
|
73
|
-
const seeded = seedValue(propSchema, root, mintScalars);
|
|
74
|
-
if (seeded !== undefined) result[name] = seeded;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const propClassified = classifySchema(propSchema, root);
|
|
78
|
-
const propDefault = explicitDefault(propSchema, propClassified.schema);
|
|
79
|
-
if (propDefault.present) result[name] = propDefault.value;
|
|
80
|
-
}
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
114
|
+
case 'object':
|
|
115
|
+
return seedObject(model, root, mintScalars);
|
|
83
116
|
case 'array':
|
|
84
117
|
return [];
|
|
85
118
|
case 'record':
|
|
86
119
|
return {};
|
|
87
120
|
case 'string':
|
|
88
|
-
return mintScalars ? '' : undefined;
|
|
89
121
|
case 'number':
|
|
90
|
-
return mintScalars ? 0 : undefined;
|
|
91
122
|
case 'boolean':
|
|
92
|
-
return mintScalars
|
|
123
|
+
return seedScalar(model.kind, mintScalars);
|
|
93
124
|
case 'enum':
|
|
94
125
|
case 'union':
|
|
95
126
|
case 'json':
|
|
@@ -9,7 +9,7 @@ import type { ReactNode } from 'react';
|
|
|
9
9
|
import { Field } from '../components/field';
|
|
10
10
|
import { RadioGroup } from '../components/radio-group';
|
|
11
11
|
import { Select } from '../components/select';
|
|
12
|
-
import type { FieldModel } from './
|
|
12
|
+
import type { FieldModel } from './field-model';
|
|
13
13
|
|
|
14
14
|
const RADIO_MAX_OPTIONS = 3;
|
|
15
15
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer-facing result types produced by `classifySchema`: the discriminated
|
|
3
|
+
* `FieldModel`, the `ClassifiedField` envelope, and the annotation/variant shapes
|
|
4
|
+
* they carry. Kept apart from the classifier logic so both the renderer and the
|
|
5
|
+
* validator import the vocabulary without pulling in classification code.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnumOption } from './resolve';
|
|
8
|
+
import type { JsonSchema } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How a string field accepts binary content as a JSON-serializable string.
|
|
12
|
+
* Derived from a field's JSON-Schema media annotations (see the `SchemaForm` module).
|
|
13
|
+
*/
|
|
14
|
+
export interface MediaUpload {
|
|
15
|
+
/**
|
|
16
|
+
* `base64` — the emitted value is the raw base64 body (no `data:` prefix),
|
|
17
|
+
* matching `contentEncoding: "base64"`. `data-url` — the emitted value is a
|
|
18
|
+
* full `data:<mime>;base64,<body>` URL, matching `format: "data-url"`.
|
|
19
|
+
*/
|
|
20
|
+
readonly encoding: 'base64' | 'data-url';
|
|
21
|
+
/**
|
|
22
|
+
* The `contentMediaType` constraint (e.g. `image/*`, `application/pdf`), used
|
|
23
|
+
* for MIME validation and the file picker's `accept`. `undefined` for a bare
|
|
24
|
+
* `format: "data-url"` field with no declared type.
|
|
25
|
+
*/
|
|
26
|
+
readonly mediaType: string | undefined;
|
|
27
|
+
/** A per-field byte cap from `contentMaxBytes`, if the schema pins one. */
|
|
28
|
+
readonly maxBytes: number | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One top-level key of the expression's input document, with a one-line gloss. */
|
|
32
|
+
export interface ExpressionAnnotationKey {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly gloss: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The `x-tai42-expression` annotation on a string schema: the server declares
|
|
39
|
+
* that the field's value is authored in a pipeline expression language (only jq
|
|
40
|
+
* today) and optionally describes what `.` is for that expression. The renderer
|
|
41
|
+
* uses it to open the visual-editor door instead of a plain text box. The shape
|
|
42
|
+
* mirrors jq-studio's input-shape descriptor WITHOUT importing it — classification
|
|
43
|
+
* stays a pure schema concern; the renderer maps this onto a jq field declaration.
|
|
44
|
+
*/
|
|
45
|
+
export interface ExpressionAnnotation {
|
|
46
|
+
/** The declared language. An unknown language never reaches here — it classifies
|
|
47
|
+
* as a plain string, so an older client renders a newer server's field as the
|
|
48
|
+
* ordinary text input it always was. */
|
|
49
|
+
readonly language: 'jq';
|
|
50
|
+
/** Short chip label for what `.` is (e.g. "node envelope"). */
|
|
51
|
+
readonly label: string | undefined;
|
|
52
|
+
/** One sentence describing what `.` is in this field. */
|
|
53
|
+
readonly blurb: string | undefined;
|
|
54
|
+
/** The top-level keys of `.`, each with a one-liner. */
|
|
55
|
+
readonly keys: readonly ExpressionAnnotationKey[] | undefined;
|
|
56
|
+
/** What the expression must return (e.g. "true or false"). */
|
|
57
|
+
readonly returns: string | undefined;
|
|
58
|
+
/** Per-field caveats. */
|
|
59
|
+
readonly caveats: readonly string[] | undefined;
|
|
60
|
+
/** True when the annotation carries a `sample` — tracked separately because a
|
|
61
|
+
* legitimate sample may be JSON `null`. */
|
|
62
|
+
readonly hasSample: boolean;
|
|
63
|
+
/** A static skeleton of `.` for the Test panel. Meaningful only when
|
|
64
|
+
* {@link hasSample} is true. */
|
|
65
|
+
readonly sample: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One selectable variant of a union field. */
|
|
69
|
+
export interface UnionVariant {
|
|
70
|
+
readonly label: string;
|
|
71
|
+
/** The discriminator tag value, or `undefined` for a non-discriminated union. */
|
|
72
|
+
readonly tag: unknown;
|
|
73
|
+
/** The variant schema as written (unresolved — the renderer resolves it). */
|
|
74
|
+
readonly schema: JsonSchema;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The renderer-facing classification of a schema node. */
|
|
78
|
+
export type FieldModel =
|
|
79
|
+
| { readonly kind: 'const'; readonly value: unknown }
|
|
80
|
+
| { readonly kind: 'enum'; readonly options: readonly EnumOption[] }
|
|
81
|
+
| {
|
|
82
|
+
readonly kind: 'string';
|
|
83
|
+
readonly format: string | undefined;
|
|
84
|
+
/** Present when media annotations opt the field into the upload control. */
|
|
85
|
+
readonly media: MediaUpload | undefined;
|
|
86
|
+
/** Present when a well-formed `x-tai42-expression` annotation opts the
|
|
87
|
+
* field into the jq expression editor (see {@link ExpressionAnnotation}). */
|
|
88
|
+
readonly expression: ExpressionAnnotation | undefined;
|
|
89
|
+
}
|
|
90
|
+
| { readonly kind: 'number'; readonly integer: boolean }
|
|
91
|
+
| { readonly kind: 'boolean' }
|
|
92
|
+
| { readonly kind: 'array'; readonly items: JsonSchema }
|
|
93
|
+
| {
|
|
94
|
+
readonly kind: 'object';
|
|
95
|
+
readonly properties: readonly (readonly [string, JsonSchema])[];
|
|
96
|
+
readonly required: ReadonlySet<string>;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
// A string-keyed map typed by `additionalProperties` (no fixed
|
|
100
|
+
// `properties`), edited as key/value rows. `values` is the entry value
|
|
101
|
+
// schema AS WRITTEN — the entry editor resolves any `$ref` itself, exactly
|
|
102
|
+
// as `array.items` is.
|
|
103
|
+
readonly kind: 'record';
|
|
104
|
+
readonly values: JsonSchema;
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
readonly kind: 'union';
|
|
108
|
+
readonly variants: readonly UnionVariant[];
|
|
109
|
+
readonly discriminator: string | undefined;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
// A shape with no structured editor to build — a property-less object, a
|
|
113
|
+
// bare `additionalProperties`-open object, an items-less array, an empty/
|
|
114
|
+
// multi-type/`allOf` schema. It is NOT undroppable: every one of these still
|
|
115
|
+
// describes JSON, so the renderer edits it as free-form JSON in a mono
|
|
116
|
+
// textarea rather than dead-ending on a badge. `jsonType` is the container
|
|
117
|
+
// the schema commits to, so a valid buffer must parse to it (`'any'` = no
|
|
118
|
+
// constraint beyond "is JSON"); `reason` records which shape fell through.
|
|
119
|
+
readonly kind: 'json';
|
|
120
|
+
readonly jsonType: 'object' | 'array' | 'any';
|
|
121
|
+
readonly reason: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** A fully classified field: its model plus resolved metadata. */
|
|
125
|
+
export interface ClassifiedField {
|
|
126
|
+
readonly model: FieldModel;
|
|
127
|
+
/** True when JSON `null` is an accepted value (a null-union member). */
|
|
128
|
+
readonly nullable: boolean;
|
|
129
|
+
/** The effective (ref-resolved) schema this classification describes. */
|
|
130
|
+
readonly schema: JsonSchema;
|
|
131
|
+
readonly title: string | undefined;
|
|
132
|
+
readonly description: string | undefined;
|
|
133
|
+
}
|
|
@@ -47,24 +47,12 @@ function labelText(explicit: string | undefined, label: string | undefined): str
|
|
|
47
47
|
|
|
48
48
|
/** A single schema node, dispatched on its classified model kind. */
|
|
49
49
|
export function FieldNode(props: FieldNodeProps): ReactNode {
|
|
50
|
-
const {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
path,
|
|
56
|
-
label,
|
|
57
|
-
required,
|
|
58
|
-
markRequired = false,
|
|
59
|
-
errors,
|
|
60
|
-
idPrefix,
|
|
61
|
-
} = props;
|
|
62
|
-
const classified = classifySchema(schema, root);
|
|
63
|
-
const base = labelText(classified.title, label);
|
|
64
|
-
const heading = markRequired && required ? `${base} *` : base;
|
|
65
|
-
const description = classified.description;
|
|
50
|
+
const { root, value, onChange, path, required, idPrefix, errors } = props;
|
|
51
|
+
const classified = classifySchema(props.schema, root);
|
|
52
|
+
const base = labelText(classified.title, props.label);
|
|
53
|
+
const heading = props.markRequired === true && required ? `${base} *` : base;
|
|
54
|
+
const { model, description } = classified;
|
|
66
55
|
const error = errors?.[path];
|
|
67
|
-
const model = classified.model;
|
|
68
56
|
|
|
69
57
|
switch (model.kind) {
|
|
70
58
|
case 'json':
|
|
@@ -81,9 +69,12 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
|
|
|
81
69
|
);
|
|
82
70
|
case 'const':
|
|
83
71
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
72
|
+
<ConstField
|
|
73
|
+
heading={heading}
|
|
74
|
+
description={description}
|
|
75
|
+
error={error}
|
|
76
|
+
constValue={model.value}
|
|
77
|
+
/>
|
|
87
78
|
);
|
|
88
79
|
case 'enum':
|
|
89
80
|
return (
|
|
@@ -113,27 +104,24 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
|
|
|
113
104
|
);
|
|
114
105
|
case 'number':
|
|
115
106
|
return (
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
/>
|
|
125
|
-
</Field>
|
|
107
|
+
<NumberField
|
|
108
|
+
heading={heading}
|
|
109
|
+
description={description}
|
|
110
|
+
error={error}
|
|
111
|
+
integer={model.integer}
|
|
112
|
+
value={value}
|
|
113
|
+
onChange={onChange}
|
|
114
|
+
/>
|
|
126
115
|
);
|
|
127
116
|
case 'boolean':
|
|
128
117
|
return (
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
</Field>
|
|
118
|
+
<BooleanField
|
|
119
|
+
heading={heading}
|
|
120
|
+
description={description}
|
|
121
|
+
error={error}
|
|
122
|
+
value={value}
|
|
123
|
+
onChange={onChange}
|
|
124
|
+
/>
|
|
137
125
|
);
|
|
138
126
|
case 'array':
|
|
139
127
|
return (
|
|
@@ -198,3 +186,71 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
|
|
|
198
186
|
);
|
|
199
187
|
}
|
|
200
188
|
}
|
|
189
|
+
|
|
190
|
+
/** The label/description/error chrome every scalar leaf field shares. */
|
|
191
|
+
interface ScalarFieldChrome {
|
|
192
|
+
readonly heading: string;
|
|
193
|
+
readonly description: string | undefined;
|
|
194
|
+
readonly error: string | undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A `const`-pinned value: shown read-only, never editable. */
|
|
198
|
+
function ConstField({
|
|
199
|
+
heading,
|
|
200
|
+
description,
|
|
201
|
+
error,
|
|
202
|
+
constValue,
|
|
203
|
+
}: ScalarFieldChrome & { constValue: unknown }): ReactNode {
|
|
204
|
+
return (
|
|
205
|
+
<Field label={heading} description={description} error={error}>
|
|
206
|
+
<TextInput value={scalarLabel(constValue)} readOnly disabled />
|
|
207
|
+
</Field>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A number/integer field; an empty input clears the value to `undefined`. */
|
|
212
|
+
function NumberField({
|
|
213
|
+
heading,
|
|
214
|
+
description,
|
|
215
|
+
error,
|
|
216
|
+
integer,
|
|
217
|
+
value,
|
|
218
|
+
onChange,
|
|
219
|
+
}: ScalarFieldChrome & {
|
|
220
|
+
integer: boolean;
|
|
221
|
+
value: unknown;
|
|
222
|
+
onChange: (value: unknown) => void;
|
|
223
|
+
}): ReactNode {
|
|
224
|
+
return (
|
|
225
|
+
<Field label={heading} description={description} error={error}>
|
|
226
|
+
<NumberInput
|
|
227
|
+
step={integer ? '1' : 'any'}
|
|
228
|
+
value={typeof value === 'number' ? value : ''}
|
|
229
|
+
onChange={(event) => {
|
|
230
|
+
const raw = event.target.value;
|
|
231
|
+
onChange(raw === '' ? undefined : Number(raw));
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
</Field>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A boolean field rendered as a checkbox. */
|
|
239
|
+
function BooleanField({
|
|
240
|
+
heading,
|
|
241
|
+
description,
|
|
242
|
+
error,
|
|
243
|
+
value,
|
|
244
|
+
onChange,
|
|
245
|
+
}: ScalarFieldChrome & { value: unknown; onChange: (value: unknown) => void }): ReactNode {
|
|
246
|
+
return (
|
|
247
|
+
<Field label={heading} description={description} error={error}>
|
|
248
|
+
<Checkbox
|
|
249
|
+
checked={value === true}
|
|
250
|
+
onCheckedChange={(checked) => {
|
|
251
|
+
onChange(checked);
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
</Field>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -13,7 +13,7 @@ import { XCircleIcon } from '../components/icons';
|
|
|
13
13
|
import { Button } from '../components/primitives';
|
|
14
14
|
import { Field, useFieldControl, type FieldControlProps } from '../components/field';
|
|
15
15
|
import { TextInput } from '../components/inputs';
|
|
16
|
-
import type { MediaUpload } from './
|
|
16
|
+
import type { MediaUpload } from './field-model';
|
|
17
17
|
import { MaxUploadBytesContext } from './context';
|
|
18
18
|
import { decodedByteSize, effectiveMaxBytes, overCapMessage } from './media';
|
|
19
19
|
|
|
@@ -114,9 +114,7 @@ export function MediaField({
|
|
|
114
114
|
const defaultMax = useContext(MaxUploadBytesContext);
|
|
115
115
|
const maxBytes = effectiveMaxBytes(media.maxBytes, defaultMax);
|
|
116
116
|
const [uploadError, setUploadError] = useState<string | undefined>(undefined);
|
|
117
|
-
const [attached, setAttached] = useState<
|
|
118
|
-
{ name: string; previewUrl: string; value: string } | undefined
|
|
119
|
-
>(undefined);
|
|
117
|
+
const [attached, setAttached] = useState<AttachedMedia | undefined>(undefined);
|
|
120
118
|
|
|
121
119
|
const accept = (file: File | undefined): void => {
|
|
122
120
|
if (file === undefined) return;
|
|
@@ -139,19 +137,7 @@ export function MediaField({
|
|
|
139
137
|
onChange('');
|
|
140
138
|
};
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
// current field value; if the value is reset or replaced externally (an edit
|
|
144
|
-
// loads a different record, or the form resets after submit), fall back to a
|
|
145
|
-
// value-derived display so a stale filename/thumbnail can never linger.
|
|
146
|
-
const activeAttached = attached?.value === value ? attached : undefined;
|
|
147
|
-
// Prefer the just-read preview; on an edit with a pre-existing data-url value,
|
|
148
|
-
// reconstruct the preview from the value itself.
|
|
149
|
-
const previewUrl =
|
|
150
|
-
activeAttached?.previewUrl ??
|
|
151
|
-
(media.encoding === 'data-url' && value.startsWith('data:') ? value : undefined);
|
|
152
|
-
const isImage =
|
|
153
|
-
media.mediaType?.startsWith('image/') === true || previewUrl?.startsWith('data:image') === true;
|
|
154
|
-
const fileName = activeAttached?.name ?? (value !== '' ? 'Attached file' : undefined);
|
|
140
|
+
const { previewUrl, isImage, fileName } = mediaPreview(attached, value, media);
|
|
155
141
|
|
|
156
142
|
return (
|
|
157
143
|
<Field label={heading} description={description} error={error}>
|
|
@@ -161,29 +147,12 @@ export function MediaField({
|
|
|
161
147
|
onFile={accept}
|
|
162
148
|
/>
|
|
163
149
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// A thumbnail is per-instance geometry: it is capped to a fixed
|
|
171
|
-
// height and can never exceed the width it is given.
|
|
172
|
-
style={{
|
|
173
|
-
maxWidth: '100%',
|
|
174
|
-
maxHeight: '6rem',
|
|
175
|
-
borderRadius: 'var(--tai-radius-md)',
|
|
176
|
-
border: '1px solid var(--tai-color-control-border)',
|
|
177
|
-
}}
|
|
178
|
-
/>
|
|
179
|
-
) : (
|
|
180
|
-
<Badge>{fileName}</Badge>
|
|
181
|
-
)}
|
|
182
|
-
<Button type="button" variant="secondary" onClick={clear}>
|
|
183
|
-
Remove
|
|
184
|
-
</Button>
|
|
185
|
-
</div>
|
|
186
|
-
) : null}
|
|
150
|
+
<AttachedPreview
|
|
151
|
+
fileName={fileName}
|
|
152
|
+
isImage={isImage}
|
|
153
|
+
previewUrl={previewUrl}
|
|
154
|
+
onClear={clear}
|
|
155
|
+
/>
|
|
187
156
|
|
|
188
157
|
{uploadError !== undefined ? (
|
|
189
158
|
<span id={uploadErrorId} role="alert" className="tai-field-error">
|
|
@@ -192,33 +161,133 @@ export function MediaField({
|
|
|
192
161
|
</span>
|
|
193
162
|
) : null}
|
|
194
163
|
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
164
|
+
<MediaPasteInput
|
|
165
|
+
pasteId={pasteId}
|
|
166
|
+
value={value}
|
|
167
|
+
maxBytes={maxBytes}
|
|
168
|
+
uploadErrorId={uploadError === undefined ? undefined : uploadErrorId}
|
|
169
|
+
onReject={setUploadError}
|
|
170
|
+
onAccept={(next) => {
|
|
171
|
+
setUploadError(undefined);
|
|
172
|
+
setAttached(undefined);
|
|
173
|
+
onChange(next);
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
</Field>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The just-picked file, retained only while it still matches the current field value. */
|
|
181
|
+
interface AttachedMedia {
|
|
182
|
+
name: string;
|
|
183
|
+
previewUrl: string;
|
|
184
|
+
value: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The display derived from the attached file and the current value: the chip/preview
|
|
189
|
+
* applies ONLY while the attachment still matches the value (an external reset or a
|
|
190
|
+
* different record loading falls back to a value-derived display so a stale
|
|
191
|
+
* filename/thumbnail can never linger); an existing data-url value reconstructs its
|
|
192
|
+
* own preview.
|
|
193
|
+
*/
|
|
194
|
+
function mediaPreview(
|
|
195
|
+
attached: AttachedMedia | undefined,
|
|
196
|
+
value: string,
|
|
197
|
+
media: MediaUpload,
|
|
198
|
+
): { previewUrl: string | undefined; isImage: boolean; fileName: string | undefined } {
|
|
199
|
+
const activeAttached = attached?.value === value ? attached : undefined;
|
|
200
|
+
const previewUrl =
|
|
201
|
+
activeAttached?.previewUrl ??
|
|
202
|
+
(media.encoding === 'data-url' && value.startsWith('data:') ? value : undefined);
|
|
203
|
+
const isImage =
|
|
204
|
+
media.mediaType?.startsWith('image/') === true || previewUrl?.startsWith('data:image') === true;
|
|
205
|
+
const fileName = activeAttached?.name ?? (value !== '' ? 'Attached file' : undefined);
|
|
206
|
+
return { previewUrl, isImage, fileName };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The attached-file chip: an image thumbnail when previewable, else a filename badge, plus Remove. */
|
|
210
|
+
function AttachedPreview({
|
|
211
|
+
fileName,
|
|
212
|
+
isImage,
|
|
213
|
+
previewUrl,
|
|
214
|
+
onClear,
|
|
215
|
+
}: {
|
|
216
|
+
fileName: string | undefined;
|
|
217
|
+
isImage: boolean;
|
|
218
|
+
previewUrl: string | undefined;
|
|
219
|
+
onClear: () => void;
|
|
220
|
+
}): ReactNode {
|
|
221
|
+
if (fileName === undefined) return null;
|
|
222
|
+
return (
|
|
223
|
+
<div className="tai-row">
|
|
224
|
+
{isImage && previewUrl !== undefined ? (
|
|
225
|
+
<img
|
|
226
|
+
src={previewUrl}
|
|
227
|
+
alt={fileName}
|
|
228
|
+
// A thumbnail is per-instance geometry: it is capped to a fixed height and
|
|
229
|
+
// can never exceed the width it is given.
|
|
230
|
+
style={{
|
|
231
|
+
maxWidth: '100%',
|
|
232
|
+
maxHeight: '6rem',
|
|
233
|
+
borderRadius: 'var(--tai-radius-md)',
|
|
234
|
+
border: '1px solid var(--tai-color-control-border)',
|
|
218
235
|
}}
|
|
219
236
|
/>
|
|
220
|
-
|
|
221
|
-
|
|
237
|
+
) : (
|
|
238
|
+
<Badge>{fileName}</Badge>
|
|
239
|
+
)}
|
|
240
|
+
<Button type="button" variant="secondary" onClick={onClear}>
|
|
241
|
+
Remove
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The paste fallback: a text input that enforces the SAME effective byte cap the
|
|
249
|
+
* picker does, measured on the pasted string's DECODED size. An over-cap paste is
|
|
250
|
+
* rejected LOUDLY (`onReject`) and the field keeps its prior value; an accepted paste
|
|
251
|
+
* clears any attachment and emits the new value (`onAccept`).
|
|
252
|
+
*/
|
|
253
|
+
function MediaPasteInput({
|
|
254
|
+
pasteId,
|
|
255
|
+
value,
|
|
256
|
+
maxBytes,
|
|
257
|
+
uploadErrorId,
|
|
258
|
+
onReject,
|
|
259
|
+
onAccept,
|
|
260
|
+
}: {
|
|
261
|
+
pasteId: string;
|
|
262
|
+
value: string;
|
|
263
|
+
maxBytes: number;
|
|
264
|
+
/** Set while an upload error stands, so the input points at it and reads invalid. */
|
|
265
|
+
uploadErrorId: string | undefined;
|
|
266
|
+
onReject: (message: string) => void;
|
|
267
|
+
onAccept: (value: string) => void;
|
|
268
|
+
}): ReactNode {
|
|
269
|
+
return (
|
|
270
|
+
<label htmlFor={pasteId} className="tai-field">
|
|
271
|
+
<span className="tai-field-hint">Or paste a value</span>
|
|
272
|
+
<TextInput
|
|
273
|
+
id={pasteId}
|
|
274
|
+
type="text"
|
|
275
|
+
// Only the upload error's id: `TextInput` joins it with the enclosing Field's
|
|
276
|
+
// own description and error IDREFs rather than replacing them.
|
|
277
|
+
aria-describedby={uploadErrorId}
|
|
278
|
+
aria-invalid={uploadErrorId === undefined ? undefined : true}
|
|
279
|
+
value={value}
|
|
280
|
+
onChange={(event) => {
|
|
281
|
+
const next = event.target.value;
|
|
282
|
+
const size = decodedByteSize(next);
|
|
283
|
+
if (size > maxBytes) {
|
|
284
|
+
onReject(overCapMessage('The pasted value', size, maxBytes));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
onAccept(next);
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
</label>
|
|
222
291
|
);
|
|
223
292
|
}
|
|
224
293
|
|