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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { MemoryDocumentBackend } from './backends';
|
|
3
3
|
import { createDocumentHandle } from './handle';
|
|
4
4
|
import { documentChanges } from './notifications';
|
|
@@ -28,6 +28,10 @@ describe('DocumentHandle.status()', () => {
|
|
|
28
28
|
async exists() { return false; },
|
|
29
29
|
async listAllShards() { return []; },
|
|
30
30
|
async listAllDocuments() { return []; },
|
|
31
|
+
async mkdir() { },
|
|
32
|
+
async rmdir() { },
|
|
33
|
+
async renameFolder() { },
|
|
34
|
+
async listFolders() { return []; },
|
|
31
35
|
};
|
|
32
36
|
const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
|
|
33
37
|
await expect(handle.status('a.txt')).rejects.toThrow(/status/);
|
|
@@ -46,6 +50,10 @@ describe('DocumentHandle.resolveConflict()', () => {
|
|
|
46
50
|
async listAllShards() { return []; },
|
|
47
51
|
async listAllDocuments() { return []; },
|
|
48
52
|
async resolve(t, s, p, c) { resolved.push({ t, s, p, c }); },
|
|
53
|
+
async mkdir() { },
|
|
54
|
+
async rmdir() { },
|
|
55
|
+
async renameFolder() { },
|
|
56
|
+
async listFolders() { return []; },
|
|
49
57
|
};
|
|
50
58
|
const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
|
|
51
59
|
await handle.resolveConflict('a.txt', 'local');
|
|
@@ -69,6 +77,10 @@ describe('DocumentHandle.readBranch()', () => {
|
|
|
69
77
|
async listAllShards() { return []; },
|
|
70
78
|
async listAllDocuments() { return []; },
|
|
71
79
|
async readBranch(...args) { calls.push(args); return 'remote-content'; },
|
|
80
|
+
async mkdir() { },
|
|
81
|
+
async rmdir() { },
|
|
82
|
+
async renameFolder() { },
|
|
83
|
+
async listFolders() { return []; },
|
|
72
84
|
};
|
|
73
85
|
const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
|
|
74
86
|
const out = await handle.readBranch('a.txt', 'peer-1');
|
|
@@ -86,6 +98,10 @@ describe('DocumentHandle.readBranch()', () => {
|
|
|
86
98
|
async listAllShards() { return []; },
|
|
87
99
|
async listAllDocuments() { return []; },
|
|
88
100
|
async readBranch() { return null; },
|
|
101
|
+
async mkdir() { },
|
|
102
|
+
async rmdir() { },
|
|
103
|
+
async renameFolder() { },
|
|
104
|
+
async listFolders() { return []; },
|
|
89
105
|
};
|
|
90
106
|
const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
|
|
91
107
|
expect(await handle.readBranch('a.txt', 'peer-1')).toBeNull();
|
|
@@ -164,3 +180,74 @@ describe('DocumentHandle.delete()', () => {
|
|
|
164
180
|
unsub();
|
|
165
181
|
});
|
|
166
182
|
});
|
|
183
|
+
describe('DocumentHandle folder ops', () => {
|
|
184
|
+
let backend;
|
|
185
|
+
let handle;
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
backend = new MemoryDocumentBackend();
|
|
188
|
+
handle = createDocumentHandle('t', 's', backend, { format: 'text' });
|
|
189
|
+
});
|
|
190
|
+
it('mkdir forwards to backend with bound tenant/shard', async () => {
|
|
191
|
+
await handle.mkdir('a');
|
|
192
|
+
expect(await backend.listFolders('t', 's', '')).toEqual(['a']);
|
|
193
|
+
});
|
|
194
|
+
it('rmdir defaults recursive to false', async () => {
|
|
195
|
+
await handle.write('a/x.md', 'x');
|
|
196
|
+
await expect(handle.rmdir('a')).rejects.toThrow();
|
|
197
|
+
});
|
|
198
|
+
it('rmdir({recursive:true}) cascades', async () => {
|
|
199
|
+
await handle.write('a/x.md', 'x');
|
|
200
|
+
await handle.rmdir('a', { recursive: true });
|
|
201
|
+
expect(await handle.list()).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
it('renameFolder rewrites descendant paths', async () => {
|
|
204
|
+
await handle.write('old/x.md', 'x');
|
|
205
|
+
await handle.renameFolder('old', 'new');
|
|
206
|
+
const docs = (await handle.list()).map((d) => d.path).sort();
|
|
207
|
+
expect(docs).toEqual(['new/x.md']);
|
|
208
|
+
});
|
|
209
|
+
it('renameFolder refused when an autosave controller is inside the folder', async () => {
|
|
210
|
+
const ctrl = handle.autosave('a/x.md');
|
|
211
|
+
ctrl.update('content');
|
|
212
|
+
await expect(handle.renameFolder('a', 'b')).rejects.toThrow(/autosave/i);
|
|
213
|
+
await ctrl.dispose();
|
|
214
|
+
});
|
|
215
|
+
it('rmdir(recursive) refused when an autosave controller is inside the folder', async () => {
|
|
216
|
+
const ctrl = handle.autosave('a/x.md');
|
|
217
|
+
ctrl.update('content');
|
|
218
|
+
await expect(handle.rmdir('a', { recursive: true })).rejects.toThrow(/autosave/i);
|
|
219
|
+
await ctrl.dispose();
|
|
220
|
+
});
|
|
221
|
+
it('listFolders forwards to backend', async () => {
|
|
222
|
+
await handle.mkdir('a');
|
|
223
|
+
expect(await handle.listFolders()).toEqual(['a']);
|
|
224
|
+
expect(await handle.listFolders('a')).toEqual([]);
|
|
225
|
+
});
|
|
226
|
+
it('mkdir emits folder-create change event', async () => {
|
|
227
|
+
const events = [];
|
|
228
|
+
handle.watch((c) => events.push(c));
|
|
229
|
+
await handle.mkdir('a');
|
|
230
|
+
expect(events).toEqual([
|
|
231
|
+
{ type: 'folder-create', path: 'a', tenantId: 't', shardId: 's' },
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
it('rmdir(recursive) emits a single folder-delete event', async () => {
|
|
235
|
+
await handle.write('a/x.md', 'x');
|
|
236
|
+
await handle.write('a/y.md', 'y');
|
|
237
|
+
const events = [];
|
|
238
|
+
handle.watch((c) => events.push(c));
|
|
239
|
+
await handle.rmdir('a', { recursive: true });
|
|
240
|
+
expect(events).toEqual([
|
|
241
|
+
{ type: 'folder-delete', path: 'a', tenantId: 't', shardId: 's' },
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
it('renameFolder emits a single folder-rename event', async () => {
|
|
245
|
+
await handle.write('old/x.md', 'x');
|
|
246
|
+
const events = [];
|
|
247
|
+
handle.watch((c) => events.push(c));
|
|
248
|
+
await handle.renameFolder('old', 'new');
|
|
249
|
+
expect(events).toEqual([
|
|
250
|
+
{ type: 'folder-rename', path: 'new', oldPath: 'old', tenantId: 't', shardId: 's' },
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -32,4 +32,10 @@ export declare class HttpDocumentBackend implements DocumentBackend {
|
|
|
32
32
|
} | string): Promise<void>;
|
|
33
33
|
readBranch(tenantId: string, shardId: string, path: string, origin: string): Promise<string | null>;
|
|
34
34
|
rename(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
|
|
35
|
+
mkdir(tenantId: string, shardId: string, path: string): Promise<void>;
|
|
36
|
+
rmdir(tenantId: string, shardId: string, path: string, opts: {
|
|
37
|
+
recursive: boolean;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
renameFolder(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
|
|
40
|
+
listFolders(tenantId: string, shardId: string, prefix: string): Promise<string[]>;
|
|
35
41
|
}
|
|
@@ -52,8 +52,16 @@ export class HttpDocumentBackend {
|
|
|
52
52
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}`;
|
|
53
53
|
const headers = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': typeof content === 'string' ? 'text/plain' : 'application/octet-stream' });
|
|
54
54
|
const res = await apiFetch(url, { method: 'PUT', headers, body: content, credentials: 'include' });
|
|
55
|
-
if (!res.ok)
|
|
56
|
-
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
let detail = `HTTP ${res.status}`;
|
|
57
|
+
try {
|
|
58
|
+
const b = await res.json();
|
|
59
|
+
if (b.error)
|
|
60
|
+
detail = b.error;
|
|
61
|
+
}
|
|
62
|
+
catch ( /* body not JSON */_a) { /* body not JSON */ }
|
|
63
|
+
throw new Error(`Document write failed: ${detail}`);
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
async delete(tenantId, shardId, path) {
|
|
59
67
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}`;
|
|
@@ -139,6 +147,67 @@ export class HttpDocumentBackend {
|
|
|
139
147
|
throw new Error(`Document rename failed: ${res.status}`);
|
|
140
148
|
}
|
|
141
149
|
}
|
|
150
|
+
async mkdir(tenantId, shardId, path) {
|
|
151
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}/mkdir`;
|
|
152
|
+
const res = await apiFetch(url, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this),
|
|
155
|
+
credentials: 'include',
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
let detail = `HTTP ${res.status}`;
|
|
159
|
+
try {
|
|
160
|
+
const b = await res.json();
|
|
161
|
+
if (b.error)
|
|
162
|
+
detail = b.error;
|
|
163
|
+
}
|
|
164
|
+
catch ( /* not JSON */_a) { /* not JSON */ }
|
|
165
|
+
throw new Error(`mkdir failed: ${detail}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async rmdir(tenantId, shardId, path, opts) {
|
|
169
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}/rmdir`;
|
|
170
|
+
const res = await apiFetch(url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
173
|
+
body: JSON.stringify({ recursive: opts.recursive }),
|
|
174
|
+
credentials: 'include',
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
let detail = `HTTP ${res.status}`;
|
|
178
|
+
try {
|
|
179
|
+
const b = await res.json();
|
|
180
|
+
if (b.error)
|
|
181
|
+
detail = b.error;
|
|
182
|
+
}
|
|
183
|
+
catch ( /* not JSON */_a) { /* not JSON */ }
|
|
184
|
+
throw new Error(`rmdir failed: ${detail}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async renameFolder(tenantId, shardId, oldPath, newPath) {
|
|
188
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${oldPath}/rename-folder`;
|
|
189
|
+
const res = await apiFetch(url, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
192
|
+
body: JSON.stringify({ to: newPath }),
|
|
193
|
+
credentials: 'include',
|
|
194
|
+
});
|
|
195
|
+
if (res.status === 404)
|
|
196
|
+
throw new Error(`Folder not found at ${oldPath}`);
|
|
197
|
+
if (res.status === 409)
|
|
198
|
+
throw new Error(`Folder already exists at ${newPath}`);
|
|
199
|
+
if (!res.ok)
|
|
200
|
+
throw new Error(`renameFolder failed: ${res.status}`);
|
|
201
|
+
}
|
|
202
|
+
async listFolders(tenantId, shardId, prefix) {
|
|
203
|
+
const base = prefix
|
|
204
|
+
? `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${prefix}?folders=1`
|
|
205
|
+
: `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}?folders=1`;
|
|
206
|
+
const res = await apiFetch(base, { credentials: 'include' });
|
|
207
|
+
if (!res.ok)
|
|
208
|
+
throw new Error(`listFolders failed: ${res.status}`);
|
|
209
|
+
return res.json();
|
|
210
|
+
}
|
|
142
211
|
}
|
|
143
212
|
_HttpDocumentBackend_baseUrl = new WeakMap(), _HttpDocumentBackend_apiKey = new WeakMap(), _HttpDocumentBackend_instances = new WeakSet(), _HttpDocumentBackend_authHeaders = function _HttpDocumentBackend_authHeaders() {
|
|
144
213
|
if (!__classPrivateFieldGet(this, _HttpDocumentBackend_apiKey, "f"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
2
|
import { HttpDocumentBackend } from './http-backend';
|
|
3
3
|
const originalFetch = globalThis.fetch;
|
|
4
4
|
describe('HttpDocumentBackend.readBranch', () => {
|
|
@@ -79,3 +79,53 @@ describe('HttpDocumentBackend.rename', () => {
|
|
|
79
79
|
.rejects.toThrow(/rename failed/i);
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
|
+
describe('HttpDocumentBackend folder ops', () => {
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
globalThis.fetch = originalFetch;
|
|
85
|
+
});
|
|
86
|
+
it('mkdir POSTs to /:path/mkdir', async () => {
|
|
87
|
+
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
88
|
+
globalThis.fetch = fetchMock;
|
|
89
|
+
const backend = new HttpDocumentBackend('http://server');
|
|
90
|
+
await backend.mkdir('t', 's', 'a/b');
|
|
91
|
+
expect(fetchMock).toHaveBeenCalledWith('http://server/api/docs/t/s/a/b/mkdir', expect.objectContaining({ method: 'POST' }));
|
|
92
|
+
});
|
|
93
|
+
it('rmdir POSTs to /:path/rmdir with recursive flag', async () => {
|
|
94
|
+
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
95
|
+
globalThis.fetch = fetchMock;
|
|
96
|
+
const backend = new HttpDocumentBackend('http://server');
|
|
97
|
+
await backend.rmdir('t', 's', 'a', { recursive: true });
|
|
98
|
+
const call = fetchMock.mock.calls[0];
|
|
99
|
+
expect(call[0]).toBe('http://server/api/docs/t/s/a/rmdir');
|
|
100
|
+
const body = JSON.parse(call[1].body);
|
|
101
|
+
expect(body).toEqual({ recursive: true });
|
|
102
|
+
});
|
|
103
|
+
it('rmdir throws on 409', async () => {
|
|
104
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ error: 'not empty' }), { status: 409 }));
|
|
105
|
+
const backend = new HttpDocumentBackend('http://server');
|
|
106
|
+
await expect(backend.rmdir('t', 's', 'a', { recursive: false })).rejects.toThrow();
|
|
107
|
+
});
|
|
108
|
+
it('renameFolder POSTs to /:path/rename-folder with { to }', async () => {
|
|
109
|
+
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
110
|
+
globalThis.fetch = fetchMock;
|
|
111
|
+
const backend = new HttpDocumentBackend('http://server');
|
|
112
|
+
await backend.renameFolder('t', 's', 'old', 'new');
|
|
113
|
+
const call = fetchMock.mock.calls[0];
|
|
114
|
+
expect(call[0]).toBe('http://server/api/docs/t/s/old/rename-folder');
|
|
115
|
+
expect(JSON.parse(call[1].body)).toEqual({ to: 'new' });
|
|
116
|
+
});
|
|
117
|
+
it('listFolders at root uses ?folders=1 on the shard list URL', async () => {
|
|
118
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify(['a', 'b']), { status: 200 }));
|
|
119
|
+
const backend = new HttpDocumentBackend('http://server');
|
|
120
|
+
const result = await backend.listFolders('t', 's', '');
|
|
121
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('http://server/api/docs/t/s?folders=1', expect.any(Object));
|
|
122
|
+
expect(result).toEqual(['a', 'b']);
|
|
123
|
+
});
|
|
124
|
+
it('listFolders with prefix uses ?folders=1 on the path URL', async () => {
|
|
125
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify(['c']), { status: 200 }));
|
|
126
|
+
const backend = new HttpDocumentBackend('http://server');
|
|
127
|
+
const result = await backend.listFolders('t', 's', 'a');
|
|
128
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('http://server/api/docs/t/s/a?folders=1', expect.any(Object));
|
|
129
|
+
expect(result).toEqual(['c']);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export type { DocumentFormat, DocumentHandleOptions, DocumentMeta, DocumentChange, DocumentBackend, DocumentHandle, AutosaveController, } from './types';
|
|
1
|
+
export type { DocumentFormat, DocumentHandleOptions, DocumentMeta, DocumentChange, DocumentBackend, DocumentHandle, AutosaveController, ScopeOption, } from './types';
|
|
2
2
|
export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
|
|
3
3
|
export { HttpDocumentBackend } from './http-backend';
|
|
4
4
|
export { createDocumentHandle } from './handle';
|
|
5
5
|
export { documentChanges } from './notifications';
|
|
6
|
-
export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, } from './config';
|
|
6
|
+
export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, __setScopeResolver, } from './config';
|
|
7
7
|
export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './sync-types';
|
|
8
8
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
|
package/dist/documents/index.js
CHANGED
|
@@ -5,5 +5,5 @@ export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
|
|
|
5
5
|
export { HttpDocumentBackend } from './http-backend';
|
|
6
6
|
export { createDocumentHandle } from './handle';
|
|
7
7
|
export { documentChanges } from './notifications';
|
|
8
|
-
export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, } from './config';
|
|
8
|
+
export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, __setScopeResolver, } from './config';
|
|
9
9
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
|
|
@@ -3,14 +3,16 @@ import type { DocEntry, OpenerValue, SaverValue } from '../primitives/widgets/Do
|
|
|
3
3
|
export type DocListFn = () => Promise<DocEntry[]>;
|
|
4
4
|
/** Options for `ctx.documentPicker.open()`. */
|
|
5
5
|
export interface DocumentOpenOptions {
|
|
6
|
-
/**
|
|
6
|
+
/** When set, the browser opens as a popup anchored to this element.
|
|
7
|
+
* When omitted (default), the browser opens as a centered modal. */
|
|
7
8
|
anchor?: HTMLElement;
|
|
8
9
|
}
|
|
9
10
|
/** Options for `ctx.documentPicker.save()`. */
|
|
10
11
|
export interface DocumentSaveOptions {
|
|
11
12
|
/** Pre-fill the filename input in the save dialog. */
|
|
12
13
|
suggestedName?: string;
|
|
13
|
-
/**
|
|
14
|
+
/** When set, the browser opens as a popup anchored to this element.
|
|
15
|
+
* When omitted (default), the browser opens as a centered modal. */
|
|
14
16
|
anchor?: HTMLElement;
|
|
15
17
|
}
|
|
16
18
|
/** Programmatic document picker API — available on `ctx.documentPicker`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import '../sh3Runtime.svelte';
|
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import
|
|
2
|
+
import '../sh3Runtime.svelte'; // intercepted by vi.mock below
|
|
3
3
|
import { createDocumentPicker } from './picker-primitive';
|
|
4
|
+
const { mockPopupShow, mockModalOpen } = vi.hoisted(() => ({
|
|
5
|
+
mockPopupShow: vi.fn(),
|
|
6
|
+
mockModalOpen: vi.fn(),
|
|
7
|
+
}));
|
|
4
8
|
vi.mock('../sh3Runtime.svelte', () => ({
|
|
5
9
|
sh3: {
|
|
6
|
-
popup: {
|
|
7
|
-
|
|
8
|
-
},
|
|
10
|
+
popup: { show: mockPopupShow },
|
|
11
|
+
modal: { open: mockModalOpen },
|
|
9
12
|
},
|
|
10
13
|
}));
|
|
11
|
-
const mockShow = sh3.popup.show;
|
|
12
14
|
function mockPopup() {
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let captured = null;
|
|
16
|
+
const handle = { close: vi.fn() };
|
|
17
|
+
mockPopupShow.mockImplementation((_Content, _opts, props) => {
|
|
18
|
+
captured = props;
|
|
19
|
+
return handle;
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
commit: (v) => { captured === null || captured === void 0 ? void 0 : captured.onCommit(v); },
|
|
23
|
+
cancel: () => { captured === null || captured === void 0 ? void 0 : captured.onCancel(); },
|
|
24
|
+
dismiss: () => { handle.close(); },
|
|
25
|
+
handle,
|
|
17
26
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
}
|
|
28
|
+
function mockModal() {
|
|
29
|
+
let captured = null;
|
|
30
|
+
const handle = { close: vi.fn() };
|
|
31
|
+
mockModalOpen.mockImplementation((_Content, props) => {
|
|
32
|
+
captured = props;
|
|
21
33
|
return handle;
|
|
22
34
|
});
|
|
23
35
|
return {
|
|
24
|
-
commit: (v) => {
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
cancel: () => {
|
|
28
|
-
capturedCancel === null || capturedCancel === void 0 ? void 0 : capturedCancel();
|
|
29
|
-
},
|
|
30
|
-
dismiss: () => {
|
|
31
|
-
handle.close();
|
|
32
|
-
},
|
|
36
|
+
commit: (v) => { captured === null || captured === void 0 ? void 0 : captured.onCommit(v); },
|
|
37
|
+
cancel: () => { captured === null || captured === void 0 ? void 0 : captured.onCancel(); },
|
|
38
|
+
dismiss: () => { handle.close(); },
|
|
33
39
|
handle,
|
|
34
40
|
};
|
|
35
41
|
}
|
|
@@ -37,35 +43,36 @@ beforeEach(() => {
|
|
|
37
43
|
vi.clearAllMocks();
|
|
38
44
|
});
|
|
39
45
|
describe('createDocumentPicker', () => {
|
|
40
|
-
const sampleDoc = { shardId: 'my-shard', path: 'readme.md' };
|
|
41
|
-
describe('open()', () => {
|
|
46
|
+
const sampleDoc = { shardId: 'my-shard', path: 'readme.md', kind: 'file' };
|
|
47
|
+
describe('open() — modal (no anchor)', () => {
|
|
42
48
|
it('resolves with OpenerValue when user commits', async () => {
|
|
43
49
|
const listFn = async () => [{ shardId: 'my-shard', path: 'readme.md', size: 100, lastModified: 0 }];
|
|
44
50
|
const picker = createDocumentPicker(listFn);
|
|
45
|
-
const
|
|
51
|
+
const modal = mockModal();
|
|
46
52
|
const promise = picker.open();
|
|
47
|
-
await vi.waitFor(() => expect(
|
|
48
|
-
|
|
53
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
54
|
+
modal.commit(sampleDoc);
|
|
49
55
|
const result = await promise;
|
|
50
|
-
expect(result).toEqual({ shardId: 'my-shard', path: 'readme.md' });
|
|
56
|
+
expect(result).toEqual({ shardId: 'my-shard', path: 'readme.md', kind: 'file' });
|
|
57
|
+
expect(mockPopupShow).not.toHaveBeenCalled();
|
|
51
58
|
});
|
|
52
59
|
it('resolves with null when user cancels', async () => {
|
|
53
60
|
const listFn = async () => [];
|
|
54
61
|
const picker = createDocumentPicker(listFn);
|
|
55
|
-
const
|
|
62
|
+
const modal = mockModal();
|
|
56
63
|
const promise = picker.open();
|
|
57
|
-
await vi.waitFor(() => expect(
|
|
58
|
-
|
|
64
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
65
|
+
modal.cancel();
|
|
59
66
|
const result = await promise;
|
|
60
67
|
expect(result).toBeNull();
|
|
61
68
|
});
|
|
62
|
-
it('resolves with null when
|
|
69
|
+
it('resolves with null when modal is dismissed externally', async () => {
|
|
63
70
|
const listFn = async () => [];
|
|
64
71
|
const picker = createDocumentPicker(listFn);
|
|
65
|
-
const
|
|
72
|
+
const modal = mockModal();
|
|
66
73
|
const promise = picker.open();
|
|
67
|
-
await vi.waitFor(() => expect(
|
|
68
|
-
|
|
74
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
75
|
+
modal.dismiss();
|
|
69
76
|
const result = await promise;
|
|
70
77
|
expect(result).toBeNull();
|
|
71
78
|
});
|
|
@@ -74,59 +81,82 @@ describe('createDocumentPicker', () => {
|
|
|
74
81
|
const picker = createDocumentPicker(listFn);
|
|
75
82
|
const promise = picker.open();
|
|
76
83
|
await expect(promise).rejects.toThrow('network error');
|
|
77
|
-
expect(
|
|
84
|
+
expect(mockModalOpen).not.toHaveBeenCalled();
|
|
85
|
+
expect(mockPopupShow).not.toHaveBeenCalled();
|
|
78
86
|
});
|
|
79
|
-
it('
|
|
87
|
+
it('passes mode and dismissOnBackdrop to modal', async () => {
|
|
88
|
+
const listFn = async () => [];
|
|
89
|
+
const picker = createDocumentPicker(listFn);
|
|
90
|
+
mockModal();
|
|
91
|
+
picker.open();
|
|
92
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
93
|
+
const call = mockModalOpen.mock.calls[0];
|
|
94
|
+
expect(call[1].mode).toBe('open');
|
|
95
|
+
expect(call[2]).toEqual({ dismissOnBackdrop: true, boxStyle: 'max-width: min(800px, 95vw);' });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('open() — popup (with anchor)', () => {
|
|
99
|
+
it('uses popup when anchor element is provided', async () => {
|
|
80
100
|
const listFn = async () => [];
|
|
81
101
|
const picker = createDocumentPicker(listFn);
|
|
82
102
|
mockPopup();
|
|
83
103
|
const el = document.createElement('div');
|
|
84
104
|
picker.open({ anchor: el });
|
|
85
|
-
await vi.waitFor(() => expect(
|
|
86
|
-
|
|
105
|
+
await vi.waitFor(() => expect(mockPopupShow).toHaveBeenCalledOnce());
|
|
106
|
+
expect(mockModalOpen).not.toHaveBeenCalled();
|
|
107
|
+
const call = mockPopupShow.mock.calls[0];
|
|
87
108
|
expect(call[1].anchor).toEqual({ x: 0, y: 0 });
|
|
88
109
|
});
|
|
89
110
|
});
|
|
90
|
-
describe('save()', () => {
|
|
111
|
+
describe('save() — modal (no anchor)', () => {
|
|
91
112
|
it('resolves with SaverValue string when user commits a filename', async () => {
|
|
92
113
|
const listFn = async () => [];
|
|
93
114
|
const picker = createDocumentPicker(listFn);
|
|
94
|
-
const
|
|
115
|
+
const modal = mockModal();
|
|
95
116
|
const promise = picker.save();
|
|
96
|
-
await vi.waitFor(() => expect(
|
|
97
|
-
|
|
117
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
118
|
+
modal.commit('my-shard/report.txt');
|
|
98
119
|
const result = await promise;
|
|
99
120
|
expect(result).toBe('my-shard/report.txt');
|
|
100
121
|
});
|
|
101
122
|
it('resolves with null when user cancels', async () => {
|
|
102
123
|
const listFn = async () => [];
|
|
103
124
|
const picker = createDocumentPicker(listFn);
|
|
104
|
-
const
|
|
125
|
+
const modal = mockModal();
|
|
105
126
|
const promise = picker.save();
|
|
106
|
-
await vi.waitFor(() => expect(
|
|
107
|
-
|
|
127
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
128
|
+
modal.cancel();
|
|
108
129
|
const result = await promise;
|
|
109
130
|
expect(result).toBeNull();
|
|
110
131
|
});
|
|
111
132
|
it('passes suggestedName as prop', async () => {
|
|
112
133
|
const listFn = async () => [];
|
|
113
134
|
const picker = createDocumentPicker(listFn);
|
|
114
|
-
|
|
135
|
+
mockModal();
|
|
115
136
|
picker.save({ suggestedName: 'draft.txt' });
|
|
116
|
-
await vi.waitFor(() => expect(
|
|
117
|
-
const call =
|
|
118
|
-
expect(call[
|
|
137
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
138
|
+
const call = mockModalOpen.mock.calls[0];
|
|
139
|
+
expect(call[1].suggestedName).toBe('draft.txt');
|
|
140
|
+
});
|
|
141
|
+
it('passes mode to modal', async () => {
|
|
142
|
+
const listFn = async () => [];
|
|
143
|
+
const picker = createDocumentPicker(listFn);
|
|
144
|
+
mockModal();
|
|
145
|
+
picker.save();
|
|
146
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
147
|
+
const call = mockModalOpen.mock.calls[0];
|
|
148
|
+
expect(call[1].mode).toBe('save');
|
|
119
149
|
});
|
|
120
150
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
describe('save() — popup (with anchor)', () => {
|
|
152
|
+
it('uses popup when anchor element is provided', async () => {
|
|
153
|
+
const listFn = async () => [];
|
|
154
|
+
const picker = createDocumentPicker(listFn);
|
|
155
|
+
mockPopup();
|
|
156
|
+
const el = document.createElement('div');
|
|
157
|
+
picker.save({ anchor: el });
|
|
158
|
+
await vi.waitFor(() => expect(mockPopupShow).toHaveBeenCalledOnce());
|
|
159
|
+
expect(mockModalOpen).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
131
161
|
});
|
|
132
162
|
});
|
|
@@ -3,5 +3,9 @@ import type { DocumentPickerApi, DocListFn } from './picker-api';
|
|
|
3
3
|
* Create a document picker API bound to a document listing function.
|
|
4
4
|
* The listFn is derived from the shard's document zone + browse permission
|
|
5
5
|
* and baked in at construction time so callers don't pass their own scope.
|
|
6
|
+
*
|
|
7
|
+
* When an `anchor` element is provided the browser opens as a popup
|
|
8
|
+
* (anchored near the element). Without an anchor it opens as a centered
|
|
9
|
+
* modal (the expected default for file-browser dialogs).
|
|
6
10
|
*/
|
|
7
11
|
export declare function createDocumentPicker(listFn: DocListFn): DocumentPickerApi;
|
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
import { sh3 } from '../sh3Runtime.svelte';
|
|
2
2
|
import DocumentBrowser from '../primitives/widgets/_DocumentBrowser.svelte';
|
|
3
|
+
const BOX_STYLE = 'max-width: min(800px, 95vw);';
|
|
4
|
+
const MODAL_OPTS = { dismissOnBackdrop: true, boxStyle: BOX_STYLE };
|
|
3
5
|
/**
|
|
4
6
|
* Create a document picker API bound to a document listing function.
|
|
5
7
|
* The listFn is derived from the shard's document zone + browse permission
|
|
6
8
|
* and baked in at construction time so callers don't pass their own scope.
|
|
9
|
+
*
|
|
10
|
+
* When an `anchor` element is provided the browser opens as a popup
|
|
11
|
+
* (anchored near the element). Without an anchor it opens as a centered
|
|
12
|
+
* modal (the expected default for file-browser dialogs).
|
|
7
13
|
*/
|
|
8
14
|
export function createDocumentPicker(listFn) {
|
|
9
|
-
|
|
15
|
+
/** Resolve handle for either popup (anchored) or modal (centered) path. */
|
|
16
|
+
function openBrowser(browserProps, anchor) {
|
|
10
17
|
if (anchor) {
|
|
11
18
|
const rect = anchor.getBoundingClientRect();
|
|
12
|
-
return { x: rect.left + rect.width / 2, y: rect.top };
|
|
19
|
+
return sh3.popup.show(DocumentBrowser, { anchor: { x: rect.left + rect.width / 2, y: rect.top } }, browserProps);
|
|
13
20
|
}
|
|
14
|
-
return
|
|
21
|
+
return sh3.modal.open(DocumentBrowser, browserProps, MODAL_OPTS);
|
|
22
|
+
}
|
|
23
|
+
function wrapHandle(handle, resolve) {
|
|
24
|
+
const origClose = handle.close;
|
|
25
|
+
handle.close = () => {
|
|
26
|
+
origClose();
|
|
27
|
+
resolve(null);
|
|
28
|
+
};
|
|
15
29
|
}
|
|
16
30
|
async function open(opts) {
|
|
17
31
|
const docs = await listFn();
|
|
18
32
|
return new Promise((resolve) => {
|
|
19
|
-
const handle =
|
|
33
|
+
const handle = openBrowser({
|
|
20
34
|
mode: 'open',
|
|
21
35
|
docs,
|
|
22
|
-
onCommit: (value) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
resolve(null);
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
const origClose = handle.close;
|
|
30
|
-
handle.close = () => {
|
|
31
|
-
origClose();
|
|
32
|
-
resolve(null);
|
|
33
|
-
};
|
|
36
|
+
onCommit: (value) => { resolve(value); },
|
|
37
|
+
onCancel: () => { resolve(null); },
|
|
38
|
+
}, opts === null || opts === void 0 ? void 0 : opts.anchor);
|
|
39
|
+
wrapHandle(handle, resolve);
|
|
34
40
|
});
|
|
35
41
|
}
|
|
36
42
|
async function save(opts) {
|
|
37
43
|
const docs = await listFn();
|
|
38
44
|
return new Promise((resolve) => {
|
|
39
|
-
const handle =
|
|
45
|
+
const handle = openBrowser({
|
|
40
46
|
mode: 'save',
|
|
41
47
|
docs,
|
|
42
48
|
suggestedName: opts === null || opts === void 0 ? void 0 : opts.suggestedName,
|
|
43
|
-
onCommit: (value) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
resolve(null);
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
const origClose = handle.close;
|
|
51
|
-
handle.close = () => {
|
|
52
|
-
origClose();
|
|
53
|
-
resolve(null);
|
|
54
|
-
};
|
|
49
|
+
onCommit: (value) => { resolve(value); },
|
|
50
|
+
onCancel: () => { resolve(null); },
|
|
51
|
+
}, opts === null || opts === void 0 ? void 0 : opts.anchor);
|
|
52
|
+
wrapHandle(handle, resolve);
|
|
55
53
|
});
|
|
56
54
|
}
|
|
57
55
|
return { open, save };
|