2026-03-27 01:39:15 -07:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronRight,
|
2026-04-02 22:21:01 +05:30
|
|
|
Eye,
|
|
|
|
|
EyeOff,
|
2026-03-27 01:39:15 -07:00
|
|
|
Folder,
|
|
|
|
|
FolderOpen,
|
|
|
|
|
FolderPlus,
|
|
|
|
|
MoreHorizontal,
|
|
|
|
|
Move,
|
2026-03-27 23:26:12 +05:30
|
|
|
PenLine,
|
2026-04-02 22:21:01 +05:30
|
|
|
RefreshCw,
|
2026-03-27 01:39:15 -07:00
|
|
|
Trash2,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
|
import { useDrag, useDrop } from "react-dnd";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-03-27 17:58:04 -07:00
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2026-03-27 01:39:15 -07:00
|
|
|
import {
|
|
|
|
|
ContextMenu,
|
|
|
|
|
ContextMenuContent,
|
|
|
|
|
ContextMenuItem,
|
|
|
|
|
ContextMenuTrigger,
|
|
|
|
|
} from "@/components/ui/context-menu";
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2026-03-28 16:39:46 -07:00
|
|
|
import type { FolderSelectionState } from "./FolderTreeView";
|
2026-03-27 01:39:15 -07:00
|
|
|
|
|
|
|
|
export const DND_TYPES = {
|
|
|
|
|
FOLDER: "FOLDER",
|
|
|
|
|
DOCUMENT: "DOCUMENT",
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
type DropZone = "top" | "middle" | "bottom";
|
|
|
|
|
|
|
|
|
|
export interface FolderDisplay {
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
position: string;
|
|
|
|
|
parentId: number | null;
|
|
|
|
|
searchSpaceId: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FolderNodeProps {
|
|
|
|
|
folder: FolderDisplay;
|
|
|
|
|
depth: number;
|
|
|
|
|
isExpanded: boolean;
|
|
|
|
|
isRenaming: boolean;
|
|
|
|
|
childCount: number;
|
2026-03-27 17:58:04 -07:00
|
|
|
selectionState: FolderSelectionState;
|
|
|
|
|
onToggleSelect: (folderId: number, selectAll: boolean) => void;
|
2026-03-27 01:39:15 -07:00
|
|
|
onToggleExpand: (folderId: number) => void;
|
|
|
|
|
onRename: (folder: FolderDisplay, newName: string) => void;
|
|
|
|
|
onStartRename: (folderId: number) => void;
|
|
|
|
|
onCancelRename: () => void;
|
|
|
|
|
onDelete: (folder: FolderDisplay) => void;
|
|
|
|
|
onMove: (folder: FolderDisplay) => void;
|
|
|
|
|
onCreateSubfolder: (parentId: number) => void;
|
2026-03-27 03:17:05 -07:00
|
|
|
onDropIntoFolder?: (
|
|
|
|
|
itemType: "folder" | "document",
|
|
|
|
|
itemId: number,
|
|
|
|
|
targetFolderId: number
|
|
|
|
|
) => void;
|
2026-03-27 01:39:15 -07:00
|
|
|
onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void;
|
|
|
|
|
siblingPositions?: { before: string | null; after: string | null };
|
|
|
|
|
disabledDropIds?: Set<number>;
|
2026-03-27 23:14:10 +05:30
|
|
|
contextMenuOpen?: boolean;
|
|
|
|
|
onContextMenuOpenChange?: (open: boolean) => void;
|
2026-04-02 22:21:01 +05:30
|
|
|
isWatched?: boolean;
|
|
|
|
|
onRescan?: (folder: FolderDisplay) => void;
|
|
|
|
|
onStopWatching?: (folder: FolderDisplay) => void;
|
2026-04-02 23:46:21 +05:30
|
|
|
onViewMetadata?: (folder: FolderDisplay) => void;
|
2026-03-27 01:39:15 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 03:17:05 -07:00
|
|
|
function getDropZone(
|
|
|
|
|
monitor: { getClientOffset: () => { y: number } | null },
|
|
|
|
|
element: HTMLElement
|
|
|
|
|
): DropZone {
|
2026-03-27 01:39:15 -07:00
|
|
|
const offset = monitor.getClientOffset();
|
|
|
|
|
if (!offset) return "middle";
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
const y = offset.y - rect.top;
|
|
|
|
|
const pct = y / rect.height;
|
|
|
|
|
if (pct < 0.25) return "top";
|
|
|
|
|
if (pct > 0.75) return "bottom";
|
|
|
|
|
return "middle";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const FolderNode = React.memo(function FolderNode({
|
|
|
|
|
folder,
|
|
|
|
|
depth,
|
|
|
|
|
isExpanded,
|
|
|
|
|
isRenaming,
|
|
|
|
|
childCount,
|
2026-03-27 17:58:04 -07:00
|
|
|
selectionState,
|
|
|
|
|
onToggleSelect,
|
2026-03-27 01:39:15 -07:00
|
|
|
onToggleExpand,
|
|
|
|
|
onRename,
|
|
|
|
|
onStartRename,
|
|
|
|
|
onCancelRename,
|
|
|
|
|
onDelete,
|
|
|
|
|
onMove,
|
|
|
|
|
onCreateSubfolder,
|
|
|
|
|
onDropIntoFolder,
|
|
|
|
|
onReorderFolder,
|
|
|
|
|
siblingPositions,
|
|
|
|
|
disabledDropIds,
|
2026-03-27 23:14:10 +05:30
|
|
|
contextMenuOpen,
|
|
|
|
|
onContextMenuOpenChange,
|
2026-04-02 22:21:01 +05:30
|
|
|
isWatched,
|
|
|
|
|
onRescan,
|
|
|
|
|
onStopWatching,
|
2026-04-02 23:46:21 +05:30
|
|
|
onViewMetadata,
|
2026-03-27 01:39:15 -07:00
|
|
|
}: FolderNodeProps) {
|
|
|
|
|
const [renameValue, setRenameValue] = useState(folder.name);
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const rowRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [dropZone, setDropZone] = useState<DropZone | null>(null);
|
|
|
|
|
|
|
|
|
|
const [{ isDragging }, drag] = useDrag(
|
|
|
|
|
() => ({
|
|
|
|
|
type: DND_TYPES.FOLDER,
|
|
|
|
|
item: { id: folder.id, position: folder.position, parentId: folder.parentId },
|
|
|
|
|
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
|
|
|
|
}),
|
2026-03-27 03:17:05 -07:00
|
|
|
[folder.id, folder.position, folder.parentId]
|
2026-03-27 01:39:15 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const [{ isOver, canDrop }, drop] = useDrop(
|
|
|
|
|
() => ({
|
|
|
|
|
accept: [DND_TYPES.FOLDER, DND_TYPES.DOCUMENT],
|
|
|
|
|
canDrop: (item: { id: number }) => {
|
|
|
|
|
if (item.id === folder.id) return false;
|
|
|
|
|
if (disabledDropIds?.has(item.id)) return false;
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
hover: (_item, monitor) => {
|
|
|
|
|
if (!rowRef.current || !monitor.isOver({ shallow: true })) {
|
|
|
|
|
setDropZone(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setDropZone(getDropZone(monitor, rowRef.current));
|
|
|
|
|
},
|
|
|
|
|
drop: (item: { id: number }, monitor) => {
|
|
|
|
|
if (!rowRef.current) return;
|
|
|
|
|
const zone = getDropZone(monitor, rowRef.current);
|
|
|
|
|
const type = monitor.getItemType();
|
|
|
|
|
|
|
|
|
|
if (zone === "middle") {
|
|
|
|
|
if (type === DND_TYPES.FOLDER) {
|
|
|
|
|
onDropIntoFolder?.("folder", item.id, folder.id);
|
|
|
|
|
} else {
|
|
|
|
|
onDropIntoFolder?.("document", item.id, folder.id);
|
|
|
|
|
}
|
|
|
|
|
} else if (type === DND_TYPES.FOLDER && onReorderFolder && siblingPositions) {
|
|
|
|
|
if (zone === "top") {
|
|
|
|
|
onReorderFolder(item.id, siblingPositions.before, folder.position);
|
|
|
|
|
} else {
|
|
|
|
|
onReorderFolder(item.id, folder.position, siblingPositions.after);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setDropZone(null);
|
|
|
|
|
},
|
|
|
|
|
collect: (monitor) => ({
|
|
|
|
|
isOver: monitor.isOver({ shallow: true }),
|
|
|
|
|
canDrop: monitor.canDrop(),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
2026-03-27 03:17:05 -07:00
|
|
|
[
|
|
|
|
|
folder.id,
|
|
|
|
|
folder.position,
|
|
|
|
|
disabledDropIds,
|
|
|
|
|
onDropIntoFolder,
|
|
|
|
|
onReorderFolder,
|
|
|
|
|
siblingPositions,
|
|
|
|
|
]
|
2026-03-27 01:39:15 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOver) setDropZone(null);
|
|
|
|
|
}, [isOver]);
|
|
|
|
|
|
|
|
|
|
const attachRef = useCallback(
|
|
|
|
|
(node: HTMLDivElement | null) => {
|
|
|
|
|
rowRef.current = node;
|
|
|
|
|
drag(drop(node));
|
|
|
|
|
},
|
2026-03-27 03:17:05 -07:00
|
|
|
[drag, drop]
|
2026-03-27 01:39:15 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isRenaming && inputRef.current) {
|
|
|
|
|
inputRef.current.focus();
|
|
|
|
|
inputRef.current.select();
|
|
|
|
|
}
|
|
|
|
|
}, [isRenaming]);
|
|
|
|
|
|
|
|
|
|
const handleRenameSubmit = useCallback(() => {
|
|
|
|
|
const trimmed = renameValue.trim();
|
|
|
|
|
if (trimmed && trimmed !== folder.name) {
|
|
|
|
|
onRename(folder, trimmed);
|
|
|
|
|
}
|
|
|
|
|
onCancelRename();
|
|
|
|
|
}, [renameValue, folder, onRename, onCancelRename]);
|
|
|
|
|
|
|
|
|
|
const handleRenameKeyDown = useCallback(
|
|
|
|
|
(e: React.KeyboardEvent) => {
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleRenameSubmit();
|
|
|
|
|
} else if (e.key === "Escape") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setRenameValue(folder.name);
|
|
|
|
|
onCancelRename();
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-03-27 03:17:05 -07:00
|
|
|
[handleRenameSubmit, folder.name, onCancelRename]
|
2026-03-27 01:39:15 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const startRename = useCallback(() => {
|
|
|
|
|
setRenameValue(folder.name);
|
|
|
|
|
onStartRename(folder.id);
|
|
|
|
|
}, [folder, onStartRename]);
|
|
|
|
|
|
2026-03-27 17:58:04 -07:00
|
|
|
const handleCheckChange = useCallback(() => {
|
|
|
|
|
onToggleSelect(folder.id, selectionState !== "all");
|
|
|
|
|
}, [folder.id, selectionState, onToggleSelect]);
|
|
|
|
|
|
2026-03-27 01:39:15 -07:00
|
|
|
const FolderIcon = isExpanded ? FolderOpen : Folder;
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-27 23:14:10 +05:30
|
|
|
<ContextMenu onOpenChange={onContextMenuOpenChange}>
|
2026-03-27 01:39:15 -07:00
|
|
|
<ContextMenuTrigger asChild disabled={isRenaming}>
|
2026-03-27 03:17:05 -07:00
|
|
|
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag/drop refs */}
|
2026-03-27 01:39:15 -07:00
|
|
|
<div
|
|
|
|
|
ref={attachRef}
|
2026-03-27 03:17:05 -07:00
|
|
|
role="button"
|
|
|
|
|
tabIndex={0}
|
2026-03-27 01:39:15 -07:00
|
|
|
className={cn(
|
|
|
|
|
"group relative flex h-8 items-center gap-1 rounded-md px-1 text-sm hover:bg-accent/50 cursor-pointer select-none",
|
|
|
|
|
isExpanded && "font-medium",
|
|
|
|
|
isDragging && "opacity-40",
|
|
|
|
|
isOver && canDrop && dropZone === "middle" && "bg-accent ring-1 ring-primary/40",
|
|
|
|
|
isOver && canDrop && dropZone === "top" && "border-t-2 border-primary",
|
|
|
|
|
isOver && canDrop && dropZone === "bottom" && "border-b-2 border-primary",
|
2026-03-27 03:17:05 -07:00
|
|
|
isOver && !canDrop && "cursor-not-allowed"
|
2026-03-27 01:39:15 -07:00
|
|
|
)}
|
|
|
|
|
style={{ paddingLeft: `${depth * 16 + 4}px` }}
|
2026-04-02 23:46:21 +05:30
|
|
|
onClick={(e) => {
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && onViewMetadata) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onViewMetadata(folder);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onToggleExpand(folder.id);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onToggleExpand(folder.id);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-03-27 01:39:15 -07:00
|
|
|
onDoubleClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startRename();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
|
|
|
|
|
{isExpanded ? (
|
|
|
|
|
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
|
2026-03-27 17:58:04 -07:00
|
|
|
<Checkbox
|
2026-03-28 16:39:46 -07:00
|
|
|
checked={
|
|
|
|
|
selectionState === "all" ? true : selectionState === "some" ? "indeterminate" : false
|
|
|
|
|
}
|
2026-03-27 17:58:04 -07:00
|
|
|
onCheckedChange={handleCheckChange}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
|
2026-03-27 01:39:15 -07:00
|
|
|
<FolderIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
|
|
|
|
|
|
|
|
{isRenaming ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
type="text"
|
|
|
|
|
value={renameValue}
|
|
|
|
|
onChange={(e) => setRenameValue(e.target.value)}
|
|
|
|
|
onBlur={handleRenameSubmit}
|
|
|
|
|
onKeyDown={handleRenameKeyDown}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
2026-03-27 23:26:12 +05:30
|
|
|
placeholder="Enter folder name"
|
|
|
|
|
className="flex-1 min-w-0 bg-transparent px-1 py-0.5 text-sm outline-none caret-primary placeholder:text-muted-foreground/50"
|
2026-03-27 01:39:15 -07:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="flex-1 min-w-0 truncate">{folder.name}</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!isRenaming && childCount > 0 && (
|
|
|
|
|
<span className="shrink-0 text-[10px] text-muted-foreground tabular-nums">
|
|
|
|
|
{childCount}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!isRenaming && (
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2026-03-27 23:14:10 +05:30
|
|
|
className="hidden sm:inline-flex h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
2026-03-27 01:39:15 -07:00
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
2026-04-02 22:21:01 +05:30
|
|
|
<DropdownMenuContent align="end" className="w-40">
|
|
|
|
|
{isWatched && onRescan && (
|
2026-03-27 01:39:15 -07:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
2026-04-02 22:21:01 +05:30
|
|
|
onRescan(folder);
|
2026-03-27 01:39:15 -07:00
|
|
|
}}
|
|
|
|
|
>
|
2026-04-02 22:21:01 +05:30
|
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
|
|
|
Re-scan
|
2026-03-27 01:39:15 -07:00
|
|
|
</DropdownMenuItem>
|
2026-04-02 22:21:01 +05:30
|
|
|
)}
|
|
|
|
|
{isWatched && onStopWatching && (
|
2026-03-27 01:39:15 -07:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
2026-04-02 22:21:01 +05:30
|
|
|
onStopWatching(folder);
|
2026-03-27 01:39:15 -07:00
|
|
|
}}
|
|
|
|
|
>
|
2026-04-02 22:21:01 +05:30
|
|
|
<EyeOff className="mr-2 h-4 w-4" />
|
|
|
|
|
Stop watching
|
2026-03-27 01:39:15 -07:00
|
|
|
</DropdownMenuItem>
|
2026-04-02 22:21:01 +05:30
|
|
|
)}
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onCreateSubfolder(folder.id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<FolderPlus className="mr-2 h-4 w-4" />
|
|
|
|
|
New subfolder
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
startRename();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<PenLine className="mr-2 h-4 w-4" />
|
|
|
|
|
Rename
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onMove(folder);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Move className="mr-2 h-4 w-4" />
|
|
|
|
|
Move to...
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
className="text-destructive focus:text-destructive"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onDelete(folder);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
Delete
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
2026-03-27 01:39:15 -07:00
|
|
|
</DropdownMenu>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</ContextMenuTrigger>
|
|
|
|
|
|
2026-04-02 22:21:01 +05:30
|
|
|
{!isRenaming && contextMenuOpen && (
|
|
|
|
|
<ContextMenuContent className="w-40">
|
|
|
|
|
{isWatched && onRescan && (
|
|
|
|
|
<ContextMenuItem onClick={() => onRescan(folder)}>
|
|
|
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
|
|
|
Re-scan
|
2026-03-27 01:39:15 -07:00
|
|
|
</ContextMenuItem>
|
2026-04-02 22:21:01 +05:30
|
|
|
)}
|
|
|
|
|
{isWatched && onStopWatching && (
|
|
|
|
|
<ContextMenuItem onClick={() => onStopWatching(folder)}>
|
|
|
|
|
<EyeOff className="mr-2 h-4 w-4" />
|
|
|
|
|
Stop watching
|
2026-03-27 01:39:15 -07:00
|
|
|
</ContextMenuItem>
|
2026-04-02 22:21:01 +05:30
|
|
|
)}
|
|
|
|
|
<ContextMenuItem onClick={() => onCreateSubfolder(folder.id)}>
|
|
|
|
|
<FolderPlus className="mr-2 h-4 w-4" />
|
|
|
|
|
New subfolder
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
<ContextMenuItem onClick={() => startRename()}>
|
|
|
|
|
<PenLine className="mr-2 h-4 w-4" />
|
|
|
|
|
Rename
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
<ContextMenuItem onClick={() => onMove(folder)}>
|
|
|
|
|
<Move className="mr-2 h-4 w-4" />
|
|
|
|
|
Move to...
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
<ContextMenuItem
|
|
|
|
|
className="text-destructive focus:text-destructive"
|
|
|
|
|
onClick={() => onDelete(folder)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
Delete
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
</ContextMenuContent>
|
|
|
|
|
)}
|
2026-03-27 01:39:15 -07:00
|
|
|
</ContextMenu>
|
|
|
|
|
);
|
|
|
|
|
});
|