feat: allow file drop on path breadcrumb
This commit is contained in:
@@ -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)
|
||||||
|
@@ -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 }
|
||||||
|
}
|
||||||
|
@@ -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],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user