From 022f3c472605a7000061d5dea83b28543913fbe0 Mon Sep 17 00:00:00 2001 From: kenneth Date: Sun, 28 Sep 2025 15:58:37 +0000 Subject: [PATCH] feat: hide rename ctx menu item in multi select Co-authored-by: Ona --- .../directory-content-context-menu.tsx | 116 ++++++++++++++++++ .../directory-content-table.tsx | 101 +-------------- 2 files changed, 120 insertions(+), 97 deletions(-) create mode 100644 packages/web/src/directories/directory-page/directory-content-context-menu.tsx diff --git a/packages/web/src/directories/directory-page/directory-content-context-menu.tsx b/packages/web/src/directories/directory-page/directory-content-context-menu.tsx new file mode 100644 index 0000000..c47b421 --- /dev/null +++ b/packages/web/src/directories/directory-page/directory-content-context-menu.tsx @@ -0,0 +1,116 @@ +import { api } from "@fileone/convex/_generated/api" +import { newFileSystemHandle } from "@fileone/convex/model/filesystem" +import { useMutation } from "@tanstack/react-query" +import { useMutation as useContextMutation } from "convex/react" +import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai" +import { TextCursorInputIcon, TrashIcon } from "lucide-react" +import { toast } from "sonner" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { + contextMenuTargeItemsAtom, + itemBeingRenamedAtom, + optimisticDeletedItemsAtom, +} from "./state" + +export function DirectoryContentContextMenu({ + children, +}: { + children: React.ReactNode +}) { + const store = useStore() + const [target, setTarget] = useAtom(contextMenuTargeItemsAtom) + const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) + const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash) + const { mutate: moveToTrash } = useMutation({ + mutationFn: moveToTrashMutation, + onMutate: ({ handles }) => { + setOptimisticDeletedItems( + (prev) => + new Set([...prev, ...handles.map((handle) => handle.id)]), + ) + }, + onSuccess: ({ deleted, errors }, { handles }) => { + setOptimisticDeletedItems((prev) => { + const newSet = new Set(prev) + for (const handle of handles) { + newSet.delete(handle.id) + } + return newSet + }) + if (errors.length === 0 && deleted.length === handles.length) { + toast.success(`Moved ${handles.length} items to trash`) + } else if (errors.length === handles.length) { + toast.error("Failed to move to trash") + } else { + toast.info( + `Moved ${deleted.length} items to trash; failed to move ${errors.length} items`, + ) + } + }, + }) + + const handleDelete = () => { + const selectedItems = store.get(contextMenuTargeItemsAtom) + if (selectedItems.length > 0) { + moveToTrash({ + handles: selectedItems.map(newFileSystemHandle), + }) + } + } + + return ( + { + if (!open) { + setTarget([]) + } + }} + > + {children} + {target && ( + + + + + Move to trash + + + )} + + ) +} + +function RenameMenuItem() { + const store = useStore() + const target = useAtomValue(contextMenuTargeItemsAtom) + const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom) + + const handleRename = () => { + const selectedItems = store.get(contextMenuTargeItemsAtom) + if (selectedItems.length === 1) { + // biome-ignore lint/style/noNonNullAssertion: length is checked + const selectedItem = selectedItems[0]! + setItemBeingRenamed({ + originalItem: selectedItem, + name: selectedItem.doc.name, + }) + } + } + + // Only render if exactly one item is selected + if (target.length !== 1) { + return null + } + + return ( + + + Rename + + ) +} \ No newline at end of file diff --git a/packages/web/src/directories/directory-page/directory-content-table.tsx b/packages/web/src/directories/directory-page/directory-content-table.tsx index 6ff0fe2..5ee68b5 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -1,4 +1,3 @@ -import { api } from "@fileone/convex/_generated/api" import type { Doc } from "@fileone/convex/_generated/dataModel" import { type DirectoryHandle, @@ -11,7 +10,6 @@ import { newFileHandle, newFileSystemHandle, } from "@fileone/convex/model/filesystem" -import { useMutation } from "@tanstack/react-query" import { Link, useNavigate } from "@tanstack/react-router" import { type ColumnDef, @@ -21,19 +19,10 @@ import { type Table as TableType, useReactTable, } from "@tanstack/react-table" -import { useMutation as useContextMutation } from "convex/react" -import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai" -import { TextCursorInputIcon, TrashIcon } from "lucide-react" +import { useAtomValue, useSetAtom, useStore } from "jotai" import { useContext, useEffect, useRef } from "react" -import { toast } from "sonner" import { DirectoryIcon } from "@/components/icons/directory-icon" import { Checkbox } from "@/components/ui/checkbox" -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@/components/ui/context-menu" import { Table, TableBody, @@ -50,10 +39,10 @@ import { TextFileIcon } from "../../components/icons/text-file-icon" import { useFileDrop } from "../../files/use-file-drop" import { cn } from "../../lib/utils" import { DirectoryPageContext } from "./context" +import { DirectoryContentContextMenu } from "./directory-content-context-menu" import { contextMenuTargeItemsAtom, dragInfoAtom, - itemBeingRenamedAtom, openedFileAtom, optimisticDeletedItemsAtom, } from "./state" @@ -134,97 +123,15 @@ const columns: ColumnDef[] = [ export function DirectoryContentTable() { return ( - +
-
+ ) } -export function DirectoryContentTableContextMenu({ - children, -}: { - children: React.ReactNode -}) { - const store = useStore() - const [target, setTarget] = useAtom(contextMenuTargeItemsAtom) - const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) - const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash) - const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom) - const { mutate: moveToTrash } = useMutation({ - mutationFn: moveToTrashMutation, - onMutate: ({ handles }) => { - setOptimisticDeletedItems( - (prev) => - new Set([...prev, ...handles.map((handle) => handle.id)]), - ) - }, - onSuccess: ({ deleted, errors }, { handles }) => { - setOptimisticDeletedItems((prev) => { - const newSet = new Set(prev) - for (const handle of handles) { - newSet.delete(handle.id) - } - return newSet - }) - if (errors.length === 0 && deleted.length === handles.length) { - toast.success(`Moved ${handles.length} items to trash`) - } else if (errors.length === handles.length) { - toast.error("Failed to move to trash") - } else { - toast.info( - `Moved ${deleted.length} items to trash; failed to move ${errors.length} items`, - ) - } - }, - }) - const handleRename = () => { - const selectedItems = store.get(contextMenuTargeItemsAtom) - if (selectedItems.length === 1) { - // biome-ignore lint/style/noNonNullAssertion: length is checked - const selectedItem = selectedItems[0]! - setItemBeingRenamed({ - originalItem: selectedItem, - name: selectedItem.doc.name, - }) - } - } - - const handleDelete = () => { - const selectedItems = store.get(contextMenuTargeItemsAtom) - if (selectedItems.length > 0) { - moveToTrash({ - handles: selectedItems.map(newFileSystemHandle), - }) - } - } - - return ( - { - if (!open) { - setTarget([]) - } - }} - > - {children} - {target && ( - - - - Rename - - - - Move to trash - - - )} - - ) -} export function DirectoryContentTableContent() { const { directoryContent } = useContext(DirectoryPageContext)