@tai42/studio-sdk 30.0.0 → 31.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -10
- package/dist/components/agent-picker.d.ts +1 -1
- package/dist/components/agent-picker.d.ts.map +1 -1
- package/dist/components/agent-picker.js +1 -0
- package/dist/components/agent-picker.js.map +1 -1
- package/dist/components/channel-picker.d.ts +1 -1
- package/dist/components/channel-picker.d.ts.map +1 -1
- package/dist/components/channel-picker.js +1 -0
- package/dist/components/channel-picker.js.map +1 -1
- package/dist/components/combo-draft-editor.d.ts +31 -0
- package/dist/components/combo-draft-editor.d.ts.map +1 -0
- package/dist/components/combo-draft-editor.js +16 -0
- package/dist/components/combo-draft-editor.js.map +1 -0
- package/dist/components/committed-combo-list.d.ts +17 -0
- package/dist/components/committed-combo-list.d.ts.map +1 -0
- package/dist/components/{extension-combo-parts.js → committed-combo-list.js} +2 -22
- package/dist/components/committed-combo-list.js.map +1 -0
- package/dist/components/components.css +53 -0
- package/dist/components/explorer-body.d.ts +1 -0
- package/dist/components/explorer-body.d.ts.map +1 -1
- package/dist/components/explorer-body.js +6 -4
- package/dist/components/explorer-body.js.map +1 -1
- package/dist/components/explorer-view.d.ts +16 -7
- package/dist/components/explorer-view.d.ts.map +1 -1
- package/dist/components/explorer-view.js +27 -13
- package/dist/components/explorer-view.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +4 -4
- package/dist/components/extension-combo-builder.js +3 -2
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-config-editor.js.map +1 -1
- package/dist/components/field.d.ts +6 -0
- package/dist/components/field.d.ts.map +1 -1
- package/dist/components/field.js +15 -2
- package/dist/components/field.js.map +1 -1
- package/dist/components/folder-actions.d.ts +51 -0
- package/dist/components/folder-actions.d.ts.map +1 -0
- package/dist/components/folder-actions.js +173 -0
- package/dist/components/folder-actions.js.map +1 -0
- package/dist/components/folder-filing.d.ts +32 -0
- package/dist/components/folder-filing.d.ts.map +1 -0
- package/dist/components/folder-filing.js +51 -0
- package/dist/components/folder-filing.js.map +1 -0
- package/dist/components/folder-nav.d.ts +9 -4
- package/dist/components/folder-nav.d.ts.map +1 -1
- package/dist/components/folder-nav.js +18 -22
- package/dist/components/folder-nav.js.map +1 -1
- package/dist/components/folder-occupancy.d.ts +27 -0
- package/dist/components/folder-occupancy.d.ts.map +1 -0
- package/dist/components/folder-occupancy.js +54 -0
- package/dist/components/folder-occupancy.js.map +1 -0
- package/dist/components/named-list-picker.d.ts +5 -3
- package/dist/components/named-list-picker.d.ts.map +1 -1
- package/dist/components/named-list-picker.js +7 -7
- package/dist/components/named-list-picker.js.map +1 -1
- package/dist/components/primitives.d.ts +2 -1
- package/dist/components/primitives.d.ts.map +1 -1
- package/dist/components/primitives.js +2 -1
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/searchable-select-model.d.ts +40 -0
- package/dist/components/searchable-select-model.d.ts.map +1 -0
- package/dist/components/searchable-select-model.js +69 -0
- package/dist/components/searchable-select-model.js.map +1 -0
- package/dist/components/searchable-select.d.ts +38 -0
- package/dist/components/searchable-select.d.ts.map +1 -0
- package/dist/components/searchable-select.js +200 -0
- package/dist/components/searchable-select.js.map +1 -0
- package/dist/components/selected-tool-badges.d.ts +13 -0
- package/dist/components/selected-tool-badges.d.ts.map +1 -0
- package/dist/components/selected-tool-badges.js +14 -0
- package/dist/components/selected-tool-badges.js.map +1 -0
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +2 -2
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/table.d.ts +7 -1
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +2 -2
- package/dist/components/table.js.map +1 -1
- package/dist/components/tag-filter-field.d.ts +11 -0
- package/dist/components/tag-filter-field.d.ts.map +1 -0
- package/dist/components/tag-filter-field.js +15 -0
- package/dist/components/tag-filter-field.js.map +1 -0
- package/dist/components/tool-meta-access.d.ts +28 -0
- package/dist/components/tool-meta-access.d.ts.map +1 -0
- package/dist/components/tool-meta-access.js +65 -0
- package/dist/components/tool-meta-access.js.map +1 -0
- package/dist/components/tool-name-entry.d.ts +26 -0
- package/dist/components/tool-name-entry.d.ts.map +1 -0
- package/dist/components/tool-name-entry.js +43 -0
- package/dist/components/tool-name-entry.js.map +1 -0
- package/dist/components/tool-picker.d.ts +24 -19
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +55 -35
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/tool-select.d.ts +26 -0
- package/dist/components/tool-select.d.ts.map +1 -0
- package/dist/components/tool-select.js +82 -0
- package/dist/components/tool-select.js.map +1 -0
- package/dist/extension-combos.d.ts +6 -1
- package/dist/extension-combos.d.ts.map +1 -1
- package/dist/extension-combos.js +8 -0
- package/dist/extension-combos.js.map +1 -1
- package/dist/hooks/useApi.d.ts +1 -0
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useSystemKinds.d.ts +27 -12
- package/dist/hooks/useSystemKinds.d.ts.map +1 -1
- package/dist/hooks/useSystemKinds.js +56 -16
- package/dist/hooks/useSystemKinds.js.map +1 -1
- package/dist/index.d.ts +51 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -26
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/use-mount-install.d.ts +6 -6
- package/dist/plugin/version.d.ts +1 -1
- package/dist/plugin/version.js +1 -1
- package/dist/schema-editor/SchemaEditor.d.ts +7 -2
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
- package/dist/schema-editor/SchemaEditor.js +1 -1
- package/dist/schema-editor/SchemaEditor.js.map +1 -1
- package/dist/schema-editor/index.d.ts +2 -2
- package/dist/schema-editor/index.d.ts.map +1 -1
- package/dist/schema-form/array-field.d.ts.map +1 -1
- package/dist/schema-form/array-field.js +6 -2
- package/dist/schema-form/array-field.js.map +1 -1
- package/dist/schema-form/classify.d.ts +4 -1
- package/dist/schema-form/classify.d.ts.map +1 -1
- package/dist/schema-form/classify.js +36 -15
- package/dist/schema-form/classify.js.map +1 -1
- package/dist/schema-form/collapsed-object-field.d.ts +20 -0
- package/dist/schema-form/collapsed-object-field.d.ts.map +1 -0
- package/dist/schema-form/collapsed-object-field.js +14 -0
- package/dist/schema-form/collapsed-object-field.js.map +1 -0
- package/dist/schema-form/context.d.ts +27 -1
- package/dist/schema-form/context.d.ts.map +1 -1
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -1
- package/dist/schema-form/default-value.d.ts +21 -6
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +74 -15
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/expression-field.d.ts +48 -0
- package/dist/schema-form/expression-field.d.ts.map +1 -0
- package/dist/schema-form/expression-field.js +88 -0
- package/dist/schema-form/expression-field.js.map +1 -0
- package/dist/schema-form/field-model.d.ts +23 -4
- package/dist/schema-form/field-model.d.ts.map +1 -1
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +16 -3
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/index.d.ts +3 -3
- package/dist/schema-form/index.d.ts.map +1 -1
- package/dist/schema-form/index.js +2 -2
- package/dist/schema-form/index.js.map +1 -1
- package/dist/schema-form/object-fields.d.ts +6 -3
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +14 -3
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/record-field.d.ts.map +1 -1
- package/dist/schema-form/record-field.js +6 -2
- package/dist/schema-form/record-field.js.map +1 -1
- package/dist/schema-form/seeding-action.d.ts +15 -0
- package/dist/schema-form/seeding-action.d.ts.map +1 -0
- package/dist/schema-form/seeding-action.js +35 -0
- package/dist/schema-form/seeding-action.js.map +1 -0
- package/dist/schema-form/string-annotations.d.ts +22 -8
- package/dist/schema-form/string-annotations.d.ts.map +1 -1
- package/dist/schema-form/string-annotations.js +39 -14
- package/dist/schema-form/string-annotations.js.map +1 -1
- package/dist/schema-form/string-field.js +2 -84
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/schema-form/templated-field.d.ts +13 -0
- package/dist/schema-form/templated-field.d.ts.map +1 -0
- package/dist/schema-form/templated-field.js +43 -0
- package/dist/schema-form/templated-field.js.map +1 -0
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js +6 -2
- package/dist/schema-form/union-field.js.map +1 -1
- package/dist/schema-form/union.d.ts +4 -4
- package/dist/schema-form/union.d.ts.map +1 -1
- package/dist/schema-form/union.js +8 -1
- package/dist/schema-form/union.js.map +1 -1
- package/dist/schema-form/validate.d.ts.map +1 -1
- package/dist/schema-form/validate.js +28 -0
- package/dist/schema-form/validate.js.map +1 -1
- package/dist/search-match.d.ts +29 -0
- package/dist/search-match.d.ts.map +1 -0
- package/dist/search-match.js +38 -0
- package/dist/search-match.js.map +1 -0
- package/dist/tool-organization.d.ts +15 -0
- package/dist/tool-organization.d.ts.map +1 -0
- package/dist/tool-organization.js +6 -0
- package/dist/tool-organization.js.map +1 -0
- package/package.json +3 -2
- package/src/components/agent-picker.tsx +2 -1
- package/src/components/channel-picker.tsx +2 -1
- package/src/components/combo-draft-editor.tsx +96 -0
- package/src/components/{extension-combo-parts.tsx → committed-combo-list.tsx} +4 -105
- package/src/components/components.css +53 -0
- package/src/components/explorer-body.tsx +29 -5
- package/src/components/explorer-view.tsx +48 -17
- package/src/components/extension-combo-builder.tsx +10 -10
- package/src/components/extension-config-editor.tsx +3 -3
- package/src/components/field.tsx +18 -2
- package/src/components/folder-actions.tsx +341 -0
- package/src/components/folder-filing.tsx +109 -0
- package/src/components/folder-nav.tsx +26 -10
- package/src/components/folder-occupancy.ts +83 -0
- package/src/components/named-list-picker.tsx +11 -7
- package/src/components/primitives.tsx +2 -1
- package/src/components/searchable-select-model.ts +105 -0
- package/src/components/searchable-select.tsx +432 -0
- package/src/components/selected-tool-badges.tsx +41 -0
- package/src/components/subject-section.tsx +3 -2
- package/src/components/table.tsx +12 -2
- package/src/components/tag-filter-field.tsx +37 -0
- package/src/components/tool-meta-access.tsx +101 -0
- package/src/components/tool-name-entry.tsx +77 -0
- package/src/components/tool-picker.tsx +87 -52
- package/src/components/tool-select.tsx +136 -0
- package/src/extension-combos.ts +15 -1
- package/src/hooks/useSystemKinds.tsx +77 -17
- package/src/index.ts +98 -94
- package/src/plugin/version.ts +1 -1
- package/src/schema-editor/SchemaEditor.tsx +9 -3
- package/src/schema-editor/index.ts +2 -2
- package/src/schema-form/array-field.tsx +10 -2
- package/src/schema-form/classify.ts +41 -15
- package/src/schema-form/collapsed-object-field.tsx +56 -0
- package/src/schema-form/context.ts +33 -1
- package/src/schema-form/default-value.ts +89 -15
- package/src/schema-form/expression-field.tsx +162 -0
- package/src/schema-form/field-model.ts +27 -4
- package/src/schema-form/field-node.tsx +41 -2
- package/src/schema-form/index.ts +3 -2
- package/src/schema-form/object-fields.tsx +11 -4
- package/src/schema-form/record-field.tsx +10 -2
- package/src/schema-form/seeding-action.ts +44 -0
- package/src/schema-form/string-annotations.ts +40 -14
- package/src/schema-form/string-field.tsx +2 -147
- package/src/schema-form/templated-field.tsx +124 -0
- package/src/schema-form/union-field.tsx +11 -2
- package/src/schema-form/union.ts +12 -5
- package/src/schema-form/validate.ts +28 -0
- package/src/search-match.ts +41 -0
- package/src/tool-organization.ts +34 -0
- package/dist/components/extension-combo-parts.d.ts +0 -46
- package/dist/components/extension-combo-parts.d.ts.map +0 -1
- package/dist/components/extension-combo-parts.js.map +0 -1
- package/dist/components/tool-picker-parts.d.ts +0 -43
- package/dist/components/tool-picker-parts.d.ts.map +0 -1
- package/dist/components/tool-picker-parts.js +0 -89
- package/dist/components/tool-picker-parts.js.map +0 -1
- package/src/components/tool-picker-parts.tsx +0 -195
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filing ONE tool into a folder of the tool-organization tree — for a screen whose
|
|
3
|
+
* rows have no edit dialog of their own to carry a folder field. The folder is part
|
|
4
|
+
* of the tool's overlay row, so the write is a single `upsertToolMeta` merge-patch of
|
|
5
|
+
* `folder_id`; the tool's display name, tags and visibility are left untouched.
|
|
6
|
+
*
|
|
7
|
+
* The picker offers the FULL tree with inline create. The write resolves only after
|
|
8
|
+
* the screen's `onFoldersChanged` refresh, and a failure is shown in the dialog — a
|
|
9
|
+
* not-configured refusal as the server's off note, reported so every screen withdraws
|
|
10
|
+
* its overlay writes.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode, useState } from 'react';
|
|
13
|
+
|
|
14
|
+
import { useApi } from '../hooks/useApi';
|
|
15
|
+
import { Dialog } from './dialog';
|
|
16
|
+
import { Field } from './field';
|
|
17
|
+
import { type FoldersChanged, useCreateFolder } from './folder-actions';
|
|
18
|
+
import { type Folder, FolderPicker } from './folder-nav';
|
|
19
|
+
import { Button, Spinner } from './primitives';
|
|
20
|
+
import { ToolMetaWriteError, useReportToolMetaRefusal } from './tool-meta-access';
|
|
21
|
+
|
|
22
|
+
/** Props of {@link MoveToFolderDialog}. */
|
|
23
|
+
export interface MoveToFolderDialogProps {
|
|
24
|
+
/** The tool whose overlay row is filed (its registered tool name). */
|
|
25
|
+
readonly toolName: string;
|
|
26
|
+
/** What the dialog title calls the item (e.g. the agent's name). */
|
|
27
|
+
readonly label: string;
|
|
28
|
+
/** The tool's current folder (`null` = unfiled). */
|
|
29
|
+
readonly currentFolderId: string | null;
|
|
30
|
+
/** The FULL folder tree, offered as destinations. */
|
|
31
|
+
readonly folders: readonly Folder[];
|
|
32
|
+
/** The screen's refresh of its overlay read, awaited after the write. */
|
|
33
|
+
readonly onFoldersChanged: FoldersChanged;
|
|
34
|
+
/** Close the dialog (Cancel, dismiss, or after a successful move). */
|
|
35
|
+
readonly onClose: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A dialog that files one tool into a folder (or unfiles it). Mount while open. */
|
|
39
|
+
export function MoveToFolderDialog({
|
|
40
|
+
toolName,
|
|
41
|
+
label,
|
|
42
|
+
currentFolderId,
|
|
43
|
+
folders,
|
|
44
|
+
onFoldersChanged,
|
|
45
|
+
onClose,
|
|
46
|
+
}: MoveToFolderDialogProps): ReactNode {
|
|
47
|
+
const api = useApi();
|
|
48
|
+
const createFolder = useCreateFolder(onFoldersChanged);
|
|
49
|
+
const reportRefusal = useReportToolMetaRefusal();
|
|
50
|
+
const [folderId, setFolderId] = useState<string | null>(currentFolderId);
|
|
51
|
+
const [pending, setPending] = useState(false);
|
|
52
|
+
const [error, setError] = useState<unknown>(null);
|
|
53
|
+
|
|
54
|
+
const save = (): void => {
|
|
55
|
+
if (pending) return;
|
|
56
|
+
setPending(true);
|
|
57
|
+
setError(null);
|
|
58
|
+
api
|
|
59
|
+
.upsertToolMeta(toolName, { folder_id: folderId })
|
|
60
|
+
.then(() => onFoldersChanged())
|
|
61
|
+
.then(
|
|
62
|
+
() => {
|
|
63
|
+
setPending(false);
|
|
64
|
+
onClose();
|
|
65
|
+
},
|
|
66
|
+
(failure: unknown) => {
|
|
67
|
+
setError(failure);
|
|
68
|
+
setPending(false);
|
|
69
|
+
reportRefusal(failure);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog
|
|
76
|
+
title={`Move ${label} to a folder`}
|
|
77
|
+
open
|
|
78
|
+
onOpenChange={(next) => {
|
|
79
|
+
if (!next) onClose();
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Field label="Destination folder" description="Choose a folder, or no folder." group>
|
|
83
|
+
<FolderPicker
|
|
84
|
+
value={folderId}
|
|
85
|
+
onChange={setFolderId}
|
|
86
|
+
folders={folders}
|
|
87
|
+
onCreateFolder={createFolder}
|
|
88
|
+
unfiledLabel="No folder"
|
|
89
|
+
disabled={pending}
|
|
90
|
+
/>
|
|
91
|
+
</Field>
|
|
92
|
+
<ToolMetaWriteError error={error} />
|
|
93
|
+
<div className="tai-dialog-actions">
|
|
94
|
+
<Button type="button" onClick={onClose}>
|
|
95
|
+
Cancel
|
|
96
|
+
</Button>
|
|
97
|
+
<Button
|
|
98
|
+
type="button"
|
|
99
|
+
variant="primary"
|
|
100
|
+
disabled={folderId === currentFolderId || pending}
|
|
101
|
+
onClick={save}
|
|
102
|
+
>
|
|
103
|
+
{pending ? <Spinner label="Moving" /> : null}
|
|
104
|
+
Move
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</Dialog>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* `FolderPicker` carries inline new-folder creation, so a folder can be created from
|
|
13
13
|
* EVERY consuming screen through this one component — never one screen's own
|
|
14
|
-
* affordance.
|
|
14
|
+
* affordance. A failed creation is shown loudly under the draft by the picker
|
|
15
|
+
* itself, so every screen that files through it surfaces the failure the same way.
|
|
15
16
|
*/
|
|
17
|
+
import type { FolderRecord } from '@tai42/api-client';
|
|
16
18
|
import { type ReactNode, useState } from 'react';
|
|
17
19
|
|
|
18
20
|
import { ChevronRightIcon, FolderIcon } from './icons';
|
|
19
21
|
import { TextInput } from './inputs';
|
|
20
22
|
import { Button } from './primitives';
|
|
21
23
|
import { Select } from './select';
|
|
24
|
+
import { ToolMetaWriteError } from './tool-meta-access';
|
|
22
25
|
|
|
23
26
|
/** A folder in the tree, in the SDK's camelCase UI shape (`parentId`, `null` = root). */
|
|
24
27
|
export interface Folder {
|
|
@@ -27,6 +30,15 @@ export interface Folder {
|
|
|
27
30
|
readonly parentId: string | null;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/** Map the overlay's wire folder records into the UI {@link Folder} shape. */
|
|
34
|
+
export function toFolders(records: readonly FolderRecord[]): Folder[] {
|
|
35
|
+
return records.map((record) => ({
|
|
36
|
+
id: record.id,
|
|
37
|
+
name: record.name,
|
|
38
|
+
parentId: record.parent_id,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
/** The subfolders directly under `parentId` (`null` = the root directory), name-sorted. */
|
|
31
43
|
export function childFolders(folders: readonly Folder[], parentId: string | null): Folder[] {
|
|
32
44
|
return folders
|
|
@@ -143,10 +155,11 @@ export interface FolderPickerProps {
|
|
|
143
155
|
readonly folders: readonly Folder[];
|
|
144
156
|
/**
|
|
145
157
|
* Create a folder under `parentId` (the current selection) and resolve with its
|
|
146
|
-
* id; the picker selects it.
|
|
147
|
-
*
|
|
158
|
+
* id; the picker selects it. On a rejection the picker shows the error and keeps
|
|
159
|
+
* the draft in place so the operator can retry. Without it the picker offers no
|
|
160
|
+
* create — the caller may not create folders.
|
|
148
161
|
*/
|
|
149
|
-
readonly onCreateFolder
|
|
162
|
+
readonly onCreateFolder?: (name: string, parentId: string | null) => Promise<string>;
|
|
150
163
|
readonly disabled?: boolean;
|
|
151
164
|
/** The unfiled option's label (e.g. "No folder"). */
|
|
152
165
|
readonly unfiledLabel: string;
|
|
@@ -164,22 +177,23 @@ export function FolderPicker({
|
|
|
164
177
|
const [creating, setCreating] = useState(false);
|
|
165
178
|
const [draft, setDraft] = useState('');
|
|
166
179
|
const [busy, setBusy] = useState(false);
|
|
180
|
+
const [createError, setCreateError] = useState<unknown>(null);
|
|
167
181
|
|
|
168
182
|
const options = [{ value: UNFILED_VALUE, label: unfiledLabel }, ...folderOptions(folders)];
|
|
169
183
|
|
|
170
184
|
const create = async (): Promise<void> => {
|
|
171
185
|
const name = draft.trim();
|
|
172
|
-
if (name === '' || busy) return;
|
|
186
|
+
if (name === '' || busy || onCreateFolder === undefined) return;
|
|
173
187
|
setBusy(true);
|
|
188
|
+
setCreateError(null);
|
|
174
189
|
try {
|
|
175
190
|
const id = await onCreateFolder(name, value);
|
|
176
191
|
onChange(id);
|
|
177
192
|
setDraft('');
|
|
178
193
|
setCreating(false);
|
|
179
|
-
} catch {
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
// operator can retry — never to clear it or select nothing.
|
|
194
|
+
} catch (error: unknown) {
|
|
195
|
+
// Shown under the draft, which stays OPEN with its text for a retry.
|
|
196
|
+
setCreateError(error);
|
|
183
197
|
} finally {
|
|
184
198
|
setBusy(false);
|
|
185
199
|
}
|
|
@@ -196,7 +210,7 @@ export function FolderPicker({
|
|
|
196
210
|
}}
|
|
197
211
|
disabled={disabled ?? busy}
|
|
198
212
|
/>
|
|
199
|
-
{creating ? (
|
|
213
|
+
{onCreateFolder === undefined ? null : creating ? (
|
|
200
214
|
<div className="tai-row">
|
|
201
215
|
<div style={{ flex: 1 }}>
|
|
202
216
|
<TextInput
|
|
@@ -231,6 +245,7 @@ export function FolderPicker({
|
|
|
231
245
|
onClick={() => {
|
|
232
246
|
setCreating(false);
|
|
233
247
|
setDraft('');
|
|
248
|
+
setCreateError(null);
|
|
234
249
|
}}
|
|
235
250
|
disabled={busy}
|
|
236
251
|
>
|
|
@@ -250,6 +265,7 @@ export function FolderPicker({
|
|
|
250
265
|
<span>New folder</span>
|
|
251
266
|
</Button>
|
|
252
267
|
)}
|
|
268
|
+
<ToolMetaWriteError error={createError} />
|
|
253
269
|
</div>
|
|
254
270
|
);
|
|
255
271
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which folders of the ONE tool-organization tree a screen draws. Every tool has one
|
|
3
|
+
* folder; a screen lists only its own population (a subset of the tools), so it draws
|
|
4
|
+
* only the part of the tree that concerns that population:
|
|
5
|
+
*
|
|
6
|
+
* - a folder whose subtree holds at least one of the screen's items is drawn;
|
|
7
|
+
* - a folder whose subtree holds no tool at all is drawn (a fresh folder belongs to
|
|
8
|
+
* every screen until something is filed in it);
|
|
9
|
+
* - a folder whose subtree holds only other tools is not drawn;
|
|
10
|
+
* - the ancestors of a drawn folder are drawn, so every drawn folder stays reachable
|
|
11
|
+
* from the root.
|
|
12
|
+
*
|
|
13
|
+
* Occupancy counts EVERY overlay row with a `folder_id` — hidden tools and rows of
|
|
14
|
+
* tools no longer registered included — so a folder never reads as empty while it
|
|
15
|
+
* still holds something. A screen whose population is every tool draws the whole tree.
|
|
16
|
+
*/
|
|
17
|
+
import type { ToolMetaRecord } from '@tai42/api-client';
|
|
18
|
+
|
|
19
|
+
import type { Folder } from './folder-nav';
|
|
20
|
+
|
|
21
|
+
/** What a folder's subtree holds, and whether the folder is drawn. */
|
|
22
|
+
interface SubtreeState {
|
|
23
|
+
readonly own: boolean;
|
|
24
|
+
readonly any: boolean;
|
|
25
|
+
readonly drawn: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NOTHING: SubtreeState = { own: false, any: false, drawn: false };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The folders a screen draws, in the input order.
|
|
32
|
+
*
|
|
33
|
+
* `rows` are the overlay rows (each tool's `folder_id`), `population` the tool names
|
|
34
|
+
* the screen lists. Linear in folders + rows; the subtree walk is cycle-guarded, so a
|
|
35
|
+
* malformed parent chain yields a bounded result rather than looping.
|
|
36
|
+
*/
|
|
37
|
+
export function foldersForView(
|
|
38
|
+
folders: readonly Folder[],
|
|
39
|
+
rows: readonly Pick<ToolMetaRecord, 'tool_name' | 'folder_id'>[],
|
|
40
|
+
population: ReadonlySet<string>,
|
|
41
|
+
): Folder[] {
|
|
42
|
+
const direct = new Map<string, { own: boolean; any: boolean }>();
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
if (row.folder_id === null) continue;
|
|
45
|
+
const entry = direct.get(row.folder_id) ?? { own: false, any: false };
|
|
46
|
+
entry.any = true;
|
|
47
|
+
if (population.has(row.tool_name)) entry.own = true;
|
|
48
|
+
direct.set(row.folder_id, entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const childrenByParent = new Map<string, string[]>();
|
|
52
|
+
for (const folder of folders) {
|
|
53
|
+
if (folder.parentId === null) continue;
|
|
54
|
+
const siblings = childrenByParent.get(folder.parentId) ?? [];
|
|
55
|
+
siblings.push(folder.id);
|
|
56
|
+
childrenByParent.set(folder.parentId, siblings);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const settled = new Map<string, SubtreeState>();
|
|
60
|
+
const visiting = new Set<string>();
|
|
61
|
+
const stateOf = (id: string): SubtreeState => {
|
|
62
|
+
const cached = settled.get(id);
|
|
63
|
+
if (cached !== undefined) return cached;
|
|
64
|
+
if (visiting.has(id)) return NOTHING;
|
|
65
|
+
visiting.add(id);
|
|
66
|
+
const held = direct.get(id);
|
|
67
|
+
let own = held?.own ?? false;
|
|
68
|
+
let any = held?.any ?? false;
|
|
69
|
+
let childDrawn = false;
|
|
70
|
+
for (const childId of childrenByParent.get(id) ?? []) {
|
|
71
|
+
const child = stateOf(childId);
|
|
72
|
+
own = own || child.own;
|
|
73
|
+
any = any || child.any;
|
|
74
|
+
childDrawn = childDrawn || child.drawn;
|
|
75
|
+
}
|
|
76
|
+
visiting.delete(id);
|
|
77
|
+
const state = { own, any, drawn: own || !any || childDrawn };
|
|
78
|
+
settled.set(id, state);
|
|
79
|
+
return state;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return folders.filter((folder) => stateOf(folder.id).drawn);
|
|
83
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The single-select both `AgentPicker` and `ChannelPicker` render: one list
|
|
3
|
-
* names, drawn in its own `Field` with the read's states (pending, error with Retry,
|
|
2
|
+
* The searchable single-select both `AgentPicker` and `ChannelPicker` render: one list
|
|
3
|
+
* read of names, drawn in its own `Field` with the read's states (pending, error with Retry,
|
|
4
4
|
* refused on a 403, loaded and empty) and a stored value the loaded list does not
|
|
5
5
|
* hold flagged with the picker's unlisted label. Each picker hands in its own copy.
|
|
6
6
|
* The host's `error` shows, and the `Field` claims its control id, exactly while a
|
|
7
|
-
*
|
|
7
|
+
* select renders.
|
|
8
8
|
*/
|
|
9
9
|
import { type ReactNode, useId } from 'react';
|
|
10
10
|
|
|
@@ -12,7 +12,8 @@ import { errorMessage, isForbiddenError } from '../errors';
|
|
|
12
12
|
import { Badge } from './badge';
|
|
13
13
|
import { Field } from './field';
|
|
14
14
|
import { ErrorState } from './primitives';
|
|
15
|
-
import {
|
|
15
|
+
import { SearchableSelect } from './searchable-select';
|
|
16
|
+
import type { SelectOption } from './select';
|
|
16
17
|
|
|
17
18
|
const MUTED_LINE = { margin: 0, color: 'var(--tai-color-text-muted)' } as const;
|
|
18
19
|
|
|
@@ -35,6 +36,8 @@ export interface NamedListCopy {
|
|
|
35
36
|
readonly emptyNote: string;
|
|
36
37
|
readonly emptyAction: ReactNode;
|
|
37
38
|
readonly unlistedLabel: string;
|
|
39
|
+
/** Accessible name of the select's search box. */
|
|
40
|
+
readonly searchLabel: string;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export interface NamedListPickerProps {
|
|
@@ -107,7 +110,7 @@ function namedListView(
|
|
|
107
110
|
return { refused, failed, pending, unlisted, empty, options, selectRenders };
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
/** The loaded, pending or refused body: the
|
|
113
|
+
/** The loaded, pending or refused body: the select while one renders, then its notes. */
|
|
111
114
|
function NamedListBody({
|
|
112
115
|
view,
|
|
113
116
|
value,
|
|
@@ -123,13 +126,14 @@ function NamedListBody({
|
|
|
123
126
|
return (
|
|
124
127
|
<>
|
|
125
128
|
{view.selectRenders ? (
|
|
126
|
-
<
|
|
129
|
+
<SearchableSelect
|
|
127
130
|
options={view.options}
|
|
128
131
|
value={value}
|
|
129
132
|
placeholder={view.pending ? copy.loadingPlaceholder : copy.placeholder}
|
|
130
133
|
disabled={view.pending}
|
|
131
134
|
onValueChange={onValueChange}
|
|
132
135
|
aria-describedby={view.unlisted ? unlistedId : undefined}
|
|
136
|
+
searchLabel={copy.searchLabel}
|
|
133
137
|
/>
|
|
134
138
|
) : null}
|
|
135
139
|
{view.refused ? <MutedLine text={copy.forbiddenNote} /> : null}
|
|
@@ -163,7 +167,7 @@ export function NamedListPicker({
|
|
|
163
167
|
label={copy.label}
|
|
164
168
|
description={copy.description}
|
|
165
169
|
error={view.selectRenders ? error : undefined}
|
|
166
|
-
// Only the
|
|
170
|
+
// Only the select claims the field's control id; with nothing to hold no
|
|
167
171
|
// labelable element renders, so `for` would dangle.
|
|
168
172
|
group={!view.selectRenders}
|
|
169
173
|
>
|
|
@@ -68,7 +68,8 @@ function normalizeHref(href: string): string {
|
|
|
68
68
|
* evil.com.
|
|
69
69
|
*
|
|
70
70
|
* Callers render the returned string rather than their input, so the URL that was
|
|
71
|
-
* judged is always the URL that is navigated.
|
|
71
|
+
* judged is always the URL that is navigated. `isSafeHttpUrl` gives the yes/no
|
|
72
|
+
* answer for a caller that does not need the URL itself.
|
|
72
73
|
*/
|
|
73
74
|
export function safeHttpUrl(href: string): string | undefined {
|
|
74
75
|
const normalized = normalizeHref(href);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The option model behind `SearchableSelect`: the caller's options indexed once for
|
|
3
|
+
* search, the sections a query leaves, and the keyboard's movement over them. Pure,
|
|
4
|
+
* so the component re-derives nothing per row and the filter is testable alone.
|
|
5
|
+
*/
|
|
6
|
+
import { searchNeedle, textHasNeedle } from '../search-match';
|
|
7
|
+
import type { SelectGroup, SelectOption } from './select';
|
|
8
|
+
|
|
9
|
+
/** One option, with its position in the full list and its label in search form. */
|
|
10
|
+
export interface SearchEntry {
|
|
11
|
+
readonly option: SelectOption;
|
|
12
|
+
/** The option's position across every section; stable while the reader filters. */
|
|
13
|
+
readonly position: number;
|
|
14
|
+
readonly loweredLabel: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A run of entries under one group label, or under none for a flat list. */
|
|
18
|
+
export interface SearchSection {
|
|
19
|
+
readonly label: string | undefined;
|
|
20
|
+
readonly entries: readonly SearchEntry[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What a query leaves: the non-empty sections and their entries in reading order. */
|
|
24
|
+
export interface VisibleOptions {
|
|
25
|
+
readonly sections: readonly SearchSection[];
|
|
26
|
+
readonly entries: readonly SearchEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Index the caller's options (a flat run, or labelled groups) for search. */
|
|
30
|
+
export function indexSections(
|
|
31
|
+
options: readonly SelectOption[] | undefined,
|
|
32
|
+
groups: readonly SelectGroup[] | undefined,
|
|
33
|
+
): SearchSection[] {
|
|
34
|
+
const runs: { label: string | undefined; options: readonly SelectOption[] }[] =
|
|
35
|
+
groups !== undefined
|
|
36
|
+
? groups.map((group) => ({ label: group.label, options: group.options }))
|
|
37
|
+
: [{ label: undefined, options: options ?? [] }];
|
|
38
|
+
let position = 0;
|
|
39
|
+
return runs.map((run) => ({
|
|
40
|
+
label: run.label,
|
|
41
|
+
entries: run.options.map((option) => ({
|
|
42
|
+
option,
|
|
43
|
+
position: position++,
|
|
44
|
+
loweredLabel: option.label.toLowerCase(),
|
|
45
|
+
})),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The sections and entries whose label contains `query`. A blank query returns every
|
|
51
|
+
* section as indexed; either way each kept entry is the indexed object itself, so a
|
|
52
|
+
* row memoised on its entry does not re-render because the reader typed.
|
|
53
|
+
*/
|
|
54
|
+
export function visibleOptions(sections: readonly SearchSection[], query: string): VisibleOptions {
|
|
55
|
+
const needle = searchNeedle(query);
|
|
56
|
+
const kept =
|
|
57
|
+
needle === ''
|
|
58
|
+
? sections
|
|
59
|
+
: sections.map((section) => ({
|
|
60
|
+
label: section.label,
|
|
61
|
+
entries: section.entries.filter((entry) => textHasNeedle(entry.loweredLabel, needle)),
|
|
62
|
+
}));
|
|
63
|
+
const nonEmpty = kept.filter((section) => section.entries.length > 0);
|
|
64
|
+
return { sections: nonEmpty, entries: nonEmpty.flatMap((section) => section.entries) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function enabled(entry: SearchEntry): boolean {
|
|
68
|
+
return entry.option.disabled !== true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The entry the keyboard highlight moves to from `current` by one step in
|
|
73
|
+
* `direction`, skipping disabled entries and stopping at either end. With no current
|
|
74
|
+
* highlight among `entries`, a step lands on the first (down) or last (up) one.
|
|
75
|
+
*/
|
|
76
|
+
export function stepEntry(
|
|
77
|
+
entries: readonly SearchEntry[],
|
|
78
|
+
current: SearchEntry | undefined,
|
|
79
|
+
direction: 1 | -1,
|
|
80
|
+
): SearchEntry | undefined {
|
|
81
|
+
const at = current === undefined ? -1 : entries.indexOf(current);
|
|
82
|
+
if (at === -1) return direction === 1 ? entries.find(enabled) : entries.findLast(enabled);
|
|
83
|
+
for (let index = at + direction; index >= 0 && index < entries.length; index += direction) {
|
|
84
|
+
const entry = entries[index];
|
|
85
|
+
if (entry !== undefined && enabled(entry)) return entry;
|
|
86
|
+
}
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The entry to draw highlighted: the choosable visible entry whose value is `wanted`,
|
|
92
|
+
* else the first choosable one. Keyed by value, so the highlight stays on its option
|
|
93
|
+
* when the list around it changes, and a query that filters it away moves it to the
|
|
94
|
+
* top of what is left instead of to nothing.
|
|
95
|
+
*/
|
|
96
|
+
export function shownEntry(
|
|
97
|
+
entries: readonly SearchEntry[],
|
|
98
|
+
wanted: string | undefined,
|
|
99
|
+
): SearchEntry | undefined {
|
|
100
|
+
const kept =
|
|
101
|
+
wanted === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: entries.find((entry) => entry.option.value === wanted && enabled(entry));
|
|
104
|
+
return kept ?? entries.find(enabled);
|
|
105
|
+
}
|