feat: open notes from tags

This commit is contained in:
tianma-if committed 2026-08-17 01:05:27 +08:00
1 parent 9156aed2d6
commit 51267d357a
17 files changed
+159 -25

No files matched your search

+16
View File
@@ -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, {
+9
View File
@@ -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<ListMemosResult> => {
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");
+2 -1
View File
@@ -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",
+1
View File
@@ -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"),
+13 -8
View File
@@ -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 && (
<div className="mb-3 flex items-center justify-between gap-3 lg:hidden">
<div className="flex min-w-0 items-center gap-2">
{view === "trash" && (
{(view === "trash" || selectedTag) && (
<button
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-slate-600 transition hover:bg-slate-100 hover:text-slate-900"
type="button"
title={t("notebookPane.backToList")}
aria-label={t("notebookPane.backToList")}
onClick={onBackFromTrash}
onClick={view === "trash" ? onBackFromTrash : onClearTag}
>
<ChevronLeft className="h-4 w-4" />
</button>
@@ -1073,14 +1078,14 @@ export const MemoListPane = ({
)}
<div className="mb-3 hidden min-w-0 lg:flex items-start gap-1">
{view === "trash" && (
{(view === "trash" || selectedTag) && (
<Button
className="-ml-2 mt-0.5 shrink-0"
size="icon"
variant="ghost"
title={t("notebookPane.backToList")}
aria-label={t("notebookPane.backToList")}
onClick={onBackFromTrash}
onClick={view === "trash" ? onBackFromTrash : onClearTag}
>
<ChevronLeft className="h-4 w-4" />
</Button>
@@ -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,
})}
</span>
+20 -6
View File
@@ -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<string | null>(null);
@@ -66,8 +74,8 @@ export const TagsPane = ({ onClose, repository }: { onClose: () => void; reposit
const nextName = editingTagValue.trim();
return (
<div key={tag.name} className={cn("flex min-h-12 items-center gap-3 rounded-md border border-slate-200 bg-white px-3 py-2", isEditing && "border-emerald-200 bg-emerald-50/30")}>
<span className="min-w-0 flex-1">
{isEditing ? (
{isEditing ? (
<div className="min-w-0 flex-1">
<form className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center" onSubmit={(event) => { event.preventDefault(); if (nextName && nextName !== tag.name && !renameMutation.isPending) renameMutation.mutate({ tag: tag.name, name: nextName }); }}>
<label className="sr-only" htmlFor={`tag-rename-${tag.name}`}>{t("tagsDialog.nameLabel")}</label>
<Input id={`tag-rename-${tag.name}`} className="h-9 min-w-0 flex-1 focus-visible:border-emerald-300 focus-visible:ring-emerald-500/20" value={editingTagValue} autoFocus disabled={renameMutation.isPending} maxLength={80} onChange={(event) => 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
<Button size="sm" type="button" variant="outline" onClick={cancelRename} disabled={renameMutation.isPending}>{t("common.cancel")}</Button>
</div>
</form>
) : (
</div>
) : (
<button
className="min-w-0 flex-1 rounded-md px-1 py-1 text-left transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/30"
type="button"
onClick={() => onSelectTag(tag.name)}
>
<><span className="block truncate text-sm font-semibold text-slate-950">#{tag.name}</span><span className="mt-1 block text-xs text-slate-500">{t("tagsDialog.memoCount", { count: tag.memoCount })}{tag.updatedAt ? ` · ${formatDateTime(tag.updatedAt)}` : ""}</span></>
)}
</span>
</button>
)}
{!isEditing && <><Button size="icon" variant="ghost" title={t("tagsDialog.renameTitle")} aria-label={t("tagsDialog.renameAria", { name: tag.name })} onClick={() => { setEditingTagName(tag.name); setEditingTagValue(tag.name); }}><Pencil className="h-4 w-4" /></Button><Button size="icon" variant="danger" title={t("tagsDialog.deleteTitle")} aria-label={t("tagsDialog.deleteAria", { name: tag.name })} onClick={() => setTagDeleteConfirmation(tag)}><Trash2 className="h-4 w-4" /></Button></>}
</div>
);
+39 -10
View File
@@ -700,6 +700,7 @@ export const WorkspaceApp = ({
setSelectedNotebookId,
setSelectionMoveTargetNotebookId,
} = useWorkspaceSelection();
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const autoSelectedDemoNotebookRef = useRef(false);
const [createdMemoEditId, setCreatedMemoEditId] = useState<string | null>(null);
const pendingCreatedMemoIdRef = useRef<string | null>(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 = ({
)}
>
<MemoListPane
notebook={selectedNotebook}
notebook={selectedTag ? null : selectedNotebook}
selectedTag={selectedTag}
notebooks={notebooks}
view={memoView}
memos={memos}
@@ -2835,6 +2861,7 @@ export const WorkspaceApp = ({
onOpenTrash={() => {
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" ? (
<AssetsPane onClose={handleCloseAssets} repository={repository} />
) : rightView === "tags" ? (
<TagsPane onClose={handleCloseAssets} repository={repository} />
<TagsPane onClose={handleCloseAssets} onSelectTag={handleSelectTag} repository={repository} />
) : rightView === "templates" ? (
<TemplatesPane
canCreateMemo={canCreateMemo}
@@ -3040,6 +3068,7 @@ export const WorkspaceApp = ({
memoFilterMode,
memoSortMode,
selectedNotebookDescendantIds,
selectedTag,
],
exact: true,
refetchType: "active",
@@ -3149,7 +3178,7 @@ export const WorkspaceApp = ({
)}
{mobileNotebookPickerOpen && (
<MobileNotebookPicker
currentLabel={memoView === "trash" ? t("notebookPane.trash") : undefined}
currentLabel={memoView === "trash" ? t("notebookPane.trash") : selectedTag ? `#${selectedTag}` : undefined}
notebooks={notebooks}
selectedNotebookId={selectedNotebookId}
onClose={() => setMobileNotebookPickerOpen(false)}
+5
View File
@@ -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");
}
+12
View File
@@ -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" });
+3
View File
@@ -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;
+5
View File
@@ -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);
};
+1
View File
@@ -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,
+14
View File
@@ -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({
+5
View File
@@ -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");
}
+1
View File
@@ -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",
+1
View File
@@ -758,6 +758,7 @@ export const zhCN = {
currentNotebook: "当前笔记本",
allNotebooks: "所有笔记本",
deletedMemos: "已删除笔记",
tagFilter: "标签筛选",
allMemos: "全部笔记",
memoCount: "{{count}} 条笔记",
deletedCount: "{{count}} 条已删除",
+12
View File
@@ -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<MemoListQueryData>(queryKey);
expect(cached?.pages[0]?.memos).toEqual([]);
expect(cached?.pages[0]?.totalCount).toBe(0);
});
});