From c0f852ad356a2ae93eb12592101b628357ce8a88 Mon Sep 17 00:00:00 2001 From: kenneth Date: Sat, 18 Oct 2025 22:58:23 +0000 Subject: [PATCH] fix: directory table optimistic update fix optimistic update not working for directory table and trash table --- apps/drive-web/src/components/with-atom.tsx | 63 ++++++++++++++++--- .../directory-content-table.tsx | 19 ++++-- .../directories/directory-path-breadcrumb.tsx | 2 +- .../directories.$directoryId.tsx | 46 +++++++++++--- .../trash.directories.$directoryId.tsx | 46 ++++++++------ 5 files changed, 137 insertions(+), 39 deletions(-) diff --git a/apps/drive-web/src/components/with-atom.tsx b/apps/drive-web/src/components/with-atom.tsx index 917ceff..cb02eda 100644 --- a/apps/drive-web/src/components/with-atom.tsx +++ b/apps/drive-web/src/components/with-atom.tsx @@ -1,15 +1,64 @@ -import { type PrimitiveAtom, useAtom } from "jotai" +import { + type Atom, + type ExtractAtomArgs, + type ExtractAtomResult, + type ExtractAtomValue, + type PrimitiveAtom, + type SetStateAction, + useAtom, + type WritableAtom, +} from "jotai" +import type * as React from "react" -export function WithAtom({ +type SetAtom = (...args: Args) => Result + +export function WithAtom(props: { + atom: WritableAtom + children: ( + value: Awaited, + setAtom: SetAtom, + ) => React.ReactNode +}): React.ReactNode +export function WithAtom(props: { + atom: PrimitiveAtom + children: ( + value: Awaited, + setAtom: SetAtom<[SetStateAction], void>, + ) => React.ReactNode +}): React.ReactNode +export function WithAtom(props: { + atom: Atom + children: (value: Awaited, setAtom: never) => React.ReactNode +}): React.ReactNode +export function WithAtom< + AtomType extends WritableAtom, +>(props: { + atom: AtomType + children: ( + value: Awaited>, + setAtom: SetAtom< + ExtractAtomArgs, + ExtractAtomResult + >, + ) => React.ReactNode +}): React.ReactNode +export function WithAtom>(props: { + atom: AtomType + children: ( + value: Awaited>, + setAtom: never, + ) => React.ReactNode +}): React.ReactNode +export function WithAtom({ atom, children, }: { - atom: PrimitiveAtom + atom: Atom | WritableAtom children: ( - value: Value, - setValue: (value: Value | ((current: Value) => Value)) => void, + value: Awaited, + setAtom: SetAtom | never, ) => React.ReactNode }) { - const [value, setValue] = useAtom(atom) - return children(value, setValue) + const [value, setAtom] = useAtom(atom as WritableAtom) + return children(value, setAtom) } diff --git a/apps/drive-web/src/directories/directory-page/directory-content-table.tsx b/apps/drive-web/src/directories/directory-page/directory-content-table.tsx index bf3e417..23f8462 100644 --- a/apps/drive-web/src/directories/directory-page/directory-content-table.tsx +++ b/apps/drive-web/src/directories/directory-page/directory-content-table.tsx @@ -15,6 +15,7 @@ import { type ColumnDef, flexRender, getCoreRowModel, + getFilteredRowModel, type Row, type Table as TableType, useReactTable, @@ -40,8 +41,10 @@ import { type FileDragInfo, useFileDrop } from "../../files/use-file-drop" import { cn } from "../../lib/utils" import { DirectoryPageContext } from "./context" +type DirectoryContentTableItemIdFilter = Set + type DirectoryContentTableProps = { - filterFn: (item: FileSystemItem) => boolean + hiddenItems: DirectoryContentTableItemIdFilter directoryUrlFn: (directory: Doc<"directories">) => string fileDragInfoAtom: PrimitiveAtom onContextMenu: ( @@ -150,7 +153,7 @@ function useTableColumns( } export function DirectoryContentTable({ - filterFn, + hiddenItems, directoryUrlFn, onContextMenu, fileDragInfoAtom, @@ -164,10 +167,18 @@ export function DirectoryContentTable({ data: directoryContent || [], columns: useTableColumns(onOpenFile, directoryUrlFn), getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), enableRowSelection: true, enableGlobalFilter: true, - globalFilterFn: (row, _columnId, _filterValue, _addMeta) => - filterFn(row.original), + state: { + globalFilter: hiddenItems, + }, + globalFilterFn: ( + row, + _columnId, + filterValue: DirectoryContentTableItemIdFilter, + _addMeta, + ) => !filterValue.has(row.original.doc._id), getRowId: (row) => row.doc._id, }) diff --git a/apps/drive-web/src/directories/directory-path-breadcrumb.tsx b/apps/drive-web/src/directories/directory-path-breadcrumb.tsx index 2165301..b327636 100644 --- a/apps/drive-web/src/directories/directory-path-breadcrumb.tsx +++ b/apps/drive-web/src/directories/directory-path-breadcrumb.tsx @@ -39,7 +39,7 @@ export function DirectoryPathBreadcrumb({ directory: DirectoryInfo rootLabel: string directoryUrlFn: (directory: Id<"directories">) => string - fileDragInfoAtom: PrimitiveAtom + fileDragInfoAtom?: PrimitiveAtom }) { const breadcrumbItems: React.ReactNode[] = [ (null) +const tableFilterAtom = atom((get) => { + const optimisticDeletedItems = get(optimisticDeletedItemsAtom) + console.log("optimisticDeletedItems", optimisticDeletedItems) + return (item: FileSystemItem) => { + const test = !optimisticDeletedItems.has(item.doc._id) + console.log("test", test) + return test + } +}) + // MARK: page entry function RouteComponent() { const { directoryId } = Route.useParams() @@ -160,13 +171,17 @@ function RouteComponent() { {/* DirectoryContentContextMenu must wrap div instead of DirectoryContentTable, otherwise radix will throw "event.preventDefault is not a function" error, idk why */}
- + + {(optimisticDeletedItems) => ( + + )} +
@@ -224,8 +239,9 @@ function RouteComponent() { } // ================================== -// MARK: DirectoryContentContextMenu +// MARK: ctx menu +// tags: ctxmenu contextmenu directorycontextmenu function DirectoryContentContextMenu({ children, }: { @@ -234,17 +250,22 @@ function DirectoryContentContextMenu({ const store = useStore() const [target, setTarget] = useAtom(contextMenuTargetItemsAtom) const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) + const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash) const { mutate: moveToTrash } = useMutation({ mutationFn: moveToTrashMutation, onMutate: ({ handles }) => { + setBackgroundTaskProgress({ + label: "Moving items to trash…", + }) setOptimisticDeletedItems( (prev) => new Set([...prev, ...handles.map((handle) => handle.id)]), ) }, onSuccess: ({ deleted, errors }, { handles }) => { + setBackgroundTaskProgress(null) setOptimisticDeletedItems((prev) => { const newSet = new Set(prev) for (const handle of handles) { @@ -262,6 +283,15 @@ function DirectoryContentContextMenu({ ) } }, + onError: (_err, { handles }) => { + setOptimisticDeletedItems((prev) => { + const newSet = new Set(prev) + for (const handle of handles) { + newSet.delete(handle.id) + } + return newSet + }) + }, }) const handleDelete = () => { diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/trash.directories.$directoryId.tsx b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/trash.directories.$directoryId.tsx index 1adc54d..962f5df 100644 --- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/trash.directories.$directoryId.tsx +++ b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/trash.directories.$directoryId.tsx @@ -14,7 +14,7 @@ import { } from "convex/react" import { atom, useAtom, useSetAtom, useStore } from "jotai" import { ShredderIcon, TrashIcon, UndoIcon } from "lucide-react" -import { useCallback, useContext, useEffect } from "react" +import { useCallback, useContext } from "react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { @@ -120,13 +120,17 @@ function RouteComponent() {
- true} - directoryUrlFn={directoryUrlFn} - fileDragInfoAtom={fileDragInfoAtom} - onContextMenu={handleContextMenuRequest} - onOpenFile={setOpenedFile} - /> + + {(optimisticRemovedItems) => ( + + )} +
@@ -174,14 +178,19 @@ function RestoreContextMenuItem() { const store = useStore() const setOptimisticRemovedItems = useSetAtom(optimisticRemovedItemsAtom) const restoreItemsMutation = useConvexMutation(api.filesystem.restoreItems) - const { mutate: restoreItems, isPending: isRestoring } = useMutation({ + + const { mutate: restoreItems } = useMutation({ mutationFn: restoreItemsMutation, onMutate: ({ handles }) => { + setBackgroundTaskProgress({ + label: "Restoring items…", + }) setOptimisticRemovedItems( new Set(handles.map((handle) => handle.id)), ) }, onSuccess: ({ restored, errors }) => { + setBackgroundTaskProgress(null) if (errors.length === 0) { if (restored.files > 0 && restored.directories > 0) { toast.success( @@ -200,19 +209,18 @@ function RestoreContextMenuItem() { ) } }, + onError: (_err, { handles }) => { + setOptimisticRemovedItems((prev) => { + const newSet = new Set(prev) + for (const handle of handles) { + newSet.delete(handle.id) + } + return newSet + }) + }, }) const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) - useEffect(() => { - if (isRestoring) { - setBackgroundTaskProgress({ - label: "Restoring items…", - }) - } else { - setBackgroundTaskProgress(null) - } - }, [isRestoring, setBackgroundTaskProgress]) - const onClick = () => { const targetItems = store.get(contextMenuTargetItemsAtom) restoreItems({