feat: allow file drop on path breadcrumb

This commit is contained in:
2025-09-20 23:54:27 +00:00
parent 0f5b1f79ff
commit 7eefe2b96e
5 changed files with 98 additions and 35 deletions

View File

@@ -5,6 +5,7 @@ import type {
} from "../functions" } from "../functions"
import * as Err from "./error" import * as Err from "./error"
import type { FilePath, ReverseFilePath } from "./filesystem" import type { FilePath, ReverseFilePath } from "./filesystem"
import { newDirectoryHandle } from "./filesystem"
type Directory = { type Directory = {
kind: "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 let parentDirId = directory.parentId
while (parentDirId) { while (parentDirId) {
const parentDir = await ctx.db.get(parentDirId) const parentDir = await ctx.db.get(parentDirId)
if (parentDir) { if (parentDir) {
path.push({ id: parentDir._id, name: parentDir.name }) path.push({
handle: newDirectoryHandle(parentDir._id),
name: parentDir.name,
})
parentDirId = parentDir.parentId parentDirId = parentDir.parentId
} else { } else {
throw Err.create(Err.Code.Internal) throw Err.create(Err.Code.Internal)

View File

@@ -1,12 +1,12 @@
import type { Id } from "../_generated/dataModel" import type { Id } from "../_generated/dataModel"
export type DirectoryPathComponent = { export type DirectoryPathComponent = {
id: Id<"directories"> handle: DirectoryHandle
name: string name: string
} }
export type FilePathComponent = { export type FilePathComponent = {
id: Id<"files"> handle: FileHandle
name: string name: string
} }
@@ -15,3 +15,23 @@ export type PathComponent = FilePathComponent | DirectoryPathComponent
export type FilePath = [...DirectoryPathComponent[], PathComponent] export type FilePath = [...DirectoryPathComponent[], PathComponent]
export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]] 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 }
}

View File

@@ -1,6 +1,10 @@
import { api } from "@fileone/convex/_generated/api" 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 type { DirectoryItem } from "@fileone/convex/model/directories"
import {
newDirectoryHandle,
newFileHandle,
} from "@fileone/convex/model/filesystem"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import { import {
@@ -13,7 +17,7 @@ import {
import { useMutation as useContextMutation } from "convex/react" import { useMutation as useContextMutation } from "convex/react"
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai" import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
import { CheckIcon, TextCursorInputIcon, TrashIcon, XIcon } from "lucide-react" 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 { toast } from "sonner"
import { DirectoryIcon } from "@/components/icons/directory-icon" import { DirectoryIcon } from "@/components/icons/directory-icon"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
@@ -402,7 +406,10 @@ function FileItemRow({
const setDragInfo = useSetAtom(dragInfoAtom) const setDragInfo = useSetAtom(dragInfoAtom)
const { isDraggedOver, dropHandlers } = useFileDrop({ const { isDraggedOver, dropHandlers } = useFileDrop({
item: row.original, item:
row.original.kind === "directory"
? newDirectoryHandle(row.original.doc._id)
: null,
dragInfoAtom, dragInfoAtom,
}) })
@@ -412,9 +419,10 @@ function FileItemRow({
"application/x-internal", "application/x-internal",
JSON.stringify(row.original), JSON.stringify(row.original),
) )
const fileHandle = newFileHandle(row.original.doc._id)
setDragInfo({ setDragInfo({
source: row.original, source: fileHandle,
items: [row.original.doc._id], items: [fileHandle],
}) })
} }
} }

View File

@@ -1,5 +1,8 @@
import { api } from "@fileone/convex/_generated/api" 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 { useMutation } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import { useMutation as useConvexMutation } from "convex/react" import { useMutation as useConvexMutation } from "convex/react"
@@ -30,10 +33,17 @@ import {
BreadcrumbSeparator, BreadcrumbSeparator,
} from "../../components/ui/breadcrumb" } from "../../components/ui/breadcrumb"
import { Button } from "../../components/ui/button" 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 { DirectoryPageContext } from "./context"
import { DirectoryContentTable } from "./directory-content-table" import { DirectoryContentTable } from "./directory-content-table"
import { RenameFileDialog } from "./rename-file-dialog" import { RenameFileDialog } from "./rename-file-dialog"
import { newItemKindAtom, openedFileAtom } from "./state" import { dragInfoAtom, newItemKindAtom, openedFileAtom } from "./state"
export function DirectoryPage() { export function DirectoryPage() {
return ( return (
@@ -57,12 +67,10 @@ export function DirectoryPage() {
function FilePathBreadcrumb() { function FilePathBreadcrumb() {
const { rootDirectory, directory } = useContext(DirectoryPageContext) const { rootDirectory, directory } = useContext(DirectoryPageContext)
console.log(directory.path)
const breadcrumbItems: React.ReactNode[] = [] const breadcrumbItems: React.ReactNode[] = []
for (let i = 1; i < directory.path.length - 1; i++) { for (let i = 1; i < directory.path.length - 1; i++) {
breadcrumbItems.push( breadcrumbItems.push(
<Fragment key={directory.path[i]!.id}> <Fragment key={directory.path[i]!.handle.id}>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<FilePathBreadcrumbItem component={directory.path[i]!} /> <FilePathBreadcrumbItem component={directory.path[i]!} />
</Fragment>, </Fragment>,
@@ -90,14 +98,29 @@ function FilePathBreadcrumb() {
} }
function FilePathBreadcrumbItem({ component }: { component: PathComponent }) { function FilePathBreadcrumbItem({ component }: { component: PathComponent }) {
const { isDraggedOver, dropHandlers } = useFileDrop({
item: component.handle as DirectoryHandle,
dragInfoAtom,
})
const dirName = component.name || "All Files"
return ( return (
<BreadcrumbItem> <Tooltip open={isDraggedOver}>
<BreadcrumbLink asChild> <TooltipTrigger asChild>
<Link to={`/directories/${component.id}`}> <BreadcrumbItem
{component.name || "All Files"} className={cn({ "bg-muted": isDraggedOver })}
</Link> {...dropHandlers}
</BreadcrumbLink> >
</BreadcrumbItem> <BreadcrumbLink asChild>
<Link to={`/directories/${component.handle.id}`}>
{dirName}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
</TooltipTrigger>
<TooltipContent>Move to {dirName}</TooltipContent>
</Tooltip>
) )
} }

View File

@@ -1,6 +1,10 @@
import { api } from "@fileone/convex/_generated/api" import { api } from "@fileone/convex/_generated/api"
import type { Doc, Id } from "@fileone/convex/_generated/dataModel" 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 } from "@tanstack/react-query"
import { useMutation as useContextMutation } from "convex/react" import { useMutation as useContextMutation } from "convex/react"
import type { Atom } from "jotai" import type { Atom } from "jotai"
@@ -9,14 +13,17 @@ import { useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
export interface FileDragInfo { export interface FileDragInfo {
source: DirectoryItem source: FileSystemHandle
items: Id<"files">[] items: FileHandle[]
} }
export interface UseFileDropOptions { export interface UseFileDropOptions {
item: DirectoryItem item: DirectoryHandle | null
dragInfoAtom: Atom<FileDragInfo | null> dragInfoAtom: Atom<FileDragInfo | null>
onDropSuccess?: (items: Id<"files">[], targetDirectory: Doc<"directories">) => void onDropSuccess?: (
items: Id<"files">[],
targetDirectory: Doc<"directories">,
) => void
} }
export interface UseFileDropReturn { export interface UseFileDropReturn {
@@ -54,10 +61,10 @@ export function useFileDrop({
const handleDrop = (_e: React.DragEvent) => { const handleDrop = (_e: React.DragEvent) => {
const dragInfo = store.get(dragInfoAtom) const dragInfo = store.get(dragInfoAtom)
if (dragInfo && item.kind === "directory") { if (dragInfo && item) {
moveFiles({ moveFiles({
targetDirectoryId: item.doc._id, targetDirectoryId: item.id,
items: dragInfo.items, items: dragInfo.items.map((item) => item.id),
}) })
} }
setIsDraggedOver(false) setIsDraggedOver(false)
@@ -65,11 +72,7 @@ export function useFileDrop({
const handleDragOver = (e: React.DragEvent) => { const handleDragOver = (e: React.DragEvent) => {
const dragInfo = store.get(dragInfoAtom) const dragInfo = store.get(dragInfoAtom)
if ( if (dragInfo && item) {
dragInfo &&
dragInfo.source !== item &&
item.kind === "directory"
) {
e.preventDefault() e.preventDefault()
e.dataTransfer.dropEffect = "move" e.dataTransfer.dropEffect = "move"
setIsDraggedOver(true) setIsDraggedOver(true)
@@ -90,4 +93,4 @@ export function useFileDrop({
onDragLeave: handleDragLeave, onDragLeave: handleDragLeave,
}, },
} }
} }