From cd2c10fbed9a6ef2b59f7695e23bea895bd7b946 Mon Sep 17 00:00:00 2001 From: kenneth Date: Sat, 20 Sep 2025 22:43:31 +0000 Subject: [PATCH] refactor: extract file drop logic into hook Co-authored-by: Ona --- .../directory-content-table.tsx | 61 ++---------- .../src/directories/directory-page/state.ts | 6 +- packages/web/src/files/use-file-drop.ts | 93 +++++++++++++++++++ 3 files changed, 104 insertions(+), 56 deletions(-) create mode 100644 packages/web/src/files/use-file-drop.ts diff --git a/packages/web/src/directories/directory-page/directory-content-table.tsx b/packages/web/src/directories/directory-page/directory-content-table.tsx index 9209be9..4e9f1d3 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -34,6 +34,7 @@ import { import { TextFileIcon } from "../../components/icons/text-file-icon" import { Button } from "../../components/ui/button" import { LoadingSpinner } from "../../components/ui/loading-spinner" +import { useFileDrop } from "../../files/use-file-drop" import { withDefaultOnError } from "../../lib/error" import { cn } from "../../lib/utils" import { DirectoryPageContext } from "./context" @@ -397,27 +398,15 @@ function FileItemRow({ onClick: () => void onContextMenu: (e: React.MouseEvent) => void }) { - const [isDraggedOver, setIsDraggedOver] = useState(false) const ref = useRef(null) const setDragInfo = useSetAtom(dragInfoAtom) - const store = useStore() - const { mutate: moveFiles } = useMutation({ - mutationFn: useContextMutation(api.files.moveFiles), - onSuccess: ({ - items, - targetDirectory, - }: { - items: Id<"files">[] - targetDirectory: Doc<"directories"> - }) => { - toast.success( - `${items.length} files moved to ${targetDirectory.name}`, - ) - }, + const { isDraggedOver, dropHandlers } = useFileDrop({ + item: row.original, + dragInfoAtom, }) - function onDragStart(e: React.DragEvent) { + const handleDragStart = (e: React.DragEvent) => { if (row.original.kind === "file") { e.dataTransfer.setData( "application/x-internal", @@ -430,37 +419,7 @@ function FileItemRow({ } } - function onDrop(_e: React.DragEvent) { - const dragInfo = store.get(dragInfoAtom) - if (dragInfo && row.original.kind === "directory") { - moveFiles({ - targetDirectoryId: row.original.doc._id, - items: dragInfo.items, - }) - } - } - - function onDragOver(e: React.DragEvent) { - const dragInfo = store.get(dragInfoAtom) - if ( - dragInfo && - dragInfo.source !== row.original && - row.original.kind === "directory" - ) { - e.preventDefault() - e.dataTransfer.dropEffect = "move" - setIsDraggedOver(true) - } else { - e.dataTransfer.dropEffect = "none" - } - } - - function onDragLeave() { - setIsDraggedOver(false) - } - - function onDragEnd() { - setIsDraggedOver(false) + const handleDragEnd = () => { setDragInfo(null) } @@ -472,11 +431,9 @@ function FileItemRow({ data-state={row.getIsSelected() && "selected"} onClick={onClick} onContextMenu={onContextMenu} - onDragStart={onDragStart} - onDrop={onDrop} - onDragOver={onDragOver} - onDragLeave={onDragLeave} - onDragEnd={onDragEnd} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + {...dropHandlers} className={cn({ "bg-muted": isDraggedOver })} > {row.getVisibleCells().map((cell) => ( diff --git a/packages/web/src/directories/directory-page/state.ts b/packages/web/src/directories/directory-page/state.ts index dd5ac26..c823c8c 100644 --- a/packages/web/src/directories/directory-page/state.ts +++ b/packages/web/src/directories/directory-page/state.ts @@ -5,6 +5,7 @@ import type { } from "@fileone/convex/model/directories" import type { RowSelectionState } from "@tanstack/react-table" import { atom } from "jotai" +import type { FileDragInfo } from "../../files/use-file-drop" export const contextMenuTargeItemAtom = atom(null) export const optimisticDeletedItemsAtom = atom( @@ -23,7 +24,4 @@ export const itemBeingRenamedAtom = atom<{ export const openedFileAtom = atom | null>(null) -export const dragInfoAtom = atom<{ - source: DirectoryItem - items: Id<"files">[] -} | null>(null) +export const dragInfoAtom = atom(null) diff --git a/packages/web/src/files/use-file-drop.ts b/packages/web/src/files/use-file-drop.ts new file mode 100644 index 0000000..e2fcecb --- /dev/null +++ b/packages/web/src/files/use-file-drop.ts @@ -0,0 +1,93 @@ +import { api } from "@fileone/convex/_generated/api" +import type { Doc, Id } from "@fileone/convex/_generated/dataModel" +import type { DirectoryItem } from "@fileone/convex/model/directories" +import { useMutation } from "@tanstack/react-query" +import { useMutation as useContextMutation } from "convex/react" +import type { Atom } from "jotai" +import { useStore } from "jotai" +import { useState } from "react" +import { toast } from "sonner" + +export interface FileDragInfo { + source: DirectoryItem + items: Id<"files">[] +} + +export interface UseFileDropOptions { + item: DirectoryItem + dragInfoAtom: Atom + onDropSuccess?: (items: Id<"files">[], targetDirectory: Doc<"directories">) => void +} + +export interface UseFileDropReturn { + isDraggedOver: boolean + dropHandlers: { + onDrop: (e: React.DragEvent) => void + onDragOver: (e: React.DragEvent) => void + onDragLeave: () => void + } +} + +export function useFileDrop({ + item, + dragInfoAtom, + onDropSuccess, +}: UseFileDropOptions): UseFileDropReturn { + const [isDraggedOver, setIsDraggedOver] = useState(false) + const store = useStore() + + const { mutate: moveFiles } = useMutation({ + mutationFn: useContextMutation(api.files.moveFiles), + onSuccess: ({ + items, + targetDirectory, + }: { + items: Id<"files">[] + targetDirectory: Doc<"directories"> + }) => { + toast.success( + `${items.length} files moved to ${targetDirectory.name}`, + ) + onDropSuccess?.(items, targetDirectory) + }, + }) + + const handleDrop = (_e: React.DragEvent) => { + const dragInfo = store.get(dragInfoAtom) + if (dragInfo && item.kind === "directory") { + moveFiles({ + targetDirectoryId: item.doc._id, + items: dragInfo.items, + }) + } + setIsDraggedOver(false) + } + + const handleDragOver = (e: React.DragEvent) => { + const dragInfo = store.get(dragInfoAtom) + if ( + dragInfo && + dragInfo.source !== item && + item.kind === "directory" + ) { + e.preventDefault() + e.dataTransfer.dropEffect = "move" + setIsDraggedOver(true) + } else { + e.dataTransfer.dropEffect = "none" + } + } + + const handleDragLeave = () => { + setIsDraggedOver(false) + } + + return { + isDraggedOver, + dropHandlers: { + onDrop: handleDrop, + onDragOver: handleDragOver, + onDragLeave: handleDragLeave, + }, + } +} \ No newline at end of file