From 51267d357a3a8aa7c4e1232b7d4532ed01f41117 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Mon, 17 Aug 2026 01:05:27 +0800 Subject: [PATCH] feat: open notes from tags --- apps/api/src/memo-list-service.test.mjs | 16 ++++++++ apps/api/src/memo-list-service.ts | 9 +++++ apps/api/src/memo-routes.test.mjs | 3 +- apps/api/src/memo-routes.ts | 1 + apps/web/src/components/MemoListPane.tsx | 21 ++++++---- apps/web/src/components/TagsPane.tsx | 26 ++++++++++--- apps/web/src/components/WorkspaceApp.tsx | 49 +++++++++++++++++++----- apps/web/src/lib/api.ts | 5 +++ apps/web/src/lib/local-mirror.test.mjs | 12 ++++++ apps/web/src/lib/local-mirror.ts | 3 ++ apps/web/src/lib/memo-list-cache.ts | 5 +++ apps/web/src/lib/repository.ts | 1 + packages/client/src/index.test.mjs | 14 +++++++ packages/client/src/index.ts | 5 +++ packages/shared/src/i18n/en-US.ts | 1 + packages/shared/src/i18n/zh-CN.ts | 1 + tests/web-memo-list-cache.test.ts | 12 ++++++ 17 files changed, 159 insertions(+), 25 deletions(-) diff --git a/apps/api/src/memo-list-service.test.mjs b/apps/api/src/memo-list-service.test.mjs index 13ec97f6..d768f856 100644 --- a/apps/api/src/memo-list-service.test.mjs +++ b/apps/api/src/memo-list-service.test.mjs @@ -83,6 +83,22 @@ describe("memo list service", () => { } }); + test("filters by one exact tag in page and count queries", async () => { + const database = createDatabase(); + await listMemos(database, { + workspaceId: "ws_1", + tag: "Demo", + }); + + expect(database.calls).toHaveLength(2); + for (const call of database.calls) { + expect(call.sql).toContain("json_each(m.tags_json)"); + expect(call.sql).toContain("LOWER(CAST(memo_tag.value AS TEXT)) = LOWER(?)"); + expect(call.parameters).toContain("Demo"); + expect(call.sql).not.toContain("m.tags_json LIKE"); + } + }); + test("combines FTS and escaped LIKE search for normal text", 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 1b33a2c3..0d9a7830 100644 --- a/apps/api/src/memo-list-service.ts +++ b/apps/api/src/memo-list-service.ts @@ -43,6 +43,7 @@ export type ListMemosInput = { notebookId?: string; includeNotebookDescendants?: boolean; query?: string; + tag?: string; includeTrash?: boolean; sort?: string; filter?: string; @@ -150,6 +151,7 @@ export const listMemos = async ( ): Promise => { const notebookId = input.notebookId; const query = input.query?.trim(); + const tag = input.tag?.trim(); const includeTrash = input.includeTrash === true; const sort = normalizeMemoListSort(input.sort); const filter = normalizeMemoListFilter(input.filter); @@ -186,6 +188,13 @@ export const listMemos = async ( } } + if (tag) { + baseConditions.push( + "EXISTS (SELECT 1 FROM json_each(m.tags_json) AS memo_tag WHERE LOWER(CAST(memo_tag.value AS TEXT)) = LOWER(?))", + ); + baseBinds.push(tag); + } + if (filter === "tagged") baseConditions.push("m.tags_json <> '[]'"); else if (filter === "untagged") baseConditions.push("m.tags_json = '[]'"); else if (filter === "pinned") baseConditions.push("m.is_pinned = 1"); diff --git a/apps/api/src/memo-routes.test.mjs b/apps/api/src/memo-routes.test.mjs index d473f7dc..61dc3505 100644 --- a/apps/api/src/memo-routes.test.mjs +++ b/apps/api/src/memo-routes.test.mjs @@ -59,7 +59,7 @@ describe("memo route contracts", () => { }, }); const response = await createApp(dependencies).request( - "/api/v1/memos?notebookId=nb_1&includeDescendants=1&q=hello&trash=1&sort=title-asc&filter=pinned&limit=25&cursor=opaque", + "/api/v1/memos?notebookId=nb_1&includeDescendants=1&q=hello&tag=Demo&trash=1&sort=title-asc&filter=pinned&limit=25&cursor=opaque", {}, environment, ); @@ -70,6 +70,7 @@ describe("memo route contracts", () => { notebookId: "nb_1", includeNotebookDescendants: true, query: "hello", + tag: "Demo", includeTrash: true, sort: "title-asc", filter: "pinned", diff --git a/apps/api/src/memo-routes.ts b/apps/api/src/memo-routes.ts index 9c9f5a3d..b98760c3 100644 --- a/apps/api/src/memo-routes.ts +++ b/apps/api/src/memo-routes.ts @@ -126,6 +126,7 @@ export const registerMemoRoutes = ( notebookId: context.req.query("notebookId"), includeNotebookDescendants: context.req.query("includeDescendants") === "1", query: context.req.query("q"), + tag: context.req.query("tag"), includeTrash: context.req.query("trash") === "1", sort: context.req.query("sort"), filter: context.req.query("filter"), diff --git a/apps/web/src/components/MemoListPane.tsx b/apps/web/src/components/MemoListPane.tsx index bd3d8557..4065570d 100644 --- a/apps/web/src/components/MemoListPane.tsx +++ b/apps/web/src/components/MemoListPane.tsx @@ -331,6 +331,7 @@ const CheckCircleCheck = ({ className }: { className?: string }) => ( export const MemoListPane = ({ notebooks, notebook, + selectedTag, memos, totalMemoCount, hasMoreMemos, @@ -375,6 +376,7 @@ export const MemoListPane = ({ onOpenAssets, onOpenTrash, onBackFromTrash, + onClearTag, onOpenSettings, onSyncMemos, onCreateMemo, @@ -404,6 +406,7 @@ export const MemoListPane = ({ multiSelectKeyDown: boolean; notebooks: Notebook[]; notebook: Notebook | null; + selectedTag: string | null; memos: MemoSummary[]; totalMemoCount: number; hasMoreMemos: boolean; @@ -448,6 +451,7 @@ export const MemoListPane = ({ onOpenAssets: () => void; onOpenTrash: () => void; onBackFromTrash: () => void; + onClearTag: () => void; onOpenSettings: () => void; onSyncMemos: () => void; onCreateMemo: () => void; @@ -496,8 +500,8 @@ export const MemoListPane = ({ const canToggleVisibleMemoSelection = visibleMemoIds.length > 0; const visibleSelectionToggleLabel = allVisibleMemosSelected ? t("memoList.selectedListNone") : t("memoList.selectedListAll"); - const listTitle = view === "trash" ? t("memoList.trash") : notebook?.name ?? t("memoList.allMemos"); - const listContextLabel = view === "trash" ? t("memoList.deletedMemos") : notebook ? t("memoList.currentNotebook") : t("memoList.allNotebooks"); + const listTitle = view === "trash" ? t("memoList.trash") : selectedTag ? `#${selectedTag}` : notebook?.name ?? t("memoList.allMemos"); + const listContextLabel = view === "trash" ? t("memoList.deletedMemos") : selectedTag ? t("memoList.tagFilter") : notebook ? t("memoList.currentNotebook") : t("memoList.allNotebooks"); const visibleCount = `${memos.length}${memos.length !== totalMemoCount ? ` / ${totalMemoCount}` : ""}`; const listCountLabel = view === "trash" ? t("memoList.deletedCount", { count: visibleCount }) @@ -533,7 +537,7 @@ export const MemoListPane = ({ const moveTargetTitle = view === "trash" ? t("workspace.selection.trashCannotMove") : notebooks.length === 0 ? t("workspace.selection.noMovableNotebook") : isMoving ? t("workspace.selection.moving") : t("memoList.moveToNotebook"); const searchActive = Boolean(search.trim()); - const hasListConstraint = searchActive || filterMode !== "all"; + const hasListConstraint = searchActive || filterMode !== "all" || Boolean(selectedTag); const activeFilterLabel = filterOptions.find((option) => option.value === filterMode)?.label ?? t("options.memoFilter.all"); const activeSortLabel = memoSortOptions.find((option) => option.value === sortMode)?.label ?? t("options.memoSort.updatedDesc"); const syncMemosTitle = !canSyncMemos @@ -852,6 +856,7 @@ export const MemoListPane = ({ onClearSelection(); onFilterModeChange("all"); onSearch(""); + if (selectedTag) onClearTag(); focusSearchInput(); }; @@ -1031,13 +1036,13 @@ export const MemoListPane = ({ {!mobileSearchActive && (
- {view === "trash" && ( + {(view === "trash" || selectedTag) && ( @@ -1073,14 +1078,14 @@ export const MemoListPane = ({ )}
- {view === "trash" && ( + {(view === "trash" || selectedTag) && ( @@ -1344,7 +1349,7 @@ export const MemoListPane = ({ {searchActive ? t("memoList.searchResults", { count: totalMemoCount }) : t("memoList.constrainedCount", { - label: t("memoList.filterConstraint", { label: activeFilterLabel }), + label: selectedTag ? `#${selectedTag}` : t("memoList.filterConstraint", { label: activeFilterLabel }), count: totalMemoCount, })} diff --git a/apps/web/src/components/TagsPane.tsx b/apps/web/src/components/TagsPane.tsx index b53d77e2..82a88d5a 100644 --- a/apps/web/src/components/TagsPane.tsx +++ b/apps/web/src/components/TagsPane.tsx @@ -10,7 +10,15 @@ import type { TagSummary } from "@edgeever/shared"; import { AppConfirmDialog } from "./dialogs/ConfirmDialogs"; import type { EdgeEverRepository } from "@/lib/repository"; -export const TagsPane = ({ onClose, repository }: { onClose: () => void; repository: EdgeEverRepository }) => { +export const TagsPane = ({ + onClose, + onSelectTag, + repository, +}: { + onClose: () => void; + onSelectTag: (tag: string) => void; + repository: EdgeEverRepository; +}) => { const { t } = useTranslation(); const queryClient = useQueryClient(); const [editingTagName, setEditingTagName] = useState(null); @@ -66,8 +74,8 @@ export const TagsPane = ({ onClose, repository }: { onClose: () => void; reposit const nextName = editingTagValue.trim(); return (
- - {isEditing ? ( + {isEditing ? ( +
{ event.preventDefault(); if (nextName && nextName !== tag.name && !renameMutation.isPending) renameMutation.mutate({ tag: tag.name, name: nextName }); }}> setEditingTagValue(event.target.value)} onKeyDown={(event) => { if (event.key === "Escape") { event.preventDefault(); cancelRename(); } }} /> @@ -76,10 +84,16 @@ export const TagsPane = ({ onClose, repository }: { onClose: () => void; reposit
- ) : ( +
+ ) : ( + + )} {!isEditing && <>}
); diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx index 7ac15fe8..0302d7e0 100644 --- a/apps/web/src/components/WorkspaceApp.tsx +++ b/apps/web/src/components/WorkspaceApp.tsx @@ -700,6 +700,7 @@ export const WorkspaceApp = ({ setSelectedNotebookId, setSelectionMoveTargetNotebookId, } = useWorkspaceSelection(); + const [selectedTag, setSelectedTag] = useState(null); const autoSelectedDemoNotebookRef = useRef(false); const [createdMemoEditId, setCreatedMemoEditId] = useState(null); const pendingCreatedMemoIdRef = useRef(null); @@ -921,11 +922,11 @@ export const WorkspaceApp = ({ return; } - if (!autoSelectedDemoNotebookRef.current && selectedNotebookId === null) { + if (!autoSelectedDemoNotebookRef.current && selectedNotebookId === null && selectedTag === null) { autoSelectedDemoNotebookRef.current = true; setSelectedNotebookId(preferredNotebookId); } - }, [i18n.resolvedLanguage, notebooks, selectedNotebookId]); + }, [i18n.resolvedLanguage, notebooks, selectedNotebookId, selectedTag]); const mobileEditorReturnMemoId = route.mobileEditorReturnMemoId; const visibleActivePane: Pane = mobileEditorReturnMemoId ? "memos" : activePane; @@ -1284,15 +1285,16 @@ export const WorkspaceApp = ({ }); const selectedNotebookDescendantIds = useMemo( - () => (selectedNotebookId ? getNotebookDescendantIds(notebooks, selectedNotebookId) : []), - [notebooks, selectedNotebookId] + () => (selectedNotebookId && !selectedTag ? getNotebookDescendantIds(notebooks, selectedNotebookId) : []), + [notebooks, selectedNotebookId, selectedTag] ); const memosQuery = useInfiniteQuery({ - queryKey: ["memos", memoView, selectedNotebookId, search, memoFilterMode, memoSortMode, selectedNotebookDescendantIds], + queryKey: ["memos", memoView, selectedNotebookId, search, memoFilterMode, memoSortMode, selectedNotebookDescendantIds, selectedTag], queryFn: ({ pageParam }) => repository.listMemos({ - notebookId: memoView === "notebook" ? selectedNotebookId : null, - notebookIds: memoView === "notebook" ? selectedNotebookDescendantIds : undefined, + notebookId: memoView === "notebook" && !selectedTag ? selectedNotebookId : null, + notebookIds: memoView === "notebook" && !selectedTag ? selectedNotebookDescendantIds : undefined, q: search, + tag: memoView === "notebook" ? selectedTag ?? undefined : undefined, trash: memoView === "trash", filter: memoFilterMode, sort: memoSortMode, @@ -1402,6 +1404,7 @@ export const WorkspaceApp = ({ onSuccess: async (data) => { await putLocalNotebook(localDataScope, data.notebook); await queryClient.invalidateQueries({ queryKey: ["notebooks"] }); + setSelectedTag(null); setSelectedNotebookId(data.notebook.id); setActivePane("memos"); }, @@ -1450,6 +1453,7 @@ export const WorkspaceApp = ({ setMemoView("notebook"); setSearch(""); + setSelectedTag(null); // A newly created memo is not pinned or otherwise guaranteed to match // the active list filter. Leave filtered views so the selected memo // remains visible instead of the list effect falling back to its first @@ -1515,6 +1519,7 @@ export const WorkspaceApp = ({ setTemplatesOpen(false); setMemoView("notebook"); setSearch(""); + setSelectedTag(null); setSelectedNotebookId(targetNotebookId); cacheMemoDetail(queryClient, data.memo, "notebook"); updateMemoSummaryInLists(queryClient, memoToSummary(data.memo)); @@ -1792,6 +1797,7 @@ export const WorkspaceApp = ({ mutationFn: repository.restoreMemo, onSuccess: (data) => { setMemoView("notebook"); + setSelectedTag(null); cacheMemoDetail(queryClient, data.memo, "notebook"); void Promise.all([ queryClient.invalidateQueries({ queryKey: ["memos"] }), @@ -2150,6 +2156,7 @@ export const WorkspaceApp = ({ const handleSelectNotebook = (notebookId: string) => { navigateWorkspaceHome(); setMemoView("notebook"); + setSelectedTag(null); setSelectedNotebookId(notebookId); setMobileBottomNavActive("home"); clearMemoSelection(); @@ -2162,6 +2169,7 @@ export const WorkspaceApp = ({ const handleSelectAllMemos = () => { navigateWorkspaceHome(); setMemoView("notebook"); + setSelectedTag(null); setSelectedNotebookId(null); setRightView("editor"); setMobileBottomNavActive("home"); @@ -2178,6 +2186,7 @@ export const WorkspaceApp = ({ setMemoView("notebook"); } setMobileBottomNavActive("home"); + setSelectedTag(null); setSelectedNotebookId(null); setSearch(""); clearMemoSelection(); @@ -2186,6 +2195,20 @@ export const WorkspaceApp = ({ setActivePane("memos"); }; + const handleSelectTag = (tag: string) => { + navigateWorkspaceHome(); + setMemoView("notebook"); + setSelectedTag(tag); + setSelectedNotebookId(null); + setRightView("editor"); + setMobileBottomNavActive("home"); + clearMemoSelection(); + clearPendingCreatedMemo(); + setCreatedMemoEditId(null); + setSelectedMemoId(null); + setActivePane("memos"); + }; + const handleMobileSearch = () => { setMobileBottomNavActive("search"); setActivePane("memos"); @@ -2734,6 +2757,7 @@ export const WorkspaceApp = ({ onSelect={(notebookId) => { navigateWorkspaceHome(); setMemoView("notebook"); + setSelectedTag(null); setSelectedNotebookId(notebookId); clearMemoSelection(); setRightView("editor"); @@ -2764,6 +2788,7 @@ export const WorkspaceApp = ({ onOpenTrash={() => { navigateWorkspaceTrash(); setMemoView("trash"); + setSelectedTag(null); setSelectedNotebookId(null); setMobileBottomNavActive("home"); clearMemoSelection(); @@ -2790,7 +2815,8 @@ export const WorkspaceApp = ({ )} > { navigateWorkspaceTrash(); setMemoView("trash"); + setSelectedTag(null); setSelectedNotebookId(null); setMobileBottomNavActive("home"); clearMemoSelection(); @@ -2844,6 +2871,7 @@ export const WorkspaceApp = ({ setActivePane("memos"); }} onBackFromTrash={handleSelectAllMemos} + onClearTag={handleSelectAllMemos} onOpenMemo={(memoId) => { if (shouldNavigateHomeWhenOpeningMemo(memoView)) { navigateWorkspaceHome(); @@ -2954,7 +2982,7 @@ export const WorkspaceApp = ({ ) : rightView === "assets" ? ( ) : rightView === "tags" ? ( - + ) : rightView === "templates" ? ( setMobileNotebookPickerOpen(false)} diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 2b37baf5..c95ccfa1 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -760,6 +760,7 @@ export const api = { notebookId?: string | null; includeDescendants?: boolean; q?: string; + tag?: string; trash?: boolean; sort?: MemoSortMode; filter?: MemoFilterMode; @@ -780,6 +781,10 @@ export const api = { search.set("q", params.q.trim()); } + if (params.tag?.trim()) { + search.set("tag", params.tag.trim()); + } + if (params.trash) { search.set("trash", "1"); } diff --git a/apps/web/src/lib/local-mirror.test.mjs b/apps/web/src/lib/local-mirror.test.mjs index 3cee4dc0..1548f78d 100644 --- a/apps/web/src/lib/local-mirror.test.mjs +++ b/apps/web/src/lib/local-mirror.test.mjs @@ -94,6 +94,18 @@ describe("local mirror", () => { expect(result.memos[0]?.excerpt).toContain("Hello local first"); }); + test("filters local memos by an exact tag across notebooks", async () => { + const scope = createLocalDataScope("https://demo.edgeever.org", "user-1"); + const expected = await createLocalMemo(scope, { notebookId: "nb-a", tags: ["Demo"] }); + await createLocalMemo(scope, { notebookId: "nb-b", tags: ["demo-extra"] }); + await createLocalMemo(scope, { notebookId: "nb-b", title: "Demo without tag" }); + + const result = await listLocalMemos(scope, { tag: "demo" }); + + expect(result.totalCount).toBe(1); + expect(result.memos.map((memo) => memo.id)).toEqual([expected.id]); + }); + test("updates local content and preserves the memo identity", async () => { const scope = createLocalDataScope("https://demo.edgeever.org", "user-1"); const memo = await createLocalMemo(scope, { notebookId: "nb-inbox" }); diff --git a/apps/web/src/lib/local-mirror.ts b/apps/web/src/lib/local-mirror.ts index dce115cc..e90c8acd 100644 --- a/apps/web/src/lib/local-mirror.ts +++ b/apps/web/src/lib/local-mirror.ts @@ -10,6 +10,7 @@ export type LocalMemoListParams = { notebookId?: string | null; notebookIds?: string[]; q?: string; + tag?: string; trash?: boolean; sort?: MemoSortMode; filter?: MemoFilterMode; @@ -203,10 +204,12 @@ export const listLocalMemos = async (scope: string, params: LocalMemoListParams) let memos = await localDb.memos.where("scope").equals(scope).toArray(); const notebookIds = params.notebookIds ?? (params.notebookId ? [params.notebookId] : null); const q = params.q?.trim().toLocaleLowerCase(); + const tag = params.tag?.trim().toLocaleLowerCase(); memos = memos.filter((memo) => { if (memo.isDeleted !== Boolean(params.trash)) return false; if (notebookIds?.length && !notebookIds.includes(memo.notebookId)) return false; + if (tag && !memo.tags.some((memoTag) => memoTag.toLocaleLowerCase() === tag)) return false; if (params.filter === "tagged" && memo.tags.length === 0) return false; if (params.filter === "untagged" && memo.tags.length > 0) return false; if (params.filter === "pinned" && !memo.isPinned) return false; diff --git a/apps/web/src/lib/memo-list-cache.ts b/apps/web/src/lib/memo-list-cache.ts index f21c71cb..ed49d1d4 100644 --- a/apps/web/src/lib/memo-list-cache.ts +++ b/apps/web/src/lib/memo-list-cache.ts @@ -29,6 +29,7 @@ const memoMatchesFilter = (memo: MemoSummary, filterMode: unknown) => { const memoMatchesStaticListConstraints = (memo: MemoSummary, queryKey: readonly unknown[]) => { const [, view, notebookId, , filterMode] = queryKey; const notebookIds = Array.isArray(queryKey[6]) ? queryKey[6] : []; + const tag = typeof queryKey[7] === "string" ? queryKey[7].trim().toLocaleLowerCase() : ""; const memoView = view === "trash" ? "trash" : "notebook"; if ((memoView === "trash") !== memo.isDeleted) { @@ -44,6 +45,10 @@ const memoMatchesStaticListConstraints = (memo: MemoSummary, queryKey: readonly return false; } + if (tag && !memo.tags.some((memoTag) => memoTag.toLocaleLowerCase() === tag)) { + return false; + } + return memoMatchesFilter(memo, filterMode); }; diff --git a/apps/web/src/lib/repository.ts b/apps/web/src/lib/repository.ts index 86287384..54055406 100644 --- a/apps/web/src/lib/repository.ts +++ b/apps/web/src/lib/repository.ts @@ -388,6 +388,7 @@ export const createWebRepository = (scope: string): EdgeEverRepository => { notebookId: params.notebookId, includeDescendants: Boolean(params.notebookIds?.length), q: params.q, + tag: params.tag, trash: params.trash, filter: params.filter, sort: params.sort, diff --git a/packages/client/src/index.test.mjs b/packages/client/src/index.test.mjs index c31cf91d..232c44a8 100644 --- a/packages/client/src/index.test.mjs +++ b/packages/client/src/index.test.mjs @@ -79,6 +79,20 @@ describe("EdgeEver client HTTP contract", () => { expect(result.suggestions).toEqual([{ name: "existing", existing: true }]); }); + test("sends an exact tag constraint when listing memos", async () => { + let requestUrl; + const client = createEdgeEverClient({ + fetch: async (input) => { + requestUrl = String(input); + return jsonResponse({ memos: [], totalCount: 0, nextCursor: null }); + }, + }); + + await client.listMemos({ tag: "产品 和 交互" }); + + expect(requestUrl).toBe("/api/v1/memos?tag=%E4%BA%A7%E5%93%81+%E5%92%8C+%E4%BA%A4%E4%BA%92"); + }); + test("updates the workspace AI tag suggestion prompt", async () => { let call; const client = createEdgeEverClient({ diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 743bfeba..743c9147 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -471,6 +471,7 @@ export const createEdgeEverClient = (options: EdgeEverClientOptions = {}) => { notebookId?: string | null; includeDescendants?: boolean; q?: string; + tag?: string; trash?: boolean; sort?: MemoSortMode; filter?: MemoFilterMode; @@ -491,6 +492,10 @@ export const createEdgeEverClient = (options: EdgeEverClientOptions = {}) => { search.set("q", params.q.trim()); } + if (params.tag?.trim()) { + search.set("tag", params.tag.trim()); + } + if (params.trash) { search.set("trash", "1"); } diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index c63af44f..9e8c7351 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -758,6 +758,7 @@ export const enUS = { currentNotebook: "Current notebook", allNotebooks: "All notebooks", deletedMemos: "Deleted notes", + tagFilter: "Tag filter", allMemos: "All notes", memoCount: "{{count}} notes", deletedCount: "{{count}} deleted", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index a7757f39..02d3e9c0 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -758,6 +758,7 @@ export const zhCN = { currentNotebook: "当前笔记本", allNotebooks: "所有笔记本", deletedMemos: "已删除笔记", + tagFilter: "标签筛选", allMemos: "全部笔记", memoCount: "{{count}} 条笔记", deletedCount: "{{count}} 条已删除", diff --git a/tests/web-memo-list-cache.test.ts b/tests/web-memo-list-cache.test.ts index 2790d9c7..b923e389 100644 --- a/tests/web-memo-list-cache.test.ts +++ b/tests/web-memo-list-cache.test.ts @@ -61,4 +61,16 @@ describe("memo list cache updates", () => { expect(flattened[0]?.revision).toBe(3); expect(cached?.pages[0]?.totalCount).toBe(1); }); + + 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; + queryClient.setQueryData(queryKey, queryData([[memo({ tags: ["Demo"] })]], 1)); + + updateMemoSummaryInLists(queryClient, memo({ tags: ["demo-extra"], revision: 2 })); + + const cached = queryClient.getQueryData(queryKey); + expect(cached?.pages[0]?.memos).toEqual([]); + expect(cached?.pages[0]?.totalCount).toBe(0); + }); });