feat: implement file cutting

This commit is contained in:
2025-11-08 23:17:36 +00:00
parent ad99bca7fd
commit 879287f8bf
3 changed files with 124 additions and 4 deletions

View File

@@ -1,15 +1,25 @@
import { api } from "@fileone/convex/api" import { api } from "@fileone/convex/api"
import { Link, useLocation } from "@tanstack/react-router" import { newDirectoryHandle } from "@fileone/convex/filesystem"
import { useQuery as useConvexQuery } from "convex/react" import { useMutation } from "@tanstack/react-query"
import { useAtomValue } from "jotai" import { Link, useLocation, useParams } from "@tanstack/react-router"
import { import {
useMutation as useConvexMutation,
useQuery as useConvexQuery,
} from "convex/react"
import { useAtomValue, useSetAtom, useStore } from "jotai"
import {
CircleXIcon,
ClockIcon, ClockIcon,
FilesIcon, FilesIcon,
FolderInputIcon,
LogOutIcon, LogOutIcon,
ScissorsIcon,
SettingsIcon, SettingsIcon,
TrashIcon, TrashIcon,
User2Icon, User2Icon,
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner"
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -26,7 +36,10 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { formatError } from "@/lib/error"
import { Button } from "../components/ui/button"
import { LoadingSpinner } from "../components/ui/loading-spinner" import { LoadingSpinner } from "../components/ui/loading-spinner"
import { clearCutItemsAtom, cutHandlesAtom } from "../files/store"
import { backgroundTaskProgressAtom } from "./state" import { backgroundTaskProgressAtom } from "./state"
export function DashboardSidebar() { export function DashboardSidebar() {
@@ -46,6 +59,7 @@ export function DashboardSidebar() {
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<SidebarMenu> <SidebarMenu>
<CutItemsCard />
<BackgroundTaskProgressItem /> <BackgroundTaskProgressItem />
</SidebarMenu> </SidebarMenu>
</SidebarFooter> </SidebarFooter>
@@ -134,6 +148,93 @@ function BackgroundTaskProgressItem() {
) )
} }
/**
* Displays the number of cut items and allows the user to perform actions on them, such as moving them to a target directory.
* Visible when there are cut items.
*/
function CutItemsCard() {
const { directoryId } = useParams({ strict: false })
const cutHandles = useAtomValue(cutHandlesAtom)
const clearCutItems = useSetAtom(clearCutItemsAtom)
const setCutHandles = useSetAtom(cutHandlesAtom)
const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom)
const store = useStore()
const _moveItems = useConvexMutation(api.filesystem.moveItems)
const { mutate: moveItems } = useMutation({
mutationFn: _moveItems,
onMutate: () => {
setBackgroundTaskProgress({
label: "Moving items…",
})
const cutHandles = store.get(cutHandlesAtom)
clearCutItems()
return { cutHandles }
},
onError: (error, _variables, context) => {
if (context?.cutHandles) {
setCutHandles(context.cutHandles)
}
toast.error("Failed to move items", {
description: formatError(error),
})
},
onSuccess: () => {
toast.success("Items moved")
},
onSettled: () => {
setBackgroundTaskProgress(null)
},
})
if (cutHandles.length === 0) return null
const moveCutItems = () => {
if (directoryId) {
moveItems({
targetDirectory: newDirectoryHandle(directoryId),
items: cutHandles,
})
}
}
return (
<SidebarMenuItem>
<Card className="p-0 gap-0 rounded-md overflow-clip">
<CardHeader className="px-3.5 py-1.5! gap-0 border-b border-b-primary-foreground/10 bg-primary text-primary-foreground">
<CardTitle className="p-0 m-0 text-xs uppercase">
<div className="flex items-center gap-1.5">
<ScissorsIcon size={16} /> {cutHandles.length} Cut
Items
</div>
</CardTitle>
</CardHeader>
<CardFooter className="p-1 flex flex-col">
<Button
size="sm"
variant="ghost"
className="w-full justify-start transition-none"
disabled={!directoryId}
onClick={moveCutItems}
>
<FolderInputIcon size={16} />
Move items here
</Button>
<Button
size="sm"
variant="ghost"
className="w-full justify-start transition-none"
onClick={() => clearCutItems()}
>
<CircleXIcon size={16} />
Clear
</Button>
</CardFooter>
</Card>
</SidebarMenuItem>
)
}
function UserMenu() { function UserMenu() {
function handleSignOut() {} function handleSignOut() {}

View File

@@ -1,3 +1,4 @@
import type { FileSystemHandle } from "@fileone/convex/filesystem"
import { atom } from "jotai" import { atom } from "jotai"
import { atomFamily } from "jotai/utils" import { atomFamily } from "jotai/utils"
@@ -92,3 +93,8 @@ export const hasFileUploadsErrorAtom = atom((get) => {
} }
return false return false
}) })
export const cutHandlesAtom = atom<FileSystemHandle[]>([])
export const clearCutItemsAtom = atom(null, (_, set) => {
set(cutHandlesAtom, [])
})

View File

@@ -17,6 +17,7 @@ import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
import { import {
ChevronDownIcon, ChevronDownIcon,
PlusIcon, PlusIcon,
ScissorsIcon,
TextCursorInputIcon, TextCursorInputIcon,
TrashIcon, TrashIcon,
} from "lucide-react" } from "lucide-react"
@@ -46,7 +47,7 @@ import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-d
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog" import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
import { DirectoryPathBreadcrumb } from "@/directories/directory-path-breadcrumb" import { DirectoryPathBreadcrumb } from "@/directories/directory-path-breadcrumb"
import { FilePreviewDialog } from "@/files/file-preview-dialog" import { FilePreviewDialog } from "@/files/file-preview-dialog"
import { inProgressFileUploadCountAtom } from "@/files/store" import { cutHandlesAtom, inProgressFileUploadCountAtom } from "@/files/store"
import { UploadFileDialog } from "@/files/upload-file-dialog" import { UploadFileDialog } from "@/files/upload-file-dialog"
import type { FileDragInfo } from "@/files/use-file-drop" import type { FileDragInfo } from "@/files/use-file-drop"
@@ -250,6 +251,7 @@ function DirectoryContentContextMenu({
const [target, setTarget] = useAtom(contextMenuTargetItemsAtom) const [target, setTarget] = useAtom(contextMenuTargetItemsAtom)
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom)
const setCutHandles = useSetAtom(cutHandlesAtom)
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash) const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
const { mutate: moveToTrash } = useMutation({ const { mutate: moveToTrash } = useMutation({
@@ -293,6 +295,13 @@ function DirectoryContentContextMenu({
}, },
}) })
const handleCut = () => {
const selectedItems = store.get(contextMenuTargetItemsAtom)
if (selectedItems.length > 0) {
setCutHandles(selectedItems.map(newFileSystemHandle))
}
}
const handleDelete = () => { const handleDelete = () => {
const selectedItems = store.get(contextMenuTargetItemsAtom) const selectedItems = store.get(contextMenuTargetItemsAtom)
if (selectedItems.length > 0) { if (selectedItems.length > 0) {
@@ -314,6 +323,10 @@ function DirectoryContentContextMenu({
{target.length > 0 && ( {target.length > 0 && (
<ContextMenuContent> <ContextMenuContent>
<RenameMenuItem /> <RenameMenuItem />
<ContextMenuItem onClick={handleCut}>
<ScissorsIcon />
Cut
</ContextMenuItem>
<ContextMenuItem <ContextMenuItem
variant="destructive" variant="destructive"
onClick={handleDelete} onClick={handleDelete}