sh3-core 0.19.6 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/admin/AuthSettingsView.svelte +3 -9
- package/dist/app/admin/MountsView.svelte +276 -0
- package/dist/app/admin/MountsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +6 -6
- package/dist/app/admin/UsersView.svelte +103 -7
- package/dist/app/admin/adminApp.js +1 -0
- package/dist/app/admin/adminShard.svelte.js +10 -0
- package/dist/apps/lifecycle.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +1 -0
- package/dist/assets/icons.svg +5 -0
- package/dist/auth/admin-users.svelte.js +2 -1
- package/dist/auth/auth.svelte.d.ts +4 -5
- package/dist/auth/auth.svelte.js +5 -6
- package/dist/auth/types.d.ts +0 -2
- package/dist/chrome/CompactChrome.svelte +25 -6
- package/dist/chrome/FloatsSheet.svelte +7 -32
- package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
- package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
- package/dist/chrome/MenuSheet.svelte +154 -148
- package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
- package/dist/chrome/MenuSheet.svelte.test.js +24 -12
- package/dist/createShell.js +32 -21
- package/dist/createShell.remoteAuth.test.js +9 -3
- package/dist/documents/backends.d.ts +12 -0
- package/dist/documents/backends.js +230 -3
- package/dist/documents/backends.test.js +147 -1
- package/dist/documents/browse.d.ts +18 -1
- package/dist/documents/browse.js +40 -7
- package/dist/documents/browse.test.js +35 -35
- package/dist/documents/config.d.ts +6 -0
- package/dist/documents/config.js +18 -1
- package/dist/documents/handle.js +65 -17
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +6 -0
- package/dist/documents/http-backend.js +71 -2
- package/dist/documents/http-backend.test.js +51 -1
- package/dist/documents/index.d.ts +2 -2
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.d.ts +4 -2
- package/dist/documents/picker-api.test.d.ts +1 -1
- package/dist/documents/picker-api.test.js +89 -59
- package/dist/documents/picker-primitive.d.ts +4 -0
- package/dist/documents/picker-primitive.js +27 -29
- package/dist/documents/types.d.ts +93 -19
- package/dist/documents/types.js +6 -0
- package/dist/layout/presets.test.js +4 -4
- package/dist/layout/types.d.ts +1 -1
- package/dist/layouts-shard/LayoutsSection.svelte +3 -16
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
- package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
- package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
- package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
- package/dist/primitives/widgets/PickerList.svelte +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
- package/dist/projects-shard/ProjectManage.svelte +197 -28
- package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
- package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
- package/dist/projects-shard/ProjectsSection.svelte +3 -16
- package/dist/projects-shard/projectsApi.js +2 -1
- package/dist/registry/permission-descriptions.js +4 -0
- package/dist/server-shard/types.d.ts +21 -0
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/HomeSection.svelte +107 -0
- package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
- package/dist/sh3core-shard/Sh3Home.svelte +9 -23
- package/dist/shards/activate.svelte.d.ts +4 -0
- package/dist/shards/activate.svelte.js +11 -3
- package/dist/shards/types.d.ts +7 -0
- package/dist/shell-shard/Terminal.svelte +4 -1
- package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
- package/dist/shell-shard/dispatch.d.ts +2 -0
- package/dist/shell-shard/dispatch.js +2 -0
- package/dist/shell-shard/manifest.js +7 -1
- package/dist/shell-shard/shellShard.svelte.js +1 -1
- package/dist/shell-shard/tenant-fs-client.js +2 -1
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +35 -0
- package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cat.test.js +49 -0
- package/dist/shell-shard/verbs/index.js +12 -0
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +48 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +64 -0
- package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
- package/dist/shell-shard/verbs/mkdir.js +30 -0
- package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mkdir.test.js +48 -0
- package/dist/shell-shard/verbs/mv.d.ts +2 -0
- package/dist/shell-shard/verbs/mv.js +33 -0
- package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mv.test.js +55 -0
- package/dist/shell-shard/verbs/rm.d.ts +2 -0
- package/dist/shell-shard/verbs/rm.js +28 -0
- package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
- package/dist/shell-shard/verbs/rm.test.js +47 -0
- package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
- package/dist/shell-shard/verbs/scope-parse.js +33 -0
- package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
- package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
- package/dist/shell-shard/verbs/xfer.d.ts +2 -0
- package/dist/shell-shard/verbs/xfer.js +101 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +96 -0
- package/dist/transport/apiFetch.js +12 -5
- package/dist/verbs/types.d.ts +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,8 @@ export declare const PERMISSION_DOCUMENTS_READ = "documents:read";
|
|
|
34
34
|
* `browse`.
|
|
35
35
|
*/
|
|
36
36
|
export declare const PERMISSION_DOCUMENTS_WRITE = "documents:write";
|
|
37
|
+
/** Permission to manage document mount points (admin-only). */
|
|
38
|
+
export declare const PERMISSION_DOCUMENTS_MOUNT = "documents:mount";
|
|
37
39
|
/**
|
|
38
40
|
* Format hint for document content. Determines whether reads return a string
|
|
39
41
|
* (`text`) or an `ArrayBuffer` (`binary`).
|
|
@@ -71,26 +73,37 @@ export interface DocumentMeta {
|
|
|
71
73
|
deleted?: boolean;
|
|
72
74
|
}
|
|
73
75
|
/** Change notification payload delivered to watch callbacks. */
|
|
74
|
-
export
|
|
75
|
-
type: 'create' | 'update' | 'delete'
|
|
76
|
-
/**
|
|
77
|
-
* For 'create' / 'update' / 'delete', the affected document path.
|
|
78
|
-
* For 'rename', the new path the document now lives at.
|
|
79
|
-
*/
|
|
76
|
+
export type DocumentChange = {
|
|
77
|
+
type: 'create' | 'update' | 'delete';
|
|
80
78
|
path: string;
|
|
81
|
-
/**
|
|
82
|
-
* Populated only when type === 'rename'. The path the document
|
|
83
|
-
* used to live at before the rename.
|
|
84
|
-
*/
|
|
85
|
-
oldPath?: string;
|
|
86
79
|
tenantId: string;
|
|
87
80
|
shardId: string;
|
|
88
|
-
}
|
|
89
|
-
/** Type guard: narrows a DocumentChange to the rename variant. */
|
|
90
|
-
export declare function isRename(change: DocumentChange): change is DocumentChange & {
|
|
81
|
+
} | {
|
|
91
82
|
type: 'rename';
|
|
83
|
+
path: string;
|
|
84
|
+
oldPath: string;
|
|
85
|
+
tenantId: string;
|
|
86
|
+
shardId: string;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'folder-create' | 'folder-delete';
|
|
89
|
+
path: string;
|
|
90
|
+
tenantId: string;
|
|
91
|
+
shardId: string;
|
|
92
|
+
} | {
|
|
93
|
+
type: 'folder-rename';
|
|
94
|
+
path: string;
|
|
92
95
|
oldPath: string;
|
|
96
|
+
tenantId: string;
|
|
97
|
+
shardId: string;
|
|
93
98
|
};
|
|
99
|
+
/** Type guard: narrows a DocumentChange to the rename variant. */
|
|
100
|
+
export declare function isRename(change: DocumentChange): change is Extract<DocumentChange, {
|
|
101
|
+
type: 'rename';
|
|
102
|
+
}>;
|
|
103
|
+
/** Type guard: narrows a DocumentChange to the folder-rename variant. */
|
|
104
|
+
export declare function isFolderRename(change: DocumentChange): change is Extract<DocumentChange, {
|
|
105
|
+
type: 'folder-rename';
|
|
106
|
+
}>;
|
|
94
107
|
import type { DocStatus } from './sync-types';
|
|
95
108
|
/**
|
|
96
109
|
* File-oriented backend for the document zone.
|
|
@@ -118,6 +131,31 @@ export interface DocumentBackend {
|
|
|
118
131
|
* as folder semantics.
|
|
119
132
|
*/
|
|
120
133
|
rename(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Create an empty folder. No-op if the folder already exists.
|
|
136
|
+
* Throws if a document occupies the path.
|
|
137
|
+
*/
|
|
138
|
+
mkdir(tenantId: string, shardId: string, path: string): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Remove a folder. Throws if non-empty and `recursive` is false.
|
|
141
|
+
* When `recursive: true`, atomically removes the folder and all
|
|
142
|
+
* descendant documents and folders.
|
|
143
|
+
*/
|
|
144
|
+
rmdir(tenantId: string, shardId: string, path: string, opts: {
|
|
145
|
+
recursive: boolean;
|
|
146
|
+
}): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Rename a folder atomically. Rewrites all descendant document paths
|
|
149
|
+
* to use the new prefix. Throws if `oldPath` does not exist as a
|
|
150
|
+
* folder, or if `newPath` already exists.
|
|
151
|
+
*/
|
|
152
|
+
renameFolder(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* List immediate folder children of `prefix`. Returns folder names
|
|
155
|
+
* (not full paths). Empty `prefix` lists folders directly under the
|
|
156
|
+
* shard root.
|
|
157
|
+
*/
|
|
158
|
+
listFolders(tenantId: string, shardId: string, prefix: string): Promise<string[]>;
|
|
121
159
|
/** List all documents stored for this tenant + shard combination. */
|
|
122
160
|
list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
|
|
123
161
|
/** Return true if the document at `path` exists. */
|
|
@@ -158,23 +196,59 @@ export interface DocumentBackend {
|
|
|
158
196
|
/**
|
|
159
197
|
* Shard-facing document handle returned by `ctx.documents()`. Binds
|
|
160
198
|
* the tenant, shard, and backend so shard code only deals in paths.
|
|
199
|
+
*
|
|
200
|
+
* All read/write/list/delete/rename methods accept an optional scope
|
|
201
|
+
* override for cross-scope document transfers (e.g. moving documents
|
|
202
|
+
* between a personal tenant and a project). When omitted, the handle's
|
|
203
|
+
* bound tenant is used.
|
|
161
204
|
*/
|
|
205
|
+
/** Optional scope override for cross-tenant operations. */
|
|
206
|
+
export interface ScopeOption {
|
|
207
|
+
/** Override the handle's bound tenant for a single operation. */
|
|
208
|
+
scope?: string;
|
|
209
|
+
}
|
|
162
210
|
export interface DocumentHandle {
|
|
163
211
|
/** List documents matching the handle's extensions filter. */
|
|
164
|
-
list(): Promise<DocumentMeta[]>;
|
|
212
|
+
list(opts?: ScopeOption): Promise<DocumentMeta[]>;
|
|
165
213
|
/** Read a document by path. Returns null if not found. */
|
|
166
|
-
read(path: string): Promise<string | null>;
|
|
214
|
+
read(path: string, opts?: ScopeOption): Promise<string | null>;
|
|
167
215
|
/** Write (create or overwrite) a document. Explicit save. */
|
|
168
|
-
write(path: string, content: string): Promise<void>;
|
|
216
|
+
write(path: string, content: string, opts?: ScopeOption): Promise<void>;
|
|
169
217
|
/** Delete a document. */
|
|
170
|
-
delete(path: string): Promise<void>;
|
|
218
|
+
delete(path: string, opts?: ScopeOption): Promise<void>;
|
|
171
219
|
/**
|
|
172
220
|
* Rename a document. Throws if there is an active autosave controller
|
|
173
221
|
* for oldPath (caller must flush and dispose first). Throws if newPath
|
|
174
222
|
* already exists or if oldPath does not. Subject to the handle's
|
|
175
223
|
* extensions filter — newPath must satisfy the filter.
|
|
176
224
|
*/
|
|
177
|
-
rename(oldPath: string, newPath: string): Promise<void>;
|
|
225
|
+
rename(oldPath: string, newPath: string, opts?: ScopeOption): Promise<void>;
|
|
226
|
+
/**
|
|
227
|
+
* Create an empty folder at `path`. No-op if the folder already
|
|
228
|
+
* exists. Throws if a document occupies the path.
|
|
229
|
+
*/
|
|
230
|
+
mkdir(path: string, opts?: ScopeOption): Promise<void>;
|
|
231
|
+
/**
|
|
232
|
+
* Remove a folder. Throws if non-empty and `opts.recursive` is not
|
|
233
|
+
* true. When `opts.recursive` is true, atomically removes the folder
|
|
234
|
+
* and all descendants. Throws if any active autosave controller's
|
|
235
|
+
* path falls inside the folder.
|
|
236
|
+
*/
|
|
237
|
+
rmdir(path: string, opts?: {
|
|
238
|
+
recursive?: boolean;
|
|
239
|
+
} & ScopeOption): Promise<void>;
|
|
240
|
+
/**
|
|
241
|
+
* Rename a folder. Atomically rewrites all descendant document paths.
|
|
242
|
+
* Throws if any active autosave controller's path falls inside the
|
|
243
|
+
* folder (caller must flush and dispose first). Throws if `newPath`
|
|
244
|
+
* already exists or if `oldPath` does not.
|
|
245
|
+
*/
|
|
246
|
+
renameFolder(oldPath: string, newPath: string, opts?: ScopeOption): Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* List immediate folder children of `prefix`. Empty `prefix` (or
|
|
249
|
+
* omitted) lists folders directly under the shard root.
|
|
250
|
+
*/
|
|
251
|
+
listFolders(prefix?: string, opts?: ScopeOption): Promise<string[]>;
|
|
178
252
|
/** Check existence without reading content. */
|
|
179
253
|
exists(path: string): Promise<boolean>;
|
|
180
254
|
/** Fetch sync-state metadata for a path. Null if the doc does not exist. */
|
package/dist/documents/types.js
CHANGED
|
@@ -45,7 +45,13 @@ export const PERMISSION_DOCUMENTS_READ = 'documents:read';
|
|
|
45
45
|
* `browse`.
|
|
46
46
|
*/
|
|
47
47
|
export const PERMISSION_DOCUMENTS_WRITE = 'documents:write';
|
|
48
|
+
/** Permission to manage document mount points (admin-only). */
|
|
49
|
+
export const PERMISSION_DOCUMENTS_MOUNT = 'documents:mount';
|
|
48
50
|
/** Type guard: narrows a DocumentChange to the rename variant. */
|
|
49
51
|
export function isRename(change) {
|
|
50
52
|
return change.type === 'rename';
|
|
51
53
|
}
|
|
54
|
+
/** Type guard: narrows a DocumentChange to the folder-rename variant. */
|
|
55
|
+
export function isFolderRename(change) {
|
|
56
|
+
return change.type === 'folder-rename';
|
|
57
|
+
}
|
|
@@ -25,7 +25,7 @@ describe('normalizeInitialLayout', () => {
|
|
|
25
25
|
});
|
|
26
26
|
it('canonicalizes a preset list, using tree as default and preserving variants', () => {
|
|
27
27
|
const authorTree = { docked: leafNode, floats: [] };
|
|
28
|
-
const
|
|
28
|
+
const compactTree = {
|
|
29
29
|
docked: { type: 'slot', slotId: 's2', viewId: 'v2' },
|
|
30
30
|
floats: [],
|
|
31
31
|
};
|
|
@@ -33,7 +33,7 @@ describe('normalizeInitialLayout', () => {
|
|
|
33
33
|
{
|
|
34
34
|
name: 'author',
|
|
35
35
|
tree: authorTree,
|
|
36
|
-
variants: {
|
|
36
|
+
variants: { compact: compactTree },
|
|
37
37
|
},
|
|
38
38
|
];
|
|
39
39
|
const result = normalizeInitialLayout(presets);
|
|
@@ -42,7 +42,7 @@ describe('normalizeInitialLayout', () => {
|
|
|
42
42
|
name: 'author',
|
|
43
43
|
variants: {
|
|
44
44
|
default: authorTree,
|
|
45
|
-
|
|
45
|
+
compact: compactTree,
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
]);
|
|
@@ -54,7 +54,7 @@ describe('normalizeInitialLayout', () => {
|
|
|
54
54
|
expect(result).toEqual([{ name: 'x', variants: { default: tree } }]);
|
|
55
55
|
});
|
|
56
56
|
it('throws if a preset has neither tree nor variants.default', () => {
|
|
57
|
-
const bad = [{ name: 'broken', variants: {
|
|
57
|
+
const bad = [{ name: 'broken', variants: { compact: { docked: leafNode, floats: [] } } }];
|
|
58
58
|
expect(() => normalizeInitialLayout(bad)).toThrow(/must provide either 'tree' or 'variants.default'/);
|
|
59
59
|
});
|
|
60
60
|
it('when a preset has both tree and variants.default, variants.default wins', () => {
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -176,7 +176,7 @@ export interface LayoutTree {
|
|
|
176
176
|
* manifest; users switch between them at runtime. The ergonomic `tree`
|
|
177
177
|
* field is shorthand for `variants.default`; the normalizer canonicalizes
|
|
178
178
|
* every preset into a variants-only shape on load. v1 always uses the
|
|
179
|
-
* `default` variant; other variant keys (e.g. `
|
|
179
|
+
* `default` variant; other variant keys (e.g. `compact`) are reserved
|
|
180
180
|
* for the rescoped DF10 selection policy and are persisted but inert.
|
|
181
181
|
*/
|
|
182
182
|
export interface LayoutPreset {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { toastManager } from '../overlays/toast';
|
|
12
12
|
import { sh3 } from '../sh3Runtime.svelte';
|
|
13
13
|
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
14
|
+
import HomeSection from '../sh3core-shard/HomeSection.svelte';
|
|
14
15
|
import iconsUrl from '../assets/icons.svg';
|
|
15
16
|
|
|
16
17
|
const layouts = $derived(getLayouts());
|
|
@@ -41,8 +42,7 @@
|
|
|
41
42
|
</script>
|
|
42
43
|
|
|
43
44
|
{#if layouts.length > 0}
|
|
44
|
-
<
|
|
45
|
-
<h2 class="saved-layouts-heading">Saved Layouts</h2>
|
|
45
|
+
<HomeSection title="Saved Layouts" persistKey="layouts">
|
|
46
46
|
<div class="saved-layouts-grid">
|
|
47
47
|
{#each layouts as layout (layout.id)}
|
|
48
48
|
<button
|
|
@@ -64,23 +64,10 @@
|
|
|
64
64
|
</button>
|
|
65
65
|
{/each}
|
|
66
66
|
</div>
|
|
67
|
-
</
|
|
67
|
+
</HomeSection>
|
|
68
68
|
{/if}
|
|
69
69
|
|
|
70
70
|
<style>
|
|
71
|
-
.saved-layouts-section {
|
|
72
|
-
width: 100%;
|
|
73
|
-
max-width: 720px;
|
|
74
|
-
margin-bottom: 28px;
|
|
75
|
-
}
|
|
76
|
-
.saved-layouts-heading {
|
|
77
|
-
font-size: 13px;
|
|
78
|
-
font-weight: 600;
|
|
79
|
-
text-transform: uppercase;
|
|
80
|
-
letter-spacing: 0.06em;
|
|
81
|
-
color: var(--sh3-fg-subtle);
|
|
82
|
-
margin: 0 0 12px;
|
|
83
|
-
}
|
|
84
71
|
.saved-layouts-grid {
|
|
85
72
|
display: grid;
|
|
86
73
|
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
|
@@ -2,7 +2,11 @@ import type { DocumentMeta } from '../../documents/types';
|
|
|
2
2
|
export type DocEntry = DocumentMeta & {
|
|
3
3
|
shardId: string;
|
|
4
4
|
};
|
|
5
|
-
export type OpenerValue =
|
|
5
|
+
export type OpenerValue = {
|
|
6
|
+
shardId: string;
|
|
7
|
+
path: string;
|
|
8
|
+
kind: 'file' | 'folder';
|
|
9
|
+
} | null;
|
|
6
10
|
export type SaverValue = string | null;
|
|
7
11
|
export type FileItem = {
|
|
8
12
|
kind: 'folder';
|
|
@@ -13,7 +17,7 @@ export type FileItem = {
|
|
|
13
17
|
name: string;
|
|
14
18
|
doc: DocEntry;
|
|
15
19
|
};
|
|
16
|
-
export declare function buildTree(docs: DocEntry[], shardId: string | null, prefix: string): FileItem[];
|
|
20
|
+
export declare function buildTree(docs: DocEntry[], folders: string[], shardId: string | null, prefix: string): FileItem[];
|
|
17
21
|
export declare function formatSize(bytes: number): string;
|
|
18
22
|
export declare function formatDate(epochMs: number): string;
|
|
19
23
|
export declare function iconForFile(name: string): string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export function buildTree(docs, shardId, prefix) {
|
|
1
|
+
export function buildTree(docs, folders, shardId, prefix) {
|
|
2
2
|
if (shardId === null) {
|
|
3
3
|
const shards = [...new Set(docs.map((d) => d.shardId))].sort();
|
|
4
4
|
return shards.map((s) => ({ kind: 'folder', name: s, fullPath: s }));
|
|
5
5
|
}
|
|
6
6
|
const shardDocs = docs.filter((d) => d.shardId === shardId);
|
|
7
|
-
const
|
|
7
|
+
const folderMap = new Map();
|
|
8
8
|
const files = [];
|
|
9
9
|
const normPrefix = prefix ? prefix + '/' : '';
|
|
10
10
|
const plen = normPrefix.length;
|
|
@@ -16,14 +16,21 @@ export function buildTree(docs, shardId, prefix) {
|
|
|
16
16
|
if (slash >= 0) {
|
|
17
17
|
const name = relative.slice(0, slash);
|
|
18
18
|
const full = prefix ? `${prefix}/${name}` : name;
|
|
19
|
-
if (!
|
|
20
|
-
|
|
19
|
+
if (!folderMap.has(name))
|
|
20
|
+
folderMap.set(name, full);
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
23
|
files.push({ kind: 'file', name: relative, doc });
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// Merge explicit empty folders (immediate children of prefix)
|
|
27
|
+
for (const name of folders) {
|
|
28
|
+
if (folderMap.has(name))
|
|
29
|
+
continue;
|
|
30
|
+
const full = prefix ? `${prefix}/${name}` : name;
|
|
31
|
+
folderMap.set(name, full);
|
|
32
|
+
}
|
|
33
|
+
const folderItems = [...folderMap.entries()]
|
|
27
34
|
.map(([name, fullPath]) => ({ kind: 'folder', name, fullPath }))
|
|
28
35
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
29
36
|
const fileItems = files.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -6,24 +6,40 @@
|
|
|
6
6
|
import type { DocEntry, OpenerValue, SaverValue } from './DocumentFilePicker';
|
|
7
7
|
|
|
8
8
|
type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
|
|
9
|
+
type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
|
|
10
|
+
type HandleFn = {
|
|
11
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
12
|
+
rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
|
|
13
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
14
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
15
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
16
|
+
};
|
|
9
17
|
|
|
10
18
|
let {
|
|
11
19
|
mode,
|
|
12
20
|
value = $bindable<OpenerValue | SaverValue>(null),
|
|
13
21
|
listDocuments,
|
|
22
|
+
listFolders,
|
|
23
|
+
handle,
|
|
24
|
+
readOnlyShard,
|
|
14
25
|
disabled = false,
|
|
15
26
|
invalid = false,
|
|
16
27
|
size = 'md',
|
|
17
28
|
buttonLabel = 'Choose…',
|
|
29
|
+
selectable = 'file',
|
|
18
30
|
onchange,
|
|
19
31
|
}: {
|
|
20
32
|
mode: 'open' | 'save';
|
|
21
33
|
value?: OpenerValue | SaverValue;
|
|
22
34
|
listDocuments: DocListFn;
|
|
35
|
+
listFolders?: FolderListFn;
|
|
36
|
+
handle?: HandleFn;
|
|
37
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
23
38
|
disabled?: boolean;
|
|
24
39
|
invalid?: boolean;
|
|
25
40
|
size?: 'sm' | 'md';
|
|
26
41
|
buttonLabel?: string;
|
|
42
|
+
selectable?: 'file' | 'folder' | 'both';
|
|
27
43
|
} & CommitOnlyEvents<OpenerValue | SaverValue> = $props();
|
|
28
44
|
|
|
29
45
|
let trigger = $state<HTMLButtonElement | undefined>(undefined);
|
|
@@ -33,7 +49,9 @@
|
|
|
33
49
|
value
|
|
34
50
|
? typeof value === 'string'
|
|
35
51
|
? value
|
|
36
|
-
:
|
|
52
|
+
: value.kind === 'folder'
|
|
53
|
+
? `${value.shardId}/${value.path}/`
|
|
54
|
+
: `${value.shardId}/${value.path}`
|
|
37
55
|
: null,
|
|
38
56
|
);
|
|
39
57
|
|
|
@@ -58,21 +76,25 @@
|
|
|
58
76
|
return;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
const
|
|
79
|
+
const modalHandle = sh3.modal.open(
|
|
62
80
|
DocumentBrowser,
|
|
63
|
-
{ anchor: trigger },
|
|
64
81
|
{
|
|
65
82
|
mode,
|
|
66
83
|
docs,
|
|
84
|
+
selectable,
|
|
85
|
+
listFolders,
|
|
86
|
+
handle,
|
|
87
|
+
readOnlyShard,
|
|
67
88
|
onCommit: (result: OpenerValue | SaverValue) => {
|
|
68
89
|
handleCommit(result);
|
|
69
90
|
},
|
|
70
91
|
onCancel: () => {},
|
|
71
92
|
},
|
|
93
|
+
{ dismissOnBackdrop: true, boxStyle: 'max-width: min(800px, 95vw);' },
|
|
72
94
|
);
|
|
73
95
|
|
|
74
|
-
const origClose =
|
|
75
|
-
|
|
96
|
+
const origClose = modalHandle.close;
|
|
97
|
+
modalHandle.close = () => {
|
|
76
98
|
origClose();
|
|
77
99
|
onOpenClosed();
|
|
78
100
|
};
|
|
@@ -4,14 +4,28 @@ import type { OpenerValue, SaverValue } from './DocumentFilePicker';
|
|
|
4
4
|
type DocListFn = () => Promise<Array<DocumentMeta & {
|
|
5
5
|
shardId: string;
|
|
6
6
|
}>>;
|
|
7
|
+
type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
|
|
8
|
+
type HandleFn = {
|
|
9
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
10
|
+
rmdir: (shardId: string, path: string, opts: {
|
|
11
|
+
recursive: boolean;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
14
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
15
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
16
|
+
};
|
|
7
17
|
type $$ComponentProps = {
|
|
8
18
|
mode: 'open' | 'save';
|
|
9
19
|
value?: OpenerValue | SaverValue;
|
|
10
20
|
listDocuments: DocListFn;
|
|
21
|
+
listFolders?: FolderListFn;
|
|
22
|
+
handle?: HandleFn;
|
|
23
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
11
24
|
disabled?: boolean;
|
|
12
25
|
invalid?: boolean;
|
|
13
26
|
size?: 'sm' | 'md';
|
|
14
27
|
buttonLabel?: string;
|
|
28
|
+
selectable?: 'file' | 'folder' | 'both';
|
|
15
29
|
} & CommitOnlyEvents<OpenerValue | SaverValue>;
|
|
16
30
|
declare const DocumentFilePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
17
31
|
type DocumentFilePicker = ReturnType<typeof DocumentFilePicker>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildTree } from './DocumentFilePicker';
|
|
3
|
+
describe('buildTree with explicit folders', () => {
|
|
4
|
+
const docs = [
|
|
5
|
+
{ shardId: 'sh1', path: 'a.md', size: 0, lastModified: 0 },
|
|
6
|
+
{ shardId: 'sh1', path: 'sub/b.md', size: 0, lastModified: 0 },
|
|
7
|
+
];
|
|
8
|
+
it('shard root returns shard list (folders ignored at root)', () => {
|
|
9
|
+
const items = buildTree(docs, [], null, '');
|
|
10
|
+
expect(items.map((i) => i.kind === 'folder' && i.name)).toEqual(['sh1']);
|
|
11
|
+
});
|
|
12
|
+
it('within a shard, merges implicit folders with empty explicit folders', () => {
|
|
13
|
+
const items = buildTree(docs, ['emptyDir'], 'sh1', '');
|
|
14
|
+
const folderNames = items.filter((i) => i.kind === 'folder').map((i) => i.name).sort();
|
|
15
|
+
expect(folderNames).toEqual(['emptyDir', 'sub']);
|
|
16
|
+
});
|
|
17
|
+
it('deduplicates explicit and implicit folders with the same name', () => {
|
|
18
|
+
const items = buildTree(docs, ['sub'], 'sh1', '');
|
|
19
|
+
const folderNames = items.filter((i) => i.kind === 'folder').map((i) => i.name);
|
|
20
|
+
expect(folderNames).toEqual(['sub']);
|
|
21
|
+
});
|
|
22
|
+
it('file items carry kind:"file"', () => {
|
|
23
|
+
const items = buildTree(docs, [], 'sh1', '');
|
|
24
|
+
const fileItems = items.filter((i) => i.kind === 'file');
|
|
25
|
+
expect(fileItems[0]).toMatchObject({ kind: 'file', name: 'a.md' });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('OpenerValue type accepts folder kind', () => {
|
|
29
|
+
it('compiles with kind:"folder"', () => {
|
|
30
|
+
const v = { shardId: 'sh1', path: 'sub', kind: 'folder' };
|
|
31
|
+
expect(v === null || v === void 0 ? void 0 : v.kind).toBe('folder');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -5,10 +5,22 @@
|
|
|
5
5
|
import type { OpenerValue } from './DocumentFilePicker';
|
|
6
6
|
|
|
7
7
|
type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
|
|
8
|
+
type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
|
|
9
|
+
type HandleFn = {
|
|
10
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
11
|
+
rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
|
|
12
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
13
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
14
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
15
|
+
};
|
|
8
16
|
|
|
9
17
|
let {
|
|
10
18
|
value = $bindable<OpenerValue>(null),
|
|
11
19
|
listDocuments,
|
|
20
|
+
listFolders,
|
|
21
|
+
handle,
|
|
22
|
+
readOnlyShard,
|
|
23
|
+
selectable = 'file',
|
|
12
24
|
disabled = false,
|
|
13
25
|
invalid = false,
|
|
14
26
|
size = 'md',
|
|
@@ -17,6 +29,10 @@
|
|
|
17
29
|
}: {
|
|
18
30
|
value?: OpenerValue;
|
|
19
31
|
listDocuments: DocListFn;
|
|
32
|
+
listFolders?: FolderListFn;
|
|
33
|
+
handle?: HandleFn;
|
|
34
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
35
|
+
selectable?: 'file' | 'folder' | 'both';
|
|
20
36
|
disabled?: boolean;
|
|
21
37
|
invalid?: boolean;
|
|
22
38
|
size?: 'sm' | 'md';
|
|
@@ -28,6 +44,10 @@
|
|
|
28
44
|
mode="open"
|
|
29
45
|
bind:value
|
|
30
46
|
{listDocuments}
|
|
47
|
+
{listFolders}
|
|
48
|
+
{handle}
|
|
49
|
+
{readOnlyShard}
|
|
50
|
+
{selectable}
|
|
31
51
|
{disabled}
|
|
32
52
|
{invalid}
|
|
33
53
|
{size}
|
|
@@ -4,9 +4,23 @@ import type { OpenerValue } from './DocumentFilePicker';
|
|
|
4
4
|
type DocListFn = () => Promise<Array<DocumentMeta & {
|
|
5
5
|
shardId: string;
|
|
6
6
|
}>>;
|
|
7
|
+
type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
|
|
8
|
+
type HandleFn = {
|
|
9
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
10
|
+
rmdir: (shardId: string, path: string, opts: {
|
|
11
|
+
recursive: boolean;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
14
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
15
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
16
|
+
};
|
|
7
17
|
type $$ComponentProps = {
|
|
8
18
|
value?: OpenerValue;
|
|
9
19
|
listDocuments: DocListFn;
|
|
20
|
+
listFolders?: FolderListFn;
|
|
21
|
+
handle?: HandleFn;
|
|
22
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
23
|
+
selectable?: 'file' | 'folder' | 'both';
|
|
10
24
|
disabled?: boolean;
|
|
11
25
|
invalid?: boolean;
|
|
12
26
|
size?: 'sm' | 'md';
|
|
@@ -5,10 +5,21 @@
|
|
|
5
5
|
import type { SaverValue } from './DocumentFilePicker';
|
|
6
6
|
|
|
7
7
|
type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
|
|
8
|
+
type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
|
|
9
|
+
type HandleFn = {
|
|
10
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
11
|
+
rmdir: (shardId: string, path: string, opts: { recursive: boolean }) => Promise<void>;
|
|
12
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
13
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
14
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
15
|
+
};
|
|
8
16
|
|
|
9
17
|
let {
|
|
10
18
|
value = $bindable<SaverValue>(null),
|
|
11
19
|
listDocuments,
|
|
20
|
+
listFolders,
|
|
21
|
+
handle,
|
|
22
|
+
readOnlyShard,
|
|
12
23
|
disabled = false,
|
|
13
24
|
invalid = false,
|
|
14
25
|
size = 'md',
|
|
@@ -17,6 +28,9 @@
|
|
|
17
28
|
}: {
|
|
18
29
|
value?: SaverValue;
|
|
19
30
|
listDocuments: DocListFn;
|
|
31
|
+
listFolders?: FolderListFn;
|
|
32
|
+
handle?: HandleFn;
|
|
33
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
20
34
|
disabled?: boolean;
|
|
21
35
|
invalid?: boolean;
|
|
22
36
|
size?: 'sm' | 'md';
|
|
@@ -28,6 +42,9 @@
|
|
|
28
42
|
mode="save"
|
|
29
43
|
bind:value
|
|
30
44
|
{listDocuments}
|
|
45
|
+
{listFolders}
|
|
46
|
+
{handle}
|
|
47
|
+
{readOnlyShard}
|
|
31
48
|
{disabled}
|
|
32
49
|
{invalid}
|
|
33
50
|
{size}
|
|
@@ -4,9 +4,22 @@ import type { SaverValue } from './DocumentFilePicker';
|
|
|
4
4
|
type DocListFn = () => Promise<Array<DocumentMeta & {
|
|
5
5
|
shardId: string;
|
|
6
6
|
}>>;
|
|
7
|
+
type FolderListFn = (shardId: string, prefix: string) => Promise<string[]>;
|
|
8
|
+
type HandleFn = {
|
|
9
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
10
|
+
rmdir: (shardId: string, path: string, opts: {
|
|
11
|
+
recursive: boolean;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
14
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
15
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
16
|
+
};
|
|
7
17
|
type $$ComponentProps = {
|
|
8
18
|
value?: SaverValue;
|
|
9
19
|
listDocuments: DocListFn;
|
|
20
|
+
listFolders?: FolderListFn;
|
|
21
|
+
handle?: HandleFn;
|
|
22
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
10
23
|
disabled?: boolean;
|
|
11
24
|
invalid?: boolean;
|
|
12
25
|
size?: 'sm' | 'md';
|