@elabs-ai/components-ui 4.0.0 → 4.1.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 +10 -8
- package/dist/chunk-JKPPTL63.js +48 -0
- package/dist/chunk-JKPPTL63.js.map +1 -0
- package/dist/form.d.ts +28 -0
- package/dist/form.js +123 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2485 -167
- package/dist/index.js +10716 -2706
- package/dist/index.js.map +1 -1
- package/package.json +21 -7
- package/src/_peer-deps.test.ts +59 -0
- package/src/blocks-comparison-table.stories.tsx +2 -2
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/advanced-group/advanced-group.tsx +1 -1
- package/src/components/alert/alert.tsx +4 -1
- package/src/components/app-shell/app-shell.stories.tsx +44 -2
- package/src/components/app-shell/app-shell.tsx +35 -2
- package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
- package/src/components/attribution-panel/attribution-panel.tsx +1 -1
- package/src/components/attribution-panel/attributions.generated.ts +81 -77
- package/src/components/badge/badge.stories.tsx +8 -0
- package/src/components/badge/badge.tsx +1 -1
- package/src/components/bento-grid/bento-grid.test.tsx +1 -1
- package/src/components/bento-grid/bento-grid.tsx +1 -1
- package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
- package/src/components/breadcrumb/breadcrumb.tsx +7 -2
- package/src/components/button/button.stories.tsx +116 -8
- package/src/components/button/button.tsx +1 -1
- package/src/components/card/card.stories.tsx +11 -6
- package/src/components/card/card.tsx +1 -1
- package/src/components/change-review/change-review.stories.tsx +145 -1
- package/src/components/change-review/change-review.test.tsx +143 -0
- package/src/components/change-review/change-review.tsx +185 -4
- package/src/components/checkbox/checkbox.tsx +1 -1
- package/src/components/color-picker/color-picker.tsx +6 -5
- package/src/components/command/command.stories.tsx +16 -6
- package/src/components/command/command.test.tsx +73 -0
- package/src/components/command/command.tsx +30 -7
- package/src/components/command/index.ts +1 -0
- package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
- package/src/components/command-trigger/command-trigger.test.tsx +18 -0
- package/src/components/command-trigger/command-trigger.tsx +70 -0
- package/src/components/command-trigger/index.ts +1 -0
- package/src/components/context-rail/context-rail.stories.tsx +285 -0
- package/src/components/context-rail/context-rail.test.tsx +263 -0
- package/src/components/context-rail/context-rail.tsx +678 -0
- package/src/components/context-rail/index.ts +1 -0
- package/src/components/copyable-value/copyable-value.tsx +1 -1
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
- package/src/components/empty-state/empty-state.test.tsx +26 -0
- package/src/components/empty-state/empty-state.tsx +6 -1
- package/src/components/expand-dialog/expand-dialog.tsx +1 -2
- package/src/components/field/field-context.ts +59 -0
- package/src/components/field/field.stories.tsx +142 -0
- package/src/components/field/field.test.tsx +415 -0
- package/src/components/field/field.tsx +340 -0
- package/src/components/field/index.ts +9 -0
- package/src/components/field-row/field-row.stories.tsx +85 -1
- package/src/components/field-row/field-row.test.tsx +145 -0
- package/src/components/field-row/field-row.tsx +48 -6
- package/src/components/file-upload/file-upload.stories.tsx +16 -0
- package/src/components/file-upload/file-upload.test.tsx +7 -2
- package/src/components/file-upload/file-upload.tsx +15 -4
- package/src/components/form/form.tsx +5 -2
- package/src/components/icon-button/icon-button.test.tsx +1 -1
- package/src/components/icon-button/icon-button.tsx +2 -2
- package/src/components/input/input.stories.tsx +121 -0
- package/src/components/input/input.test.tsx +13 -0
- package/src/components/input/input.tsx +2 -2
- package/src/components/input-group/input-group.stories.tsx +74 -0
- package/src/components/input-group/input-group.tsx +13 -4
- package/src/components/input-otp/input-otp.tsx +5 -1
- package/src/components/keyboard-shortcuts/index.ts +6 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
- package/src/components/link-preview/link-preview.tsx +1 -1
- package/src/components/locale-provider/index.ts +6 -1
- package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
- package/src/components/locale-provider/locale-provider.test.tsx +257 -1
- package/src/components/locale-provider/locale-provider.tsx +63 -8
- package/src/components/locale-provider/messages.ts +501 -2
- package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
- package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
- package/src/components/mention-input/mention-input.tsx +8 -38
- package/src/components/mention-input/mention-value.ts +8 -2
- package/src/components/metric-card/metric-card.stories.tsx +5 -3
- package/src/components/metric-card/metric-card.tsx +21 -2
- package/src/components/model-picker/model-picker.stories.tsx +63 -3
- package/src/components/model-picker/model-picker.test.tsx +63 -0
- package/src/components/model-picker/model-picker.tsx +62 -36
- package/src/components/nav-main/nav-main.tsx +1 -1
- package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
- package/src/components/nav-notifications/nav-notifications.tsx +57 -5
- package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
- package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
- package/src/components/navigation-menu/navigation-menu.tsx +189 -12
- package/src/components/page-shell/page-shell.stories.tsx +163 -0
- package/src/components/page-shell/page-shell.test.tsx +229 -0
- package/src/components/page-shell/page-shell.tsx +108 -25
- package/src/components/pagination/pagination.test.tsx +90 -0
- package/src/components/pagination/pagination.tsx +9 -5
- package/src/components/radio-group/radio-group.tsx +1 -1
- package/src/components/rating/rating.tsx +1 -1
- package/src/components/resizable/resizable.stories.tsx +114 -2
- package/src/components/resizable/resizable.tsx +1 -1
- package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
- package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
- package/src/components/revision-timeline/revision-timeline.tsx +46 -3
- package/src/components/schema-form/from-json-schema.test.ts +390 -0
- package/src/components/schema-form/from-json-schema.ts +375 -0
- package/src/components/schema-form/index.ts +81 -0
- package/src/components/schema-form/schema-form-spec.ts +698 -0
- package/src/components/schema-form/schema-form.stories.tsx +566 -0
- package/src/components/schema-form/schema-form.test.tsx +900 -0
- package/src/components/schema-form/schema-form.tsx +1545 -0
- package/src/components/section-header/section-header.stories.tsx +37 -0
- package/src/components/section-header/section-header.tsx +12 -1
- package/src/components/select/select.tsx +6 -2
- package/src/components/sheet/sheet.tsx +1 -1
- package/src/components/side-dock/index.ts +1 -0
- package/src/components/side-dock/side-dock.stories.tsx +137 -0
- package/src/components/side-dock/side-dock.test.tsx +273 -0
- package/src/components/side-dock/side-dock.tsx +502 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
- package/src/components/sidebar/sidebar.stories.tsx +421 -1
- package/src/components/sidebar/sidebar.test.tsx +36 -1
- package/src/components/sidebar/sidebar.tsx +245 -33
- package/src/components/skip-link/index.ts +1 -0
- package/src/components/skip-link/skip-link.stories.tsx +106 -0
- package/src/components/skip-link/skip-link.test.tsx +20 -0
- package/src/components/skip-link/skip-link.tsx +34 -0
- package/src/components/slider/slider.tsx +1 -1
- package/src/components/split-panel/index.ts +6 -1
- package/src/components/split-panel/split-panel.stories.tsx +18 -2
- package/src/components/split-panel/split-panel.test.tsx +20 -0
- package/src/components/split-panel/split-panel.tsx +28 -8
- package/src/components/state-panel/state-panel.stories.tsx +52 -0
- package/src/components/state-panel/state-panel.test.tsx +125 -0
- package/src/components/state-panel/state-panel.tsx +96 -17
- package/src/components/status-badge/status-badge.stories.tsx +8 -0
- package/src/components/switch/switch.tsx +1 -1
- package/src/components/table/table.stories.tsx +62 -0
- package/src/components/table/table.test.tsx +142 -1
- package/src/components/table/table.tsx +60 -1
- package/src/components/tabs/tabs.tsx +8 -2
- package/src/components/tag-input/tag-input.tsx +3 -2
- package/src/components/team-switcher/team-switcher.tsx +34 -5
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/theme-switcher/theme-switcher.tsx +6 -2
- package/src/components/theme-switcher/use-theme-transition.ts +2 -2
- package/src/components/timeline/timeline.stories.tsx +27 -0
- package/src/components/timeline/timeline.tsx +11 -1
- package/src/components/toggle/toggle.tsx +1 -1
- package/src/components/toolbar/toolbar.stories.tsx +10 -2
- package/src/components/tree/tree.stories.tsx +2 -2
- package/src/components/tree/tree.tsx +14 -9
- package/src/components/typography/prose.tsx +1 -1
- package/src/components/typography/typography.stories.tsx +228 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
- package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
- package/src/components/view-toolbar/view-toolbar.tsx +37 -4
- package/src/components/virtual-select/virtual-select.test.tsx +1 -1
- package/src/components/wizard/wizard.tsx +1 -1
- package/src/components/workspace-picker/index.ts +3 -0
- package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
- package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
- package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
- package/src/components/workspace-picker/workspace-picker.tsx +172 -0
- package/src/illustrations/empty-list-illustration.tsx +26 -0
- package/src/illustrations/error-illustration.tsx +60 -0
- package/src/illustrations/first-run-illustration.tsx +27 -0
- package/src/illustrations/illustration-base.tsx +108 -0
- package/src/illustrations/illustrations.stories.tsx +94 -0
- package/src/illustrations/illustrations.test.tsx +98 -0
- package/src/illustrations/index.ts +16 -0
- package/src/illustrations/no-access-illustration.tsx +45 -0
- package/src/illustrations/no-results-illustration.tsx +46 -0
- package/src/illustrations/offline-illustration.tsx +37 -0
- package/src/illustrations/success-illustration.tsx +40 -0
- package/src/index.ts +75 -2
- package/src/lib/agent-event-model.test.ts +12 -0
- package/src/lib/agent-event-model.ts +42 -0
- package/src/lib/approval-option.test.ts +17 -0
- package/src/lib/approval-option.ts +53 -0
- package/src/lib/check-result.ts +33 -0
- package/src/lib/diff-line.test.ts +36 -0
- package/src/lib/diff-line.ts +63 -0
- package/src/lib/diff-rows.test.ts +76 -0
- package/src/lib/diff-rows.ts +92 -0
- package/src/lib/format-duration.test.ts +22 -0
- package/src/lib/format-duration.ts +30 -0
- package/src/lib/has-renderable-content.test.ts +49 -0
- package/src/lib/has-renderable-content.ts +32 -0
- package/src/lib/operating-mode.test.ts +36 -0
- package/src/lib/operating-mode.ts +81 -0
- package/src/lib/optional-peer.ts +59 -0
- package/src/lib/session-launch.ts +37 -0
- package/src/lib/slash-command.test.ts +44 -0
- package/src/lib/slash-command.ts +45 -0
- package/src/lib/trigger-query.test.ts +94 -0
- package/src/lib/trigger-query.ts +88 -0
- package/src/lib/use-mobile.ts +12 -6
- package/src/motion.stories.tsx +6 -0
- package/src/templates-object-detail-hub.stories.tsx +2 -2
- package/src/templates-screen-states.stories.tsx +5 -5
- package/src/templates-settings.stories.tsx +1 -1
- package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
- package/src/blocks/sidebar-02/logo.tsx +0 -18
- package/src/blocks/sidebar-02/nav-main.tsx +0 -13
- package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
- package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
- package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
- package/src/blocks/sidebar-04/mail-context.tsx +0 -29
- package/src/blocks/sidebar-04/nav-user.tsx +0 -6
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
- package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
- package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* from-json-schema.ts — `fromJsonSchema()`: the narrow, documented JSON
|
|
3
|
+
* Schema → `FormSpec` adapter (issue #22, maintainer ruling 2026-09-01).
|
|
4
|
+
*
|
|
5
|
+
* Many consumers already have their config described as JSON Schema (an
|
|
6
|
+
* OpenAPI request body, a connector manifest). This maps ONLY the common
|
|
7
|
+
* subset such schemas actually use onto `SchemaForm`'s `FieldSpec`
|
|
8
|
+
* vocabulary:
|
|
9
|
+
*
|
|
10
|
+
* { type: "string" } → StringFieldSpec
|
|
11
|
+
* { type: "string", enum: [...] } → EnumFieldSpec
|
|
12
|
+
* { type: "number" | "integer" } → NumberFieldSpec / IntegerFieldSpec
|
|
13
|
+
* { type: "boolean" } → BooleanFieldSpec
|
|
14
|
+
* { type: "array", items: { type: "string" } } → ListFieldSpec (free text)
|
|
15
|
+
* { type: "array",
|
|
16
|
+
* items: { type: "string", enum: [...] } } → MultiEnumFieldSpec
|
|
17
|
+
* (a CONSTRAINED array
|
|
18
|
+
* item schema maps onto
|
|
19
|
+
* the field type that
|
|
20
|
+
* actually enforces the
|
|
21
|
+
* constraint — see
|
|
22
|
+
* "Constraint keywords"
|
|
23
|
+
* below for why this
|
|
24
|
+
* one keyword IS read)
|
|
25
|
+
* { type: "object", properties: {...} } → GroupFieldSpec
|
|
26
|
+
* (variant: "advanced",
|
|
27
|
+
* one always-open branch)
|
|
28
|
+
*
|
|
29
|
+
* `title`/`description` map to `label`/`description`; `required` (the
|
|
30
|
+
* enclosing object schema's array of property names) maps to `required:
|
|
31
|
+
* true`; `default` maps when it is type-compatible; `minLength`/`maxLength`/
|
|
32
|
+
* `pattern`/`minimum`/`maximum`/`minItems`/`maxItems` map to their FieldSpec
|
|
33
|
+
* equivalents. `format` maps only the four values `StringFieldSpec` itself
|
|
34
|
+
* understands (`email`/`uri`/`date`/`date-time`) — any other `format` value
|
|
35
|
+
* (`"binary"`, `"password"`, a custom one) is ignored predictably, the same
|
|
36
|
+
* way an unknown JSON Schema `format` is defined to be advisory, not
|
|
37
|
+
* validating.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately NOT a general draft-07/2020-12 implementation (the maintainer
|
|
40
|
+
* ruling's explicit narrow option) — this module does not resolve `$ref` and
|
|
41
|
+
* does not implement the `allOf`/`oneOf`/`anyOf`/`not` combinators. Those
|
|
42
|
+
* keywords change what a schema actually MEANS, and approximating them would
|
|
43
|
+
* risk producing a field that looks plausible but is wrong (e.g. rendering a
|
|
44
|
+
* `$ref`'d object as an empty string field) — silently wrong is exactly what
|
|
45
|
+
* this adapter must never be. So `fromJsonSchema()` REFUSES: any of those
|
|
46
|
+
* five keywords, found anywhere in the input (top-level or nested inside a
|
|
47
|
+
* property/items schema), throws `UnsupportedJsonSchemaError` naming the
|
|
48
|
+
* keyword and its JSON-Pointer-style path. Pre-resolve/inline that part of
|
|
49
|
+
* the schema before calling `fromJsonSchema()`, or hand-write the affected
|
|
50
|
+
* `FieldSpec`.
|
|
51
|
+
*
|
|
52
|
+
* A property whose `type` this subset does not cover (e.g. `"null"`, a
|
|
53
|
+
* JSON-Schema-2020-12 array-of-types, an array of non-string items, an
|
|
54
|
+
* object schema with no `properties`) is DROPPED from the result —
|
|
55
|
+
* predictable and non-fatal, the same lenient-parse philosophy
|
|
56
|
+
* `normalizeFormSpec` already uses for the render path, because omitting one
|
|
57
|
+
* field is recoverable in a way a mis-mapped `$ref`/combinator is not.
|
|
58
|
+
*
|
|
59
|
+
* CONSTRAINT KEYWORDS this subset does not read: `const`, `if`/`then`/`else`,
|
|
60
|
+
* `dependentSchemas`, `dependentRequired`, `additionalProperties`,
|
|
61
|
+
* `patternProperties`, `propertyNames`, `contains`, `multipleOf`,
|
|
62
|
+
* `exclusiveMinimum`/`exclusiveMaximum`, `uniqueItems`. These are
|
|
63
|
+
* READ-IGNORED, not refused — unlike `$ref`/`allOf`/`oneOf`/`anyOf`/`not`,
|
|
64
|
+
* none of them changes what TYPE a property maps to, so ignoring one never
|
|
65
|
+
* produces a wrongly-shaped field. But ignoring one DOES widen what the
|
|
66
|
+
* rendered form accepts relative to the source schema (a `const`-pinned
|
|
67
|
+
* property renders as an ordinary editable field, not a fixed one; a
|
|
68
|
+
* `multipleOf`-constrained number accepts any value in range). This is the
|
|
69
|
+
* one place `fromJsonSchema()` is honestly silently-permissive rather than
|
|
70
|
+
* silently-wrong or refusing — call it out in review when a schema leans on
|
|
71
|
+
* one of these keywords for validation the mapped form will not enforce.
|
|
72
|
+
* `items.enum` is the one exception among these: a constrained array-item
|
|
73
|
+
* schema is common enough (and cheap enough to map faithfully) that it reads
|
|
74
|
+
* `items.enum` and maps onto `MultiEnumFieldSpec` — see the table above —
|
|
75
|
+
* rather than joining this ignore list and widening into a free-text
|
|
76
|
+
* `ListFieldSpec`.
|
|
77
|
+
*
|
|
78
|
+
* Widening this subset later (more `format`s, `$ref` resolution) is an
|
|
79
|
+
* additive, non-breaking change to this one function — see issue #22's
|
|
80
|
+
* "worth having alongside" list.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
import type { FieldSpec, FormSpec, FormValues } from "./schema-form-spec";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The JSON Schema keywords this adapter refuses to approximate. Each changes
|
|
87
|
+
* a schema's actual shape in a way a narrow field-by-field mapping cannot
|
|
88
|
+
* safely ignore.
|
|
89
|
+
*/
|
|
90
|
+
const UNSUPPORTED_COMBINATORS = ["$ref", "allOf", "oneOf", "anyOf", "not"] as const;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Thrown by `fromJsonSchema()` when the input uses a JSON Schema keyword
|
|
94
|
+
* outside its documented subset (see this module's doc comment) — never
|
|
95
|
+
* silently mismapped.
|
|
96
|
+
*/
|
|
97
|
+
export class UnsupportedJsonSchemaError extends Error {
|
|
98
|
+
constructor(keyword: string, path: string) {
|
|
99
|
+
super(
|
|
100
|
+
`fromJsonSchema: unsupported JSON Schema keyword "${keyword}" at "${path}". ` +
|
|
101
|
+
"fromJsonSchema() maps only a narrow, documented subset (string/number/" +
|
|
102
|
+
"integer/boolean/enum/array-of-string/object) and does not resolve " +
|
|
103
|
+
"$ref or implement allOf/oneOf/anyOf/not. Pre-resolve or inline this " +
|
|
104
|
+
"part of the schema before calling fromJsonSchema(), or build the " +
|
|
105
|
+
"affected FieldSpec by hand.",
|
|
106
|
+
);
|
|
107
|
+
this.name = "UnsupportedJsonSchemaError";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type JsonSchemaObject = Record<string, unknown>;
|
|
112
|
+
|
|
113
|
+
function isPlainObject(value: unknown): value is JsonSchemaObject {
|
|
114
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Recursively asserts none of `UNSUPPORTED_COMBINATORS` appears anywhere under `schema`. */
|
|
118
|
+
function assertSupported(schema: unknown, path: string): void {
|
|
119
|
+
if (!isPlainObject(schema)) return;
|
|
120
|
+
for (const keyword of UNSUPPORTED_COMBINATORS) {
|
|
121
|
+
if (keyword in schema) throw new UnsupportedJsonSchemaError(keyword, path);
|
|
122
|
+
}
|
|
123
|
+
if (isPlainObject(schema.properties)) {
|
|
124
|
+
for (const [key, sub] of Object.entries(schema.properties)) {
|
|
125
|
+
assertSupported(sub, `${path}/properties/${key}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if ("items" in schema) {
|
|
129
|
+
assertSupported(schema.items, `${path}/items`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `values` narrowed to `string[]`, or `null` if any entry isn't a string. */
|
|
134
|
+
function stringArray(values: unknown): string[] | null {
|
|
135
|
+
if (!Array.isArray(values) || !values.every((v) => typeof v === "string")) return null;
|
|
136
|
+
return values as string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const SUPPORTED_STRING_FORMATS = new Set(["email", "uri", "date", "date-time"]);
|
|
140
|
+
|
|
141
|
+
function mapProperty(name: string, schema: unknown, required: boolean): FieldSpec | null {
|
|
142
|
+
if (!isPlainObject(schema)) return null;
|
|
143
|
+
|
|
144
|
+
const title = typeof schema.title === "string" ? schema.title : undefined;
|
|
145
|
+
const description = typeof schema.description === "string" ? schema.description : undefined;
|
|
146
|
+
const base = {
|
|
147
|
+
name,
|
|
148
|
+
...(title !== undefined ? { label: title } : {}),
|
|
149
|
+
...(description !== undefined ? { description } : {}),
|
|
150
|
+
...(required ? { required: true as const } : {}),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
switch (schema.type) {
|
|
154
|
+
case "string": {
|
|
155
|
+
const enumValues = stringArray(schema.enum);
|
|
156
|
+
if (enumValues) {
|
|
157
|
+
return {
|
|
158
|
+
type: "enum",
|
|
159
|
+
...base,
|
|
160
|
+
options: enumValues,
|
|
161
|
+
...(typeof schema.default === "string" ? { default: schema.default } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const format =
|
|
165
|
+
typeof schema.format === "string" && SUPPORTED_STRING_FORMATS.has(schema.format)
|
|
166
|
+
? (schema.format as "email" | "uri" | "date" | "date-time")
|
|
167
|
+
: undefined;
|
|
168
|
+
return {
|
|
169
|
+
type: "string",
|
|
170
|
+
...base,
|
|
171
|
+
...(format !== undefined ? { format } : {}),
|
|
172
|
+
...(Number.isInteger(schema.minLength) && (schema.minLength as number) >= 0
|
|
173
|
+
? { minLength: schema.minLength as number }
|
|
174
|
+
: {}),
|
|
175
|
+
...(Number.isInteger(schema.maxLength) && (schema.maxLength as number) >= 0
|
|
176
|
+
? { maxLength: schema.maxLength as number }
|
|
177
|
+
: {}),
|
|
178
|
+
...(typeof schema.pattern === "string" ? { pattern: schema.pattern } : {}),
|
|
179
|
+
...(typeof schema.default === "string" ? { default: schema.default } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case "number": {
|
|
184
|
+
const validDefault = typeof schema.default === "number";
|
|
185
|
+
return {
|
|
186
|
+
type: "number",
|
|
187
|
+
...base,
|
|
188
|
+
...(typeof schema.minimum === "number" ? { min: schema.minimum } : {}),
|
|
189
|
+
...(typeof schema.maximum === "number" ? { max: schema.maximum } : {}),
|
|
190
|
+
...(validDefault ? { default: schema.default as number } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "integer": {
|
|
195
|
+
const validDefault = typeof schema.default === "number" && Number.isInteger(schema.default);
|
|
196
|
+
return {
|
|
197
|
+
type: "integer",
|
|
198
|
+
...base,
|
|
199
|
+
...(typeof schema.minimum === "number" ? { min: schema.minimum } : {}),
|
|
200
|
+
...(typeof schema.maximum === "number" ? { max: schema.maximum } : {}),
|
|
201
|
+
...(validDefault ? { default: schema.default as number } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case "boolean":
|
|
206
|
+
return {
|
|
207
|
+
type: "boolean",
|
|
208
|
+
...base,
|
|
209
|
+
...(typeof schema.default === "boolean" ? { default: schema.default } : {}),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
case "array": {
|
|
213
|
+
// Only array-of-string is in the documented subset (issue #22's
|
|
214
|
+
// "array-of-string" entry) — an array of numbers/objects/booleans, or
|
|
215
|
+
// one with no `items` schema at all, is dropped predictably rather than
|
|
216
|
+
// guessed at.
|
|
217
|
+
if (!isPlainObject(schema.items) || schema.items.type !== "string") return null;
|
|
218
|
+
|
|
219
|
+
// A CONSTRAINED item schema (`items.enum`) is a multi-select from a
|
|
220
|
+
// fixed option list, not free text — map it onto the FieldSpec that
|
|
221
|
+
// actually enforces that constraint (`multi-enum`) rather than
|
|
222
|
+
// relaxing it into an unconstrained `list` (fix-round-1, issue #22: a
|
|
223
|
+
// validator probe found `{type:'array', items:{type:'string',
|
|
224
|
+
// enum:[...]}}` was silently widening to free text). `minItems`/
|
|
225
|
+
// `maxItems` have no `multi-enum` equivalent and are dropped here —
|
|
226
|
+
// documented in this module's doc comment alongside the other
|
|
227
|
+
// constraint keywords this subset does not enforce.
|
|
228
|
+
const itemEnum = stringArray(schema.items.enum);
|
|
229
|
+
if (itemEnum) {
|
|
230
|
+
const enumDefault = stringArray(schema.default);
|
|
231
|
+
return {
|
|
232
|
+
type: "multi-enum",
|
|
233
|
+
...base,
|
|
234
|
+
options: itemEnum,
|
|
235
|
+
...(enumDefault && enumDefault.every((v) => itemEnum.includes(v))
|
|
236
|
+
? { default: enumDefault }
|
|
237
|
+
: {}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const listDefault = stringArray(schema.default);
|
|
242
|
+
return {
|
|
243
|
+
type: "list",
|
|
244
|
+
...base,
|
|
245
|
+
...(Number.isInteger(schema.minItems) && (schema.minItems as number) >= 0
|
|
246
|
+
? { minItems: schema.minItems as number }
|
|
247
|
+
: {}),
|
|
248
|
+
...(Number.isInteger(schema.maxItems) && (schema.maxItems as number) >= 0
|
|
249
|
+
? { maxItems: schema.maxItems as number }
|
|
250
|
+
: {}),
|
|
251
|
+
...(listDefault ? { default: listDefault } : {}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case "object": {
|
|
256
|
+
const nested = mapProperties(schema);
|
|
257
|
+
if (nested.length === 0) return null; // an empty/property-less object has nothing to render
|
|
258
|
+
return {
|
|
259
|
+
type: "group",
|
|
260
|
+
name,
|
|
261
|
+
...(title !== undefined ? { label: title } : {}),
|
|
262
|
+
...(description !== undefined ? { description } : {}),
|
|
263
|
+
...(required ? { required: true } : {}),
|
|
264
|
+
variant: "advanced",
|
|
265
|
+
groups: [{ key: name, label: title ?? name, fields: nested }],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
default:
|
|
270
|
+
// An unrecognized/absent `type` (JSON-Schema-2020-12 type arrays,
|
|
271
|
+
// `"null"`, a schema with no `type` at all) — outside the documented
|
|
272
|
+
// subset, dropped predictably rather than mismapped.
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Maps every property of an OBJECT schema onto `FieldSpec[]`, dropping unsupported ones. */
|
|
278
|
+
function mapProperties(objectSchema: JsonSchemaObject): FieldSpec[] {
|
|
279
|
+
const properties = isPlainObject(objectSchema.properties) ? objectSchema.properties : {};
|
|
280
|
+
const required = stringArray(objectSchema.required) ?? [];
|
|
281
|
+
const fields: FieldSpec[] = [];
|
|
282
|
+
for (const [name, propertySchema] of Object.entries(properties)) {
|
|
283
|
+
const field = mapProperty(name, propertySchema, required.includes(name));
|
|
284
|
+
if (field) fields.push(field);
|
|
285
|
+
}
|
|
286
|
+
return fields;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface FromJsonSchemaOptions {
|
|
290
|
+
/** `FormSpec.formName` — required; JSON Schema has no equivalent stable identifier. */
|
|
291
|
+
formName: string;
|
|
292
|
+
/** Overrides the mapped `schema.title`. */
|
|
293
|
+
title?: string;
|
|
294
|
+
/** Overrides the mapped `schema.description`. */
|
|
295
|
+
description?: string;
|
|
296
|
+
/** `FormSpec.submitLabel` — JSON Schema has no equivalent. */
|
|
297
|
+
submitLabel?: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Maps a JSON Schema OBJECT schema (an OpenAPI request body, a connector
|
|
302
|
+
* manifest) onto a `FormSpec`, using only the documented subset described in
|
|
303
|
+
* this module's doc comment. Throws `UnsupportedJsonSchemaError` if `schema`
|
|
304
|
+
* (at any depth) uses `$ref`/`allOf`/`oneOf`/`anyOf`/`not`, or if the
|
|
305
|
+
* top-level schema is not an object schema — never silently mismapped. A
|
|
306
|
+
* property whose shape falls outside the subset is dropped, not thrown on.
|
|
307
|
+
*/
|
|
308
|
+
export function fromJsonSchema(schema: unknown, options: FromJsonSchemaOptions): FormSpec {
|
|
309
|
+
assertSupported(schema, "#");
|
|
310
|
+
if (!isPlainObject(schema) || schema.type !== "object") {
|
|
311
|
+
throw new UnsupportedJsonSchemaError('type (expected "object" at the top level)', "#");
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
formName: options.formName,
|
|
315
|
+
title: options.title ?? (typeof schema.title === "string" ? schema.title : undefined),
|
|
316
|
+
description:
|
|
317
|
+
options.description ??
|
|
318
|
+
(typeof schema.description === "string" ? schema.description : undefined),
|
|
319
|
+
submitLabel: options.submitLabel,
|
|
320
|
+
fields: mapProperties(schema),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Reconstructs the NESTED JSON object shape that `fromJsonSchema()`'s
|
|
326
|
+
* `{ type: "object", properties: {...} }` mapping flattens away (PR #119
|
|
327
|
+
* review thread 3, P1). `SchemaForm` stores every field's value in one flat
|
|
328
|
+
* `FormValues` map keyed by BARE property name (`GroupFieldSpec` — see the
|
|
329
|
+
* "no notion of an active branch" comment on `schema-form-spec.ts`'s
|
|
330
|
+
* `GroupFieldSpec` — has no value slot of its own), so a form built from a
|
|
331
|
+
* nested schema like `{ address: { properties: { city, zip } } }` submits
|
|
332
|
+
* `{ city, zip }` instead of the advertised `{ address: { city, zip } }`.
|
|
333
|
+
*
|
|
334
|
+
* Call this (instead of reading `effectiveValues`/`onSubmit`'s payload
|
|
335
|
+
* directly) when the schema passed to `fromJsonSchema()` had nested
|
|
336
|
+
* `"object"` properties and the caller needs the request body back in its
|
|
337
|
+
* original shape — e.g. `SchemaFormTestAction`'s `onTest` or a submit
|
|
338
|
+
* handler that forwards the payload to the API the schema describes.
|
|
339
|
+
*
|
|
340
|
+
* Walks `spec.fields` and, for each `group` field this adapter produced
|
|
341
|
+
* (always `variant: "advanced"`, always exactly one branch — see
|
|
342
|
+
* `mapProperty`'s `"object"` case), recurses into `groups[0].fields` and
|
|
343
|
+
* nests the result under that group's own `name` instead of splicing its
|
|
344
|
+
* children into the top level.
|
|
345
|
+
*
|
|
346
|
+
* Two known, out-of-scope limits (see the reply on review thread 3):
|
|
347
|
+
* 1. Two properties at DIFFERENT nesting depths that happen to share a bare
|
|
348
|
+
* name (`{ name }` at the top level and `{ address: { name } }`) collide
|
|
349
|
+
* in the flat `FormValues` map itself, upstream of this function — this
|
|
350
|
+
* function cannot recover data `SchemaForm` never kept separate.
|
|
351
|
+
* 2. A nested object's own `required` flag (`GroupFieldSpec.required`) is
|
|
352
|
+
* mapped but never validated anywhere in `schema-form-spec.ts`'s
|
|
353
|
+
* `collectValidatableFields` — fixing that touches a file outside this
|
|
354
|
+
* unit's scope fence and is called out, unfixed, in the PR review reply.
|
|
355
|
+
*/
|
|
356
|
+
export function jsonSchemaRequestBody(spec: FormSpec, values: FormValues): Record<string, unknown> {
|
|
357
|
+
return collectRequestBody(spec.fields, values);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function collectRequestBody(fields: FieldSpec[], values: FormValues): Record<string, unknown> {
|
|
361
|
+
const body: Record<string, unknown> = {};
|
|
362
|
+
for (const field of fields) {
|
|
363
|
+
if (field.type === "group") {
|
|
364
|
+
// `fromJsonSchema()` only ever emits a single-branch `"advanced"`
|
|
365
|
+
// group per nested object (`mapProperty`'s `"object"` case) — nest
|
|
366
|
+
// the branch's reconstructed values under the group's own name.
|
|
367
|
+
const branch = field.groups[0];
|
|
368
|
+
if (branch) body[field.name] = collectRequestBody(branch.fields, values);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const value = values[field.name];
|
|
372
|
+
if (value !== undefined) body[field.name] = value;
|
|
373
|
+
}
|
|
374
|
+
return body;
|
|
375
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SchemaForm,
|
|
3
|
+
SchemaFormProvider,
|
|
4
|
+
SchemaFormRoot,
|
|
5
|
+
SchemaFormFields,
|
|
6
|
+
SchemaFormField,
|
|
7
|
+
SchemaFormSubmit,
|
|
8
|
+
SchemaFormTestAction,
|
|
9
|
+
SchemaFormError,
|
|
10
|
+
SchemaFormTitle,
|
|
11
|
+
SchemaFormDescription,
|
|
12
|
+
SchemaFormFallback,
|
|
13
|
+
type SchemaFormProps,
|
|
14
|
+
type SchemaFormProviderProps,
|
|
15
|
+
type SchemaFormRootProps,
|
|
16
|
+
type SchemaFormFieldsProps,
|
|
17
|
+
type SchemaFormFieldProps,
|
|
18
|
+
type SchemaFormSubmitProps,
|
|
19
|
+
type SchemaFormTestActionProps,
|
|
20
|
+
type SchemaFormTestActionStatus,
|
|
21
|
+
type SchemaFormErrorProps,
|
|
22
|
+
type SchemaFormTitleProps,
|
|
23
|
+
type SchemaFormDescriptionProps,
|
|
24
|
+
type SchemaFormFallbackProps,
|
|
25
|
+
} from "./schema-form";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
fromJsonSchema,
|
|
29
|
+
UnsupportedJsonSchemaError,
|
|
30
|
+
type FromJsonSchemaOptions,
|
|
31
|
+
} from "./from-json-schema";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
fieldSpecSchema,
|
|
35
|
+
formSpecSchema,
|
|
36
|
+
enumOptionSchema,
|
|
37
|
+
keyValueRowSchema,
|
|
38
|
+
stringFieldSchema,
|
|
39
|
+
numberFieldSchema,
|
|
40
|
+
integerFieldSchema,
|
|
41
|
+
booleanFieldSchema,
|
|
42
|
+
enumFieldSchema,
|
|
43
|
+
multiEnumFieldSchema,
|
|
44
|
+
listFieldSchema,
|
|
45
|
+
keyValueFieldSchema,
|
|
46
|
+
fileFieldSchema,
|
|
47
|
+
groupItemSchema,
|
|
48
|
+
groupFieldSchema,
|
|
49
|
+
optionValue,
|
|
50
|
+
optionLabel,
|
|
51
|
+
fieldLabel,
|
|
52
|
+
initialFormValues,
|
|
53
|
+
normalizeFormSpec,
|
|
54
|
+
validateField,
|
|
55
|
+
validateForm,
|
|
56
|
+
findFieldByName,
|
|
57
|
+
collectValidatableFields,
|
|
58
|
+
isFieldVisible,
|
|
59
|
+
fileMatchesAccept,
|
|
60
|
+
checkFileIssue,
|
|
61
|
+
type EnumOption,
|
|
62
|
+
type FieldKeyValueRow,
|
|
63
|
+
type StringFieldSpec,
|
|
64
|
+
type NumberFieldSpec,
|
|
65
|
+
type IntegerFieldSpec,
|
|
66
|
+
type BooleanFieldSpec,
|
|
67
|
+
type EnumFieldSpec,
|
|
68
|
+
type MultiEnumFieldSpec,
|
|
69
|
+
type ListFieldSpec,
|
|
70
|
+
type KeyValueFieldSpec,
|
|
71
|
+
type FileFieldSpec,
|
|
72
|
+
type GroupItemSpec,
|
|
73
|
+
type GroupFieldSpec,
|
|
74
|
+
type FieldSpec,
|
|
75
|
+
type FormSpec,
|
|
76
|
+
type FormValue,
|
|
77
|
+
type FormValues,
|
|
78
|
+
type FormSubmitState,
|
|
79
|
+
type NormalizedFormSpec,
|
|
80
|
+
type FileIssue,
|
|
81
|
+
} from "./schema-form-spec";
|