diff --git a/apps/drive-web/src/dashboard/dashboard-sidebar.tsx b/apps/drive-web/src/dashboard/dashboard-sidebar.tsx index 0a86991..42c695e 100644 --- a/apps/drive-web/src/dashboard/dashboard-sidebar.tsx +++ b/apps/drive-web/src/dashboard/dashboard-sidebar.tsx @@ -1,15 +1,25 @@ import { api } from "@fileone/convex/api" -import { Link, useLocation } from "@tanstack/react-router" -import { useQuery as useConvexQuery } from "convex/react" -import { useAtomValue } from "jotai" +import { newDirectoryHandle } from "@fileone/convex/filesystem" +import { useMutation } from "@tanstack/react-query" +import { Link, useLocation, useParams } from "@tanstack/react-router" import { + useMutation as useConvexMutation, + useQuery as useConvexQuery, +} from "convex/react" +import { useAtomValue, useSetAtom, useStore } from "jotai" +import { + CircleXIcon, ClockIcon, FilesIcon, + FolderInputIcon, LogOutIcon, + ScissorsIcon, SettingsIcon, TrashIcon, User2Icon, } from "lucide-react" +import { toast } from "sonner" +import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { DropdownMenu, DropdownMenuContent, @@ -26,7 +36,10 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar" +import { formatError } from "@/lib/error" +import { Button } from "../components/ui/button" import { LoadingSpinner } from "../components/ui/loading-spinner" +import { clearCutItemsAtom, cutHandlesAtom } from "../files/store" import { backgroundTaskProgressAtom } from "./state" export function DashboardSidebar() { @@ -46,6 +59,7 @@ export function DashboardSidebar() { + @@ -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 ( + + + + +
+ {cutHandles.length} Cut + Items +
+
+
+ + + + +
+
+ ) +} + function UserMenu() { function handleSignOut() {} diff --git a/apps/drive-web/src/files/store.ts b/apps/drive-web/src/files/store.ts index b74abde..f524989 100644 --- a/apps/drive-web/src/files/store.ts +++ b/apps/drive-web/src/files/store.ts @@ -1,3 +1,4 @@ +import type { FileSystemHandle } from "@fileone/convex/filesystem" import { atom } from "jotai" import { atomFamily } from "jotai/utils" @@ -92,3 +93,8 @@ export const hasFileUploadsErrorAtom = atom((get) => { } return false }) + +export const cutHandlesAtom = atom([]) +export const clearCutItemsAtom = atom(null, (_, set) => { + set(cutHandlesAtom, []) +}) diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx index 9361bd0..fa2bb25 100644 --- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx +++ b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx @@ -17,6 +17,7 @@ import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai" import { ChevronDownIcon, PlusIcon, + ScissorsIcon, TextCursorInputIcon, TrashIcon, } 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 { DirectoryPathBreadcrumb } from "@/directories/directory-path-breadcrumb" 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 type { FileDragInfo } from "@/files/use-file-drop" @@ -250,6 +251,7 @@ function DirectoryContentContextMenu({ const [target, setTarget] = useAtom(contextMenuTargetItemsAtom) const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) + const setCutHandles = useSetAtom(cutHandlesAtom) const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash) 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 selectedItems = store.get(contextMenuTargetItemsAtom) if (selectedItems.length > 0) { @@ -314,6 +323,10 @@ function DirectoryContentContextMenu({ {target.length > 0 && ( + + + Cut +