diff --git a/apps/web/src/lib/desktop-repository.test.mjs b/apps/web/src/lib/desktop-repository.test.mjs new file mode 100644 index 00000000..4cab6520 --- /dev/null +++ b/apps/web/src/lib/desktop-repository.test.mjs @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test"; + +let lastRequest = null; + +globalThis.window = { + location: { hostname: "notes.example.com", origin: "https://notes.example.com" }, + edgeeverDesktop: { + isAvailable: true, + sidecarRequest: async (method, params) => { + lastRequest = { method, params }; + return { memos: [], totalCount: 0, nextCursor: null }; + }, + }, +}; + +const { createDesktopRepository } = await import("./desktop-repository.ts"); + +describe("desktop repository notebook filters", () => { + test("uses the notebook subtree without also restricting results to the parent", async () => { + await createDesktopRepository().listMemos({ + notebookId: "parent", + notebookIds: ["parent", "child"], + }); + + expect(lastRequest).toEqual({ + method: "memo.list", + params: { + notebookId: null, + notebookIds: ["parent", "child"], + }, + }); + }); +}); diff --git a/apps/web/src/lib/desktop-repository.ts b/apps/web/src/lib/desktop-repository.ts index ac91245d..95396c9e 100644 --- a/apps/web/src/lib/desktop-repository.ts +++ b/apps/web/src/lib/desktop-repository.ts @@ -195,7 +195,7 @@ export const createDesktopRepository = (): EdgeEverRepository => ({ listMemos: (params) => { const rpcParams: DesktopMemoListParams = { ...params, - notebookId: params.notebookId ?? params.notebookIds?.[0] ?? null, + notebookId: params.notebookIds?.length ? null : (params.notebookId ?? null), }; return request("memo.list", rpcParams); }, diff --git a/crates/desktop-sidecar/src/main.rs b/crates/desktop-sidecar/src/main.rs index b3403e98..0c8b6ac3 100644 --- a/crates/desktop-sidecar/src/main.rs +++ b/crates/desktop-sidecar/src/main.rs @@ -809,12 +809,19 @@ fn list_memos(database: &Connection, params: &Value) -> Result { .unwrap_or("") .trim() .to_owned(); - let notebook_id = params.get("notebookId").and_then(Value::as_str); let notebook_ids = params .get("notebookIds") .and_then(Value::as_array) .cloned() .unwrap_or_default(); + // A notebookIds collection represents the complete notebook subtree and + // therefore supersedes the singular notebookId. Applying both filters + // would reduce the subtree back to the parent notebook alone. + let notebook_id = if notebook_ids.is_empty() { + params.get("notebookId").and_then(Value::as_str) + } else { + None + }; let notebook_ids_json = Value::Array(notebook_ids).to_string(); let filter = match params.get("filter").and_then(Value::as_str) { Some("pinned") => " AND m.is_pinned = 1", diff --git a/package.json b/package.json index 81df9a83..da271f9f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "release": "bun scripts/release.mjs", "publish:stores": "bun scripts/store-delivery.mjs", "test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs", - "test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar", + "test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar", "verify:desktop-package": "bun scripts/verify-desktop-package.mjs", "build:cloudflare": "bun run typecheck && bun run build", "build:site": "bun --cwd apps/site build", diff --git a/scripts/test-desktop-sidecar.mjs b/scripts/test-desktop-sidecar.mjs index 012ace27..ca993cbd 100644 --- a/scripts/test-desktop-sidecar.mjs +++ b/scripts/test-desktop-sidecar.mjs @@ -53,6 +53,15 @@ assert.equal((await request("memo.get", { memoId: first.memo.id })).memo.id, fir const second = await request("memo.create", { notebookId: inbox.id, title: "Second memo", contentMarkdown: "another body", tags: [] }); const search = await request("memo.list", { q: "searchable", limit: 20 }); assert.deepEqual(search.memos.map((memo) => memo.id), [first.memo.id]); +const childNotebook = (await request("notebook.create", { name: "Inbox child", parentId: inbox.id })).notebook; +const childMemo = await request("memo.create", { notebookId: childNotebook.id, title: "Nested memo", contentMarkdown: "nested body", tags: [] }); +const subtree = await request("memo.list", { + notebookId: inbox.id, + notebookIds: [inbox.id, childNotebook.id], + limit: 20, +}); +assert.ok(subtree.memos.some((memo) => memo.id === first.memo.id), "parent notebook notes should remain in a subtree query"); +assert.ok(subtree.memos.some((memo) => memo.id === childMemo.memo.id), "subtree queries should include child notebook notes"); await request("memo.update", { memoId: first.memo.id, title: "Local first updated", @@ -195,4 +204,4 @@ assert.equal((await request("sync.status")).conflict, 0); child.stdin.end(); await new Promise((resolve) => child.once("close", resolve)); -console.log(JSON.stringify({ ok: true, checked: ["memo.create", "memo.list.search", "memo.update", "memo.update.coalesce", "memo.revisions", "memo.restoreRevision", "memo.revision.cache", "tag.rename", "memo.moveBatch", "memo.pinBatch", "memo.deleteBatch", "memo.restore", "memo.emptyTrash", "memo.merge", "template.cache", "template.create.payload", "template.delete", "storage.backup", "storage.backups", "storage.restore", "sync.outbox", "sync.outbox.discard"] })); +console.log(JSON.stringify({ ok: true, checked: ["memo.create", "memo.list.search", "memo.list.subtree", "memo.update", "memo.update.coalesce", "memo.revisions", "memo.restoreRevision", "memo.revision.cache", "tag.rename", "memo.moveBatch", "memo.pinBatch", "memo.deleteBatch", "memo.restore", "memo.emptyTrash", "memo.merge", "template.cache", "template.create.payload", "template.delete", "storage.backup", "storage.backups", "storage.restore", "sync.outbox", "sync.outbox.discard"] }));