From 7eefe2b96ee39ebe34cd39312eacae007bf9cae9 Mon Sep 17 00:00:00 2001 From: kenneth Date: Sat, 20 Sep 2025 23:54:27 +0000 Subject: [PATCH] feat: allow file drop on path breadcrumb --- packages/convex/model/directories.ts | 13 ++++- packages/convex/model/filesystem.ts | 24 +++++++++- .../directory-content-table.tsx | 18 +++++-- .../directory-page/directory-page.tsx | 47 ++++++++++++++----- packages/web/src/files/use-file-drop.ts | 31 ++++++------ 5 files changed, 98 insertions(+), 35 deletions(-) diff --git a/packages/convex/model/directories.ts b/packages/convex/model/directories.ts index ba61e6f..e6f0adf 100644 --- a/packages/convex/model/directories.ts +++ b/packages/convex/model/directories.ts @@ -5,6 +5,7 @@ import type { } from "../functions" import * as Err from "./error" import type { FilePath, ReverseFilePath } from "./filesystem" +import { newDirectoryHandle } from "./filesystem" type Directory = { kind: "directory" @@ -42,12 +43,20 @@ export async function fetch( ) } - const path: ReverseFilePath = [{ id: directoryId, name: directory.name }] + const path: ReverseFilePath = [ + { + handle: newDirectoryHandle(directoryId), + name: directory.name, + }, + ] let parentDirId = directory.parentId while (parentDirId) { const parentDir = await ctx.db.get(parentDirId) if (parentDir) { - path.push({ id: parentDir._id, name: parentDir.name }) + path.push({ + handle: newDirectoryHandle(parentDir._id), + name: parentDir.name, + }) parentDirId = parentDir.parentId } else { throw Err.create(Err.Code.Internal) diff --git a/packages/convex/model/filesystem.ts b/packages/convex/model/filesystem.ts index c281b2c..ae10f2b 100644 --- a/packages/convex/model/filesystem.ts +++ b/packages/convex/model/filesystem.ts @@ -1,12 +1,12 @@ import type { Id } from "../_generated/dataModel" export type DirectoryPathComponent = { - id: Id<"directories"> + handle: DirectoryHandle name: string } export type FilePathComponent = { - id: Id<"files"> + handle: FileHandle name: string } @@ -15,3 +15,23 @@ export type PathComponent = FilePathComponent | DirectoryPathComponent export type FilePath = [...DirectoryPathComponent[], PathComponent] export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]] + +export type FileHandle = { + kind: "file" + id: Id<"files"> +} + +export type DirectoryHandle = { + kind: "directory" + id: Id<"directories"> +} + +export type FileSystemHandle = DirectoryHandle | FileHandle + +export function newDirectoryHandle(id: Id<"directories">): DirectoryHandle { + return { kind: "directory", id } +} + +export function newFileHandle(id: Id<"files">): FileHandle { + return { kind: "file", id } +} 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 4e9f1d3..ee4977f 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -1,6 +1,10 @@ import { api } from "@fileone/convex/_generated/api" -import type { Doc, Id } from "@fileone/convex/_generated/dataModel" +import type { Doc } from "@fileone/convex/_generated/dataModel" import type { DirectoryItem } from "@fileone/convex/model/directories" +import { + newDirectoryHandle, + newFileHandle, +} from "@fileone/convex/model/filesystem" import { useMutation } from "@tanstack/react-query" import { Link } from "@tanstack/react-router" import { @@ -13,7 +17,7 @@ import { import { useMutation as useContextMutation } from "convex/react" import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai" import { CheckIcon, TextCursorInputIcon, TrashIcon, XIcon } from "lucide-react" -import { useContext, useEffect, useId, useRef, useState } from "react" +import { useContext, useEffect, useId, useRef } from "react" import { toast } from "sonner" import { DirectoryIcon } from "@/components/icons/directory-icon" import { Checkbox } from "@/components/ui/checkbox" @@ -402,7 +406,10 @@ function FileItemRow({ const setDragInfo = useSetAtom(dragInfoAtom) const { isDraggedOver, dropHandlers } = useFileDrop({ - item: row.original, + item: + row.original.kind === "directory" + ? newDirectoryHandle(row.original.doc._id) + : null, dragInfoAtom, }) @@ -412,9 +419,10 @@ function FileItemRow({ "application/x-internal", JSON.stringify(row.original), ) + const fileHandle = newFileHandle(row.original.doc._id) setDragInfo({ - source: row.original, - items: [row.original.doc._id], + source: fileHandle, + items: [fileHandle], }) } } diff --git a/packages/web/src/directories/directory-page/directory-page.tsx b/packages/web/src/directories/directory-page/directory-page.tsx index 5f3f13d..caee4cd 100644 --- a/packages/web/src/directories/directory-page/directory-page.tsx +++ b/packages/web/src/directories/directory-page/directory-page.tsx @@ -1,5 +1,8 @@ import { api } from "@fileone/convex/_generated/api" -import type { PathComponent } from "@fileone/convex/model/filesystem" +import type { + DirectoryHandle, + PathComponent, +} from "@fileone/convex/model/filesystem" import { useMutation } from "@tanstack/react-query" import { Link } from "@tanstack/react-router" import { useMutation as useConvexMutation } from "convex/react" @@ -30,10 +33,17 @@ import { BreadcrumbSeparator, } from "../../components/ui/breadcrumb" import { Button } from "../../components/ui/button" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../components/ui/tooltip" +import { useFileDrop } from "../../files/use-file-drop" +import { cn } from "../../lib/utils" import { DirectoryPageContext } from "./context" import { DirectoryContentTable } from "./directory-content-table" import { RenameFileDialog } from "./rename-file-dialog" -import { newItemKindAtom, openedFileAtom } from "./state" +import { dragInfoAtom, newItemKindAtom, openedFileAtom } from "./state" export function DirectoryPage() { return ( @@ -57,12 +67,10 @@ export function DirectoryPage() { function FilePathBreadcrumb() { const { rootDirectory, directory } = useContext(DirectoryPageContext) - console.log(directory.path) - const breadcrumbItems: React.ReactNode[] = [] for (let i = 1; i < directory.path.length - 1; i++) { breadcrumbItems.push( - + , @@ -90,14 +98,29 @@ function FilePathBreadcrumb() { } function FilePathBreadcrumbItem({ component }: { component: PathComponent }) { + const { isDraggedOver, dropHandlers } = useFileDrop({ + item: component.handle as DirectoryHandle, + dragInfoAtom, + }) + + const dirName = component.name || "All Files" + return ( - - - - {component.name || "All Files"} - - - + + + + + + {dirName} + + + + + Move to {dirName} + ) } diff --git a/packages/web/src/files/use-file-drop.ts b/packages/web/src/files/use-file-drop.ts index e2fcecb..e297b54 100644 --- a/packages/web/src/files/use-file-drop.ts +++ b/packages/web/src/files/use-file-drop.ts @@ -1,6 +1,10 @@ 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 type { + DirectoryHandle, + FileHandle, + FileSystemHandle, +} from "@fileone/convex/model/filesystem" import { useMutation } from "@tanstack/react-query" import { useMutation as useContextMutation } from "convex/react" import type { Atom } from "jotai" @@ -9,14 +13,17 @@ import { useState } from "react" import { toast } from "sonner" export interface FileDragInfo { - source: DirectoryItem - items: Id<"files">[] + source: FileSystemHandle + items: FileHandle[] } export interface UseFileDropOptions { - item: DirectoryItem + item: DirectoryHandle | null dragInfoAtom: Atom - onDropSuccess?: (items: Id<"files">[], targetDirectory: Doc<"directories">) => void + onDropSuccess?: ( + items: Id<"files">[], + targetDirectory: Doc<"directories">, + ) => void } export interface UseFileDropReturn { @@ -54,10 +61,10 @@ export function useFileDrop({ const handleDrop = (_e: React.DragEvent) => { const dragInfo = store.get(dragInfoAtom) - if (dragInfo && item.kind === "directory") { + if (dragInfo && item) { moveFiles({ - targetDirectoryId: item.doc._id, - items: dragInfo.items, + targetDirectoryId: item.id, + items: dragInfo.items.map((item) => item.id), }) } setIsDraggedOver(false) @@ -65,11 +72,7 @@ export function useFileDrop({ const handleDragOver = (e: React.DragEvent) => { const dragInfo = store.get(dragInfoAtom) - if ( - dragInfo && - dragInfo.source !== item && - item.kind === "directory" - ) { + if (dragInfo && item) { e.preventDefault() e.dataTransfer.dropEffect = "move" setIsDraggedOver(true) @@ -90,4 +93,4 @@ export function useFileDrop({ onDragLeave: handleDragLeave, }, } -} \ No newline at end of file +}