@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
|
@@ -122,6 +122,7 @@ export interface ExplorerBodyProps<T> {
|
|
|
122
122
|
readonly columns: readonly ExplorerColumn[];
|
|
123
123
|
readonly pageEntries: readonly ExplorerEntry<T>[];
|
|
124
124
|
readonly getItemKey: (item: T) => string;
|
|
125
|
+
readonly selectedItemKey: string | undefined;
|
|
125
126
|
readonly renderRow: (item: T) => ReactNode;
|
|
126
127
|
readonly renderCard: (item: T) => ReactNode;
|
|
127
128
|
readonly openProps: (item: T) => OpenTargetProps;
|
|
@@ -134,7 +135,15 @@ export interface ExplorerBodyProps<T> {
|
|
|
134
135
|
function CardEntry<T>(
|
|
135
136
|
props: ExplorerBodyProps<T> & { readonly entry: ExplorerEntry<T> },
|
|
136
137
|
): ReactNode {
|
|
137
|
-
const {
|
|
138
|
+
const {
|
|
139
|
+
entry,
|
|
140
|
+
onNavigate,
|
|
141
|
+
renderFolderActions,
|
|
142
|
+
getItemKey,
|
|
143
|
+
selectedItemKey,
|
|
144
|
+
renderCard,
|
|
145
|
+
openProps,
|
|
146
|
+
} = props;
|
|
138
147
|
if (entry.kind === 'folder') {
|
|
139
148
|
return (
|
|
140
149
|
<div role="listitem" key={`folder:${entry.folder.id}`}>
|
|
@@ -148,8 +157,14 @@ function CardEntry<T>(
|
|
|
148
157
|
</div>
|
|
149
158
|
);
|
|
150
159
|
}
|
|
160
|
+
const key = getItemKey(entry.item);
|
|
151
161
|
return (
|
|
152
|
-
<div
|
|
162
|
+
<div
|
|
163
|
+
role="listitem"
|
|
164
|
+
key={`item:${key}`}
|
|
165
|
+
data-selected={key === selectedItemKey ? 'true' : undefined}
|
|
166
|
+
{...openProps(entry.item)}
|
|
167
|
+
>
|
|
153
168
|
{renderCard(entry.item)}
|
|
154
169
|
</div>
|
|
155
170
|
);
|
|
@@ -159,8 +174,16 @@ function CardEntry<T>(
|
|
|
159
174
|
function RowEntry<T>(
|
|
160
175
|
props: ExplorerBodyProps<T> & { readonly entry: ExplorerEntry<T> },
|
|
161
176
|
): ReactNode {
|
|
162
|
-
const {
|
|
163
|
-
|
|
177
|
+
const {
|
|
178
|
+
entry,
|
|
179
|
+
columns,
|
|
180
|
+
onNavigate,
|
|
181
|
+
renderFolderActions,
|
|
182
|
+
getItemKey,
|
|
183
|
+
selectedItemKey,
|
|
184
|
+
renderRow,
|
|
185
|
+
openProps,
|
|
186
|
+
} = props;
|
|
164
187
|
if (entry.kind === 'folder') {
|
|
165
188
|
return (
|
|
166
189
|
<TR key={`folder:${entry.folder.id}`}>
|
|
@@ -174,8 +197,9 @@ function RowEntry<T>(
|
|
|
174
197
|
</TR>
|
|
175
198
|
);
|
|
176
199
|
}
|
|
200
|
+
const key = getItemKey(entry.item);
|
|
177
201
|
return (
|
|
178
|
-
<TR key={`item:${
|
|
202
|
+
<TR key={`item:${key}`} selected={key === selectedItemKey} {...openProps(entry.item)}>
|
|
179
203
|
{renderRow(entry.item)}
|
|
180
204
|
</TR>
|
|
181
205
|
);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { type ReactNode, useEffect, useState } from 'react';
|
|
16
16
|
|
|
17
|
+
import { matchesSearch } from '../search-match';
|
|
17
18
|
import { ExplorerBody, type ExplorerEntry } from './explorer-body';
|
|
18
19
|
import { usePageSize } from './explorer-page-size';
|
|
19
20
|
import { TagFilterRow } from './explorer-tag-filter';
|
|
@@ -50,12 +51,13 @@ export interface ExplorerTags<T> {
|
|
|
50
51
|
readonly maxVisibleTags?: number;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/** Controlled free-text search.
|
|
54
|
-
*
|
|
54
|
+
/** Controlled free-text search. The caller names the texts an item is searched by;
|
|
55
|
+
* the explorer matches them with the SDK's one search rule (`matchesSearch`). */
|
|
55
56
|
export interface ExplorerSearch<T> {
|
|
56
57
|
readonly value: string;
|
|
57
58
|
readonly onChange: (value: string) => void;
|
|
58
|
-
|
|
59
|
+
/** The texts the reader sees for `item` (its name, display name, description…). */
|
|
60
|
+
readonly texts: (item: T) => readonly string[];
|
|
59
61
|
/** The input's accessible name. */
|
|
60
62
|
readonly label: string;
|
|
61
63
|
readonly placeholder?: string;
|
|
@@ -69,10 +71,11 @@ export interface ExplorerEmptyState {
|
|
|
69
71
|
|
|
70
72
|
/** The folder-aware empty-state ladder — every label caller-supplied. */
|
|
71
73
|
export interface ExplorerEmptyStates {
|
|
72
|
-
/** No items
|
|
74
|
+
/** No item exists at all. Folders are places for items, so with no item there is
|
|
75
|
+
* nothing to browse: this state shows whatever folders exist. */
|
|
73
76
|
readonly empty: ExplorerEmptyState;
|
|
74
|
-
/** The current folder holds nothing — no items, no subfolders —
|
|
75
|
-
*
|
|
77
|
+
/** The current folder holds nothing — no items, no subfolders — while items
|
|
78
|
+
* exist elsewhere. */
|
|
76
79
|
readonly emptyFolder: ExplorerEmptyState;
|
|
77
80
|
/** The current folder has items but the active tag/search filter excludes
|
|
78
81
|
* every one, and it has no subfolders to show. */
|
|
@@ -86,6 +89,8 @@ export interface ExplorerViewProps<T> {
|
|
|
86
89
|
readonly getFolderId: (item: T) => string | null;
|
|
87
90
|
|
|
88
91
|
readonly folders: readonly Folder[];
|
|
92
|
+
/** The open folder. One that is not among `folders` (deleted, or no longer drawn
|
|
93
|
+
* on this screen) shows as the root, and the explorer asks `onNavigate(null)`. */
|
|
89
94
|
readonly currentFolderId: string | null;
|
|
90
95
|
readonly onNavigate: (folderId: string | null) => void;
|
|
91
96
|
readonly rootLabel: string;
|
|
@@ -107,6 +112,12 @@ export interface ExplorerViewProps<T> {
|
|
|
107
112
|
* originating on a nested control is left to that control. */
|
|
108
113
|
readonly onOpenItem?: (item: T) => void;
|
|
109
114
|
|
|
115
|
+
/** The key (as `getItemKey` gives it) of the item the screen has selected. Its
|
|
116
|
+
* row/card carries `data-selected="true"`, which the stylesheet draws as the one
|
|
117
|
+
* selected look every explorer shares. The screen still marks the item's name
|
|
118
|
+
* link `aria-current`, which is what assistive tech reads. */
|
|
119
|
+
readonly selectedItemKey?: string;
|
|
120
|
+
|
|
110
121
|
readonly tags?: ExplorerTags<T>;
|
|
111
122
|
readonly search?: ExplorerSearch<T>;
|
|
112
123
|
|
|
@@ -183,19 +194,35 @@ function filterInFolder<T>(
|
|
|
183
194
|
tags: ExplorerTags<T> | undefined,
|
|
184
195
|
selectedSet: ReadonlySet<string> | undefined,
|
|
185
196
|
search: ExplorerSearch<T> | undefined,
|
|
186
|
-
query: string,
|
|
187
197
|
): readonly T[] {
|
|
188
198
|
return inFolder.filter((item) => {
|
|
189
199
|
if (tags !== undefined && selectedSet !== undefined && selectedSet.size > 0) {
|
|
190
200
|
if (!matchesSelectedTags(tags.getTags(item), selectedSet)) return false;
|
|
191
201
|
}
|
|
192
|
-
if (search !== undefined &&
|
|
193
|
-
if (!search.matches(item, query)) return false;
|
|
194
|
-
}
|
|
202
|
+
if (search !== undefined && !matchesSearch(search.value, ...search.texts(item))) return false;
|
|
195
203
|
return true;
|
|
196
204
|
});
|
|
197
205
|
}
|
|
198
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The folder the explorer shows. The open folder can stop being drawn while the user
|
|
209
|
+
* is inside it (deleted, or its last item on this screen filed elsewhere); the
|
|
210
|
+
* explorer then shows the root, so the breadcrumb and the body agree, and hands the
|
|
211
|
+
* root back to the screen's navigation state. One linear pass over the drawn folders.
|
|
212
|
+
*/
|
|
213
|
+
function useShownFolderId(
|
|
214
|
+
currentFolderId: string | null,
|
|
215
|
+
folders: readonly Folder[],
|
|
216
|
+
onNavigate: (folderId: string | null) => void,
|
|
217
|
+
): string | null {
|
|
218
|
+
const currentDrawn =
|
|
219
|
+
currentFolderId === null || folders.some((folder) => folder.id === currentFolderId);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!currentDrawn) onNavigate(null);
|
|
222
|
+
}, [currentDrawn, onNavigate]);
|
|
223
|
+
return currentDrawn ? currentFolderId : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
199
226
|
export function ExplorerView<T>({
|
|
200
227
|
items,
|
|
201
228
|
getItemKey,
|
|
@@ -210,6 +237,7 @@ export function ExplorerView<T>({
|
|
|
210
237
|
renderRow,
|
|
211
238
|
renderCard,
|
|
212
239
|
onOpenItem,
|
|
240
|
+
selectedItemKey,
|
|
213
241
|
tags,
|
|
214
242
|
search,
|
|
215
243
|
renderFolderActions,
|
|
@@ -222,25 +250,27 @@ export function ExplorerView<T>({
|
|
|
222
250
|
const query = search ? search.value.trim() : '';
|
|
223
251
|
const tagKey = tags !== undefined ? tags.selected.join('\0') : '';
|
|
224
252
|
|
|
253
|
+
const folderId = useShownFolderId(currentFolderId, folders, onNavigate);
|
|
254
|
+
|
|
225
255
|
// A new filter view starts on its first page; the clamp below then handles a
|
|
226
256
|
// set that shrinks under the current page without a reset.
|
|
227
257
|
useEffect(() => {
|
|
228
258
|
setPage(1);
|
|
229
|
-
}, [
|
|
259
|
+
}, [folderId, query, tagKey, pageSize]);
|
|
230
260
|
|
|
231
261
|
if (viewSurface === '') throw new Error('ExplorerView requires a non-empty viewSurface key.');
|
|
232
262
|
if (columns.length === 0) throw new Error('ExplorerView requires at least one column.');
|
|
233
263
|
|
|
234
|
-
//
|
|
235
|
-
if (items.length === 0
|
|
264
|
+
// No item anywhere: a bare empty state, no controls — whatever folders exist.
|
|
265
|
+
if (items.length === 0) {
|
|
236
266
|
return <EmptyState {...emptyStates.empty} />;
|
|
237
267
|
}
|
|
238
268
|
|
|
239
|
-
const subfolders = childFolders(folders,
|
|
240
|
-
const inFolder = items.filter((item) => getFolderId(item) ===
|
|
269
|
+
const subfolders = childFolders(folders, folderId);
|
|
270
|
+
const inFolder = items.filter((item) => getFolderId(item) === folderId);
|
|
241
271
|
|
|
242
272
|
const selectedSet = tags !== undefined ? new Set(tags.selected) : undefined;
|
|
243
|
-
const filtered = filterInFolder(inFolder, tags, selectedSet, search
|
|
273
|
+
const filtered = filterInFolder(inFolder, tags, selectedSet, search);
|
|
244
274
|
|
|
245
275
|
const vocabulary =
|
|
246
276
|
tags !== undefined
|
|
@@ -292,7 +322,7 @@ export function ExplorerView<T>({
|
|
|
292
322
|
|
|
293
323
|
<FolderBreadcrumb
|
|
294
324
|
folders={folders}
|
|
295
|
-
currentFolderId={
|
|
325
|
+
currentFolderId={folderId}
|
|
296
326
|
onNavigate={onNavigate}
|
|
297
327
|
rootLabel={rootLabel}
|
|
298
328
|
/>
|
|
@@ -316,6 +346,7 @@ export function ExplorerView<T>({
|
|
|
316
346
|
columns={columns}
|
|
317
347
|
pageEntries={pageEntries}
|
|
318
348
|
getItemKey={getItemKey}
|
|
349
|
+
selectedItemKey={selectedItemKey}
|
|
319
350
|
renderRow={renderRow}
|
|
320
351
|
renderCard={renderCard}
|
|
321
352
|
openProps={openProps}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* preset/tool extensions wire carries. It is PRESENTATIONAL: the caller fetches the
|
|
7
7
|
* extension catalog (`available`) and owns the `value`; this component fetches nothing.
|
|
8
8
|
*
|
|
9
|
-
* The surface has two
|
|
10
|
-
* - the current combos, each an ordered row of name `Badge`s
|
|
11
|
-
* (pull it back into the draft) and Remove;
|
|
12
|
-
* - a combo UNDER CONSTRUCTION, authored with the shared `ExtensionPicker` (grouped
|
|
9
|
+
* The surface has two sections:
|
|
10
|
+
* - the current combos (`CommittedComboList`), each an ordered row of name `Badge`s
|
|
11
|
+
* with a per-combo Edit (pull it back into the draft) and Remove;
|
|
12
|
+
* - a combo UNDER CONSTRUCTION (`ComboDraftEditor`), authored with the shared `ExtensionPicker` (grouped
|
|
13
13
|
* by kind, single-select for the non-stackable `backend` kind). An extension whose
|
|
14
14
|
* catalog entry declares a `config_schema` is authored as `{ name, config }` through
|
|
15
15
|
* its settings form; any other name stays a bare element, or the edited combo's own
|
|
@@ -38,15 +38,15 @@
|
|
|
38
38
|
import type { Extension, PresetExtensionElement } from '@tai42/api-client';
|
|
39
39
|
import { type ReactNode, useEffect, useState } from 'react';
|
|
40
40
|
|
|
41
|
-
import { comboElementNames, extensionElementName } from '../extension-combos';
|
|
42
|
-
import type { JsonSchema } from '../schema-form/types';
|
|
43
41
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
type ConfigurableExtension,
|
|
42
|
+
comboElementNames,
|
|
43
|
+
extensionElementName,
|
|
47
44
|
sameNames,
|
|
48
45
|
unknownNames,
|
|
49
|
-
} from '
|
|
46
|
+
} from '../extension-combos';
|
|
47
|
+
import type { JsonSchema } from '../schema-form/types';
|
|
48
|
+
import { ComboDraftEditor, type ConfigurableExtension } from './combo-draft-editor';
|
|
49
|
+
import { CommittedComboList } from './committed-combo-list';
|
|
50
50
|
import type { ExtensionConfigChange } from './extension-config-editor';
|
|
51
51
|
|
|
52
52
|
type Seeds = Readonly<Record<string, Record<string, unknown> | undefined>>;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
16
16
|
|
|
17
17
|
import { collectEnvRefs } from '../env-markers';
|
|
18
|
-
import { SchemaEditor, type
|
|
18
|
+
import { SchemaEditor, type SchemaEditorDraft } from '../schema-editor';
|
|
19
19
|
import { defaultValueForSchema } from '../schema-form/default-value';
|
|
20
20
|
import { JSON_SCHEMA_META_URI } from '../schema-form/json-schema-meta';
|
|
21
21
|
import { SchemaForm } from '../schema-form/SchemaForm';
|
|
@@ -40,7 +40,7 @@ export interface ExtensionConfigEditorProps {
|
|
|
40
40
|
readonly idPrefix: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
type SchemaSettings = Readonly<Record<string,
|
|
43
|
+
type SchemaSettings = Readonly<Record<string, SchemaEditorDraft>>;
|
|
44
44
|
|
|
45
45
|
const isMetaRef = (node: JsonSchema): boolean => node.$ref === JSON_SCHEMA_META_URI;
|
|
46
46
|
|
|
@@ -240,7 +240,7 @@ function RenderableSettings({
|
|
|
240
240
|
return seeded;
|
|
241
241
|
});
|
|
242
242
|
const [schemaSettings, setSchemaSettings] = useState<SchemaSettings>(() => {
|
|
243
|
-
const seeded: Record<string,
|
|
243
|
+
const seeded: Record<string, SchemaEditorDraft> = {};
|
|
244
244
|
for (const key of schemaKeys) {
|
|
245
245
|
const stored = value?.[key];
|
|
246
246
|
seeded[key] = { schema: isRecord(stored) ? stored : null, valid: true };
|
package/src/components/field.tsx
CHANGED
|
@@ -13,6 +13,8 @@ import { XCircleIcon } from './icons';
|
|
|
13
13
|
|
|
14
14
|
interface FieldContextValue {
|
|
15
15
|
readonly controlId: string | undefined;
|
|
16
|
+
/** The label element's id, published only while `controlId` is (a non-group Field). */
|
|
17
|
+
readonly labelId: string | undefined;
|
|
16
18
|
readonly describedBy: string | undefined;
|
|
17
19
|
readonly invalid: boolean;
|
|
18
20
|
}
|
|
@@ -48,6 +50,15 @@ export function useFieldControl(): FieldControlProps {
|
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The id of the enclosing Field's label while it names one control, else `undefined`.
|
|
55
|
+
* A control whose popup holds a second labelled surface (a listbox, a dialog) points
|
|
56
|
+
* that surface's `aria-labelledby` at it, so the popup carries the field's name.
|
|
57
|
+
*/
|
|
58
|
+
export function useFieldLabelId(): string | undefined {
|
|
59
|
+
return useContext(FieldContext)?.labelId;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
export interface FieldProps {
|
|
52
63
|
readonly label: string;
|
|
53
64
|
readonly description?: string;
|
|
@@ -133,8 +144,13 @@ export function Field({
|
|
|
133
144
|
// if the group Field withheld it too, a group Field carrying an `error` would
|
|
134
145
|
// expose its invalid state NOWHERE in the accessibility tree.
|
|
135
146
|
const value: FieldContextValue = group
|
|
136
|
-
? {
|
|
137
|
-
|
|
147
|
+
? {
|
|
148
|
+
controlId: undefined,
|
|
149
|
+
labelId: undefined,
|
|
150
|
+
describedBy: undefined,
|
|
151
|
+
invalid: error !== undefined,
|
|
152
|
+
}
|
|
153
|
+
: { controlId, labelId, describedBy, invalid: error !== undefined };
|
|
138
154
|
|
|
139
155
|
// The label keeps its `htmlFor`/`id` wiring either way — `hideLabel` only drops
|
|
140
156
|
// its PAINT, so the accessible name it gives the control is unchanged.
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The folder WRITES of the tool-organization tree — create, rename, move and delete —
|
|
3
|
+
* shared by every screen that shows the tree, so each offers the same actions over
|
|
4
|
+
* the same client doors (`createFolder` / `renameFolder` / `moveFolder` /
|
|
5
|
+
* `deleteFolder`).
|
|
6
|
+
*
|
|
7
|
+
* The SDK owns no query cache, so every write takes the screen's
|
|
8
|
+
* `onFoldersChanged`: the screen re-reads the overlay under its own key, and the
|
|
9
|
+
* write resolves only after that refresh, so the tree on screen is never stale when
|
|
10
|
+
* a dialog closes. Each dialog owns its own pending and error state, so a failure on
|
|
11
|
+
* one folder never bleeds onto another, and every failure is shown in its dialog.
|
|
12
|
+
*
|
|
13
|
+
* Move offers every folder EXCEPT the folder itself and its descendants (a folder
|
|
14
|
+
* cannot become its own ancestor), with inline create. Delete asks first and lets the
|
|
15
|
+
* server enforce that only an empty folder can go; a refusal is shown in the dialog.
|
|
16
|
+
*
|
|
17
|
+
* Each action is offered only where the caller may make that write
|
|
18
|
+
* ({@link useToolMetaWriteAccess}): its own route, and the tool_meta store not known
|
|
19
|
+
* off. A not-configured refusal is reported, so every screen withdraws its overlay
|
|
20
|
+
* writes; the open dialog stays to show the server's off note.
|
|
21
|
+
*/
|
|
22
|
+
import { type ReactNode, useCallback, useState } from 'react';
|
|
23
|
+
|
|
24
|
+
import { errorMessage } from '../errors';
|
|
25
|
+
import { FeatureDisabled, featureDisabledMessage, isFeatureDisabled } from '../feature-disabled';
|
|
26
|
+
import { useApi } from '../hooks/useApi';
|
|
27
|
+
import { ConfirmDialog } from './confirm-dialog';
|
|
28
|
+
import { Dialog } from './dialog';
|
|
29
|
+
import { Field } from './field';
|
|
30
|
+
import { type Folder, FolderPicker } from './folder-nav';
|
|
31
|
+
import { TextInput } from './inputs';
|
|
32
|
+
import { Button, Spinner } from './primitives';
|
|
33
|
+
import {
|
|
34
|
+
ToolMetaWriteError,
|
|
35
|
+
useReportToolMetaRefusal,
|
|
36
|
+
useToolMetaWriteAccess,
|
|
37
|
+
} from './tool-meta-access';
|
|
38
|
+
|
|
39
|
+
/** Re-read the folder tree after a write; the write resolves once this settles. */
|
|
40
|
+
export type FoldersChanged = () => Promise<void>;
|
|
41
|
+
|
|
42
|
+
/** Create a folder under `parentId` and resolve with its id. */
|
|
43
|
+
export type CreateFolder = (name: string, parentId: string | null) => Promise<string>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a folder through the folder door and refresh the screen's tree, resolving
|
|
47
|
+
* with the new folder's id — the shape {@link FolderPicker}'s `onCreateFolder`
|
|
48
|
+
* takes. `undefined` when the caller may not create a folder, so the picker offers
|
|
49
|
+
* no create. A rejection propagates to the picker, which shows it; a not-configured
|
|
50
|
+
* refusal is also reported, withdrawing every overlay write.
|
|
51
|
+
*/
|
|
52
|
+
export function useCreateFolder(onFoldersChanged: FoldersChanged): CreateFolder | undefined {
|
|
53
|
+
const api = useApi();
|
|
54
|
+
const { createFolder: allowed } = useToolMetaWriteAccess();
|
|
55
|
+
const reportRefusal = useReportToolMetaRefusal();
|
|
56
|
+
const create = useCallback(
|
|
57
|
+
async (name: string, parentId: string | null): Promise<string> => {
|
|
58
|
+
let created: { readonly id: string };
|
|
59
|
+
try {
|
|
60
|
+
created = await api.createFolder(name, parentId);
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
reportRefusal(error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
await onFoldersChanged();
|
|
66
|
+
return created.id;
|
|
67
|
+
},
|
|
68
|
+
[api, onFoldersChanged, reportRefusal],
|
|
69
|
+
);
|
|
70
|
+
return allowed ? create : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One write's pending + error state: `run` clears the last error, awaits the write
|
|
74
|
+
* and keeps a rejection for the dialog to show, reporting a not-configured one. */
|
|
75
|
+
function useFolderWrite(write: () => Promise<void>): {
|
|
76
|
+
readonly run: () => void;
|
|
77
|
+
readonly pending: boolean;
|
|
78
|
+
readonly error: unknown;
|
|
79
|
+
} {
|
|
80
|
+
const reportRefusal = useReportToolMetaRefusal();
|
|
81
|
+
const [pending, setPending] = useState(false);
|
|
82
|
+
const [error, setError] = useState<unknown>(null);
|
|
83
|
+
const run = (): void => {
|
|
84
|
+
if (pending) return;
|
|
85
|
+
setPending(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
write().then(
|
|
88
|
+
() => {
|
|
89
|
+
setPending(false);
|
|
90
|
+
},
|
|
91
|
+
(failure: unknown) => {
|
|
92
|
+
setError(failure);
|
|
93
|
+
setPending(false);
|
|
94
|
+
reportRefusal(failure);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
return { run, pending, error };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The ids of `folder` and every folder nested beneath it — linear in the folder
|
|
102
|
+
* count and safe on a cyclic parent chain. */
|
|
103
|
+
function subtreeIds(folder: Folder, folders: readonly Folder[]): Set<string> {
|
|
104
|
+
const childrenByParent = new Map<string, string[]>();
|
|
105
|
+
for (const candidate of folders) {
|
|
106
|
+
if (candidate.parentId === null) continue;
|
|
107
|
+
const siblings = childrenByParent.get(candidate.parentId) ?? [];
|
|
108
|
+
siblings.push(candidate.id);
|
|
109
|
+
childrenByParent.set(candidate.parentId, siblings);
|
|
110
|
+
}
|
|
111
|
+
const ids = new Set<string>([folder.id]);
|
|
112
|
+
const queue = [folder.id];
|
|
113
|
+
for (let next = queue.pop(); next !== undefined; next = queue.pop()) {
|
|
114
|
+
for (const childId of childrenByParent.get(next) ?? []) {
|
|
115
|
+
if (ids.has(childId)) continue;
|
|
116
|
+
ids.add(childId);
|
|
117
|
+
queue.push(childId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return ids;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface FolderDialogProps {
|
|
124
|
+
readonly folder: Folder;
|
|
125
|
+
readonly onFoldersChanged: FoldersChanged;
|
|
126
|
+
readonly onClose: () => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function RenameFolderDialog({ folder, onFoldersChanged, onClose }: FolderDialogProps): ReactNode {
|
|
130
|
+
const api = useApi();
|
|
131
|
+
const [name, setName] = useState(folder.name);
|
|
132
|
+
const trimmed = name.trim();
|
|
133
|
+
const rename = useFolderWrite(async () => {
|
|
134
|
+
await api.renameFolder(folder.id, trimmed);
|
|
135
|
+
await onFoldersChanged();
|
|
136
|
+
onClose();
|
|
137
|
+
});
|
|
138
|
+
const blocked = trimmed === '' || trimmed === folder.name || rename.pending;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Dialog
|
|
142
|
+
title="Rename folder"
|
|
143
|
+
open
|
|
144
|
+
onOpenChange={(next) => {
|
|
145
|
+
if (!next) onClose();
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<Field label="Folder name">
|
|
149
|
+
<TextInput
|
|
150
|
+
value={name}
|
|
151
|
+
autoComplete="off"
|
|
152
|
+
onChange={(event) => {
|
|
153
|
+
setName(event.target.value);
|
|
154
|
+
}}
|
|
155
|
+
onKeyDown={(event) => {
|
|
156
|
+
if (event.key === 'Enter' && !blocked) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
rename.run();
|
|
159
|
+
}
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
</Field>
|
|
163
|
+
<ToolMetaWriteError error={rename.error} />
|
|
164
|
+
<div className="tai-dialog-actions">
|
|
165
|
+
<Button type="button" onClick={onClose}>
|
|
166
|
+
Cancel
|
|
167
|
+
</Button>
|
|
168
|
+
<Button type="button" variant="primary" disabled={blocked} onClick={rename.run}>
|
|
169
|
+
{rename.pending ? <Spinner label="Renaming folder" /> : null}
|
|
170
|
+
Rename
|
|
171
|
+
</Button>
|
|
172
|
+
</div>
|
|
173
|
+
</Dialog>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function MoveFolderDialog({
|
|
178
|
+
folder,
|
|
179
|
+
folders,
|
|
180
|
+
onFoldersChanged,
|
|
181
|
+
onClose,
|
|
182
|
+
}: FolderDialogProps & { readonly folders: readonly Folder[] }): ReactNode {
|
|
183
|
+
const api = useApi();
|
|
184
|
+
const [parentId, setParentId] = useState<string | null>(folder.parentId);
|
|
185
|
+
const createFolder = useCreateFolder(onFoldersChanged);
|
|
186
|
+
const move = useFolderWrite(async () => {
|
|
187
|
+
await api.moveFolder(folder.id, parentId);
|
|
188
|
+
await onFoldersChanged();
|
|
189
|
+
onClose();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const forbidden = subtreeIds(folder, folders);
|
|
193
|
+
const targets = folders.filter((candidate) => !forbidden.has(candidate.id));
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<Dialog
|
|
197
|
+
title={`Move ${folder.name}`}
|
|
198
|
+
open
|
|
199
|
+
onOpenChange={(next) => {
|
|
200
|
+
if (!next) onClose();
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<Field label="Destination folder" description="Choose a new parent, or the root." group>
|
|
204
|
+
<FolderPicker
|
|
205
|
+
value={parentId}
|
|
206
|
+
onChange={setParentId}
|
|
207
|
+
folders={targets}
|
|
208
|
+
onCreateFolder={createFolder}
|
|
209
|
+
unfiledLabel="Root (no folder)"
|
|
210
|
+
/>
|
|
211
|
+
</Field>
|
|
212
|
+
<ToolMetaWriteError error={move.error} />
|
|
213
|
+
<div className="tai-dialog-actions">
|
|
214
|
+
<Button type="button" onClick={onClose}>
|
|
215
|
+
Cancel
|
|
216
|
+
</Button>
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
variant="primary"
|
|
220
|
+
disabled={parentId === folder.parentId || move.pending}
|
|
221
|
+
onClick={move.run}
|
|
222
|
+
>
|
|
223
|
+
{move.pending ? <Spinner label="Moving folder" /> : null}
|
|
224
|
+
Move
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
</Dialog>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function DeleteFolderDialog({ folder, onFoldersChanged, onClose }: FolderDialogProps): ReactNode {
|
|
232
|
+
const api = useApi();
|
|
233
|
+
const remove = useFolderWrite(async () => {
|
|
234
|
+
await api.deleteFolder(folder.id);
|
|
235
|
+
await onFoldersChanged();
|
|
236
|
+
onClose();
|
|
237
|
+
});
|
|
238
|
+
const off = isFeatureDisabled(remove.error);
|
|
239
|
+
return (
|
|
240
|
+
<ConfirmDialog
|
|
241
|
+
title={`Delete ${folder.name}?`}
|
|
242
|
+
confirmLabel="Delete folder"
|
|
243
|
+
pendingLabel="Deleting folder"
|
|
244
|
+
onConfirm={remove.run}
|
|
245
|
+
onClose={onClose}
|
|
246
|
+
isPending={remove.pending}
|
|
247
|
+
error={remove.error !== null && !off ? errorMessage(remove.error) : null}
|
|
248
|
+
disabledNote={
|
|
249
|
+
off ? (
|
|
250
|
+
<FeatureDisabled feature="Tool metadata" message={featureDisabledMessage(remove.error)} />
|
|
251
|
+
) : undefined
|
|
252
|
+
}
|
|
253
|
+
>
|
|
254
|
+
<p>
|
|
255
|
+
Only an empty folder can be deleted — one that still holds tools (hidden ones count) or
|
|
256
|
+
subfolders cannot be.
|
|
257
|
+
</p>
|
|
258
|
+
</ConfirmDialog>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Props of {@link FolderActionsMenu}. */
|
|
263
|
+
export interface FolderActionsMenuProps {
|
|
264
|
+
/** The folder the actions act on. */
|
|
265
|
+
readonly folder: Folder;
|
|
266
|
+
/** The FULL folder tree, offered as move destinations. */
|
|
267
|
+
readonly folders: readonly Folder[];
|
|
268
|
+
/** The screen's refresh of its overlay read, awaited after every write. */
|
|
269
|
+
readonly onFoldersChanged: FoldersChanged;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The Rename, Move and Delete buttons for one folder row/card, each owning its
|
|
273
|
+
* dialog — for an explorer's `renderFolderActions` slot. Each button shows only
|
|
274
|
+
* where the caller may make that write; a dialog already open stays open after its
|
|
275
|
+
* button is withdrawn, so its outcome is still shown. Renders nothing for a caller
|
|
276
|
+
* who may make none of the three writes. */
|
|
277
|
+
export function FolderActionsMenu({
|
|
278
|
+
folder,
|
|
279
|
+
folders,
|
|
280
|
+
onFoldersChanged,
|
|
281
|
+
}: FolderActionsMenuProps): ReactNode {
|
|
282
|
+
const access = useToolMetaWriteAccess();
|
|
283
|
+
const [action, setAction] = useState<'rename' | 'move' | 'delete' | null>(null);
|
|
284
|
+
const close = (): void => {
|
|
285
|
+
setAction(null);
|
|
286
|
+
};
|
|
287
|
+
if (!access.renameFolder && !access.moveFolder && !access.deleteFolder && action === null) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return (
|
|
291
|
+
<div className="tai-row">
|
|
292
|
+
{access.renameFolder ? (
|
|
293
|
+
<Button
|
|
294
|
+
variant="ghost"
|
|
295
|
+
aria-label={`Rename folder ${folder.name}`}
|
|
296
|
+
onClick={() => {
|
|
297
|
+
setAction('rename');
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
Rename
|
|
301
|
+
</Button>
|
|
302
|
+
) : null}
|
|
303
|
+
{access.moveFolder ? (
|
|
304
|
+
<Button
|
|
305
|
+
variant="ghost"
|
|
306
|
+
aria-label={`Move folder ${folder.name}`}
|
|
307
|
+
onClick={() => {
|
|
308
|
+
setAction('move');
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
Move
|
|
312
|
+
</Button>
|
|
313
|
+
) : null}
|
|
314
|
+
{access.deleteFolder ? (
|
|
315
|
+
<Button
|
|
316
|
+
variant="ghost"
|
|
317
|
+
aria-label={`Delete folder ${folder.name}`}
|
|
318
|
+
onClick={() => {
|
|
319
|
+
setAction('delete');
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
Delete
|
|
323
|
+
</Button>
|
|
324
|
+
) : null}
|
|
325
|
+
{action === 'rename' ? (
|
|
326
|
+
<RenameFolderDialog folder={folder} onFoldersChanged={onFoldersChanged} onClose={close} />
|
|
327
|
+
) : null}
|
|
328
|
+
{action === 'move' ? (
|
|
329
|
+
<MoveFolderDialog
|
|
330
|
+
folder={folder}
|
|
331
|
+
folders={folders}
|
|
332
|
+
onFoldersChanged={onFoldersChanged}
|
|
333
|
+
onClose={close}
|
|
334
|
+
/>
|
|
335
|
+
) : null}
|
|
336
|
+
{action === 'delete' ? (
|
|
337
|
+
<DeleteFolderDialog folder={folder} onFoldersChanged={onFoldersChanged} onClose={close} />
|
|
338
|
+
) : null}
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|