@tai42/studio-sdk 9.3.0 → 11.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 +45 -6
- package/SECURITY.md +10 -2
- package/dist/components/components.css +18 -0
- package/dist/components/dirty-guard.d.ts +23 -10
- package/dist/components/dirty-guard.d.ts.map +1 -1
- package/dist/components/dirty-guard.js +22 -12
- package/dist/components/dirty-guard.js.map +1 -1
- package/dist/components/field.d.ts.map +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/field.js.map +1 -1
- package/dist/components/fleet-report.d.ts +12 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +28 -9
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/icons.d.ts +4 -0
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/components/icons.js +5 -0
- package/dist/components/icons.js.map +1 -1
- package/dist/hooks/useApi.d.ts +348 -2
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +18 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -11
- package/dist/index.js.map +1 -1
- package/dist/navigation/context.d.ts +6 -2
- package/dist/navigation/context.d.ts.map +1 -1
- package/dist/navigation/context.js +10 -2
- package/dist/navigation/context.js.map +1 -1
- package/dist/navigation/types.d.ts +1 -0
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/host-state.d.ts +16 -0
- package/dist/plugin/host-state.d.ts.map +1 -1
- package/dist/plugin/host-state.js +1 -0
- package/dist/plugin/host-state.js.map +1 -1
- package/dist/plugin/types.d.ts +17 -7
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/schema-form/SchemaForm.d.ts +4 -3
- package/dist/schema-form/SchemaForm.d.ts.map +1 -1
- package/dist/schema-form/SchemaForm.js.map +1 -1
- package/dist/schema-form/SecretRefField.d.ts.map +1 -1
- package/dist/schema-form/SecretRefField.js +3 -2
- package/dist/schema-form/SecretRefField.js.map +1 -1
- package/dist/schema-form/context.d.ts +2 -2
- package/dist/schema-form/default-value.d.ts +12 -0
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +34 -3
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/field-node.d.ts +7 -0
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +3 -2
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/index.d.ts +11 -11
- package/dist/schema-form/index.js +11 -11
- package/dist/schema-form/object-fields.d.ts +3 -1
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +2 -2
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/string-field.d.ts +3 -2
- package/dist/schema-form/string-field.d.ts.map +1 -1
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/schema-form/union-field.d.ts +5 -2
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js +68 -15
- package/dist/schema-form/union-field.js.map +1 -1
- package/package.json +3 -3
- package/src/components/components.css +18 -0
- package/src/components/dirty-guard.tsx +23 -13
- package/src/components/field.tsx +10 -5
- package/src/components/fleet-report.tsx +44 -13
- package/src/components/icons.tsx +19 -0
- package/src/host.ts +5 -1
- package/src/index.ts +24 -31
- package/src/navigation/context.tsx +16 -1
- package/src/navigation/types.ts +3 -0
- package/src/plugin/host-state.ts +18 -0
- package/src/plugin/types.ts +17 -7
- package/src/schema-form/SchemaForm.tsx +4 -3
- package/src/schema-form/SecretRefField.tsx +3 -2
- package/src/schema-form/context.ts +2 -2
- package/src/schema-form/default-value.ts +36 -3
- package/src/schema-form/field-node.tsx +21 -2
- package/src/schema-form/index.ts +11 -11
- package/src/schema-form/object-fields.tsx +4 -0
- package/src/schema-form/string-field.tsx +3 -2
- package/src/schema-form/union-field.tsx +85 -19
- package/dist/jq.d.ts +0 -31
- package/dist/jq.d.ts.map +0 -1
- package/dist/jq.js +0 -41
- package/dist/jq.js.map +0 -1
- package/src/jq.ts +0 -61
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
type AriaAttributes,
|
|
11
|
+
type CSSProperties,
|
|
11
12
|
type MouseEvent,
|
|
12
13
|
type ReactNode,
|
|
13
14
|
} from 'react';
|
|
@@ -453,6 +454,10 @@ export interface AppLinkProps<T extends RouteToken> {
|
|
|
453
454
|
className?: string;
|
|
454
455
|
'aria-label'?: string;
|
|
455
456
|
'aria-current'?: AriaAttributes['aria-current'];
|
|
457
|
+
/** Native tooltip text (also a sensible non-AT hover hint). */
|
|
458
|
+
title?: string;
|
|
459
|
+
/** Inline styles for the anchor. */
|
|
460
|
+
style?: CSSProperties;
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
/**
|
|
@@ -467,6 +472,8 @@ export function AppLink<T extends RouteToken>({
|
|
|
467
472
|
className,
|
|
468
473
|
'aria-label': ariaLabel,
|
|
469
474
|
'aria-current': ariaCurrent,
|
|
475
|
+
title,
|
|
476
|
+
style,
|
|
470
477
|
}: AppLinkProps<T>): ReactNode {
|
|
471
478
|
const { navigate, resolvePath } = useNavigation();
|
|
472
479
|
const href = resolvePath(to, search);
|
|
@@ -482,7 +489,15 @@ export function AppLink<T extends RouteToken>({
|
|
|
482
489
|
);
|
|
483
490
|
return createElement(
|
|
484
491
|
'a',
|
|
485
|
-
{
|
|
492
|
+
{
|
|
493
|
+
href,
|
|
494
|
+
className,
|
|
495
|
+
'aria-label': ariaLabel,
|
|
496
|
+
'aria-current': ariaCurrent,
|
|
497
|
+
title,
|
|
498
|
+
style,
|
|
499
|
+
onClick,
|
|
500
|
+
},
|
|
486
501
|
children,
|
|
487
502
|
);
|
|
488
503
|
}
|
package/src/navigation/types.ts
CHANGED
|
@@ -41,6 +41,9 @@ export interface RouteSearchByToken {
|
|
|
41
41
|
q?: string;
|
|
42
42
|
};
|
|
43
43
|
connectors: { connection?: string };
|
|
44
|
+
// The Served Endpoints surface (the promoted sub-MCP servers): a curated subset
|
|
45
|
+
// of tools served on a transport. No search parameters — a flat list + create form.
|
|
46
|
+
servedEndpoints: Record<string, never>;
|
|
44
47
|
hooks: Record<string, never>;
|
|
45
48
|
templates: { template?: string; q?: string };
|
|
46
49
|
storage: { q?: string };
|
package/src/plugin/host-state.ts
CHANGED
|
@@ -16,6 +16,17 @@ import { useSyncExternalStore } from 'react';
|
|
|
16
16
|
import type { PluginContributions } from './types';
|
|
17
17
|
import { getContributions } from './registry';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Identity of a successfully-loaded plugin, for host chrome that must attribute a
|
|
21
|
+
* contribution to its origin (e.g. the shell's provenance badge on plugin nav). The
|
|
22
|
+
* `name` is the same id the registry stamps on every contribution (`pluginId`); the
|
|
23
|
+
* `version` comes from the plugin's manifest.
|
|
24
|
+
*/
|
|
25
|
+
export interface LoadedPlugin {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly version: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* The plugin load pass's public state, mirrored for feature pages to read. It is
|
|
21
32
|
* the shape the app shell's loader store already exposes: `status` gates when the
|
|
@@ -26,6 +37,12 @@ export interface PluginLoaderState {
|
|
|
26
37
|
readonly status: 'idle' | 'loading' | 'ready';
|
|
27
38
|
/** Names of plugins whose bundle imported and registered successfully. */
|
|
28
39
|
readonly loaded: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Identity (name + version) of every successfully-loaded plugin, parallel to
|
|
42
|
+
* `loaded`. The host reads it to attribute a contribution to its plugin — e.g. the
|
|
43
|
+
* nav provenance badge's "Plugin: name version". Empty before any plugin loads.
|
|
44
|
+
*/
|
|
45
|
+
readonly plugins: readonly LoadedPlugin[];
|
|
29
46
|
/** Plugin name → loud error message (version mismatch / load failure). */
|
|
30
47
|
readonly errors: Readonly<Record<string, string>>;
|
|
31
48
|
/** A registry-listing failure that is NOT a 401 (surfaced loudly, never hidden). */
|
|
@@ -35,6 +52,7 @@ export interface PluginLoaderState {
|
|
|
35
52
|
const INITIAL_HOST_STATE: PluginLoaderState = {
|
|
36
53
|
status: 'idle',
|
|
37
54
|
loaded: [],
|
|
55
|
+
plugins: [],
|
|
38
56
|
errors: {},
|
|
39
57
|
registryError: null,
|
|
40
58
|
};
|
package/src/plugin/types.ts
CHANGED
|
@@ -142,13 +142,15 @@ export interface RegisteredPage extends PageContribution {
|
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
144
|
* The sidebar section a nav entry renders in. A named core section groups the
|
|
145
|
-
* entry there, after that section's core rows
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
145
|
+
* entry there, after that section's core rows. An absent field or any value that
|
|
146
|
+
* is not a live core section renders the entry as a FIRST-CLASS section of its own,
|
|
147
|
+
* named for the contributing plugin (grouped with that plugin's other such entries),
|
|
148
|
+
* placed after the core sections. The shell tolerates an unknown value at runtime
|
|
149
|
+
* (a bundle newer than this host), falling back to that self-named section, so the
|
|
150
|
+
* field is a placement hint, never a hard contract.
|
|
149
151
|
*/
|
|
150
152
|
export type NavEntrySection =
|
|
151
|
-
'Capabilities' | '
|
|
153
|
+
'Capabilities' | 'Connections' | 'Triggers' | 'Activity' | 'Administration';
|
|
152
154
|
|
|
153
155
|
export interface NavEntryContribution {
|
|
154
156
|
/**
|
|
@@ -160,10 +162,18 @@ export interface NavEntryContribution {
|
|
|
160
162
|
readonly title: string;
|
|
161
163
|
/**
|
|
162
164
|
* Optional target sidebar section (see {@link NavEntrySection}); absent ⇒ the
|
|
163
|
-
*
|
|
164
|
-
* plugin API version.
|
|
165
|
+
* plugin's own self-named section. Additive — an older bundle omits it and stays
|
|
166
|
+
* on the same plugin API version.
|
|
165
167
|
*/
|
|
166
168
|
readonly section?: NavEntrySection;
|
|
169
|
+
/**
|
|
170
|
+
* Optional sort weight WITHIN the entry's section (whether a core section or the
|
|
171
|
+
* plugin's own self-named one): entries render in ascending `order`, and every
|
|
172
|
+
* entry that omits it sorts AFTER the ordered ones, in registration order. Two
|
|
173
|
+
* entries with the same `order` keep registration order (a stable sort). Additive —
|
|
174
|
+
* an older bundle omits it and stays on the same plugin API version.
|
|
175
|
+
*/
|
|
176
|
+
readonly order?: number;
|
|
167
177
|
/**
|
|
168
178
|
* Optional icon rendered before the title. It must be a square inline SVG that
|
|
169
179
|
* fills its box and draws with `currentColor`; the host constrains the slot,
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
* checks live in the sibling `media` module.
|
|
35
35
|
*
|
|
36
36
|
* EXPRESSION FIELDS ARE OPT-IN. A string field carrying `x-tai42-expression`
|
|
37
|
-
* renders an expression-authoring door —
|
|
37
|
+
* renders an expression-authoring door — `JqField` from `@tai42/jq-studio` — only
|
|
38
|
+
* when the host
|
|
38
39
|
* INJECTS one, through {@link SchemaFormProps.expressionField} or the ambient
|
|
39
40
|
* {@link ExpressionFieldContext}; with no door it renders the plain string input.
|
|
40
41
|
* The form owns no edge to the door: importing it here, even dynamically behind
|
|
@@ -88,8 +89,8 @@ export interface SchemaFormProps {
|
|
|
88
89
|
readonly maxUploadBytes?: number;
|
|
89
90
|
/**
|
|
90
91
|
* Optional. The expression-authoring door for `x-tai42-expression` fields —
|
|
91
|
-
*
|
|
92
|
-
* the module doc-comment). Omitted, the enclosing
|
|
92
|
+
* `JqField` from `@tai42/jq-studio`, which the host imports directly and passes
|
|
93
|
+
* here (see the module doc-comment). Omitted, the enclosing
|
|
93
94
|
* {@link ExpressionFieldContext} supplies it; absent both, an annotated field
|
|
94
95
|
* renders the plain string input.
|
|
95
96
|
*/
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* EITHER a reference to an existing env key OR a freshly pasted secret. It knows
|
|
4
4
|
* nothing about the `!ENV ${KEY}` marker syntax or any wire format: it emits a
|
|
5
5
|
* discriminated {@link SecretRef} and the host maps that to whatever it stores
|
|
6
|
-
* (the consumer that mounts this — the
|
|
7
|
-
* marker for a `key` ref and runs the combined
|
|
6
|
+
* (the consumer that mounts this — the connectors page's McpServersSection —
|
|
7
|
+
* writes an `!ENV ${KEY}` marker for a `key` ref and runs the combined
|
|
8
|
+
* store-then-mark op for a `paste`).
|
|
8
9
|
*
|
|
9
10
|
* WRITE-ONLY for a pasted secret: the plaintext lives only in local editor state
|
|
10
11
|
* until the user commits it, at which point it leaves the DOM entirely (held only
|
|
@@ -98,8 +98,8 @@ export interface ExpressionFieldProps {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* The expression-authoring door a host INJECTS into the form — `JqField`,
|
|
102
|
-
*
|
|
101
|
+
* The expression-authoring door a host INJECTS into the form — `JqField`, which the
|
|
102
|
+
* host imports straight from `@tai42/jq-studio`.
|
|
103
103
|
*
|
|
104
104
|
* It is a COMPONENT, rendered as an element, never called as a function: the door
|
|
105
105
|
* owns hooks and state (editor open, worker handle), which only survive as its own
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* `default` — an optional object/array is NOT auto-materialized, so its own
|
|
11
11
|
* required children can't raise spurious errors on a field the user never
|
|
12
12
|
* opted into.
|
|
13
|
+
*
|
|
14
|
+
* `skeletonValueForSchema` is the sibling entry for programmatic reseeds (a
|
|
15
|
+
* union variant switch): same walk, but required scalars mint their type's
|
|
16
|
+
* empty value so the committed seed validates against its own schema.
|
|
13
17
|
*/
|
|
14
18
|
import { classifySchema } from './classify';
|
|
15
19
|
import type { JsonSchema } from './types';
|
|
@@ -30,6 +34,30 @@ function explicitDefault(
|
|
|
30
34
|
* whole-tool-schema call).
|
|
31
35
|
*/
|
|
32
36
|
export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
|
|
37
|
+
return seedValue(schema, root, false);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A SELF-CONSISTENT skeleton for a schema — the seed a variant switch (or any
|
|
42
|
+
* other programmatic reseed) commits as the field's new value. Where
|
|
43
|
+
* {@link defaultValueForSchema} deliberately leaves a required scalar absent so
|
|
44
|
+
* a fresh form flags it, a committed skeleton must validate against its own
|
|
45
|
+
* schema, so here EVERY required field with a known empty value is minted:
|
|
46
|
+
* required strings seed `''`, numbers `0`, booleans `false` — consistently with
|
|
47
|
+
* the `[]` / `{}` / nested-object seeds the default path already mints. Kinds
|
|
48
|
+
* with no empty value to name (enum, union, free-form JSON) stay absent either
|
|
49
|
+
* way: inventing a choice would be a lie the user never made.
|
|
50
|
+
*/
|
|
51
|
+
export function skeletonValueForSchema(schema: JsonSchema, root: JsonSchema = schema): unknown {
|
|
52
|
+
return seedValue(schema, root, true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The shared seeding walk. `mintScalars` distinguishes the two entry points
|
|
57
|
+
* above: `false` leaves value-less required scalars absent; `true` mints their
|
|
58
|
+
* type's empty value.
|
|
59
|
+
*/
|
|
60
|
+
function seedValue(schema: JsonSchema, root: JsonSchema, mintScalars: boolean): unknown {
|
|
33
61
|
const classified = classifySchema(schema, root);
|
|
34
62
|
const explicit = explicitDefault(schema, classified.schema);
|
|
35
63
|
if (explicit.present) return explicit.value;
|
|
@@ -42,7 +70,7 @@ export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = sch
|
|
|
42
70
|
const result: Record<string, unknown> = {};
|
|
43
71
|
for (const [name, propSchema] of model.properties) {
|
|
44
72
|
if (model.required.has(name)) {
|
|
45
|
-
const seeded =
|
|
73
|
+
const seeded = seedValue(propSchema, root, mintScalars);
|
|
46
74
|
if (seeded !== undefined) result[name] = seeded;
|
|
47
75
|
continue;
|
|
48
76
|
}
|
|
@@ -56,15 +84,20 @@ export function defaultValueForSchema(schema: JsonSchema, root: JsonSchema = sch
|
|
|
56
84
|
return [];
|
|
57
85
|
case 'record':
|
|
58
86
|
return {};
|
|
59
|
-
case 'enum':
|
|
60
87
|
case 'string':
|
|
88
|
+
return mintScalars ? '' : undefined;
|
|
61
89
|
case 'number':
|
|
90
|
+
return mintScalars ? 0 : undefined;
|
|
62
91
|
case 'boolean':
|
|
92
|
+
return mintScalars ? false : undefined;
|
|
93
|
+
case 'enum':
|
|
63
94
|
case 'union':
|
|
64
95
|
case 'json':
|
|
65
96
|
// A free-form JSON field starts ABSENT (its editor seeds an empty buffer),
|
|
66
97
|
// like the other value-less kinds: an optional one never invents a value,
|
|
67
|
-
// and a required one is left absent so validation flags it.
|
|
98
|
+
// and a required one is left absent so validation flags it. These kinds
|
|
99
|
+
// have no honest empty value even for a minted skeleton — an enum or a
|
|
100
|
+
// union would need a CHOICE invented on the user's behalf.
|
|
68
101
|
return undefined;
|
|
69
102
|
}
|
|
70
103
|
}
|
|
@@ -30,6 +30,13 @@ interface FieldNodeProps {
|
|
|
30
30
|
/** The label to show — a property key, array-item caption, or `undefined` at root. */
|
|
31
31
|
readonly label: string | undefined;
|
|
32
32
|
readonly required: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Append the design system's required marker (a trailing ` *`, the same
|
|
35
|
+
* convention host forms use on their own labels) to the heading when the
|
|
36
|
+
* field is required. Opt-in per face: the union variant surface sets it so
|
|
37
|
+
* the ACTIVE variant's obligations read at a glance.
|
|
38
|
+
*/
|
|
39
|
+
readonly markRequired?: boolean;
|
|
33
40
|
readonly errors: SchemaFormErrors | undefined;
|
|
34
41
|
readonly idPrefix: string;
|
|
35
42
|
}
|
|
@@ -40,9 +47,21 @@ function labelText(explicit: string | undefined, label: string | undefined): str
|
|
|
40
47
|
|
|
41
48
|
/** A single schema node, dispatched on its classified model kind. */
|
|
42
49
|
export function FieldNode(props: FieldNodeProps): ReactNode {
|
|
43
|
-
const {
|
|
50
|
+
const {
|
|
51
|
+
schema,
|
|
52
|
+
root,
|
|
53
|
+
value,
|
|
54
|
+
onChange,
|
|
55
|
+
path,
|
|
56
|
+
label,
|
|
57
|
+
required,
|
|
58
|
+
markRequired = false,
|
|
59
|
+
errors,
|
|
60
|
+
idPrefix,
|
|
61
|
+
} = props;
|
|
44
62
|
const classified = classifySchema(schema, root);
|
|
45
|
-
const
|
|
63
|
+
const base = labelText(classified.title, label);
|
|
64
|
+
const heading = markRequired && required ? `${base} *` : base;
|
|
46
65
|
const description = classified.description;
|
|
47
66
|
const error = errors?.[path];
|
|
48
67
|
const model = classified.model;
|
package/src/schema-form/index.ts
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* component plus the value/validation helpers a caller needs to seed, validate,
|
|
5
5
|
* and submit.
|
|
6
6
|
*
|
|
7
|
-
* THE
|
|
8
|
-
* `@tai42/studio-sdk/schema-form
|
|
9
|
-
* standalone widget rather than a host serving the SDK as a shared module —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `SchemaForm`'s `expressionField`),
|
|
17
|
-
* jq bytes.
|
|
7
|
+
* THE FORM-ONLY ENTRY POINT. This module is published as
|
|
8
|
+
* `@tai42/studio-sdk/schema-form` for a consumer that BUNDLES the SDK — a
|
|
9
|
+
* standalone widget rather than a host serving the SDK as a shared module — and
|
|
10
|
+
* wants the form without the barrel: no CSS side effects, and no module graph
|
|
11
|
+
* beyond what the form itself renders.
|
|
12
|
+
*
|
|
13
|
+
* It is also jq-free, and so is the barrel: the visual editor lives in the
|
|
14
|
+
* separately published `@tai42/jq-studio`, which nothing under the SDK imports at
|
|
15
|
+
* runtime. Expression authoring reaches a form only by INJECTION (see
|
|
16
|
+
* `SchemaForm`'s `expressionField` and `ExpressionFieldContext`), so a consumer
|
|
17
|
+
* that never injects a door ships zero jq bytes through either entry point.
|
|
18
18
|
*
|
|
19
19
|
* The design system reaches this entry point through relative imports, so the
|
|
20
20
|
* form renders complete; only the barrel's three CSS side-effect imports stay
|
|
@@ -23,6 +23,7 @@ export function ObjectFields({
|
|
|
23
23
|
errors,
|
|
24
24
|
idPrefix,
|
|
25
25
|
skip,
|
|
26
|
+
markRequired = false,
|
|
26
27
|
}: {
|
|
27
28
|
properties: readonly (readonly [string, JsonSchema])[];
|
|
28
29
|
requiredKeys: ReadonlySet<string>;
|
|
@@ -33,6 +34,8 @@ export function ObjectFields({
|
|
|
33
34
|
errors: SchemaFormErrors | undefined;
|
|
34
35
|
idPrefix: string;
|
|
35
36
|
skip: string | undefined;
|
|
37
|
+
/** Mark each required property's label with the DS required marker. */
|
|
38
|
+
markRequired?: boolean;
|
|
36
39
|
}): ReactNode {
|
|
37
40
|
const obj = isPlainObject(value) ? value : {};
|
|
38
41
|
return (
|
|
@@ -52,6 +55,7 @@ export function ObjectFields({
|
|
|
52
55
|
path={childPath}
|
|
53
56
|
label={name}
|
|
54
57
|
required={requiredKeys.has(name)}
|
|
58
|
+
markRequired={markRequired}
|
|
55
59
|
errors={errors}
|
|
56
60
|
idPrefix={idPrefix}
|
|
57
61
|
/>
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* control instead; and when the form supplies a completion provider, the input is
|
|
7
7
|
* backed by argument autocomplete.
|
|
8
8
|
*
|
|
9
|
-
* NO JQ EDGE. This module — and every module under `schema-form
|
|
10
|
-
*
|
|
9
|
+
* NO JQ EDGE. This module — and every module under `schema-form`, and the package
|
|
10
|
+
* barrel above them — names no jq type and imports `@tai42/jq-studio` neither
|
|
11
|
+
* statically NOR dynamically.
|
|
11
12
|
* The jq authoring door drags a heavy subgraph (the xyflow visual editor, a Web
|
|
12
13
|
* Worker entry, and a multi-megabyte wasm engine), and a bundler EMITS that whole
|
|
13
14
|
* subgraph for a dynamic import just as it does for a static one — `lazy` defers
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The union field renderer: a variant picker plus the active variant's fields.
|
|
3
3
|
* An object variant renders its properties inline (skipping the discriminator
|
|
4
|
-
* tag, which the picker owns)
|
|
5
|
-
*
|
|
4
|
+
* tag, which the picker owns), with the variant's required fields carrying the
|
|
5
|
+
* design system's required marker; a scalar/enum variant renders as a single
|
|
6
|
+
* nested {@link FieldNode}. Selecting a variant seeds a concrete value for it,
|
|
7
|
+
* preserving the fields the outgoing value shares with the new variant (see
|
|
8
|
+
* {@link switchVariantValue}).
|
|
6
9
|
*/
|
|
7
10
|
import type { ReactNode } from 'react';
|
|
8
11
|
|
|
9
12
|
import { Field } from '../components/field';
|
|
10
13
|
import { Select } from '../components/select';
|
|
11
|
-
import { classifySchema, type FieldModel } from './classify';
|
|
12
|
-
import {
|
|
14
|
+
import { classifySchema, type FieldModel, type UnionVariant } from './classify';
|
|
15
|
+
import { skeletonValueForSchema } from './default-value';
|
|
13
16
|
import { FieldGroup } from './field-group';
|
|
14
17
|
import { FieldNode } from './field-node';
|
|
15
18
|
import { ObjectFields } from './object-fields';
|
|
@@ -18,28 +21,87 @@ import { groupClass } from './styles';
|
|
|
18
21
|
import { activeVariantIndex } from './union';
|
|
19
22
|
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
20
23
|
|
|
24
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
|
-
* A concrete seed for a chosen union variant. Object variants get their
|
|
23
|
-
* required
|
|
29
|
+
* A concrete seed for a chosen union variant. Object variants get their full
|
|
30
|
+
* skeleton (every required field minted, so the seed validates against the
|
|
31
|
+
* variant's own schema); a scalar/enum variant that has no explicit default
|
|
24
32
|
* still needs a non-`undefined` value so the selection sticks (otherwise
|
|
25
33
|
* `activeVariantIndex` reads it back as "no variant" and the picker snaps empty).
|
|
26
34
|
*/
|
|
27
35
|
function seedVariantValue(schema: JsonSchema, root: JsonSchema): unknown {
|
|
28
|
-
const seeded =
|
|
36
|
+
const seeded = skeletonValueForSchema(schema, root);
|
|
29
37
|
if (seeded !== undefined) return seeded;
|
|
30
38
|
const resolved = resolveRef(schema, root);
|
|
31
39
|
if (resolved.const !== undefined) return resolved.const;
|
|
32
40
|
if (resolved.enum !== undefined && resolved.enum.length > 0) return resolved.enum[0];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The value committed when the user switches to `target`. Typed values SURVIVE
|
|
46
|
+
* the switch: the target's skeleton is minted first, then every top-level field
|
|
47
|
+
* of the current value whose NAME exists in the target variant's properties is
|
|
48
|
+
* carried over wholesale — the merge happens at the top level only, each carried
|
|
49
|
+
* field keeping its nested contents as-is — so fields both variants share are
|
|
50
|
+
* never silently discarded. Only fields absent from the current value keep
|
|
51
|
+
* their freshly minted seed. The discriminator tag is never carried: it always
|
|
52
|
+
* comes from the target's seed (the old tag would re-select the old variant).
|
|
53
|
+
* A non-object side (scalar variant, or a non-object current value) has no
|
|
54
|
+
* fields to carry, so it reseeds wholesale.
|
|
55
|
+
*/
|
|
56
|
+
function switchVariantValue(
|
|
57
|
+
current: unknown,
|
|
58
|
+
target: UnionVariant,
|
|
59
|
+
discriminator: string | undefined,
|
|
60
|
+
root: JsonSchema,
|
|
61
|
+
): unknown {
|
|
62
|
+
const seeded = seedVariantValue(target.schema, root);
|
|
63
|
+
if (!isPlainObject(current) || !isPlainObject(seeded)) return seeded;
|
|
64
|
+
const { model } = classifySchema(target.schema, root);
|
|
65
|
+
if (model.kind !== 'object') return seeded;
|
|
66
|
+
const targetNames = new Set(model.properties.map(([name]) => name));
|
|
67
|
+
const merged: Record<string, unknown> = { ...seeded };
|
|
68
|
+
for (const [name, held] of Object.entries(current)) {
|
|
69
|
+
if (name === discriminator) continue;
|
|
70
|
+
if (targetNames.has(name)) merged[name] = held;
|
|
71
|
+
}
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Title-case a property name: `kind` → "Kind", `content_type` → "Content Type". */
|
|
76
|
+
function titleCaseName(name: string): string {
|
|
77
|
+
return name
|
|
78
|
+
.split(/[_\-\s]+/)
|
|
79
|
+
.filter((part) => part.length > 0)
|
|
80
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
81
|
+
.join(' ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The variant picker's label. A discriminated union names the picker after the
|
|
86
|
+
* discriminator property it sets — the property's schema `title` when a variant
|
|
87
|
+
* declares one, else the property name title-cased — so the picker reads as the
|
|
88
|
+
* field it is ("Kind", "Format"). Only a union without a discriminator falls
|
|
89
|
+
* back to the generic "Variant".
|
|
90
|
+
*/
|
|
91
|
+
function pickerLabel(
|
|
92
|
+
discriminator: string | undefined,
|
|
93
|
+
variants: readonly UnionVariant[],
|
|
94
|
+
root: JsonSchema,
|
|
95
|
+
): string {
|
|
96
|
+
if (discriminator === undefined) return 'Variant';
|
|
97
|
+
for (const variant of variants) {
|
|
98
|
+
const property = resolveRef(variant.schema, root).properties?.[discriminator];
|
|
99
|
+
if (property === undefined) continue;
|
|
100
|
+
const title = property.title ?? resolveRef(property, root).title;
|
|
101
|
+
if (typeof title === 'string' && title.length > 0) return title;
|
|
42
102
|
}
|
|
103
|
+
const cased = titleCaseName(discriminator);
|
|
104
|
+
return cased.length > 0 ? cased : 'Variant';
|
|
43
105
|
}
|
|
44
106
|
|
|
45
107
|
export function UnionField({
|
|
@@ -74,15 +136,18 @@ export function UnionField({
|
|
|
74
136
|
const position = Number(key);
|
|
75
137
|
const variant = model.variants[position];
|
|
76
138
|
if (variant === undefined) return;
|
|
77
|
-
onChange(
|
|
139
|
+
onChange(switchVariantValue(value, variant, model.discriminator, root));
|
|
78
140
|
};
|
|
79
141
|
|
|
80
142
|
const active = index === -1 ? undefined : model.variants[index];
|
|
81
143
|
const activeClassified = active === undefined ? undefined : classifySchema(active.schema, root);
|
|
82
144
|
|
|
83
145
|
return (
|
|
84
|
-
|
|
85
|
-
|
|
146
|
+
// At the form root the union sits inside a field the HOST already labels
|
|
147
|
+
// and describes — a synthetic heading there is noise, so it is suppressed
|
|
148
|
+
// exactly as a root object's is; nested unions keep theirs.
|
|
149
|
+
<FieldGroup heading={heading} description={description} error={error} atRoot={path === ''}>
|
|
150
|
+
<Field label={pickerLabel(model.discriminator, model.variants, root)}>
|
|
86
151
|
<Select
|
|
87
152
|
options={options}
|
|
88
153
|
value={index === -1 ? '' : String(index)}
|
|
@@ -102,6 +167,7 @@ export function UnionField({
|
|
|
102
167
|
errors={errors}
|
|
103
168
|
idPrefix={idPrefix}
|
|
104
169
|
skip={model.discriminator}
|
|
170
|
+
markRequired
|
|
105
171
|
/>
|
|
106
172
|
</div>
|
|
107
173
|
) : active !== undefined ? (
|
package/dist/jq.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The SDK's jq surface — a THIN re-export of `@tai42/jq-studio`, the standalone,
|
|
3
|
-
* embeddable visual jq editor. There is no tai42-owned jq component: the platform
|
|
4
|
-
* owns zero jq code. This module exists only so a feature package (which depends
|
|
5
|
-
* on `@tai42/studio-sdk` alone, never on `@tai42/jq-studio` directly) can reach
|
|
6
|
-
* the drop-in `JqField`, the lower-level `JqEditorDialog`, the agnostic
|
|
7
|
-
* field-declaration contract, and the runtime loader/worker helpers.
|
|
8
|
-
*
|
|
9
|
-
* NOTHING IN THE SDK IMPORTS THIS MODULE. Every edge to it runs the other way: a
|
|
10
|
-
* host imports `JqField` from here and hands it in — to a `SchemaForm` through its
|
|
11
|
-
* `expressionField` prop or the ambient `ExpressionFieldContext`, or straight into
|
|
12
|
-
* its own JSX. That is what keeps this heavy subgraph — the visual editor, its
|
|
13
|
-
* worker file, and its wasm engine — OUT of the bundle of a consumer that imports
|
|
14
|
-
* the SDK for anything else; a bundler emits a dynamic import's chunks as surely
|
|
15
|
-
* as a static import's, so `lazy` inside the SDK would not have kept them out.
|
|
16
|
-
*
|
|
17
|
-
* BUNDLE + WORKER SHARING. A plugin bundle externalises `@tai42/studio-sdk`, so
|
|
18
|
-
* the SDK — and the one copy of `@tai42/jq-studio` it carries — ships once for the
|
|
19
|
-
* whole deployment. A plugin that renders `JqField` shares the host's single jq
|
|
20
|
-
* runtime, its single primitives injection (see the host's `PrimitivesProvider`
|
|
21
|
-
* wiring), and the single worker the host installs at boot
|
|
22
|
-
* ({@link installDefaultJqWorker}); it never bundles its own.
|
|
23
|
-
*
|
|
24
|
-
* The host app injects the SDK's design-system primitives into jq-studio ONCE at
|
|
25
|
-
* the root through jq-studio's `PrimitivesProvider` and installs the shared worker
|
|
26
|
-
* there, so every `JqField` — host feature or plugin — paints in the SDK design
|
|
27
|
-
* system and evaluates off the main thread with no per-site wiring.
|
|
28
|
-
*/
|
|
29
|
-
export { JqField, JQEditorDialog, JqEditorDialog, preloadJq, installDefaultJqWorker, PrimitivesProvider, } from '@tai42/jq-studio';
|
|
30
|
-
export type { JqFieldProps, JQEditorDialogProps, JqEditorDialogProps, ExpressionLanguage, JqInputKey, JqInputShapeDescriptor, SampleInputProvider, ServerValidationResult, ServerValidateHook, JqFieldDeclaration, Primitives, } from '@tai42/jq-studio';
|
|
31
|
-
//# sourceMappingURL=jq.d.ts.map
|
package/dist/jq.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jq.d.ts","sourceRoot":"","sources":["../src/jq.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EACL,OAAO,EACP,cAAc,EACd,cAAc,EACd,SAAS,EACT,sBAAsB,EAWtB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,EAGlB,UAAU,GACX,MAAM,kBAAkB,CAAC"}
|
package/dist/jq.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The SDK's jq surface — a THIN re-export of `@tai42/jq-studio`, the standalone,
|
|
3
|
-
* embeddable visual jq editor. There is no tai42-owned jq component: the platform
|
|
4
|
-
* owns zero jq code. This module exists only so a feature package (which depends
|
|
5
|
-
* on `@tai42/studio-sdk` alone, never on `@tai42/jq-studio` directly) can reach
|
|
6
|
-
* the drop-in `JqField`, the lower-level `JqEditorDialog`, the agnostic
|
|
7
|
-
* field-declaration contract, and the runtime loader/worker helpers.
|
|
8
|
-
*
|
|
9
|
-
* NOTHING IN THE SDK IMPORTS THIS MODULE. Every edge to it runs the other way: a
|
|
10
|
-
* host imports `JqField` from here and hands it in — to a `SchemaForm` through its
|
|
11
|
-
* `expressionField` prop or the ambient `ExpressionFieldContext`, or straight into
|
|
12
|
-
* its own JSX. That is what keeps this heavy subgraph — the visual editor, its
|
|
13
|
-
* worker file, and its wasm engine — OUT of the bundle of a consumer that imports
|
|
14
|
-
* the SDK for anything else; a bundler emits a dynamic import's chunks as surely
|
|
15
|
-
* as a static import's, so `lazy` inside the SDK would not have kept them out.
|
|
16
|
-
*
|
|
17
|
-
* BUNDLE + WORKER SHARING. A plugin bundle externalises `@tai42/studio-sdk`, so
|
|
18
|
-
* the SDK — and the one copy of `@tai42/jq-studio` it carries — ships once for the
|
|
19
|
-
* whole deployment. A plugin that renders `JqField` shares the host's single jq
|
|
20
|
-
* runtime, its single primitives injection (see the host's `PrimitivesProvider`
|
|
21
|
-
* wiring), and the single worker the host installs at boot
|
|
22
|
-
* ({@link installDefaultJqWorker}); it never bundles its own.
|
|
23
|
-
*
|
|
24
|
-
* The host app injects the SDK's design-system primitives into jq-studio ONCE at
|
|
25
|
-
* the root through jq-studio's `PrimitivesProvider` and installs the shared worker
|
|
26
|
-
* there, so every `JqField` — host feature or plugin — paints in the SDK design
|
|
27
|
-
* system and evaluates off the main thread with no per-site wiring.
|
|
28
|
-
*/
|
|
29
|
-
export { JqField, JQEditorDialog, JqEditorDialog, preloadJq, installDefaultJqWorker,
|
|
30
|
-
// The design-system injection point. jq-studio renders its editor chrome
|
|
31
|
-
// through primitives it reads from a module-scoped React context; the host
|
|
32
|
-
// mounts `PrimitivesProvider` ONCE at the root to substitute the SDK design
|
|
33
|
-
// system. It is re-exported HERE — off the same `@tai42/jq-studio` instance
|
|
34
|
-
// `JqField` comes from — so the host reaches it through the ONE shared SDK the
|
|
35
|
-
// import map serves, and the provider's context is the SAME object `JqField`
|
|
36
|
-
// reads. A host that imported `PrimitivesProvider` from `@tai42/jq-studio`
|
|
37
|
-
// directly would bundle a SECOND jq-studio copy: its `createContext` would be a
|
|
38
|
-
// different object, the injection would silently fall back to the built-ins,
|
|
39
|
-
// and the shell would ship a duplicate (orphan) jq worker + wasm.
|
|
40
|
-
PrimitivesProvider, } from '@tai42/jq-studio';
|
|
41
|
-
//# sourceMappingURL=jq.js.map
|
package/dist/jq.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jq.js","sourceRoot":"","sources":["../src/jq.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EACL,OAAO,EACP,cAAc,EACd,cAAc,EACd,SAAS,EACT,sBAAsB;AACtB,yEAAyE;AACzE,2EAA2E;AAC3E,4EAA4E;AAC5E,4EAA4E;AAC5E,+EAA+E;AAC/E,6EAA6E;AAC7E,2EAA2E;AAC3E,gFAAgF;AAChF,6EAA6E;AAC7E,kEAAkE;AAClE,kBAAkB,GACnB,MAAM,kBAAkB,CAAC"}
|