@tai42/studio-sdk 20.0.0 → 22.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/dist/components/chip-group.d.ts +42 -0
- package/dist/components/chip-group.d.ts.map +1 -0
- package/dist/components/chip-group.js +85 -0
- package/dist/components/chip-group.js.map +1 -0
- package/dist/components/components.css +16 -3
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-grouping.d.ts +8 -12
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +12 -15
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +3 -4
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +10 -10
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +10 -5
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +20 -3
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/json-tree-model.d.ts +19 -7
- package/dist/components/json-tree-model.d.ts.map +1 -1
- package/dist/components/json-tree-model.js +32 -8
- package/dist/components/json-tree-model.js.map +1 -1
- package/dist/components/json-tree.d.ts +10 -1
- package/dist/components/json-tree.d.ts.map +1 -1
- package/dist/components/json-tree.js +7 -4
- package/dist/components/json-tree.js.map +1 -1
- package/dist/components/select.d.ts +6 -0
- package/dist/components/select.d.ts.map +1 -1
- package/dist/components/select.js +3 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/served-media.d.ts +50 -4
- package/dist/components/served-media.d.ts.map +1 -1
- package/dist/components/served-media.js +69 -19
- package/dist/components/served-media.js.map +1 -1
- package/dist/env-markers.d.ts +25 -0
- package/dist/env-markers.d.ts.map +1 -0
- package/dist/env-markers.js +63 -0
- package/dist/env-markers.js.map +1 -0
- package/dist/hooks/interactions-stream.d.ts.map +1 -1
- package/dist/hooks/interactions-stream.js +9 -7
- package/dist/hooks/interactions-stream.js.map +1 -1
- package/dist/hooks/useApi.d.ts +2264 -912
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +5 -9
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +8 -33
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +8 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin/coverage.d.ts +14 -0
- package/dist/plugin/coverage.d.ts.map +1 -0
- package/dist/plugin/coverage.js +14 -0
- package/dist/plugin/coverage.js.map +1 -0
- package/dist/plugin/registry.d.ts +11 -1
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +33 -4
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +43 -1
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/schema-form/SchemaForm.d.ts +11 -1
- package/dist/schema-form/SchemaForm.d.ts.map +1 -1
- package/dist/schema-form/SchemaForm.js +2 -2
- package/dist/schema-form/SchemaForm.js.map +1 -1
- package/dist/schema-form/classify.d.ts.map +1 -1
- package/dist/schema-form/classify.js +54 -2
- package/dist/schema-form/classify.js.map +1 -1
- package/dist/schema-form/date-range-field.d.ts +44 -0
- package/dist/schema-form/date-range-field.d.ts.map +1 -0
- package/dist/schema-form/date-range-field.js +72 -0
- package/dist/schema-form/date-range-field.js.map +1 -0
- package/dist/schema-form/enum-field.d.ts +3 -1
- package/dist/schema-form/enum-field.d.ts.map +1 -1
- package/dist/schema-form/enum-field.js +8 -2
- package/dist/schema-form/enum-field.js.map +1 -1
- package/dist/schema-form/field-model.d.ts +20 -1
- package/dist/schema-form/field-model.d.ts.map +1 -1
- package/dist/schema-form/field-node.d.ts +10 -0
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +6 -3
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/index.d.ts +3 -1
- package/dist/schema-form/index.d.ts.map +1 -1
- package/dist/schema-form/index.js +1 -0
- package/dist/schema-form/index.js.map +1 -1
- package/dist/schema-form/multiselect-field.d.ts.map +1 -1
- package/dist/schema-form/multiselect-field.js +11 -0
- package/dist/schema-form/multiselect-field.js.map +1 -1
- package/dist/schema-form/object-fields.d.ts +9 -1
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +16 -3
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/string-field.d.ts +6 -1
- package/dist/schema-form/string-field.d.ts.map +1 -1
- package/dist/schema-form/string-field.js +42 -8
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/schema-form/types.d.ts +36 -0
- package/dist/schema-form/types.d.ts.map +1 -1
- package/dist/state-binding/UpdateRow.d.ts +2 -2
- package/dist/state-binding/UpdateRow.js +1 -1
- package/dist/state-binding/UpdateRow.js.map +1 -1
- package/dist/state-binding/adapter.d.ts +12 -8
- package/dist/state-binding/adapter.d.ts.map +1 -1
- package/dist/state-binding/adapter.js +30 -13
- package/dist/state-binding/adapter.js.map +1 -1
- package/package.json +5 -4
- package/src/components/chip-group.tsx +157 -0
- package/src/components/components.css +16 -3
- package/src/components/execution-key-picker.tsx +1 -1
- package/src/components/extension-grouping.ts +22 -22
- package/src/components/extension-picker.tsx +13 -14
- package/src/components/fleet-report.tsx +66 -7
- package/src/components/json-tree-model.ts +36 -8
- package/src/components/json-tree.tsx +24 -4
- package/src/components/select.tsx +11 -1
- package/src/components/served-media.tsx +150 -16
- package/src/env-markers.ts +70 -0
- package/src/hooks/interactions-stream.ts +9 -7
- package/src/hooks/useCapabilities.ts +8 -31
- package/src/host.ts +2 -0
- package/src/index.ts +22 -2
- package/src/plugin/coverage.ts +23 -0
- package/src/plugin/registry.ts +58 -3
- package/src/plugin/types.ts +45 -1
- package/src/schema-form/SchemaForm.tsx +10 -0
- package/src/schema-form/classify.ts +59 -3
- package/src/schema-form/date-range-field.tsx +167 -0
- package/src/schema-form/enum-field.tsx +20 -0
- package/src/schema-form/field-model.ts +24 -2
- package/src/schema-form/field-node.tsx +19 -0
- package/src/schema-form/index.ts +10 -1
- package/src/schema-form/multiselect-field.tsx +21 -0
- package/src/schema-form/object-fields.tsx +36 -2
- package/src/schema-form/string-field.tsx +72 -1
- package/src/schema-form/types.ts +38 -0
- package/src/state-binding/README.md +10 -7
- package/src/state-binding/UpdateRow.tsx +3 -3
- package/src/state-binding/adapter.ts +31 -16
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import './components/components.css';
|
|
|
41
41
|
|
|
42
42
|
// -- Plugin API --------------------------------------------------------------
|
|
43
43
|
export type {
|
|
44
|
+
ChannelConversationRoute,
|
|
44
45
|
NavEntryContribution,
|
|
45
46
|
NavEntrySection,
|
|
46
47
|
PageContribution,
|
|
@@ -51,8 +52,11 @@ export type {
|
|
|
51
52
|
PluginPageProps,
|
|
52
53
|
RegisteredNavEntry,
|
|
53
54
|
RegisteredPage,
|
|
55
|
+
RegisteredRoutePanel,
|
|
54
56
|
RegisteredSettingsTab,
|
|
55
57
|
RequiredCapabilities,
|
|
58
|
+
RoutePanelContribution,
|
|
59
|
+
RoutePanelProps,
|
|
56
60
|
SettingsTabContribution,
|
|
57
61
|
SettingsTabProps,
|
|
58
62
|
ToolPanelContribution,
|
|
@@ -60,6 +64,8 @@ export type {
|
|
|
60
64
|
} from './plugin/types';
|
|
61
65
|
export type { VersionGateResult } from './plugin/version';
|
|
62
66
|
export { checkPluginApiVersion, STUDIO_PLUGIN_API_VERSION } from './plugin/version';
|
|
67
|
+
/** What `useApi().runTool` resolves with: the run-tool door's answer, read by its `kind`. */
|
|
68
|
+
export type { RunAnswer } from '@tai42/api-client';
|
|
63
69
|
|
|
64
70
|
// -- Hooks -------------------------------------------------------------------
|
|
65
71
|
export type { ApiClient } from './hooks/useApi';
|
|
@@ -133,6 +139,8 @@ export {
|
|
|
133
139
|
|
|
134
140
|
// -- Utilities ---------------------------------------------------------------
|
|
135
141
|
export { downloadBlob } from './download';
|
|
142
|
+
export type { EnvRef } from './env-markers';
|
|
143
|
+
export { collectEnvRefs, ENV_MARKER_PREFIX, formatEnvMarker, parseEnvMarker } from './env-markers';
|
|
136
144
|
export { errorMessage } from './errors';
|
|
137
145
|
|
|
138
146
|
// -- Disabled-feature idiom (one helper + component, every consumer) ----------
|
|
@@ -163,6 +171,8 @@ export type { BadgeProps } from './components/badge';
|
|
|
163
171
|
export { Badge } from './components/badge';
|
|
164
172
|
export type { CheckboxProps } from './components/checkbox';
|
|
165
173
|
export { Checkbox } from './components/checkbox';
|
|
174
|
+
export type { ChipGroupProps, ChipOption } from './components/chip-group';
|
|
175
|
+
export { ChipGroup } from './components/chip-group';
|
|
166
176
|
export type { ConfirmDialogProps } from './components/confirm-dialog';
|
|
167
177
|
export { ConfirmDialog } from './components/confirm-dialog';
|
|
168
178
|
export type { DialogProps } from './components/dialog';
|
|
@@ -292,11 +302,18 @@ export type { HeadingLevel, MarkdownBlock, MarkdownProps } from './components/ma
|
|
|
292
302
|
export { Markdown, parseMarkdown } from './components/markdown';
|
|
293
303
|
export type { OpenTargetOptions, OpenTargetProps } from './components/open-target';
|
|
294
304
|
export { openTargetProps } from './components/open-target';
|
|
295
|
-
export type {
|
|
305
|
+
export type {
|
|
306
|
+
MediaDocumentProps,
|
|
307
|
+
MediaImageProps,
|
|
308
|
+
MediaPlayerProps,
|
|
309
|
+
ServedMediaGrammar,
|
|
310
|
+
} from './components/served-media';
|
|
296
311
|
export {
|
|
297
312
|
isRenderableMediaSrc,
|
|
298
313
|
isServedMediaUrl,
|
|
314
|
+
MediaDocument,
|
|
299
315
|
MediaImage,
|
|
316
|
+
MediaPlayer,
|
|
300
317
|
resolveMediaSrc,
|
|
301
318
|
} from './components/served-media';
|
|
302
319
|
// The URL half of the link-safety pair: `isSafeHttpUrl` answers yes/no, this one
|
|
@@ -313,7 +330,6 @@ export {
|
|
|
313
330
|
groupByKind,
|
|
314
331
|
groupIntoFamilies,
|
|
315
332
|
kindVariant,
|
|
316
|
-
NON_STACKABLE_KIND,
|
|
317
333
|
} from './components/extension-grouping';
|
|
318
334
|
export type { ExtensionPickerProps } from './components/extension-picker';
|
|
319
335
|
export { ExtensionPicker } from './components/extension-picker';
|
|
@@ -399,11 +415,14 @@ export {
|
|
|
399
415
|
// -- Schema-driven forms -----------------------------------------------------
|
|
400
416
|
export type {
|
|
401
417
|
CompletionProvider,
|
|
418
|
+
DateRangePair,
|
|
402
419
|
Discriminator,
|
|
403
420
|
ExpressionFieldComponent,
|
|
404
421
|
ExpressionFieldProps,
|
|
405
422
|
ExpressionInputKey,
|
|
406
423
|
ExpressionInputShape,
|
|
424
|
+
FormControl,
|
|
425
|
+
FormInputMode,
|
|
407
426
|
JsonSchema,
|
|
408
427
|
JsonSchemaType,
|
|
409
428
|
RecordEntryContext,
|
|
@@ -414,6 +433,7 @@ export type {
|
|
|
414
433
|
SecretRefFieldProps,
|
|
415
434
|
} from './schema-form';
|
|
416
435
|
export {
|
|
436
|
+
dateRangePairs,
|
|
417
437
|
defaultValueForSchema,
|
|
418
438
|
// The expression-door injection point: a host that wants `x-tai42-expression`
|
|
419
439
|
// fields to author through the visual editor imports `JqField` from
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a plugin contribution is visible under a capability projection — the one
|
|
3
|
+
* evaluator every host surface applies to a contribution's optional
|
|
4
|
+
* `requiredCapabilities`. Host-only (`@tai42/studio-sdk/host`).
|
|
5
|
+
*/
|
|
6
|
+
import type { MeProjection } from '@tai42/api-client';
|
|
7
|
+
|
|
8
|
+
import { coversAnyRoute, isFullProjection } from '../hooks/useCapabilities';
|
|
9
|
+
import type { RequiredCapabilities } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A full projection shows every contribution; an ABSENT requirement renders only for a
|
|
13
|
+
* full projection (safe-by-default); a declared requirement renders when the projection
|
|
14
|
+
* reaches at least one of its routes.
|
|
15
|
+
*/
|
|
16
|
+
export function contributionCovered(
|
|
17
|
+
projection: MeProjection,
|
|
18
|
+
required: RequiredCapabilities | undefined,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (isFullProjection(projection)) return true;
|
|
21
|
+
if (required === undefined) return false;
|
|
22
|
+
return coversAnyRoute(projection, required.routes);
|
|
23
|
+
}
|
package/src/plugin/registry.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type {
|
|
|
14
14
|
PluginEntry,
|
|
15
15
|
RegisteredNavEntry,
|
|
16
16
|
RegisteredPage,
|
|
17
|
+
RegisteredRoutePanel,
|
|
17
18
|
RegisteredSettingsTab,
|
|
19
|
+
RoutePanelContribution,
|
|
18
20
|
SettingsTabContribution,
|
|
19
21
|
ToolPanelContribution,
|
|
20
22
|
} from './types';
|
|
@@ -23,6 +25,18 @@ const toolPanels = new Map<string, ToolPanelContribution>();
|
|
|
23
25
|
const pages: RegisteredPage[] = [];
|
|
24
26
|
const settingsTabs: RegisteredSettingsTab[] = [];
|
|
25
27
|
const navEntries: RegisteredNavEntry[] = [];
|
|
28
|
+
const routePanels = new Map<string, RegisteredRoutePanel>();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the host knows about a plugin from its registry listing entry: `mounts`, the
|
|
32
|
+
* plugin's own route items and their resolved absolute bases (`{item: "/api/<base>"}`),
|
|
33
|
+
* and `declaredRoutePanels`, the channels its manifest lists under
|
|
34
|
+
* `contributions.route_panels`.
|
|
35
|
+
*/
|
|
36
|
+
export interface PluginLoadInputs {
|
|
37
|
+
readonly mounts: Readonly<Record<string, string>>;
|
|
38
|
+
readonly declaredRoutePanels: readonly string[];
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
/** The contributions one plugin stages before they commit as a batch. */
|
|
28
42
|
interface StagedContributions {
|
|
@@ -31,6 +45,7 @@ interface StagedContributions {
|
|
|
31
45
|
readonly settingsTabs: RegisteredSettingsTab[];
|
|
32
46
|
readonly tabIds: Set<string>;
|
|
33
47
|
readonly navEntries: RegisteredNavEntry[];
|
|
48
|
+
readonly routePanels: Map<string, RegisteredRoutePanel>;
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
/**
|
|
@@ -48,6 +63,7 @@ function emptyStaged(): StagedContributions {
|
|
|
48
63
|
settingsTabs: [],
|
|
49
64
|
tabIds: new Set(),
|
|
50
65
|
navEntries: [],
|
|
66
|
+
routePanels: new Map(),
|
|
51
67
|
};
|
|
52
68
|
}
|
|
53
69
|
|
|
@@ -63,6 +79,7 @@ function makeStagingContext(
|
|
|
63
79
|
pluginId: string,
|
|
64
80
|
staged: StagedContributions,
|
|
65
81
|
seal: Seal,
|
|
82
|
+
host: PluginLoadInputs,
|
|
66
83
|
): PluginContext {
|
|
67
84
|
const assertOpen = (): void => {
|
|
68
85
|
if (seal.closed) {
|
|
@@ -120,6 +137,16 @@ function makeStagingContext(
|
|
|
120
137
|
}
|
|
121
138
|
staged.navEntries.push({ ...contribution, pluginId });
|
|
122
139
|
},
|
|
140
|
+
registerRoutePanel(contribution: RoutePanelContribution): void {
|
|
141
|
+
assertOpen();
|
|
142
|
+
if (staged.routePanels.has(contribution.channel) || routePanels.has(contribution.channel)) {
|
|
143
|
+
throw new Error(`a route panel is already registered for channel ${contribution.channel}`);
|
|
144
|
+
}
|
|
145
|
+
staged.routePanels.set(contribution.channel, { ...contribution, pluginId });
|
|
146
|
+
},
|
|
147
|
+
routeBase(itemName: string): string | null {
|
|
148
|
+
return Object.hasOwn(host.mounts, itemName) ? (host.mounts[itemName] ?? null) : null;
|
|
149
|
+
},
|
|
123
150
|
};
|
|
124
151
|
}
|
|
125
152
|
|
|
@@ -142,6 +169,27 @@ function assertNavEntriesHavePages(pluginId: string, staged: StagedContributions
|
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The channels a plugin registered route panels for must be exactly the channels its
|
|
174
|
+
* manifest declares: a bundle and its manifest are built together, so a mismatch is a
|
|
175
|
+
* build fault. Throwing BEFORE any commit keeps atomicity.
|
|
176
|
+
*/
|
|
177
|
+
function assertRoutePanelsDeclared(
|
|
178
|
+
pluginId: string,
|
|
179
|
+
staged: StagedContributions,
|
|
180
|
+
declared: readonly string[],
|
|
181
|
+
): void {
|
|
182
|
+
const registered = [...staged.routePanels.keys()].sort();
|
|
183
|
+
const manifest = [...new Set(declared)].sort();
|
|
184
|
+
const same =
|
|
185
|
+
registered.length === manifest.length && registered.every((c, i) => c === manifest[i]);
|
|
186
|
+
if (!same) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`plugin “${pluginId}” registered route panels [${registered.join(', ')}] but its manifest declares [${manifest.join(', ')}]`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
145
193
|
/**
|
|
146
194
|
* Commit the staged contributions as one batch, so the registry only ever holds a
|
|
147
195
|
* plugin's complete set — never a partial set left behind by a mid-registration failure.
|
|
@@ -151,6 +199,7 @@ function commitStaged(staged: StagedContributions): void {
|
|
|
151
199
|
for (const [toolName, contribution] of staged.toolPanels) toolPanels.set(toolName, contribution);
|
|
152
200
|
for (const tab of staged.settingsTabs) settingsTabs.push(tab);
|
|
153
201
|
for (const navEntry of staged.navEntries) navEntries.push(navEntry);
|
|
202
|
+
for (const [channel, panel] of staged.routePanels) routePanels.set(channel, panel);
|
|
154
203
|
}
|
|
155
204
|
|
|
156
205
|
/**
|
|
@@ -164,10 +213,14 @@ function commitStaged(staged: StagedContributions): void {
|
|
|
164
213
|
* during `entry`'s synchronous body — including a microtask it enqueues then — are
|
|
165
214
|
* still captured and committed. A `register` that throws commits nothing.
|
|
166
215
|
*/
|
|
167
|
-
export async function loadPlugin(
|
|
216
|
+
export async function loadPlugin(
|
|
217
|
+
pluginId: string,
|
|
218
|
+
entry: PluginEntry,
|
|
219
|
+
host: PluginLoadInputs,
|
|
220
|
+
): Promise<void> {
|
|
168
221
|
const staged = emptyStaged();
|
|
169
222
|
const seal: Seal = { closed: false };
|
|
170
|
-
const context = makeStagingContext(pluginId, staged, seal);
|
|
223
|
+
const context = makeStagingContext(pluginId, staged, seal, host);
|
|
171
224
|
|
|
172
225
|
try {
|
|
173
226
|
await entry(context);
|
|
@@ -178,12 +231,13 @@ export async function loadPlugin(pluginId: string, entry: PluginEntry): Promise<
|
|
|
178
231
|
}
|
|
179
232
|
|
|
180
233
|
assertNavEntriesHavePages(pluginId, staged);
|
|
234
|
+
assertRoutePanelsDeclared(pluginId, staged, host.declaredRoutePanels);
|
|
181
235
|
commitStaged(staged);
|
|
182
236
|
}
|
|
183
237
|
|
|
184
238
|
/** The shell reads this after loading every installed plugin bundle. */
|
|
185
239
|
export function getContributions(): PluginContributions {
|
|
186
|
-
return { toolPanels, pages, settingsTabs, navEntries };
|
|
240
|
+
return { toolPanels, pages, settingsTabs, navEntries, routePanels };
|
|
187
241
|
}
|
|
188
242
|
|
|
189
243
|
/** Tests reset the module-level registry between cases. */
|
|
@@ -192,4 +246,5 @@ export function __resetContributions(): void {
|
|
|
192
246
|
pages.length = 0;
|
|
193
247
|
settingsTabs.length = 0;
|
|
194
248
|
navEntries.length = 0;
|
|
249
|
+
routePanels.clear();
|
|
195
250
|
}
|
package/src/plugin/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The Studio-plugin authoring contract. A platform plugin ships an ESM bundle
|
|
3
3
|
* that exports a `register(context)` entry ({@link PluginEntry}); the host calls
|
|
4
4
|
* it with a {@link PluginContext} through which the plugin contributes tool
|
|
5
|
-
* panels, pages, settings tabs,
|
|
5
|
+
* panels, pages, settings tabs, sidebar nav entries and conversation route panels.
|
|
6
6
|
*
|
|
7
7
|
* Two styling paths are sanctioned, and only these two:
|
|
8
8
|
* 1. SDK components plus inline styles that read the design-system tokens
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* its own. Bundled third-party base CSS is exempt from rule (c) but rules
|
|
21
21
|
* (a) and (b) still bind it.
|
|
22
22
|
*/
|
|
23
|
+
import type { ConversationRouteView } from '@tai42/api-client';
|
|
23
24
|
import type { ComponentType } from 'react';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -219,12 +220,43 @@ export interface RegisteredSettingsTab extends SettingsTabContribution {
|
|
|
219
220
|
readonly pluginId: string;
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
/** A conversation route row as `GET /api/conversations` lists it, on the `channel` door. */
|
|
224
|
+
export type ChannelConversationRoute = Extract<ConversationRouteView, { readonly door: 'channel' }>;
|
|
225
|
+
|
|
226
|
+
/** Props a contributed route panel receives. */
|
|
227
|
+
export interface RoutePanelProps {
|
|
228
|
+
readonly pluginId: string;
|
|
229
|
+
/** The route row the panel decorates; its `channel` is the contribution's channel. */
|
|
230
|
+
readonly route: ChannelConversationRoute;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A panel the conversations drill-in renders above the thread list of every route on
|
|
235
|
+
* one channel. One panel per channel deployment-wide.
|
|
236
|
+
*/
|
|
237
|
+
export interface RoutePanelContribution {
|
|
238
|
+
/** The channel name whose routes this panel decorates (`ConversationRouteView.channel`). */
|
|
239
|
+
readonly channel: string;
|
|
240
|
+
/** Names the panel region (its aria-label and the error card label). */
|
|
241
|
+
readonly title: string;
|
|
242
|
+
readonly component: ComponentType<RoutePanelProps>;
|
|
243
|
+
/** Capability gate (see {@link RequiredCapabilities}); absent ⇒ full-only. */
|
|
244
|
+
readonly requiredCapabilities?: RequiredCapabilities;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** A route panel as stored in the registry, stamped with the plugin that registered it. */
|
|
248
|
+
export interface RegisteredRoutePanel extends RoutePanelContribution {
|
|
249
|
+
readonly pluginId: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
/** Everything a single plugin bundle has registered at import time. */
|
|
223
253
|
export interface PluginContributions {
|
|
224
254
|
readonly toolPanels: ReadonlyMap<string, ToolPanelContribution>;
|
|
225
255
|
readonly pages: readonly RegisteredPage[];
|
|
226
256
|
readonly settingsTabs: readonly RegisteredSettingsTab[];
|
|
227
257
|
readonly navEntries: readonly RegisteredNavEntry[];
|
|
258
|
+
/** Route panels keyed by the channel they decorate. */
|
|
259
|
+
readonly routePanels: ReadonlyMap<string, RegisteredRoutePanel>;
|
|
228
260
|
}
|
|
229
261
|
|
|
230
262
|
/**
|
|
@@ -250,6 +282,18 @@ export interface PluginContext {
|
|
|
250
282
|
* registers (a nav entry with no page is a dead link and is rejected loudly).
|
|
251
283
|
*/
|
|
252
284
|
registerNavEntry(contribution: NavEntryContribution): void;
|
|
285
|
+
/**
|
|
286
|
+
* Register a panel for the routes of one channel. The plugin's manifest must list the
|
|
287
|
+
* same channels under `contributions.route_panels`, or the plugin fails to load.
|
|
288
|
+
*/
|
|
289
|
+
registerRoutePanel(contribution: RoutePanelContribution): void;
|
|
290
|
+
/**
|
|
291
|
+
* The resolved absolute base of one of this plugin's route items, e.g. `/api/channels/web`,
|
|
292
|
+
* or `null` when the item has no mount on this deployment (an optional router not listed, a
|
|
293
|
+
* channel not loaded). Never throws; a feature that needs the base shows its own unavailable
|
|
294
|
+
* state for `null`. Call it during `register` and keep the value.
|
|
295
|
+
*/
|
|
296
|
+
routeBase(itemName: string): string | null;
|
|
253
297
|
}
|
|
254
298
|
|
|
255
299
|
/**
|
|
@@ -95,6 +95,14 @@ export interface SchemaFormProps {
|
|
|
95
95
|
* renders the plain string input.
|
|
96
96
|
*/
|
|
97
97
|
readonly expressionField?: ExpressionFieldComponent;
|
|
98
|
+
/**
|
|
99
|
+
* Optional. Per-field picker placeholders, keyed by the root object's property
|
|
100
|
+
* name. A top-level `enum` field that draws as a select shows its entry's
|
|
101
|
+
* `choiceLabel` as the select placeholder (in place of the default `Select…`);
|
|
102
|
+
* other controls and nested fields are unaffected. A field with no entry draws
|
|
103
|
+
* its own default.
|
|
104
|
+
*/
|
|
105
|
+
readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string }>>;
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
/**
|
|
@@ -110,6 +118,7 @@ export function SchemaForm({
|
|
|
110
118
|
completionProvider,
|
|
111
119
|
maxUploadBytes = DEFAULT_MAX_UPLOAD_BYTES,
|
|
112
120
|
expressionField,
|
|
121
|
+
fieldWording,
|
|
113
122
|
}: SchemaFormProps): ReactNode {
|
|
114
123
|
// The prop overrides the ambient door for this form only; omitted, the
|
|
115
124
|
// enclosing one flows through unchanged (providing `undefined` would BLIND the
|
|
@@ -130,6 +139,7 @@ export function SchemaForm({
|
|
|
130
139
|
required
|
|
131
140
|
errors={errors}
|
|
132
141
|
idPrefix={idPrefix}
|
|
142
|
+
fieldWording={fieldWording}
|
|
133
143
|
/>
|
|
134
144
|
</div>
|
|
135
145
|
</ExpressionFieldContext.Provider>
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
variantTag,
|
|
20
20
|
} from './resolve';
|
|
21
21
|
import { expressionAnnotation, mediaUpload } from './string-annotations';
|
|
22
|
-
import type { JsonSchema } from './types';
|
|
22
|
+
import type { FormControl, FormInputMode, JsonSchema } from './types';
|
|
23
23
|
|
|
24
24
|
function firstString(...values: readonly unknown[]): string | undefined {
|
|
25
25
|
for (const value of values) {
|
|
@@ -64,6 +64,27 @@ function classifyConst(
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// The supported `control` values, as a record so the type and the runtime set
|
|
68
|
+
// stay coupled: a value added to FormControl is a compile error here until it is
|
|
69
|
+
// listed.
|
|
70
|
+
const FORM_CONTROLS: Record<FormControl, true> = {
|
|
71
|
+
chips: true,
|
|
72
|
+
textarea: true,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The `control` keyword off a property, narrowed to a supported value, or
|
|
77
|
+
* `undefined` for an absent or unrecognized one. Read defensively (the schema is
|
|
78
|
+
* permissive): a value outside the set reads as absent, so the field renders its
|
|
79
|
+
* default control — the server ask door is the authority that refuses it.
|
|
80
|
+
*/
|
|
81
|
+
function controlOf(resolved: JsonSchema): FormControl | undefined {
|
|
82
|
+
const raw: unknown = resolved.control;
|
|
83
|
+
return typeof raw === 'string' && Object.hasOwn(FORM_CONTROLS, raw)
|
|
84
|
+
? (raw as FormControl)
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
// enum → a fixed choice list.
|
|
68
89
|
function classifyEnum(
|
|
69
90
|
resolved: JsonSchema,
|
|
@@ -73,7 +94,13 @@ function classifyEnum(
|
|
|
73
94
|
const enumValues = resolved.enum ?? [];
|
|
74
95
|
const options: EnumOption[] = enumValues.map((value) => ({ value, label: scalarLabel(value) }));
|
|
75
96
|
const nullable = enumValues.some((value) => value === null);
|
|
76
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
model: { kind: 'enum', options, control: controlOf(resolved) },
|
|
99
|
+
nullable,
|
|
100
|
+
schema: resolved,
|
|
101
|
+
title,
|
|
102
|
+
description,
|
|
103
|
+
};
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
// anyOf/oneOf union normalization.
|
|
@@ -214,6 +241,31 @@ function dateConstraints(resolved: JsonSchema): DateConstraints | undefined {
|
|
|
214
241
|
return { min, max, unavailable, rangeStart, minDays, maxDays };
|
|
215
242
|
}
|
|
216
243
|
|
|
244
|
+
// The supported `inputMode` values, as a record so the type and the runtime set
|
|
245
|
+
// stay coupled: a value added to FormInputMode is a compile error here until it
|
|
246
|
+
// is listed.
|
|
247
|
+
const INPUT_MODES: Record<FormInputMode, true> = {
|
|
248
|
+
text: true,
|
|
249
|
+
numeric: true,
|
|
250
|
+
decimal: true,
|
|
251
|
+
tel: true,
|
|
252
|
+
email: true,
|
|
253
|
+
url: true,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The `inputMode` keyword off a string property, narrowed to a supported value, or
|
|
258
|
+
* `undefined` for an absent or unrecognized one. Read defensively (the schema is
|
|
259
|
+
* permissive): a value outside the set reads as absent, so the field renders as a
|
|
260
|
+
* plain text box — the server ask door is the authority that refuses it.
|
|
261
|
+
*/
|
|
262
|
+
function inputModeOf(resolved: JsonSchema): FormInputMode | undefined {
|
|
263
|
+
const raw: unknown = resolved.inputMode;
|
|
264
|
+
return typeof raw === 'string' && Object.hasOwn(INPUT_MODES, raw)
|
|
265
|
+
? (raw as FormInputMode)
|
|
266
|
+
: undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
217
269
|
function classifyString(
|
|
218
270
|
resolved: JsonSchema,
|
|
219
271
|
title: string | undefined,
|
|
@@ -224,6 +276,10 @@ function classifyString(
|
|
|
224
276
|
model: {
|
|
225
277
|
kind: 'string',
|
|
226
278
|
format: resolved.format,
|
|
279
|
+
inputMode: inputModeOf(resolved),
|
|
280
|
+
multiline: controlOf(resolved) === 'textarea',
|
|
281
|
+
maxLength: typeof resolved.maxLength === 'number' ? resolved.maxLength : undefined,
|
|
282
|
+
minLength: typeof resolved.minLength === 'number' ? resolved.minLength : undefined,
|
|
227
283
|
media: mediaUpload(resolved),
|
|
228
284
|
expression: expressionAnnotation(resolved),
|
|
229
285
|
date: dateConstraints(resolved),
|
|
@@ -264,7 +320,7 @@ function classifyArray(
|
|
|
264
320
|
if (isStringItems && enumValues !== undefined && enumValues.length > 0) {
|
|
265
321
|
const options = enumValues.map((value) => ({ value, label: scalarLabel(value) }));
|
|
266
322
|
return {
|
|
267
|
-
model: { kind: 'multiselect', options },
|
|
323
|
+
model: { kind: 'multiselect', options, control: controlOf(resolved) },
|
|
268
324
|
nullable,
|
|
269
325
|
schema: resolved,
|
|
270
326
|
title,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A date RANGE drawn as ONE control. The platform declares a range as two
|
|
3
|
+
* `string`/`format: date` properties where the END names its START in `rangeStart`;
|
|
4
|
+
* the answer stays the two date strings, nothing is recombined. Where a pair is
|
|
5
|
+
* drawable as one control (below), `ObjectFields` renders this {@link DateRangeField}
|
|
6
|
+
* — a group with the two ends' labels over two native date inputs — at the START's
|
|
7
|
+
* slot and skips the END; otherwise the two properties draw as two ordinary date
|
|
8
|
+
* fields (a documented, valid layout).
|
|
9
|
+
*
|
|
10
|
+
* {@link dateRangePairs} is the ONE definition of "drawable as one control", reused
|
|
11
|
+
* by every surface so each surface's merge decision agrees.
|
|
12
|
+
*/
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
|
|
15
|
+
import { Field } from '../components/field';
|
|
16
|
+
import { TextInput } from '../components/inputs';
|
|
17
|
+
import type { JsonSchema } from './types';
|
|
18
|
+
|
|
19
|
+
/** A START/END pair drawable as one range control, resolved off a page's properties. */
|
|
20
|
+
export interface DateRangePair {
|
|
21
|
+
readonly startName: string;
|
|
22
|
+
readonly startSchema: JsonSchema;
|
|
23
|
+
readonly endName: string;
|
|
24
|
+
readonly endSchema: JsonSchema;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A property's `visibleWhen` as a stable key for deep-equality (absent ⇒ `''`). */
|
|
28
|
+
function visibilityKey(schema: JsonSchema): string {
|
|
29
|
+
return schema.visibleWhen === undefined ? '' : JSON.stringify(schema.visibleWhen);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDateString(schema: JsonSchema): boolean {
|
|
33
|
+
return schema.format === 'date';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The date-range pairs among `properties` that are drawable as ONE control. A pair
|
|
38
|
+
* qualifies when: the END names an existing sibling START in `rangeStart`; BOTH are
|
|
39
|
+
* `format: date`; both carry the SAME `visibleWhen` (deep-equal, or both absent); AND
|
|
40
|
+
* no OTHER field's `visibleWhen` names either end — otherwise merging the pair would
|
|
41
|
+
* leave that field's predicate pointing at a control that no longer draws on its own,
|
|
42
|
+
* so the pair stays two date fields (the documented degrade). `properties` is ONE
|
|
43
|
+
* page's ordered list; a field is used in at most one pair.
|
|
44
|
+
*/
|
|
45
|
+
export function dateRangePairs(
|
|
46
|
+
properties: readonly (readonly [string, JsonSchema])[],
|
|
47
|
+
): readonly DateRangePair[] {
|
|
48
|
+
const byName = new Map<string, JsonSchema>(properties.map(([name, schema]) => [name, schema]));
|
|
49
|
+
const pairs: DateRangePair[] = [];
|
|
50
|
+
const paired = new Set<string>();
|
|
51
|
+
|
|
52
|
+
for (const [endName, endSchema] of properties) {
|
|
53
|
+
if (paired.has(endName)) continue;
|
|
54
|
+
const startName = endSchema.rangeStart;
|
|
55
|
+
if (typeof startName !== 'string' || startName === endName || paired.has(startName)) continue;
|
|
56
|
+
const startSchema = byName.get(startName);
|
|
57
|
+
if (startSchema === undefined) continue;
|
|
58
|
+
if (!isDateString(startSchema) || !isDateString(endSchema)) continue;
|
|
59
|
+
if (visibilityKey(startSchema) !== visibilityKey(endSchema)) continue;
|
|
60
|
+
const dependedOn = properties.some(
|
|
61
|
+
([name, schema]) =>
|
|
62
|
+
name !== startName &&
|
|
63
|
+
name !== endName &&
|
|
64
|
+
typeof schema.visibleWhen?.field === 'string' &&
|
|
65
|
+
(schema.visibleWhen.field === startName || schema.visibleWhen.field === endName),
|
|
66
|
+
);
|
|
67
|
+
if (dependedOn) continue;
|
|
68
|
+
pairs.push({ startName, startSchema, endName, endSchema });
|
|
69
|
+
paired.add(startName);
|
|
70
|
+
paired.add(endName);
|
|
71
|
+
}
|
|
72
|
+
return pairs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The field's own `title`, else its property name. */
|
|
76
|
+
function titleOf(schema: JsonSchema, name: string): string {
|
|
77
|
+
return typeof schema.title === 'string' && schema.title.length > 0 ? schema.title : name;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One end of the range: a labelled native date input emitting the date string. */
|
|
81
|
+
function RangeEnd({
|
|
82
|
+
heading,
|
|
83
|
+
schema,
|
|
84
|
+
value,
|
|
85
|
+
required,
|
|
86
|
+
error,
|
|
87
|
+
onChange,
|
|
88
|
+
}: {
|
|
89
|
+
heading: string;
|
|
90
|
+
schema: JsonSchema;
|
|
91
|
+
value: unknown;
|
|
92
|
+
required: boolean;
|
|
93
|
+
error: string | undefined;
|
|
94
|
+
onChange: (value: unknown) => void;
|
|
95
|
+
}): ReactNode {
|
|
96
|
+
const current = typeof value === 'string' ? value : '';
|
|
97
|
+
return (
|
|
98
|
+
<Field label={heading} error={error}>
|
|
99
|
+
<TextInput
|
|
100
|
+
type="date"
|
|
101
|
+
// The native control enforces its inclusive bounds; the validator enforces
|
|
102
|
+
// the rest (unavailable days, the range span) that the control cannot draw.
|
|
103
|
+
min={typeof schema.minDate === 'string' ? schema.minDate : undefined}
|
|
104
|
+
max={typeof schema.maxDate === 'string' ? schema.maxDate : undefined}
|
|
105
|
+
required={required}
|
|
106
|
+
value={current}
|
|
107
|
+
onChange={(event) => {
|
|
108
|
+
const next = event.target.value;
|
|
109
|
+
// An empty optional end drops its key; a required one keeps the empty
|
|
110
|
+
// string so the validator can flag it.
|
|
111
|
+
onChange(next === '' && !required ? undefined : next);
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</Field>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function DateRangeField({
|
|
119
|
+
pair,
|
|
120
|
+
startValue,
|
|
121
|
+
endValue,
|
|
122
|
+
startRequired,
|
|
123
|
+
endRequired,
|
|
124
|
+
markRequired,
|
|
125
|
+
startError,
|
|
126
|
+
endError,
|
|
127
|
+
onChangeStart,
|
|
128
|
+
onChangeEnd,
|
|
129
|
+
}: {
|
|
130
|
+
readonly pair: DateRangePair;
|
|
131
|
+
readonly startValue: unknown;
|
|
132
|
+
readonly endValue: unknown;
|
|
133
|
+
readonly startRequired: boolean;
|
|
134
|
+
readonly endRequired: boolean;
|
|
135
|
+
readonly markRequired: boolean;
|
|
136
|
+
readonly startError: string | undefined;
|
|
137
|
+
readonly endError: string | undefined;
|
|
138
|
+
readonly onChangeStart: (value: unknown) => void;
|
|
139
|
+
readonly onChangeEnd: (value: unknown) => void;
|
|
140
|
+
}): ReactNode {
|
|
141
|
+
const startTitle = titleOf(pair.startSchema, pair.startName);
|
|
142
|
+
const endTitle = titleOf(pair.endSchema, pair.endName);
|
|
143
|
+
const mark = (title: string, required: boolean): string =>
|
|
144
|
+
markRequired && required ? `${title} *` : title;
|
|
145
|
+
return (
|
|
146
|
+
<Field label={`${startTitle} – ${endTitle}`} group>
|
|
147
|
+
<div className="tai-row">
|
|
148
|
+
<RangeEnd
|
|
149
|
+
heading={mark(startTitle, startRequired)}
|
|
150
|
+
schema={pair.startSchema}
|
|
151
|
+
value={startValue}
|
|
152
|
+
required={startRequired}
|
|
153
|
+
error={startError}
|
|
154
|
+
onChange={onChangeStart}
|
|
155
|
+
/>
|
|
156
|
+
<RangeEnd
|
|
157
|
+
heading={mark(endTitle, endRequired)}
|
|
158
|
+
schema={pair.endSchema}
|
|
159
|
+
value={endValue}
|
|
160
|
+
required={endRequired}
|
|
161
|
+
error={endError}
|
|
162
|
+
onChange={onChangeEnd}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</Field>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ReactNode } from 'react';
|
|
8
8
|
|
|
9
|
+
import { ChipGroup } from '../components/chip-group';
|
|
9
10
|
import { Field } from '../components/field';
|
|
10
11
|
import { RadioGroup } from '../components/radio-group';
|
|
11
12
|
import { Select } from '../components/select';
|
|
@@ -22,6 +23,7 @@ export function EnumField({
|
|
|
22
23
|
model,
|
|
23
24
|
value,
|
|
24
25
|
onChange,
|
|
26
|
+
choiceLabel,
|
|
25
27
|
}: {
|
|
26
28
|
heading: string;
|
|
27
29
|
description: string | undefined;
|
|
@@ -29,6 +31,8 @@ export function EnumField({
|
|
|
29
31
|
model: Extract<FieldModel, { kind: 'enum' }>;
|
|
30
32
|
value: unknown;
|
|
31
33
|
onChange: (value: unknown) => void;
|
|
34
|
+
/** The select's placeholder when the field draws as a select; absent shows `Select…`. */
|
|
35
|
+
choiceLabel?: string;
|
|
32
36
|
}): ReactNode {
|
|
33
37
|
const options = model.options.map((option, index) => ({
|
|
34
38
|
key: String(index),
|
|
@@ -44,6 +48,21 @@ export function EnumField({
|
|
|
44
48
|
if (match !== undefined) onChange(match.value);
|
|
45
49
|
};
|
|
46
50
|
|
|
51
|
+
// `control: chips` draws the choice as a chip grid in place of the radio/select,
|
|
52
|
+
// at any option count.
|
|
53
|
+
if (model.control === 'chips') {
|
|
54
|
+
return (
|
|
55
|
+
<Field label={heading} description={description} error={error} group>
|
|
56
|
+
<ChipGroup
|
|
57
|
+
mode="single"
|
|
58
|
+
options={options.map((option) => ({ value: option.key, label: option.label }))}
|
|
59
|
+
value={selectedKey}
|
|
60
|
+
onValueChange={emit}
|
|
61
|
+
/>
|
|
62
|
+
</Field>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
if (options.length <= RADIO_MAX_OPTIONS) {
|
|
48
67
|
return (
|
|
49
68
|
<Field label={heading} description={description} error={error} group>
|
|
@@ -61,6 +80,7 @@ export function EnumField({
|
|
|
61
80
|
options={options.map((option) => ({ value: option.key, label: option.label }))}
|
|
62
81
|
value={selectedKey}
|
|
63
82
|
onValueChange={emit}
|
|
83
|
+
placeholder={choiceLabel}
|
|
64
84
|
/>
|
|
65
85
|
</Field>
|
|
66
86
|
);
|