mirror of
https://github.com/get-drexa/drive.git
synced 2025-12-01 05:51:39 +00:00
feat: add ctx menu to recent file items
This commit is contained in:
@@ -1,19 +1,84 @@
|
||||
import type { Doc } from "@fileone/convex/dataModel"
|
||||
import { TextFileIcon } from "../components/icons/text-file-icon"
|
||||
import { MiddleTruncatedText } from "../components/ui/middle-truncated-text"
|
||||
import type { Doc, Id } from "@fileone/convex/dataModel"
|
||||
import { memo, useCallback } from "react"
|
||||
import { TextFileIcon } from "@/components/icons/text-file-icon"
|
||||
import { MiddleTruncatedText } from "@/components/ui/middle-truncated-text"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type FileGridSelection = Set<Id<"files">>
|
||||
|
||||
export function FileGrid({
|
||||
files,
|
||||
selectedFiles = new Set(),
|
||||
onSelectionChange,
|
||||
onContextMenu,
|
||||
}: {
|
||||
files: Doc<"files">[]
|
||||
selectedFiles?: FileGridSelection
|
||||
onSelectionChange?: (selection: FileGridSelection) => void
|
||||
onContextMenu?: (file: Doc<"files">, event: React.MouseEvent) => void
|
||||
}) {
|
||||
const onItemSelect = useCallback(
|
||||
(file: Doc<"files">) => {
|
||||
onSelectionChange?.(new Set([file._id]))
|
||||
},
|
||||
[onSelectionChange],
|
||||
)
|
||||
|
||||
const onItemContextMenu = useCallback(
|
||||
(file: Doc<"files">, event: React.MouseEvent) => {
|
||||
onContextMenu?.(file, event)
|
||||
onSelectionChange?.(new Set([file._id]))
|
||||
},
|
||||
[onContextMenu, onSelectionChange],
|
||||
)
|
||||
|
||||
export function FileGrid({ files }: { files: Doc<"files">[] }) {
|
||||
return (
|
||||
<div className="grid auto-cols-max grid-flow-col gap-4">
|
||||
<div className="grid auto-cols-max grid-flow-col gap-3">
|
||||
{files.map((file) => (
|
||||
<div
|
||||
<FileGridItem
|
||||
selected={selectedFiles.has(file._id)}
|
||||
key={file._id}
|
||||
className="flex flex-col gap-2 items-center justify-center w-24"
|
||||
>
|
||||
<TextFileIcon className="size-10" />
|
||||
<MiddleTruncatedText>{file.name}</MiddleTruncatedText>
|
||||
</div>
|
||||
file={file}
|
||||
onSelect={onItemSelect}
|
||||
onContextMenu={onItemContextMenu}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FileGridItem = memo(function FileGridItem({
|
||||
selected,
|
||||
file,
|
||||
onSelect,
|
||||
onContextMenu,
|
||||
}: {
|
||||
selected: boolean
|
||||
file: Doc<"files">
|
||||
onSelect?: (file: Doc<"files">) => void
|
||||
onContextMenu?: (file: Doc<"files">, event: React.MouseEvent) => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={file._id}
|
||||
className={cn(
|
||||
"flex flex-col gap-2 items-center justify-center w-24 p-[calc(var(--spacing)*1+1px)] rounded-md",
|
||||
{ "bg-muted border border-border p-1": selected },
|
||||
)}
|
||||
onClick={() => {
|
||||
onSelect?.(file)
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
onContextMenu?.(file, event)
|
||||
}}
|
||||
>
|
||||
<TextFileIcon className="size-10" />
|
||||
<MiddleTruncatedText className="text-sm">
|
||||
{file.name}
|
||||
</MiddleTruncatedText>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
export { FileGridItem }
|
||||
|
||||
@@ -1,22 +1,119 @@
|
||||
import { api } from "@fileone/convex/api"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useQuery as useConvexQuery } from "convex/react"
|
||||
import type { Doc } from "@fileone/convex/dataModel"
|
||||
import { newFileHandle } from "@fileone/convex/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link } from "@tanstack/react-router"
|
||||
import {
|
||||
useMutation as useConvexMutation,
|
||||
useQuery as useConvexQuery,
|
||||
} from "convex/react"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { FolderInputIcon, TrashIcon } from "lucide-react"
|
||||
import { useCallback } from "react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import { backgroundTaskProgressAtom } from "@/dashboard/state"
|
||||
import type { FileGridSelection } from "@/files/file-grid"
|
||||
import { FileGrid } from "@/files/file-grid"
|
||||
import { formatError } from "@/lib/error"
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/_sidebar-layout/recent")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
const selectedFilesAtom = atom(new Set() as FileGridSelection)
|
||||
const contextMenuTargetItem = atom<Doc<"files"> | null>(null)
|
||||
|
||||
function RouteComponent() {
|
||||
const recentFiles = useConvexQuery(api.filesystem.fetchRecentFiles, {
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
console.log("recentFiles", recentFiles)
|
||||
|
||||
return (
|
||||
<main className="p-4">
|
||||
<FileGrid files={recentFiles ?? []} />
|
||||
<RecentFilesContextMenu>
|
||||
<RecentFilesGrid />
|
||||
</RecentFilesContextMenu>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function RecentFilesGrid() {
|
||||
const recentFiles = useConvexQuery(api.filesystem.fetchRecentFiles, {
|
||||
limit: 100,
|
||||
})
|
||||
const [selectedFiles, setSelectedFiles] = useAtom(selectedFilesAtom)
|
||||
const setContextMenuTargetItem = useSetAtom(contextMenuTargetItem)
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(file: Doc<"files">, _event: React.MouseEvent) => {
|
||||
setContextMenuTargetItem(file)
|
||||
},
|
||||
[setContextMenuTargetItem],
|
||||
)
|
||||
|
||||
return (
|
||||
<FileGrid
|
||||
files={recentFiles ?? []}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectionChange={setSelectedFiles}
|
||||
onContextMenu={handleContextMenu}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RecentFilesContextMenu({ children }: { children: React.ReactNode }) {
|
||||
const targetItem = useAtomValue(contextMenuTargetItem)
|
||||
const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom)
|
||||
|
||||
const { mutate: moveToTrash } = useMutation({
|
||||
mutationFn: useConvexMutation(api.filesystem.moveToTrash),
|
||||
onMutate: () => {
|
||||
setBackgroundTaskProgress({
|
||||
label: "Moving to trash…",
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
setBackgroundTaskProgress(null)
|
||||
toast.success("Moved to trash")
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to move to trash", {
|
||||
description: formatError(error),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div>{children}</div>
|
||||
</ContextMenuTrigger>
|
||||
{targetItem && (
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>
|
||||
<Link
|
||||
to={`/directories/${targetItem.directoryId}`}
|
||||
className="flex flex-row items-center gap-2"
|
||||
>
|
||||
<FolderInputIcon />
|
||||
Open in directory
|
||||
</Link>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
moveToTrash({
|
||||
handles: [newFileHandle(targetItem._id)],
|
||||
})
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
Move to trash
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user