@tai42/studio-sdk 30.0.1 → 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/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-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/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/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 +23 -18
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +51 -33
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/tool-select.d.ts +6 -5
- package/dist/components/tool-select.d.ts.map +1 -1
- package/dist/components/tool-select.js +31 -14
- package/dist/components/tool-select.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/components.css +53 -0
- package/src/components/explorer-body.tsx +29 -5
- package/src/components/explorer-view.tsx +48 -17
- 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/subject-section.tsx +3 -2
- package/src/components/table.tsx +12 -2
- package/src/components/tool-meta-access.tsx +101 -0
- package/src/components/tool-name-entry.tsx +77 -0
- package/src/components/tool-picker.tsx +83 -50
- package/src/components/tool-select.tsx +49 -38
- 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
|
@@ -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}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|