diff --git a/apps/api/src/memo-list-service.test.mjs b/apps/api/src/memo-list-service.test.mjs index 40620c64..c9576e29 100644 --- a/apps/api/src/memo-list-service.test.mjs +++ b/apps/api/src/memo-list-service.test.mjs @@ -91,6 +91,51 @@ describe("memo list service", () => { } }); + test("paginates title Z-A by reverse title and keeps the update-time tie break", async () => { + const titleExpression = "LOWER(COALESCE(NULLIF(m.title, ''), '无标题笔记'))"; + const firstDatabase = createDatabase({ + rows: [memoRow("memo_1", { title: "Zulu" }), memoRow("memo_2", { title: "Alpha" })], + totalCount: 2, + }); + const first = await listMemos(firstDatabase, { + workspaceId: "ws_1", + sort: "title-desc", + limit: 1, + }); + const orderQuery = firstDatabase.calls.find((call) => call.sql.includes("ORDER BY")); + + expect(orderQuery.sql).toContain(`m.is_pinned DESC, ${titleExpression} DESC, m.updated_at DESC, m.id DESC`); + expect(orderQuery.sql).not.toContain(`${titleExpression} ASC`); + + const secondDatabase = createDatabase({ rows: [] }); + await listMemos(secondDatabase, { + workspaceId: "ws_1", + sort: "title-desc", + limit: 1, + cursor: first.nextCursor, + }); + const pageQuery = secondDatabase.calls.find((call) => call.sql.includes("SELECT m.id")); + + expect(pageQuery.sql).toContain(`${titleExpression} < ?`); + expect(pageQuery.sql).not.toContain(`${titleExpression} > ?`); + expect(pageQuery.sql).toContain("m.updated_at < ?"); + expect(pageQuery.parameters).toContain("zulu"); + expect(pageQuery.parameters).toContain("2026-08-01T01:00:00.000Z"); + }); + + test("keeps trash ordered by deletion time when the title sort is reversed", async () => { + const database = createDatabase(); + await listMemos(database, { + workspaceId: "ws_1", + includeTrash: true, + sort: "title-desc", + }); + const pageQuery = database.calls.find((call) => call.sql.includes("ORDER BY")); + + expect(pageQuery.sql).toContain("ORDER BY m.deleted_at DESC, m.id DESC"); + expect(pageQuery.sql).not.toContain("NULLIF(m.title, '')"); + }); + test("filters by one exact tag in page and count queries", async () => { const database = createDatabase(); await listMemos(database, { diff --git a/apps/api/src/memo-list-service.ts b/apps/api/src/memo-list-service.ts index b274f3d3..929339b4 100644 --- a/apps/api/src/memo-list-service.ts +++ b/apps/api/src/memo-list-service.ts @@ -30,7 +30,7 @@ export type MemoSummaryRow = { revision: number; }; -type MemoListSortMode = "updated-desc" | "created-desc" | "title-asc"; +type MemoListSortMode = "updated-desc" | "created-desc" | "title-asc" | "title-desc"; type MemoListFilterMode = "all" | "tagged" | "untagged" | "pinned"; type MemoListCursor = { @@ -94,7 +94,10 @@ const normalizeMemoTitle = (value: string | null | undefined) => { }; const normalizeMemoListSort = (value: string | undefined): MemoListSortMode => - value === "created-desc" || value === "title-asc" ? value : "updated-desc"; + value === "created-desc" || value === "title-asc" || value === "title-desc" ? value : "updated-desc"; + +const isTitleSort = (sort: MemoListSortMode): sort is "title-asc" | "title-desc" => + sort === "title-asc" || sort === "title-desc"; const normalizeMemoListFilter = (value: string | undefined): MemoListFilterMode => value === "tagged" || value === "untagged" || value === "pinned" ? value : "all"; @@ -110,7 +113,7 @@ const encodeMemoListCursor = ( else cursor.pinned = memo.is_pinned; if (sort === "created-desc") cursor.createdAt = memo.created_at; - else if (sort === "title-asc") { + else if (isTitleSort(sort)) { cursor.title = normalizeMemoTitle(memo.title).toLocaleLowerCase(); cursor.updatedAt = memo.updated_at; } else cursor.updatedAt = memo.updated_at; @@ -208,8 +211,9 @@ export const listMemos = async ( const getOrderBy = () => { if (includeTrash) return "m.deleted_at DESC, m.id DESC"; if (sort === "created-desc") return "m.is_pinned DESC, m.created_at DESC, m.id DESC"; - if (sort === "title-asc") { - return `m.is_pinned DESC, ${titleSortExpression} ASC, m.updated_at DESC, m.id DESC`; + if (isTitleSort(sort)) { + const direction = sort === "title-desc" ? "DESC" : "ASC"; + return `m.is_pinned DESC, ${titleSortExpression} ${direction}, m.updated_at DESC, m.id DESC`; } return "m.is_pinned DESC, m.updated_at DESC, m.id DESC"; }; @@ -223,9 +227,10 @@ export const listMemos = async ( } else if (sort === "created-desc") { cursorConditions.push("(m.is_pinned < ? OR (m.is_pinned = ? AND (m.created_at < ? OR (m.created_at = ? AND m.id < ?))))"); cursorBinds.push(cursor.pinned ?? 0, cursor.pinned ?? 0, cursor.createdAt ?? "", cursor.createdAt ?? "", cursor.id); - } else if (sort === "title-asc") { + } else if (isTitleSort(sort)) { + const titleOperator = sort === "title-desc" ? "<" : ">"; cursorConditions.push( - `(m.is_pinned < ? OR (m.is_pinned = ? AND (${titleSortExpression} > ? OR (${titleSortExpression} = ? AND (m.updated_at < ? OR (m.updated_at = ? AND m.id < ?))))))`, + `(m.is_pinned < ? OR (m.is_pinned = ? AND (${titleSortExpression} ${titleOperator} ? OR (${titleSortExpression} = ? AND (m.updated_at < ? OR (m.updated_at = ? AND m.id < ?))))))`, ); cursorBinds.push(cursor.pinned ?? 0, cursor.pinned ?? 0, cursor.title ?? "", cursor.title ?? "", cursor.updatedAt ?? "", cursor.updatedAt ?? "", cursor.id); } else { diff --git a/apps/ios/EdgeEver/Data/Database/LocalMirrorRepository.swift b/apps/ios/EdgeEver/Data/Database/LocalMirrorRepository.swift index dfdf61ed..a671241e 100644 --- a/apps/ios/EdgeEver/Data/Database/LocalMirrorRepository.swift +++ b/apps/ios/EdgeEver/Data/Database/LocalMirrorRepository.swift @@ -133,6 +133,7 @@ final class LocalMirrorRepository: @unchecked Sendable { switch params.sort { case .createdDesc: orderBy = "is_pinned DESC, created_at DESC, id DESC" case .titleAsc: orderBy = "is_pinned DESC, title COLLATE NOCASE ASC, updated_at DESC, id DESC" + case .titleDesc: orderBy = "is_pinned DESC, title COLLATE NOCASE DESC, updated_at DESC, id DESC" case .updatedDesc: orderBy = "is_pinned DESC, updated_at DESC, id DESC" } } diff --git a/apps/ios/EdgeEver/Data/Models/Models.swift b/apps/ios/EdgeEver/Data/Models/Models.swift index f8d55022..fb09b2e6 100644 --- a/apps/ios/EdgeEver/Data/Models/Models.swift +++ b/apps/ios/EdgeEver/Data/Models/Models.swift @@ -422,6 +422,7 @@ enum MemoSortMode: String, CaseIterable, Sendable { case updatedDesc = "updated-desc" case createdDesc = "created-desc" case titleAsc = "title-asc" + case titleDesc = "title-desc" } enum MemoFilterMode: String, CaseIterable, Sendable { diff --git a/apps/ios/EdgeEver/Features/Workspace/ListActionsSheet.swift b/apps/ios/EdgeEver/Features/Workspace/ListActionsSheet.swift index a7421e77..aa3a60a4 100644 --- a/apps/ios/EdgeEver/Features/Workspace/ListActionsSheet.swift +++ b/apps/ios/EdgeEver/Features/Workspace/ListActionsSheet.swift @@ -120,6 +120,15 @@ struct ListActionsSheet: View { store.reload(env: env) dismiss() } + optionRow( + active: store.sort == .titleDesc, + icon: nil, + label: env.preferences.t("标题 Z-A", en: "Title Z-A", ja: "タイトル Z-A", pl: "Tytuł Z–A") + ) { + store.sort = .titleDesc + store.reload(env: env) + dismiss() + } divider sheetItem( diff --git a/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift b/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift index 004aa954..40569e4a 100644 --- a/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift +++ b/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift @@ -158,6 +158,7 @@ final class MobileUIParityTests: XCTestCase { XCTAssertEqual(MemoListTimestampField.resolve(for: .createdDesc).value(from: memo), memo.createdAt) XCTAssertEqual(MemoListTimestampField.resolve(for: .updatedDesc).value(from: memo), memo.updatedAt) XCTAssertEqual(MemoListTimestampField.resolve(for: .titleAsc).value(from: memo), memo.updatedAt) + XCTAssertEqual(MemoListTimestampField.resolve(for: .titleDesc).value(from: memo), memo.updatedAt) } func testMemoDetailDateIncludesHistoricalYear() { diff --git a/apps/mobile/src/lib/local-mirror.ts b/apps/mobile/src/lib/local-mirror.ts index 3a662a6a..01f65914 100644 --- a/apps/mobile/src/lib/local-mirror.ts +++ b/apps/mobile/src/lib/local-mirror.ts @@ -136,7 +136,9 @@ export const listLocalMemos = async (scope: string, params: LocalMemoListParams) ? "is_pinned DESC, created_at DESC, id DESC" : params.sort === "title-asc" ? "is_pinned DESC, title COLLATE NOCASE ASC, updated_at DESC, id DESC" - : "is_pinned DESC, updated_at DESC, id DESC"; + : params.sort === "title-desc" + ? "is_pinned DESC, title COLLATE NOCASE DESC, updated_at DESC, id DESC" + : "is_pinned DESC, updated_at DESC, id DESC"; const tag = params.tag?.trim() ?? ""; const limit = params.limit ?? 50; const offset = Math.max(0, params.offset ?? 0); diff --git a/apps/mobile/src/screens/WorkspaceActionSheets.tsx b/apps/mobile/src/screens/WorkspaceActionSheets.tsx index a126e2f8..05f23ac4 100644 --- a/apps/mobile/src/screens/WorkspaceActionSheets.tsx +++ b/apps/mobile/src/screens/WorkspaceActionSheets.tsx @@ -148,6 +148,7 @@ export const NotesActionsModal = ({ onSortModeChange("updated-desc")} /> onSortModeChange("created-desc")} /> onSortModeChange("title-asc")} /> + onSortModeChange("title-desc")} /> diff --git a/apps/mobile/src/screens/workspace-utils.test.ts b/apps/mobile/src/screens/workspace-utils.test.ts index 67288e8d..5940ffc3 100644 --- a/apps/mobile/src/screens/workspace-utils.test.ts +++ b/apps/mobile/src/screens/workspace-utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import type { Notebook } from "@edgeever/shared"; +import type { MemoSummary, Notebook } from "@edgeever/shared"; import { filterCollapsedNotebookOptions, formatMemoDetailDate, @@ -11,6 +11,7 @@ import { isEnglishMobileLocale, markdownToLocalText, parseTags, + sortMemoSummaries, } from "./workspace-utils"; const notebook = (id: string, parentId: string | null, sortOrder: number): Notebook => ({ @@ -91,4 +92,25 @@ describe("mobile workspace utilities", () => { Intl.DateTimeFormat.prototype.resolvedOptions = original; } }); + + test("reverses title order and keeps equal titles in their original order", () => { + const memo = (id: string, title: string): MemoSummary => ({ + id, + notebookId: "nb", + title, + excerpt: "", + tags: [], + isPinned: false, + isArchived: false, + isDeleted: false, + revision: 1, + createdAt: "2026-08-01T00:00:00.000Z", + updatedAt: "2026-08-01T00:00:00.000Z", + deletedAt: null, + }); + const memos = [memo("b", "Alpha"), memo("a", "Alpha"), memo("z", "Zulu")]; + + expect(sortMemoSummaries(memos, "title-asc").map((item) => item.id)).toEqual(["b", "a", "z"]); + expect(sortMemoSummaries(memos, "title-desc").map((item) => item.id)).toEqual(["z", "b", "a"]); + }); }); diff --git a/apps/mobile/src/screens/workspace-utils.ts b/apps/mobile/src/screens/workspace-utils.ts index 303f6d05..59f33e60 100644 --- a/apps/mobile/src/screens/workspace-utils.ts +++ b/apps/mobile/src/screens/workspace-utils.ts @@ -166,8 +166,9 @@ export const formatMemoDetailDate = (value: string, locale: string) => { export const sortMemoSummaries = (memos: MemoSummary[], sortMode: unknown) => [...memos].sort((left, right) => { - if (sortMode === "title-asc") { - return (left.title || DEFAULT_MEMO_TITLE).localeCompare(right.title || DEFAULT_MEMO_TITLE); + if (sortMode === "title-asc" || sortMode === "title-desc") { + const titleCompare = (left.title || DEFAULT_MEMO_TITLE).localeCompare(right.title || DEFAULT_MEMO_TITLE); + return sortMode === "title-desc" ? -titleCompare : titleCompare; } if (sortMode === "created-desc") { return right.createdAt.localeCompare(left.createdAt); diff --git a/apps/web/src/lib/app-helpers.ts b/apps/web/src/lib/app-helpers.ts index 04bdf32b..445c79cb 100644 --- a/apps/web/src/lib/app-helpers.ts +++ b/apps/web/src/lib/app-helpers.ts @@ -169,6 +169,7 @@ export const getMemoSortOptions = (t: TFunction): Array<{ value: MemoSortMode; l { value: "updated-desc", label: t("options.memoSort.updatedDesc") }, { value: "created-desc", label: t("options.memoSort.createdDesc") }, { value: "title-asc", label: t("options.memoSort.titleAsc") }, + { value: "title-desc", label: t("options.memoSort.titleDesc") }, ]; const NOTEBOOK_SORT_VALUES: NotebookSortMode[] = ["custom", "name-asc", "memo-count-desc", "updated-desc"]; @@ -707,14 +708,14 @@ export const sortMemos = (memos: MemoSummary[], sortMode: MemoSortMode, prioriti return first.isPinned ? -1 : 1; } - if (sortMode === "title-asc") { + if (sortMode === "title-asc" || sortMode === "title-desc") { const titleCompare = getMemoTitle(first.title).localeCompare(getMemoTitle(second.title), "zh-CN", { numeric: true, sensitivity: "base", }); if (titleCompare !== 0) { - return titleCompare; + return sortMode === "title-desc" ? -titleCompare : titleCompare; } return compareDateDesc(first.updatedAt, second.updatedAt); diff --git a/apps/web/src/lib/local-mirror.ts b/apps/web/src/lib/local-mirror.ts index 20261fa7..32652b43 100644 --- a/apps/web/src/lib/local-mirror.ts +++ b/apps/web/src/lib/local-mirror.ts @@ -264,7 +264,11 @@ export const listLocalMemos = async (scope: string, params: LocalMemoListParams) memos.sort((left, right) => { if (params.trash) return (right.deletedAt ?? "").localeCompare(left.deletedAt ?? "") || right.id.localeCompare(left.id); if (params.sort === "created-desc") return right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id); - if (params.sort === "title-asc") return (left.title || left.excerpt).localeCompare(right.title || right.excerpt, "zh-CN") || right.updatedAt.localeCompare(left.updatedAt); + if (params.sort === "title-asc" || params.sort === "title-desc") { + const titleCompare = (left.title || left.excerpt).localeCompare(right.title || right.excerpt, "zh-CN"); + if (titleCompare !== 0) return params.sort === "title-desc" ? -titleCompare : titleCompare; + return right.updatedAt.localeCompare(left.updatedAt); + } return Number(right.isPinned) - Number(left.isPinned) || right.updatedAt.localeCompare(left.updatedAt) || right.id.localeCompare(left.id); }); diff --git a/apps/web/src/lib/memo-list-cache.ts b/apps/web/src/lib/memo-list-cache.ts index e80befdf..820b82bf 100644 --- a/apps/web/src/lib/memo-list-cache.ts +++ b/apps/web/src/lib/memo-list-cache.ts @@ -59,11 +59,14 @@ const sortMemoSummariesForList = (memos: MemoSummary[], queryKey: readonly unkno const sortMode = queryKey[5]; const sorted = [...memos]; - if (sortMode === "title-asc") { + if (sortMode === "title-asc" || sortMode === "title-desc") { + const direction = sortMode === "title-desc" ? -1 : 1; return sorted.sort((left, right) => { const leftTitle = left.title?.trim() || left.excerpt || DEFAULT_MEMO_TITLE; const rightTitle = right.title?.trim() || right.excerpt || DEFAULT_MEMO_TITLE; - return leftTitle.localeCompare(rightTitle, "zh-CN") || left.id.localeCompare(right.id); + const titleCompare = leftTitle.localeCompare(rightTitle, "zh-CN"); + if (titleCompare !== 0) return titleCompare * direction; + return left.id.localeCompare(right.id); }); } diff --git a/crates/desktop-sidecar/src/memo.rs b/crates/desktop-sidecar/src/memo.rs index d76c5972..1234bb62 100644 --- a/crates/desktop-sidecar/src/memo.rs +++ b/crates/desktop-sidecar/src/memo.rs @@ -436,6 +436,9 @@ pub(crate) fn list_memos(database: &Connection, params: &Value) -> Result { "COALESCE(m.title, m.excerpt) COLLATE NOCASE ASC, m.updated_at DESC, m.id DESC" } + Some("title-desc") => { + "COALESCE(m.title, m.excerpt) COLLATE NOCASE DESC, m.updated_at DESC, m.id DESC" + } _ if trash => "m.deleted_at DESC, m.id DESC", _ => "m.is_pinned DESC, m.updated_at DESC, m.id DESC", }; diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 0ade8c7a..b4ae88ca 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -176,7 +176,7 @@ const result = await context.notes.query({ notebookId: "optional-notebook-id", text: "keyword", tags: ["todo"], - sort: "updated-desc", // "updated-desc" | "created-desc" | "title-asc" + sort: "updated-desc", // "updated-desc" | "created-desc" | "title-asc" | "title-desc" limit: 50, offset: 0 }); diff --git a/docs/plugin-development.zh-CN.md b/docs/plugin-development.zh-CN.md index 8397bbe5..fffe341a 100644 --- a/docs/plugin-development.zh-CN.md +++ b/docs/plugin-development.zh-CN.md @@ -176,7 +176,7 @@ const result = await context.notes.query({ notebookId: "optional-notebook-id", text: "关键词", tags: ["todo"], - sort: "updated-desc", // "updated-desc" | "created-desc" | "title-asc" + sort: "updated-desc", // "updated-desc" | "created-desc" | "title-asc" | "title-desc" limit: 50, offset: 0 }); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index e2694572..a9174de7 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -131,7 +131,7 @@ export type InstanceRelease = { }; export type MemoFilterMode = "all" | "tagged" | "untagged" | "pinned"; -export type MemoSortMode = "updated-desc" | "created-desc" | "title-asc"; +export type MemoSortMode = "updated-desc" | "created-desc" | "title-asc" | "title-desc"; export type ListNotebooksResponse = { notebooks: Notebook[]; diff --git a/packages/plugin-api/dist/index.d.ts b/packages/plugin-api/dist/index.d.ts index 9789831a..163aee19 100644 --- a/packages/plugin-api/dist/index.d.ts +++ b/packages/plugin-api/dist/index.d.ts @@ -199,7 +199,7 @@ export interface PluginNoteQuery { notebookId?: string; text?: string; tags?: string[]; - sort?: "updated-desc" | "created-desc" | "title-asc"; + sort?: "updated-desc" | "created-desc" | "title-asc" | "title-desc"; limit?: number; offset?: number; } diff --git a/packages/plugin-api/src/index.ts b/packages/plugin-api/src/index.ts index 800209b6..4bcb1a5e 100644 --- a/packages/plugin-api/src/index.ts +++ b/packages/plugin-api/src/index.ts @@ -239,7 +239,7 @@ export interface PluginNoteQuery { notebookId?: string; text?: string; tags?: string[]; - sort?: "updated-desc" | "created-desc" | "title-asc"; + sort?: "updated-desc" | "created-desc" | "title-asc" | "title-desc"; limit?: number; offset?: number; } diff --git a/packages/shared/src/desktop-rpc.ts b/packages/shared/src/desktop-rpc.ts index 02bc2c43..a62f444e 100644 --- a/packages/shared/src/desktop-rpc.ts +++ b/packages/shared/src/desktop-rpc.ts @@ -66,7 +66,7 @@ export type DesktopMemoListParams = { q?: string; tag?: string; trash?: boolean; - sort?: "updated-desc" | "created-desc" | "title-asc"; + sort?: "updated-desc" | "created-desc" | "title-asc" | "title-desc"; filter?: "all" | "pinned" | "tagged" | "untagged"; limit?: number; offset?: number; diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 64dd7de4..aeacb756 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -2083,6 +2083,7 @@ export const enUS = { updatedDesc: "Recently updated", createdDesc: "Created time", titleAsc: "Title A-Z", + titleDesc: "Title Z-A", }, notebookSort: { custom: "Custom", diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index 9893da29..3006bbd1 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -2072,6 +2072,7 @@ export const ja = { updatedDesc: "最近の更新", createdDesc: "作成日時", titleAsc: "タイトル A-Z", + titleDesc: "タイトル Z-A", }, notebookSort: { custom: "カスタム", diff --git a/packages/shared/src/i18n/pl.ts b/packages/shared/src/i18n/pl.ts index af00e00f..fa49f909 100644 --- a/packages/shared/src/i18n/pl.ts +++ b/packages/shared/src/i18n/pl.ts @@ -2072,6 +2072,7 @@ export const pl = { updatedDesc: "Ostatnio zaktualizowane", createdDesc: "Data utworzenia", titleAsc: "Tytuł A–Z", + titleDesc: "Tytuł Z–A", }, notebookSort: { custom: "Własna", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 51bd947d..9058b965 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -2083,6 +2083,7 @@ export const zhCN = { updatedDesc: "最近更新", createdDesc: "创建时间", titleAsc: "标题 A-Z", + titleDesc: "标题 Z-A", }, notebookSort: { custom: "自定义", diff --git a/packages/shared/src/memo-timestamps.test.mjs b/packages/shared/src/memo-timestamps.test.mjs index 6eb75bb4..3dc0ede9 100644 --- a/packages/shared/src/memo-timestamps.test.mjs +++ b/packages/shared/src/memo-timestamps.test.mjs @@ -24,5 +24,9 @@ describe("memo list timestamps", () => { field: "updatedAt", value: timestamps.updatedAt, }); + expect(getMemoListTimestamp(timestamps, "title-desc")).toEqual({ + field: "updatedAt", + value: timestamps.updatedAt, + }); }); }); diff --git a/tests/web-memo-list-cache.test.ts b/tests/web-memo-list-cache.test.ts index 174040ed..2b27c6a6 100644 --- a/tests/web-memo-list-cache.test.ts +++ b/tests/web-memo-list-cache.test.ts @@ -78,6 +78,21 @@ describe("memo list cache updates", () => { expect(cached?.pages[0]?.totalCount).toBe(1); }); + test("reorders a cached list from Z to A without reversing the id tie break", () => { + const queryClient = new QueryClient(); + const queryKey = ["memos", "notebook", null, "", "all", "title-desc"] as const; + queryClient.setQueryData(queryKey, queryData([[ + memo({ id: "memo-b", title: "Alpha" }), + memo({ id: "memo-a", title: "Alpha" }), + memo({ id: "memo-z", title: "Zulu" }), + ]])); + + updateMemoSummaryInLists(queryClient, memo({ id: "memo-z", title: "Zulu", revision: 2 })); + + const titles = queryClient.getQueryData(queryKey)?.pages[0]?.memos.map((item) => item.id); + expect(titles).toEqual(["memo-z", "memo-a", "memo-b"]); + }); + test("removes a note when an edit no longer matches the selected exact tag", () => { const queryClient = new QueryClient(); const queryKey = ["memos", "notebook", null, "", "all", "updated-desc", [], "demo"] as const;