diff --git a/apps/web/src/components/MemoListPane.tsx b/apps/web/src/components/MemoListPane.tsx index 00ac812d..bb7f5939 100644 --- a/apps/web/src/components/MemoListPane.tsx +++ b/apps/web/src/components/MemoListPane.tsx @@ -130,8 +130,8 @@ export const MemoSelectionActionBar = ({ const { t } = useTranslation(); return ( -
-
+
+
{getSelectionCountLabel(selectedCount, t)} diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx index 81df05dd..216cb9ca 100644 --- a/apps/web/src/components/WorkspaceApp.tsx +++ b/apps/web/src/components/WorkspaceApp.tsx @@ -147,6 +147,13 @@ type MemoDeleteOptimisticContext = { previousSelectedMemoId: string | null; }; +type EmptyTrashOptimisticContext = { + previousMemoLists: Array<[readonly unknown[], MemoListQueryData | undefined]>; + previousMemoDetails: Array<[readonly unknown[], { memo: MemoDetail } | undefined]>; + previousActivePane: Pane; + previousSelectedMemoId: string | null; +}; + const memoToSummary = (memo: MemoDetail): MemoSummary => ({ id: memo.id, notebookId: memo.notebookId, @@ -331,6 +338,19 @@ const removeMemoSummariesFromLists = (queryClient: QueryClient, memoIds: Set { + for (const [queryKey, current] of queryClient.getQueriesData({ queryKey: ["memos", "trash"] })) { + if (!current) { + continue; + } + + queryClient.setQueryData(queryKey, { + ...current, + pages: current.pages.map((page) => ({ ...page, memos: [], totalCount: 0, nextCursor: null })), + }); + } +}; + const decrementNotebookMemoCounts = (queryClient: QueryClient, removedMemos: MemoSummary[]) => { if (removedMemos.length === 0) { return; @@ -1504,16 +1524,51 @@ export const WorkspaceApp = ({ const emptyTrashMutation = useMutation({ mutationFn: api.emptyTrash, - onSuccess: async () => { + onMutate: async (): Promise => { setEmptyTrashConfirmationOpen(false); clearMemoSelection(); setSelectedMemoId(null); setActivePane("memos"); await Promise.all([ - queryClient.invalidateQueries({ queryKey: ["memos"] }), - queryClient.invalidateQueries({ queryKey: ["memo"] }), - queryClient.invalidateQueries({ queryKey: ["resources"] }), + queryClient.cancelQueries({ queryKey: ["memos"] }), + queryClient.cancelQueries({ queryKey: ["memo"] }), + queryClient.cancelQueries({ queryKey: ["resources"] }), + ]); + + const previousMemoLists = queryClient.getQueriesData({ queryKey: ["memos"] }); + const previousMemoDetails = queryClient.getQueriesData<{ memo: MemoDetail }>({ queryKey: ["memo"] }); + + clearTrashMemoLists(queryClient); + for (const [queryKey] of previousMemoDetails) { + if (queryKey[2] === "trash") { + queryClient.removeQueries({ queryKey }); + } + } + + return { previousMemoLists, previousMemoDetails, previousActivePane: activePane, previousSelectedMemoId: selectedMemoId }; + }, + onError: (_error, _variables, context) => { + context?.previousMemoLists.forEach(([queryKey, data]) => { + queryClient.setQueryData(queryKey, data); + }); + context?.previousMemoDetails.forEach(([queryKey, data]) => { + queryClient.setQueryData(queryKey, data); + }); + setSelectedMemoId(context?.previousSelectedMemoId ?? null); + setActivePane(context?.previousActivePane ?? "memos"); + setAppNoticeDialog({ + title: t("workspaceDialogs.emptyTrashFailedTitle"), + description: t("workspaceDialogs.emptyTrashFailedDescription"), + }); + }, + onSettled: (_data, error) => { + const refetchType = error ? "active" : "inactive"; + + void Promise.all([ + queryClient.invalidateQueries({ queryKey: ["memos"], refetchType }), + queryClient.invalidateQueries({ queryKey: ["memo"], refetchType }), + queryClient.invalidateQueries({ queryKey: ["resources"], refetchType }), ]); }, }); diff --git a/apps/web/src/i18n/resources/en-US.ts b/apps/web/src/i18n/resources/en-US.ts index d4e6f89a..f6f8d500 100644 --- a/apps/web/src/i18n/resources/en-US.ts +++ b/apps/web/src/i18n/resources/en-US.ts @@ -486,6 +486,8 @@ export const enUS = { emptyTrashTitle: "Empty trash", emptyTrashDescription: "All notes in Trash and still-linked attachments will be deleted. This action cannot be undone.", emptyTrashConfirm: "Empty trash", + emptyTrashFailedTitle: "Couldn't empty trash", + emptyTrashFailedDescription: "Your notes have been restored to Trash. Please try again.", deleteNotebookTitle: "Delete notebook “{{name}}”", deleteNotebookDescription: "Clear its notes and child notebooks first. It cannot be restored from here after deletion.", ok: "OK", diff --git a/apps/web/src/i18n/resources/zh-CN.ts b/apps/web/src/i18n/resources/zh-CN.ts index fbeec7da..70b7c10c 100644 --- a/apps/web/src/i18n/resources/zh-CN.ts +++ b/apps/web/src/i18n/resources/zh-CN.ts @@ -486,6 +486,8 @@ export const zhCN = { emptyTrashTitle: "清空回收站", emptyTrashDescription: "回收站中的全部笔记和仍关联的附件都会删除,这个操作不可恢复。", emptyTrashConfirm: "清空回收站", + emptyTrashFailedTitle: "清空回收站失败", + emptyTrashFailedDescription: "笔记已恢复到回收站,请稍后重试。", deleteNotebookTitle: "删除笔记本「{{name}}」", deleteNotebookDescription: "请先清空其中的笔记和子笔记本。删除后无法从这里恢复。", ok: "知道了",