From 5484a0863688aa8de2ecf57c3d0e76b15f81ed54 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 16 Dec 2025 22:35:56 +0000 Subject: [PATCH] fix: dir content invalidation on move to trash --- .../directories.$directoryId.tsx | 54 ++++----- apps/drive-web/src/vfs/api.ts | 111 +++++++++++++----- 2 files changed, 110 insertions(+), 55 deletions(-) diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx index c1c7867..35b9a32 100644 --- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx +++ b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx @@ -49,6 +49,7 @@ import type { DirectoryItem, FileInfo, } from "@/vfs/vfs" +import { formatError } from "../../../lib/error" export const Route = createFileRoute( "/_authenticated/_sidebar-layout/directories/$directoryId", @@ -86,17 +87,22 @@ const itemBeingRenamedAtom = atom<{ // MARK: page entry function RouteComponent() { const { directoryId } = Route.useParams() - const { data: directoryInfo, isLoading: isLoadingDirectoryInfo, error: directoryInfoError } = useQuery( - useAtomValue(directoryInfoQueryAtom(directoryId)), - ) - const { data: directoryContent, isLoading: isLoadingDirectoryContent, error: directoryContentError } = - useQuery(useAtomValue(directoryContentQueryAtom(directoryId))) + const { + data: directoryInfo, + isLoading: isLoadingDirectoryInfo, + error: directoryInfoError, + } = useQuery(useAtomValue(directoryInfoQueryAtom(directoryId))) + const { + data: directoryContent, + isLoading: isLoadingDirectoryContent, + error: directoryContentError, + } = useQuery(useAtomValue(directoryContentQueryAtom(directoryId))) const directoryUrlById = useCallback( (directoryId: string) => `/directories/${directoryId}`, [], ) - + console.log({ directoryInfoError, directoryContentError }) if (isLoadingDirectoryInfo || isLoadingDirectoryContent) { @@ -236,39 +242,31 @@ function DirectoryContentContextMenu({ }) { const store = useStore() const [target, setTarget] = useAtom(contextMenuTargetItemsAtom) - const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) const setCutItems = useSetAtom(cutItemsAtom) + const moveToTrashMutation = useAtomValue(moveToTrashMutationAtom) const { mutate: moveToTrash } = useMutation({ - ...useAtomValue(moveToTrashMutationAtom), - onMutate: (items) => { + ...moveToTrashMutation, + onMutate: (vars, ctx) => { setBackgroundTaskProgress({ label: "Moving items to trash…", }) - setOptimisticDeletedItems( - (prev) => new Set([...prev, ...items.map((item) => item.id)]), + return ( + moveToTrashMutation.onMutate?.(vars, ctx) ?? { + prevDirContentMap: new Map(), + } ) }, - onSuccess: (trashedItems) => { + onSuccess: (data, vars, result, ctx) => { + moveToTrashMutation.onSuccess?.(data, vars, result, ctx) setBackgroundTaskProgress(null) - setOptimisticDeletedItems((prev) => { - const newSet = new Set(prev) - for (const item of trashedItems) { - newSet.delete(item.id) - } - return newSet - }) - toast.success(`Moved ${trashedItems.length} items to trash`) + toast.success(`Moved ${data.length} items to trash`) }, - onError: (_err, items) => { - setOptimisticDeletedItems((prev) => { - const newSet = new Set(prev) - for (const item of items) { - newSet.delete(item.id) - } - return newSet - }) + onError: (err, vars, mutateResult, context) => { + moveToTrashMutation.onError?.(err, vars, mutateResult, context) + toast.error(formatError(err)) + setBackgroundTaskProgress(null) }, }) diff --git a/apps/drive-web/src/vfs/api.ts b/apps/drive-web/src/vfs/api.ts index 4d4bdd9..0328c82 100644 --- a/apps/drive-web/src/vfs/api.ts +++ b/apps/drive-web/src/vfs/api.ts @@ -163,7 +163,7 @@ export const moveDirectoryItemsMutationAtom = atom((get) => if (item.parentId) { const s = movedItems.get(item.parentId) if (!s) { - movedItems.set(item.parentId, new Set()) + movedItems.set(item.parentId, new Set(s)) } else { s.add(item.id) } @@ -192,7 +192,6 @@ export const moveDirectoryItemsMutationAtom = atom((get) => typeof targetDirectory === "string" ? targetDirectory : targetDirectory.id - console.log(dirId) client.invalidateQueries(get(directoryContentQueryAtom(dirId))) for (const item of items) { if (item.parentId) { @@ -238,36 +237,94 @@ export const moveToTrashMutationAtom = atom((get) => } } - const fileDeleteParams = new URLSearchParams() - fileDeleteParams.set("id", fileIds.join(",")) - fileDeleteParams.set("trash", "true") - const deleteFilesPromise = fetchApi( - "DELETE", - `/accounts/${account.id}/files?${fileDeleteParams.toString()}`, - { - returns: FileInfo.array(), - }, - ) + let deleteFilesPromise: Promise + if (fileIds.length > 0) { + const fileDeleteParams = new URLSearchParams() + fileDeleteParams.set("id", fileIds.join(",")) + fileDeleteParams.set("trash", "true") + deleteFilesPromise = fetchApi( + "DELETE", + `/accounts/${account.id}/files?${fileDeleteParams.toString()}`, + { + returns: FileInfo.array(), + }, + ).then(([_, result]) => result) + } else { + deleteFilesPromise = Promise.resolve([]) + } - const directoryDeleteParams = new URLSearchParams() - directoryDeleteParams.set("id", directoryIds.join(",")) - directoryDeleteParams.set("trash", "true") - const deleteDirectoriesPromise = fetchApi( - "DELETE", - `/accounts/${account.id}/directories?${directoryDeleteParams.toString()}`, - { - returns: DirectoryInfo.array(), - }, - ) + let deleteDirectoriesPromise: Promise + if (directoryIds.length > 0) { + const directoryDeleteParams = new URLSearchParams() + directoryDeleteParams.set("id", directoryIds.join(",")) + directoryDeleteParams.set("trash", "true") + deleteDirectoriesPromise = fetchApi( + "DELETE", + `/accounts/${account.id}/directories?${directoryDeleteParams.toString()}`, + { + returns: DirectoryInfo.array(), + }, + ).then(([_, result]) => result) + } else { + deleteDirectoriesPromise = Promise.resolve([]) + } - const [[, deletedFiles], [, deletedDirectories]] = - await Promise.all([ - deleteFilesPromise, - deleteDirectoriesPromise, - ]) + const [deletedFiles, deletedDirectories] = await Promise.all([ + deleteFilesPromise, + deleteDirectoriesPromise, + ]) return [...deletedFiles, ...deletedDirectories] }, + onMutate: (items, { client }) => { + const trashedItems = new Map>() + for (const item of items) { + if (item.parentId) { + const s = trashedItems.get(item.parentId) + if (!s) { + trashedItems.set(item.parentId, new Set(s)) + } else { + s.add(item.id) + } + } + } + + const prevDirContentMap = new Map< + string, + DirectoryItem[] | undefined + >() + trashedItems.forEach((s, parentId) => { + const query = get(directoryContentQueryAtom(parentId)) + const prevDirContent = client.getQueryData(query.queryKey) + client.setQueryData( + query.queryKey, + (prev) => prev?.filter((it) => !s.has(it.id)) ?? prev, + ) + prevDirContentMap.set(parentId, prevDirContent) + }) + return { prevDirContentMap } + }, + onSuccess: (_data, items, _result, { client }) => { + for (const item of items) { + if (item.parentId) { + client.invalidateQueries( + get(directoryContentQueryAtom(item.parentId)), + ) + } + } + }, + onError: (_error, items, context, { client }) => { + if (context) { + context.prevDirContentMap.forEach( + (prevDirContent, parentId) => { + client.setQueryData( + get(directoryContentQueryAtom(parentId)).queryKey, + prevDirContent, + ) + }, + ) + } + }, }), )