mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
fix: directory table optimistic update
fix optimistic update not working for directory table and trash table
This commit is contained in:
@@ -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<Value>({
|
type SetAtom<Args extends unknown[], Result> = (...args: Args) => Result
|
||||||
|
|
||||||
|
export function WithAtom<Value, Args extends unknown[], Result>(props: {
|
||||||
|
atom: WritableAtom<Value, Args, Result>
|
||||||
|
children: (
|
||||||
|
value: Awaited<Value>,
|
||||||
|
setAtom: SetAtom<Args, Result>,
|
||||||
|
) => React.ReactNode
|
||||||
|
}): React.ReactNode
|
||||||
|
export function WithAtom<Value>(props: {
|
||||||
|
atom: PrimitiveAtom<Value>
|
||||||
|
children: (
|
||||||
|
value: Awaited<Value>,
|
||||||
|
setAtom: SetAtom<[SetStateAction<Value>], void>,
|
||||||
|
) => React.ReactNode
|
||||||
|
}): React.ReactNode
|
||||||
|
export function WithAtom<Value>(props: {
|
||||||
|
atom: Atom<Value>
|
||||||
|
children: (value: Awaited<Value>, setAtom: never) => React.ReactNode
|
||||||
|
}): React.ReactNode
|
||||||
|
export function WithAtom<
|
||||||
|
AtomType extends WritableAtom<unknown, never[], unknown>,
|
||||||
|
>(props: {
|
||||||
|
atom: AtomType
|
||||||
|
children: (
|
||||||
|
value: Awaited<ExtractAtomValue<AtomType>>,
|
||||||
|
setAtom: SetAtom<
|
||||||
|
ExtractAtomArgs<AtomType>,
|
||||||
|
ExtractAtomResult<AtomType>
|
||||||
|
>,
|
||||||
|
) => React.ReactNode
|
||||||
|
}): React.ReactNode
|
||||||
|
export function WithAtom<AtomType extends Atom<unknown>>(props: {
|
||||||
|
atom: AtomType
|
||||||
|
children: (
|
||||||
|
value: Awaited<ExtractAtomValue<AtomType>>,
|
||||||
|
setAtom: never,
|
||||||
|
) => React.ReactNode
|
||||||
|
}): React.ReactNode
|
||||||
|
export function WithAtom<Value, Args extends unknown[], Result>({
|
||||||
atom,
|
atom,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
atom: PrimitiveAtom<Value>
|
atom: Atom<Value> | WritableAtom<Value, Args, Result>
|
||||||
children: (
|
children: (
|
||||||
value: Value,
|
value: Awaited<Value>,
|
||||||
setValue: (value: Value | ((current: Value) => Value)) => void,
|
setAtom: SetAtom<Args, Result> | never,
|
||||||
) => React.ReactNode
|
) => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [value, setValue] = useAtom(atom)
|
const [value, setAtom] = useAtom(atom as WritableAtom<Value, Args, Result>)
|
||||||
return children(value, setValue)
|
return children(value, setAtom)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
type Row,
|
type Row,
|
||||||
type Table as TableType,
|
type Table as TableType,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
@@ -40,8 +41,10 @@ import { type FileDragInfo, useFileDrop } from "../../files/use-file-drop"
|
|||||||
import { cn } from "../../lib/utils"
|
import { cn } from "../../lib/utils"
|
||||||
import { DirectoryPageContext } from "./context"
|
import { DirectoryPageContext } from "./context"
|
||||||
|
|
||||||
|
type DirectoryContentTableItemIdFilter = Set<FileSystemItem["doc"]["_id"]>
|
||||||
|
|
||||||
type DirectoryContentTableProps = {
|
type DirectoryContentTableProps = {
|
||||||
filterFn: (item: FileSystemItem) => boolean
|
hiddenItems: DirectoryContentTableItemIdFilter
|
||||||
directoryUrlFn: (directory: Doc<"directories">) => string
|
directoryUrlFn: (directory: Doc<"directories">) => string
|
||||||
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
|
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
|
||||||
onContextMenu: (
|
onContextMenu: (
|
||||||
@@ -150,7 +153,7 @@ function useTableColumns(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DirectoryContentTable({
|
export function DirectoryContentTable({
|
||||||
filterFn,
|
hiddenItems,
|
||||||
directoryUrlFn,
|
directoryUrlFn,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
fileDragInfoAtom,
|
fileDragInfoAtom,
|
||||||
@@ -164,10 +167,18 @@ export function DirectoryContentTable({
|
|||||||
data: directoryContent || [],
|
data: directoryContent || [],
|
||||||
columns: useTableColumns(onOpenFile, directoryUrlFn),
|
columns: useTableColumns(onOpenFile, directoryUrlFn),
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
enableGlobalFilter: true,
|
enableGlobalFilter: true,
|
||||||
globalFilterFn: (row, _columnId, _filterValue, _addMeta) =>
|
state: {
|
||||||
filterFn(row.original),
|
globalFilter: hiddenItems,
|
||||||
|
},
|
||||||
|
globalFilterFn: (
|
||||||
|
row,
|
||||||
|
_columnId,
|
||||||
|
filterValue: DirectoryContentTableItemIdFilter,
|
||||||
|
_addMeta,
|
||||||
|
) => !filterValue.has(row.original.doc._id),
|
||||||
getRowId: (row) => row.doc._id,
|
getRowId: (row) => row.doc._id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function DirectoryPathBreadcrumb({
|
|||||||
directory: DirectoryInfo
|
directory: DirectoryInfo
|
||||||
rootLabel: string
|
rootLabel: string
|
||||||
directoryUrlFn: (directory: Id<"directories">) => string
|
directoryUrlFn: (directory: Id<"directories">) => string
|
||||||
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
|
fileDragInfoAtom?: PrimitiveAtom<FileDragInfo | null>
|
||||||
}) {
|
}) {
|
||||||
const breadcrumbItems: React.ReactNode[] = [
|
const breadcrumbItems: React.ReactNode[] = [
|
||||||
<FilePathBreadcrumbItem
|
<FilePathBreadcrumbItem
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { WithAtom } from "@/components/with-atom"
|
import { WithAtom } from "@/components/with-atom"
|
||||||
|
import { backgroundTaskProgressAtom } from "@/dashboard/state"
|
||||||
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
||||||
import { DirectoryContentTable } from "@/directories/directory-page/directory-content-table"
|
import { DirectoryContentTable } from "@/directories/directory-page/directory-content-table"
|
||||||
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
||||||
@@ -82,6 +83,16 @@ const itemBeingRenamedAtom = atom<{
|
|||||||
name: string
|
name: string
|
||||||
} | null>(null)
|
} | null>(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
|
// MARK: page entry
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { directoryId } = Route.useParams()
|
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 */}
|
{/* DirectoryContentContextMenu must wrap div instead of DirectoryContentTable, otherwise radix will throw "event.preventDefault is not a function" error, idk why */}
|
||||||
<DirectoryContentContextMenu>
|
<DirectoryContentContextMenu>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DirectoryContentTable
|
<WithAtom atom={optimisticDeletedItemsAtom}>
|
||||||
filterFn={tableFilter}
|
{(optimisticDeletedItems) => (
|
||||||
directoryUrlFn={directoryUrlFn}
|
<DirectoryContentTable
|
||||||
fileDragInfoAtom={fileDragInfoAtom}
|
hiddenItems={optimisticDeletedItems}
|
||||||
onContextMenu={handleContextMenuRequest}
|
directoryUrlFn={directoryUrlFn}
|
||||||
onOpenFile={openFile}
|
fileDragInfoAtom={fileDragInfoAtom}
|
||||||
/>
|
onContextMenu={handleContextMenuRequest}
|
||||||
|
onOpenFile={openFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</WithAtom>
|
||||||
</div>
|
</div>
|
||||||
</DirectoryContentContextMenu>
|
</DirectoryContentContextMenu>
|
||||||
|
|
||||||
@@ -224,8 +239,9 @@ function RouteComponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================================
|
// ==================================
|
||||||
// MARK: DirectoryContentContextMenu
|
// MARK: ctx menu
|
||||||
|
|
||||||
|
// tags: ctxmenu contextmenu directorycontextmenu
|
||||||
function DirectoryContentContextMenu({
|
function DirectoryContentContextMenu({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -234,17 +250,22 @@ function DirectoryContentContextMenu({
|
|||||||
const store = useStore()
|
const store = useStore()
|
||||||
const [target, setTarget] = useAtom(contextMenuTargetItemsAtom)
|
const [target, setTarget] = useAtom(contextMenuTargetItemsAtom)
|
||||||
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
||||||
|
const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom)
|
||||||
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
|
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
|
||||||
|
|
||||||
const { mutate: moveToTrash } = useMutation({
|
const { mutate: moveToTrash } = useMutation({
|
||||||
mutationFn: moveToTrashMutation,
|
mutationFn: moveToTrashMutation,
|
||||||
onMutate: ({ handles }) => {
|
onMutate: ({ handles }) => {
|
||||||
|
setBackgroundTaskProgress({
|
||||||
|
label: "Moving items to trash…",
|
||||||
|
})
|
||||||
setOptimisticDeletedItems(
|
setOptimisticDeletedItems(
|
||||||
(prev) =>
|
(prev) =>
|
||||||
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSuccess: ({ deleted, errors }, { handles }) => {
|
onSuccess: ({ deleted, errors }, { handles }) => {
|
||||||
|
setBackgroundTaskProgress(null)
|
||||||
setOptimisticDeletedItems((prev) => {
|
setOptimisticDeletedItems((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev)
|
||||||
for (const handle of handles) {
|
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 = () => {
|
const handleDelete = () => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "convex/react"
|
} from "convex/react"
|
||||||
import { atom, useAtom, useSetAtom, useStore } from "jotai"
|
import { atom, useAtom, useSetAtom, useStore } from "jotai"
|
||||||
import { ShredderIcon, TrashIcon, UndoIcon } from "lucide-react"
|
import { ShredderIcon, TrashIcon, UndoIcon } from "lucide-react"
|
||||||
import { useCallback, useContext, useEffect } from "react"
|
import { useCallback, useContext } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -120,13 +120,17 @@ function RouteComponent() {
|
|||||||
|
|
||||||
<TableContextMenu>
|
<TableContextMenu>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DirectoryContentTable
|
<WithAtom atom={optimisticRemovedItemsAtom}>
|
||||||
filterFn={() => true}
|
{(optimisticRemovedItems) => (
|
||||||
directoryUrlFn={directoryUrlFn}
|
<DirectoryContentTable
|
||||||
fileDragInfoAtom={fileDragInfoAtom}
|
hiddenItems={optimisticRemovedItems}
|
||||||
onContextMenu={handleContextMenuRequest}
|
directoryUrlFn={directoryUrlFn}
|
||||||
onOpenFile={setOpenedFile}
|
fileDragInfoAtom={fileDragInfoAtom}
|
||||||
/>
|
onContextMenu={handleContextMenuRequest}
|
||||||
|
onOpenFile={setOpenedFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</WithAtom>
|
||||||
</div>
|
</div>
|
||||||
</TableContextMenu>
|
</TableContextMenu>
|
||||||
|
|
||||||
@@ -174,14 +178,19 @@ function RestoreContextMenuItem() {
|
|||||||
const store = useStore()
|
const store = useStore()
|
||||||
const setOptimisticRemovedItems = useSetAtom(optimisticRemovedItemsAtom)
|
const setOptimisticRemovedItems = useSetAtom(optimisticRemovedItemsAtom)
|
||||||
const restoreItemsMutation = useConvexMutation(api.filesystem.restoreItems)
|
const restoreItemsMutation = useConvexMutation(api.filesystem.restoreItems)
|
||||||
const { mutate: restoreItems, isPending: isRestoring } = useMutation({
|
|
||||||
|
const { mutate: restoreItems } = useMutation({
|
||||||
mutationFn: restoreItemsMutation,
|
mutationFn: restoreItemsMutation,
|
||||||
onMutate: ({ handles }) => {
|
onMutate: ({ handles }) => {
|
||||||
|
setBackgroundTaskProgress({
|
||||||
|
label: "Restoring items…",
|
||||||
|
})
|
||||||
setOptimisticRemovedItems(
|
setOptimisticRemovedItems(
|
||||||
new Set(handles.map((handle) => handle.id)),
|
new Set(handles.map((handle) => handle.id)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSuccess: ({ restored, errors }) => {
|
onSuccess: ({ restored, errors }) => {
|
||||||
|
setBackgroundTaskProgress(null)
|
||||||
if (errors.length === 0) {
|
if (errors.length === 0) {
|
||||||
if (restored.files > 0 && restored.directories > 0) {
|
if (restored.files > 0 && restored.directories > 0) {
|
||||||
toast.success(
|
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)
|
const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRestoring) {
|
|
||||||
setBackgroundTaskProgress({
|
|
||||||
label: "Restoring items…",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setBackgroundTaskProgress(null)
|
|
||||||
}
|
|
||||||
}, [isRestoring, setBackgroundTaskProgress])
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
const targetItems = store.get(contextMenuTargetItemsAtom)
|
const targetItems = store.get(contextMenuTargetItemsAtom)
|
||||||
restoreItems({
|
restoreItems({
|
||||||
|
|||||||
Reference in New Issue
Block a user