sh3-core 0.20.1 → 0.20.3
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/BrandSlot.svelte +2 -2
- package/dist/actions/ctx-actions.svelte.test.js +2 -2
- package/dist/artifact.d.ts +2 -0
- package/dist/boot/satellitePayload.d.ts +2 -0
- package/dist/boot/satellitePayload.test.js +19 -0
- package/dist/build.d.ts +7 -1
- package/dist/build.js +22 -3
- package/dist/build.test.js +27 -1
- package/dist/createShell.js +34 -9
- 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 +20 -0
- package/dist/documents/browse.js +35 -0
- package/dist/documents/browse.test.js +125 -0
- package/dist/documents/config.d.ts +2 -4
- package/dist/documents/config.js +3 -7
- package/dist/documents/handle.js +40 -0
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +11 -0
- package/dist/documents/http-backend.js +86 -0
- package/dist/documents/http-backend.test.js +117 -1
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.test.js +2 -2
- package/dist/documents/types.d.ts +87 -14
- package/dist/documents/types.js +4 -0
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/layout/slotHostPool.svelte.js +2 -2
- package/dist/overlays/FloatFrame.svelte +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
- 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/_DocumentBrowser.svelte +414 -27
- 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/session-state.svelte.d.ts +3 -0
- package/dist/projects/session-state.svelte.js +25 -0
- package/dist/projects/session-state.test.js +43 -2
- package/dist/projects-shard/ProjectsSection.svelte +14 -18
- package/dist/runtime/runVerb-shell.test.js +2 -2
- package/dist/runtime/runVerb.test.js +2 -2
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/appActions.js +5 -2
- package/dist/shards/activate-browse.test.js +2 -2
- package/dist/shards/activate-contributions.test.js +2 -2
- package/dist/shards/activate-error-isolation.test.js +3 -3
- package/dist/shards/activate-on-key-revoked.test.js +2 -2
- package/dist/shards/activate-runtime.test.js +2 -2
- package/dist/shards/activate.svelte.js +5 -5
- package/dist/shards/ctx-fetch.test.js +4 -4
- 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/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 +87 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +107 -0
- 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
|
@@ -128,6 +128,31 @@ export class HttpDocumentBackend {
|
|
|
128
128
|
throw new Error(`readBranch failed: ${res.status}`);
|
|
129
129
|
return res.text();
|
|
130
130
|
}
|
|
131
|
+
async xfer(srcTenant, srcPath, dstTenant, dstPath, opts) {
|
|
132
|
+
var _a;
|
|
133
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/xfer`;
|
|
134
|
+
const res = await apiFetch(url, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
src: { scope: srcTenant, path: srcPath },
|
|
139
|
+
dst: { scope: dstTenant, path: dstPath },
|
|
140
|
+
move: (_a = opts === null || opts === void 0 ? void 0 : opts.move) !== null && _a !== void 0 ? _a : false,
|
|
141
|
+
}),
|
|
142
|
+
credentials: 'include',
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
let detail = `HTTP ${res.status}`;
|
|
146
|
+
try {
|
|
147
|
+
const b = await res.json();
|
|
148
|
+
if (b.error)
|
|
149
|
+
detail = b.error;
|
|
150
|
+
}
|
|
151
|
+
catch ( /* not JSON */_b) { /* not JSON */ }
|
|
152
|
+
throw new Error(`xfer failed: ${detail}`);
|
|
153
|
+
}
|
|
154
|
+
return res.json();
|
|
155
|
+
}
|
|
131
156
|
async rename(tenantId, shardId, oldPath, newPath) {
|
|
132
157
|
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${oldPath}/rename`;
|
|
133
158
|
const headers = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' });
|
|
@@ -147,6 +172,67 @@ export class HttpDocumentBackend {
|
|
|
147
172
|
throw new Error(`Document rename failed: ${res.status}`);
|
|
148
173
|
}
|
|
149
174
|
}
|
|
175
|
+
async mkdir(tenantId, shardId, path) {
|
|
176
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}/mkdir`;
|
|
177
|
+
const res = await apiFetch(url, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this),
|
|
180
|
+
credentials: 'include',
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok) {
|
|
183
|
+
let detail = `HTTP ${res.status}`;
|
|
184
|
+
try {
|
|
185
|
+
const b = await res.json();
|
|
186
|
+
if (b.error)
|
|
187
|
+
detail = b.error;
|
|
188
|
+
}
|
|
189
|
+
catch ( /* not JSON */_a) { /* not JSON */ }
|
|
190
|
+
throw new Error(`mkdir failed: ${detail}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async rmdir(tenantId, shardId, path, opts) {
|
|
194
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${path}/rmdir`;
|
|
195
|
+
const res = await apiFetch(url, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
198
|
+
body: JSON.stringify({ recursive: opts.recursive }),
|
|
199
|
+
credentials: 'include',
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
let detail = `HTTP ${res.status}`;
|
|
203
|
+
try {
|
|
204
|
+
const b = await res.json();
|
|
205
|
+
if (b.error)
|
|
206
|
+
detail = b.error;
|
|
207
|
+
}
|
|
208
|
+
catch ( /* not JSON */_a) { /* not JSON */ }
|
|
209
|
+
throw new Error(`rmdir failed: ${detail}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async renameFolder(tenantId, shardId, oldPath, newPath) {
|
|
213
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${oldPath}/rename-folder`;
|
|
214
|
+
const res = await apiFetch(url, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: Object.assign(Object.assign({}, __classPrivateFieldGet(this, _HttpDocumentBackend_instances, "m", _HttpDocumentBackend_authHeaders).call(this)), { 'Content-Type': 'application/json' }),
|
|
217
|
+
body: JSON.stringify({ to: newPath }),
|
|
218
|
+
credentials: 'include',
|
|
219
|
+
});
|
|
220
|
+
if (res.status === 404)
|
|
221
|
+
throw new Error(`Folder not found at ${oldPath}`);
|
|
222
|
+
if (res.status === 409)
|
|
223
|
+
throw new Error(`Folder already exists at ${newPath}`);
|
|
224
|
+
if (!res.ok)
|
|
225
|
+
throw new Error(`renameFolder failed: ${res.status}`);
|
|
226
|
+
}
|
|
227
|
+
async listFolders(tenantId, shardId, prefix) {
|
|
228
|
+
const base = prefix
|
|
229
|
+
? `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}/${prefix}?folders=1`
|
|
230
|
+
: `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/${shardId}?folders=1`;
|
|
231
|
+
const res = await apiFetch(base, { credentials: 'include' });
|
|
232
|
+
if (!res.ok)
|
|
233
|
+
throw new Error(`listFolders failed: ${res.status}`);
|
|
234
|
+
return res.json();
|
|
235
|
+
}
|
|
150
236
|
}
|
|
151
237
|
_HttpDocumentBackend_baseUrl = new WeakMap(), _HttpDocumentBackend_apiKey = new WeakMap(), _HttpDocumentBackend_instances = new WeakSet(), _HttpDocumentBackend_authHeaders = function _HttpDocumentBackend_authHeaders() {
|
|
152
238
|
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,119 @@ 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
|
+
});
|
|
132
|
+
describe('HttpDocumentBackend.xfer', () => {
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
globalThis.fetch = originalFetch;
|
|
135
|
+
});
|
|
136
|
+
it('POSTs to /api/xfer with src, dst, and move in the body', async () => {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
const calls = [];
|
|
139
|
+
globalThis.fetch = (async (url, init) => {
|
|
140
|
+
calls.push({ url: String(url), init });
|
|
141
|
+
return new Response(JSON.stringify({ ok: true, existed: false }), { status: 200 });
|
|
142
|
+
});
|
|
143
|
+
const be = new HttpDocumentBackend('http://server', 'apikey-1');
|
|
144
|
+
const result = await be.xfer('alice', 'notes/draft.md', 'proj-1', 'notes/draft.md', { move: true });
|
|
145
|
+
expect(calls).toHaveLength(1);
|
|
146
|
+
expect(calls[0].url).toBe('http://server/api/xfer');
|
|
147
|
+
expect((_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.method).toBe('POST');
|
|
148
|
+
const body = JSON.parse((_b = calls[0].init) === null || _b === void 0 ? void 0 : _b.body);
|
|
149
|
+
expect(body).toEqual({
|
|
150
|
+
src: { scope: 'alice', path: 'notes/draft.md' },
|
|
151
|
+
dst: { scope: 'proj-1', path: 'notes/draft.md' },
|
|
152
|
+
move: true,
|
|
153
|
+
});
|
|
154
|
+
expect(result).toEqual({ ok: true, existed: false });
|
|
155
|
+
});
|
|
156
|
+
it('sends move=false when opts.move is false', async () => {
|
|
157
|
+
var _a;
|
|
158
|
+
const calls = [];
|
|
159
|
+
globalThis.fetch = (async (url, init) => {
|
|
160
|
+
calls.push({ url: String(url), init });
|
|
161
|
+
return new Response(JSON.stringify({ ok: true, existed: true }), { status: 200 });
|
|
162
|
+
});
|
|
163
|
+
const be = new HttpDocumentBackend('http://server');
|
|
164
|
+
await be.xfer('alice', 'notes/a.md', 'proj-1', 'notes/a.md', { move: false });
|
|
165
|
+
const body = JSON.parse((_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.body);
|
|
166
|
+
expect(body.move).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('sends move=false when opts is omitted', async () => {
|
|
169
|
+
var _a;
|
|
170
|
+
const calls = [];
|
|
171
|
+
globalThis.fetch = (async (url, init) => {
|
|
172
|
+
calls.push({ url: String(url), init });
|
|
173
|
+
return new Response(JSON.stringify({ ok: true, existed: false }), { status: 200 });
|
|
174
|
+
});
|
|
175
|
+
const be = new HttpDocumentBackend('http://server');
|
|
176
|
+
await be.xfer('alice', 'notes/a.md', 'proj-1', 'notes/a.md');
|
|
177
|
+
const body = JSON.parse((_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.body);
|
|
178
|
+
expect(body.move).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
it('includes Authorization header when apiKey is set', async () => {
|
|
181
|
+
var _a;
|
|
182
|
+
const calls = [];
|
|
183
|
+
globalThis.fetch = (async (url, init) => {
|
|
184
|
+
calls.push({ url: String(url), init });
|
|
185
|
+
return new Response(JSON.stringify({ ok: true, existed: false }), { status: 200 });
|
|
186
|
+
});
|
|
187
|
+
const be = new HttpDocumentBackend('http://server', 'my-key');
|
|
188
|
+
await be.xfer('alice', 'notes/a.md', 'proj-1', 'notes/a.md');
|
|
189
|
+
const headers = (_a = calls[0].init) === null || _a === void 0 ? void 0 : _a.headers;
|
|
190
|
+
expect(headers['Authorization']).toBe('Bearer my-key');
|
|
191
|
+
});
|
|
192
|
+
it('throws with server error message on non-ok response', async () => {
|
|
193
|
+
globalThis.fetch = (async () => new Response(JSON.stringify({ error: 'Source document not found' }), { status: 404 }));
|
|
194
|
+
const be = new HttpDocumentBackend('http://server');
|
|
195
|
+
await expect(be.xfer('alice', 'notes/missing.md', 'proj-1', 'notes/missing.md')).rejects.toThrow('Source document not found');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -3,6 +3,6 @@ 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,
|
|
6
|
+
export { getActiveScopeId, getDocumentBackend, __setActiveScope, __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,
|
|
8
|
+
export { getActiveScopeId, getDocumentBackend, __setActiveScope, __setDocumentBackend, __setScopeResolver, } from './config';
|
|
9
9
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
|
|
@@ -43,7 +43,7 @@ beforeEach(() => {
|
|
|
43
43
|
vi.clearAllMocks();
|
|
44
44
|
});
|
|
45
45
|
describe('createDocumentPicker', () => {
|
|
46
|
-
const sampleDoc = { shardId: 'my-shard', path: 'readme.md' };
|
|
46
|
+
const sampleDoc = { shardId: 'my-shard', path: 'readme.md', kind: 'file' };
|
|
47
47
|
describe('open() — modal (no anchor)', () => {
|
|
48
48
|
it('resolves with OpenerValue when user commits', async () => {
|
|
49
49
|
const listFn = async () => [{ shardId: 'my-shard', path: 'readme.md', size: 100, lastModified: 0 }];
|
|
@@ -53,7 +53,7 @@ describe('createDocumentPicker', () => {
|
|
|
53
53
|
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
54
54
|
modal.commit(sampleDoc);
|
|
55
55
|
const result = await promise;
|
|
56
|
-
expect(result).toEqual({ shardId: 'my-shard', path: 'readme.md' });
|
|
56
|
+
expect(result).toEqual({ shardId: 'my-shard', path: 'readme.md', kind: 'file' });
|
|
57
57
|
expect(mockPopupShow).not.toHaveBeenCalled();
|
|
58
58
|
});
|
|
59
59
|
it('resolves with null when user cancels', async () => {
|
|
@@ -73,26 +73,37 @@ export interface DocumentMeta {
|
|
|
73
73
|
deleted?: boolean;
|
|
74
74
|
}
|
|
75
75
|
/** Change notification payload delivered to watch callbacks. */
|
|
76
|
-
export
|
|
77
|
-
type: 'create' | 'update' | 'delete'
|
|
78
|
-
/**
|
|
79
|
-
* For 'create' / 'update' / 'delete', the affected document path.
|
|
80
|
-
* For 'rename', the new path the document now lives at.
|
|
81
|
-
*/
|
|
76
|
+
export type DocumentChange = {
|
|
77
|
+
type: 'create' | 'update' | 'delete';
|
|
82
78
|
path: string;
|
|
83
|
-
/**
|
|
84
|
-
* Populated only when type === 'rename'. The path the document
|
|
85
|
-
* used to live at before the rename.
|
|
86
|
-
*/
|
|
87
|
-
oldPath?: string;
|
|
88
79
|
tenantId: string;
|
|
89
80
|
shardId: string;
|
|
90
|
-
}
|
|
91
|
-
/** Type guard: narrows a DocumentChange to the rename variant. */
|
|
92
|
-
export declare function isRename(change: DocumentChange): change is DocumentChange & {
|
|
81
|
+
} | {
|
|
93
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;
|
|
94
95
|
oldPath: string;
|
|
96
|
+
tenantId: string;
|
|
97
|
+
shardId: string;
|
|
95
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
|
+
}>;
|
|
96
107
|
import type { DocStatus } from './sync-types';
|
|
97
108
|
/**
|
|
98
109
|
* File-oriented backend for the document zone.
|
|
@@ -120,6 +131,31 @@ export interface DocumentBackend {
|
|
|
120
131
|
* as folder semantics.
|
|
121
132
|
*/
|
|
122
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[]>;
|
|
123
159
|
/** List all documents stored for this tenant + shard combination. */
|
|
124
160
|
list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
|
|
125
161
|
/** Return true if the document at `path` exists. */
|
|
@@ -156,6 +192,17 @@ export interface DocumentBackend {
|
|
|
156
192
|
* Optional; only supported by HttpDocumentBackend in v1.
|
|
157
193
|
*/
|
|
158
194
|
readBranch?(tenantId: string, shardId: string, path: string, origin: string): Promise<string | null>;
|
|
195
|
+
/**
|
|
196
|
+
* Server-side cross-scope transfer. When implemented, `transferBetweenScopes`
|
|
197
|
+
* delegates to this rather than doing client-side read+write, bypassing the
|
|
198
|
+
* per-shard app allowlist check. `srcPath` and `dstPath` are in the form
|
|
199
|
+
* `shardId/filePath`. Returns whether the destination document already existed.
|
|
200
|
+
*/
|
|
201
|
+
xfer?(srcTenant: string, srcPath: string, dstTenant: string, dstPath: string, opts?: {
|
|
202
|
+
move?: boolean;
|
|
203
|
+
}): Promise<{
|
|
204
|
+
existed: boolean;
|
|
205
|
+
}>;
|
|
159
206
|
}
|
|
160
207
|
/**
|
|
161
208
|
* Shard-facing document handle returned by `ctx.documents()`. Binds
|
|
@@ -187,6 +234,32 @@ export interface DocumentHandle {
|
|
|
187
234
|
* extensions filter — newPath must satisfy the filter.
|
|
188
235
|
*/
|
|
189
236
|
rename(oldPath: string, newPath: string, opts?: ScopeOption): Promise<void>;
|
|
237
|
+
/**
|
|
238
|
+
* Create an empty folder at `path`. No-op if the folder already
|
|
239
|
+
* exists. Throws if a document occupies the path.
|
|
240
|
+
*/
|
|
241
|
+
mkdir(path: string, opts?: ScopeOption): Promise<void>;
|
|
242
|
+
/**
|
|
243
|
+
* Remove a folder. Throws if non-empty and `opts.recursive` is not
|
|
244
|
+
* true. When `opts.recursive` is true, atomically removes the folder
|
|
245
|
+
* and all descendants. Throws if any active autosave controller's
|
|
246
|
+
* path falls inside the folder.
|
|
247
|
+
*/
|
|
248
|
+
rmdir(path: string, opts?: {
|
|
249
|
+
recursive?: boolean;
|
|
250
|
+
} & ScopeOption): Promise<void>;
|
|
251
|
+
/**
|
|
252
|
+
* Rename a folder. Atomically rewrites all descendant document paths.
|
|
253
|
+
* Throws if any active autosave controller's path falls inside the
|
|
254
|
+
* folder (caller must flush and dispose first). Throws if `newPath`
|
|
255
|
+
* already exists or if `oldPath` does not.
|
|
256
|
+
*/
|
|
257
|
+
renameFolder(oldPath: string, newPath: string, opts?: ScopeOption): Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* List immediate folder children of `prefix`. Empty `prefix` (or
|
|
260
|
+
* omitted) lists folders directly under the shard root.
|
|
261
|
+
*/
|
|
262
|
+
listFolders(prefix?: string, opts?: ScopeOption): Promise<string[]>;
|
|
190
263
|
/** Check existence without reading content. */
|
|
191
264
|
exists(path: string): Promise<boolean>;
|
|
192
265
|
/** Fetch sync-state metadata for a path. Null if the doc does not exist. */
|
package/dist/documents/types.js
CHANGED
|
@@ -51,3 +51,7 @@ export const PERMISSION_DOCUMENTS_MOUNT = 'documents:mount';
|
|
|
51
51
|
export function isRename(change) {
|
|
52
52
|
return change.type === 'rename';
|
|
53
53
|
}
|
|
54
|
+
/** Type guard: narrows a DocumentChange to the folder-rename variant. */
|
|
55
|
+
export function isFolderRename(change) {
|
|
56
|
+
return change.type === 'folder-rename';
|
|
57
|
+
}
|
package/dist/host-entry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
|
|
2
2
|
export type { BootstrapConfig } from './host';
|
|
3
|
-
export { __setActiveScope,
|
|
3
|
+
export { __setActiveScope, __setDocumentBackend } from './host';
|
|
4
4
|
export type { Backend } from './state/types';
|
|
5
5
|
export type { DocumentBackend } from './documents/types';
|
|
6
6
|
export { HttpDocumentBackend } from './documents/http-backend';
|
package/dist/host-entry.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* should touch this path. Shards and apps must not import from here.
|
|
7
7
|
*/
|
|
8
8
|
export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
|
|
9
|
-
export { __setActiveScope,
|
|
9
|
+
export { __setActiveScope, __setDocumentBackend } from './host';
|
|
10
10
|
export { HttpDocumentBackend } from './documents/http-backend';
|
|
11
11
|
export { IndexedDBDocumentBackend, MemoryDocumentBackend } from './documents/backends';
|
|
12
12
|
export { __setEnvServerUrl } from './env/index';
|
package/dist/host.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { __setBackend } from './state/zones.svelte';
|
|
|
4
4
|
import { setLocalOwner } from './auth/index';
|
|
5
5
|
export { __setBackend };
|
|
6
6
|
export { setLocalOwner };
|
|
7
|
-
export { __setActiveScope,
|
|
7
|
+
export { __setActiveScope, __setDocumentBackend } from './documents/config';
|
|
8
8
|
export declare function registerShard(shard: Parameters<typeof registerShardInternal>[0]): void;
|
|
9
9
|
export { registerApp };
|
|
10
10
|
export interface BootstrapConfig {
|
package/dist/host.js
CHANGED
|
@@ -38,7 +38,7 @@ import { installWebEmitter } from './navigation/platform-web';
|
|
|
38
38
|
import { returnToHome } from './apps/lifecycle';
|
|
39
39
|
export { __setBackend };
|
|
40
40
|
export { setLocalOwner };
|
|
41
|
-
export { __setActiveScope,
|
|
41
|
+
export { __setActiveScope, __setDocumentBackend } from './documents/config';
|
|
42
42
|
import { getActiveScopeId } from './documents/config';
|
|
43
43
|
export function registerShard(shard) {
|
|
44
44
|
registerShardInternal(shard);
|
|
@@ -75,7 +75,7 @@ function onViewRegistered(viewId, factory) {
|
|
|
75
75
|
if (entry.handle !== undefined)
|
|
76
76
|
return; // already mounted by a race
|
|
77
77
|
entry.handle = factory.mount(entry.host, ctx);
|
|
78
|
-
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
|
|
78
|
+
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:') || slotId.startsWith('standalone:')) {
|
|
79
79
|
closableState[slotId] = true;
|
|
80
80
|
}
|
|
81
81
|
if ((_b = entry.handle) === null || _b === void 0 ? void 0 : _b.onResize) {
|
|
@@ -179,7 +179,7 @@ function createHost(slotId, viewId, label, meta) {
|
|
|
179
179
|
},
|
|
180
180
|
};
|
|
181
181
|
entry.handle = factory === null || factory === void 0 ? void 0 : factory.mount(host, ctx);
|
|
182
|
-
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
|
|
182
|
+
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:') || slotId.startsWith('standalone:')) {
|
|
183
183
|
closableState[slotId] = true;
|
|
184
184
|
}
|
|
185
185
|
// The pool owns the ResizeObserver so its lifetime matches the
|
|
@@ -262,6 +262,7 @@
|
|
|
262
262
|
title: entry.title,
|
|
263
263
|
size: { w: entry.size.w, h: entry.size.h },
|
|
264
264
|
activateShards: walkShardsForContent(entry.content),
|
|
265
|
+
projectId: sh3.getActiveScope().isProject ? sh3.getActiveScope().id : undefined,
|
|
265
266
|
});
|
|
266
267
|
floatManager.close(entry.id);
|
|
267
268
|
} catch (err) {
|
|
@@ -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
|
|
|
@@ -63,6 +81,10 @@
|
|
|
63
81
|
{
|
|
64
82
|
mode,
|
|
65
83
|
docs,
|
|
84
|
+
selectable,
|
|
85
|
+
listFolders,
|
|
86
|
+
handle,
|
|
87
|
+
readOnlyShard,
|
|
66
88
|
onCommit: (result: OpenerValue | SaverValue) => {
|
|
67
89
|
handleCommit(result);
|
|
68
90
|
},
|
|
@@ -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 {};
|