584 lines
19 KiB
TypeScript
584 lines
19 KiB
TypeScript
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdownMenu";
|
|
import { Input } from "@/components/ui/input";
|
|
import { ScrollArea } from "@/components/ui/scrollArea";
|
|
import { Folder, Prompt } from "@/lib/types/prompts";
|
|
import { cn } from "@/lib/utils";
|
|
import { DragDropProvider, useDraggable, useDroppable } from "@dnd-kit/react";
|
|
import {
|
|
ChevronDown,
|
|
ChevronRight,
|
|
FileText,
|
|
Folder as FolderIcon,
|
|
FolderOpen,
|
|
MoreHorizontal,
|
|
Pencil,
|
|
Plus,
|
|
PlusIcon,
|
|
Search,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { usePromptContext } from "../context";
|
|
|
|
/**
|
|
* Renders the prompt-manager sidebar including search, folder hierarchy, root prompts, and drag-and-drop reorganization.
|
|
*
|
|
* The sidebar supports creating, renaming, and deleting folders and prompts (when permitted), selecting prompts, auto-expanding the folder that contains the selected prompt, filtering by search query, and dragging prompts between folders or to the root. Visual drag-over feedback and permission gating for create/update/delete actions are applied.
|
|
*
|
|
* @returns The sidebar React element containing the search input, folder list, root prompt drop zone, and drag-and-drop provider.
|
|
*/
|
|
export function PromptSidebar() {
|
|
const {
|
|
folders,
|
|
prompts,
|
|
selectedPromptId,
|
|
handleSelectPrompt: onSelectPrompt,
|
|
setFolderSheet,
|
|
setDeleteFolderDialog,
|
|
setPromptSheet,
|
|
setDeletePromptDialog,
|
|
handleMovePrompt: onMovePrompt,
|
|
canCreate,
|
|
canUpdate,
|
|
canDelete,
|
|
} = usePromptContext();
|
|
|
|
const onCreateFolder = useCallback(() => setFolderSheet({ open: true }), [setFolderSheet]);
|
|
const onEditFolder = useCallback((folder: Folder) => setFolderSheet({ open: true, folder }), [setFolderSheet]);
|
|
const onDeleteFolder = useCallback((folder: Folder) => setDeleteFolderDialog({ open: true, folder }), [setDeleteFolderDialog]);
|
|
const onCreatePrompt = useCallback((folderId?: string) => setPromptSheet({ open: true, folderId }), [setPromptSheet]);
|
|
const onEditPrompt = useCallback((prompt: Prompt) => setPromptSheet({ open: true, prompt }), [setPromptSheet]);
|
|
const onDeletePrompt = useCallback((prompt: Prompt) => setDeletePromptDialog({ open: true, prompt }), [setDeletePromptDialog]);
|
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [dragOverTarget, setDragOverTarget] = useState<string | null>(null);
|
|
|
|
// Auto-expand the folder containing the selected prompt
|
|
useEffect(() => {
|
|
if (!selectedPromptId) return;
|
|
const prompt = prompts.find((p) => p.id === selectedPromptId);
|
|
if (prompt?.folder_id) {
|
|
setExpandedFolders((prev) => {
|
|
if (prev.has(prompt.folder_id!)) return prev;
|
|
const next = new Set(prev);
|
|
next.add(prompt.folder_id!);
|
|
return next;
|
|
});
|
|
}
|
|
}, [selectedPromptId, prompts]);
|
|
|
|
const toggleFolder = useCallback((folderId: string) => {
|
|
setExpandedFolders((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(folderId)) {
|
|
next.delete(folderId);
|
|
} else {
|
|
next.add(folderId);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
// Group prompts by folder, root prompts have no folder_id
|
|
const { promptsByFolder, rootPrompts } = useMemo(() => {
|
|
const map = new Map<string, Prompt[]>();
|
|
const root: Prompt[] = [];
|
|
for (const prompt of prompts) {
|
|
if (!prompt.folder_id) {
|
|
root.push(prompt);
|
|
} else {
|
|
const list = map.get(prompt.folder_id) || [];
|
|
list.push(prompt);
|
|
map.set(prompt.folder_id, list);
|
|
}
|
|
}
|
|
return { promptsByFolder: map, rootPrompts: root };
|
|
}, [prompts]);
|
|
|
|
// Filter folders and prompts based on search
|
|
const filteredData = useMemo(() => {
|
|
if (!searchQuery.trim()) {
|
|
return { folders, promptsByFolder, rootPrompts };
|
|
}
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
const matchedFolderIds = new Set<string>();
|
|
const filteredPromptsByFolder = new Map<string, Prompt[]>();
|
|
const filteredRootPrompts: Prompt[] = [];
|
|
|
|
for (const prompt of prompts) {
|
|
if (prompt.name.toLowerCase().includes(query)) {
|
|
if (!prompt.folder_id) {
|
|
filteredRootPrompts.push(prompt);
|
|
} else {
|
|
matchedFolderIds.add(prompt.folder_id);
|
|
const list = filteredPromptsByFolder.get(prompt.folder_id) || [];
|
|
list.push(prompt);
|
|
filteredPromptsByFolder.set(prompt.folder_id, list);
|
|
}
|
|
}
|
|
}
|
|
|
|
const filteredFolders = folders.filter((folder) => folder.name.toLowerCase().includes(query) || matchedFolderIds.has(folder.id));
|
|
|
|
return {
|
|
folders: filteredFolders,
|
|
promptsByFolder: filteredPromptsByFolder,
|
|
rootPrompts: filteredRootPrompts,
|
|
};
|
|
}, [folders, prompts, promptsByFolder, rootPrompts, searchQuery]);
|
|
|
|
// Prompt lookup for drag events
|
|
const promptMap = useMemo(() => {
|
|
const map = new Map<string, Prompt>();
|
|
for (const p of prompts) map.set(p.id, p);
|
|
return map;
|
|
}, [prompts]);
|
|
|
|
return (
|
|
<DragDropProvider
|
|
onDragOver={(event) => {
|
|
if (!canUpdate) return;
|
|
const targetId = event.operation.target?.id as string | undefined;
|
|
setDragOverTarget(targetId ?? null);
|
|
}}
|
|
onDragEnd={(event) => {
|
|
setDragOverTarget(null);
|
|
if (!canUpdate) return;
|
|
if (event.canceled || !onMovePrompt) return;
|
|
|
|
const sourceId = event.operation.source?.id as string | undefined;
|
|
const targetId = event.operation.target?.id as string | undefined;
|
|
if (!sourceId || !targetId) return;
|
|
|
|
const promptId = sourceId.startsWith("prompt-") ? sourceId.slice(7) : null;
|
|
if (!promptId) return;
|
|
|
|
const prompt = promptMap.get(promptId);
|
|
if (!prompt) return;
|
|
|
|
let targetFolderId: string | null = null;
|
|
if (targetId === "root-drop-zone") {
|
|
targetFolderId = null;
|
|
} else if (targetId.startsWith("folder-")) {
|
|
targetFolderId = targetId.slice(7);
|
|
} else {
|
|
return;
|
|
}
|
|
if ((prompt.folder_id ?? null) === targetFolderId) return;
|
|
onMovePrompt(promptId, targetFolderId);
|
|
}}
|
|
>
|
|
<div className="flex h-full flex-col">
|
|
{/* Search */}
|
|
<div className="flex items-center gap-2 border-b p-3">
|
|
<div className="relative grow">
|
|
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
|
|
<Input
|
|
placeholder="Search prompts..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
data-testid="sidebar-search"
|
|
className="h-8 pl-8"
|
|
/>
|
|
</div>
|
|
{canCreate && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className="h-8 w-8 shrink-0 bg-transparent"
|
|
data-testid="sidebar-create-menu"
|
|
aria-label="Create prompt or folder"
|
|
>
|
|
<PlusIcon className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
data-testid="sidebar-create-prompt"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCreatePrompt();
|
|
}}
|
|
>
|
|
New Prompt
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
data-testid="sidebar-create-folder"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCreateFolder();
|
|
}}
|
|
>
|
|
New Folder
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
|
|
<ScrollArea className="grow overflow-y-auto" viewportClassName="no-table viewport-table-height-full">
|
|
<div className="flex flex-col p-2 px-3">
|
|
{filteredData.folders.length === 0 && filteredData.rootPrompts.length === 0 ? (
|
|
<div className="text-muted-foreground py-8 text-center text-sm">{searchQuery ? "No results found" : "No prompts yet"}</div>
|
|
) : (
|
|
<>
|
|
{filteredData.folders.map((folder) => (
|
|
<DroppableFolder
|
|
key={folder.id}
|
|
folder={folder}
|
|
prompts={filteredData.promptsByFolder.get(folder.id) || promptsByFolder.get(folder.id) || []}
|
|
isExpanded={expandedFolders.has(folder.id) || !!searchQuery}
|
|
isDragOver={dragOverTarget === `folder-${folder.id}`}
|
|
selectedPromptId={selectedPromptId}
|
|
onToggle={() => toggleFolder(folder.id)}
|
|
onSelectPrompt={onSelectPrompt}
|
|
onEdit={() => onEditFolder(folder)}
|
|
onDelete={() => onDeleteFolder(folder)}
|
|
onCreatePrompt={() => onCreatePrompt(folder.id)}
|
|
onEditPrompt={onEditPrompt}
|
|
onDeletePrompt={onDeletePrompt}
|
|
canCreate={canCreate}
|
|
canUpdate={canUpdate}
|
|
canDelete={canDelete}
|
|
/>
|
|
))}
|
|
<RootDropZone
|
|
isDragOver={dragOverTarget === "root-drop-zone"}
|
|
rootPrompts={filteredData.rootPrompts}
|
|
selectedPromptId={selectedPromptId}
|
|
onSelectPrompt={onSelectPrompt}
|
|
onEditPrompt={onEditPrompt}
|
|
onDeletePrompt={onDeletePrompt}
|
|
canUpdate={canUpdate}
|
|
canDelete={canDelete}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</DragDropProvider>
|
|
);
|
|
}
|
|
|
|
interface RootDropZoneProps {
|
|
isDragOver: boolean;
|
|
rootPrompts: Prompt[];
|
|
selectedPromptId?: string | null;
|
|
onSelectPrompt: (promptId: string) => void;
|
|
onEditPrompt: (prompt: Prompt) => void;
|
|
onDeletePrompt: (prompt: Prompt) => void;
|
|
canUpdate: boolean;
|
|
canDelete: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders the droppable root area that lists and hosts draggable root-level prompts.
|
|
*
|
|
* @param isDragOver - Whether a draggable item is currently over the root drop zone (applies drag-over styling).
|
|
* @param rootPrompts - Array of prompts that belong at the root (no folder).
|
|
* @param selectedPromptId - ID of the currently selected prompt, used to mark its item as selected.
|
|
* @param onSelectPrompt - Callback invoked with a prompt ID when a prompt is selected.
|
|
* @param onEditPrompt - Callback invoked with a prompt when the prompt's edit action is triggered.
|
|
* @param onDeletePrompt - Callback invoked with a prompt when the prompt's delete action is triggered.
|
|
* @param canUpdate - Whether prompts are movable/editable (enables dragging).
|
|
* @param canDelete - Whether prompts may be deleted (controls delete action visibility).
|
|
* @returns The JSX element for the root drop zone containing draggable prompt items.
|
|
*/
|
|
function RootDropZone({
|
|
isDragOver,
|
|
rootPrompts,
|
|
selectedPromptId,
|
|
onSelectPrompt,
|
|
onEditPrompt,
|
|
onDeletePrompt,
|
|
canUpdate,
|
|
canDelete,
|
|
}: RootDropZoneProps) {
|
|
const { ref } = useDroppable({ id: "root-drop-zone" });
|
|
|
|
return (
|
|
<div ref={ref} className={cn("min-h-[8px] grow rounded-sm transition-colors", isDragOver && "bg-primary/10 ring-primary/30 ring-1")}>
|
|
{rootPrompts.map((prompt) => (
|
|
<DraggablePromptItem
|
|
key={prompt.id}
|
|
prompt={prompt}
|
|
isSelected={selectedPromptId === prompt.id}
|
|
onSelect={() => onSelectPrompt(prompt.id)}
|
|
onEdit={() => onEditPrompt(prompt)}
|
|
onDelete={() => onDeletePrompt(prompt)}
|
|
canUpdate={canUpdate}
|
|
canDelete={canDelete}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DroppableFolderProps {
|
|
folder: Folder;
|
|
prompts: Prompt[];
|
|
isExpanded: boolean;
|
|
isDragOver: boolean;
|
|
selectedPromptId?: string | null;
|
|
onToggle: () => void;
|
|
onSelectPrompt: (promptId: string) => void;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onCreatePrompt: () => void;
|
|
onEditPrompt: (prompt: Prompt) => void;
|
|
onDeletePrompt: (prompt: Prompt) => void;
|
|
canCreate: boolean;
|
|
canUpdate: boolean;
|
|
canDelete: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders a droppable folder header with optional action menu and its list of prompts.
|
|
*
|
|
* @param folder - Folder metadata (id, name, etc.)
|
|
* @param prompts - Prompts that belong to this folder
|
|
* @param isExpanded - Whether the folder is expanded to show its prompts
|
|
* @param isDragOver - Whether a draggable item is currently over this folder (affects visual state)
|
|
* @param selectedPromptId - ID of the currently selected prompt, used to highlight an item
|
|
* @param onToggle - Callback invoked to toggle the folder's expanded state
|
|
* @param onSelectPrompt - Callback invoked with a prompt ID when a prompt is selected
|
|
* @param onEdit - Callback invoked to start editing the folder
|
|
* @param onDelete - Callback invoked to start deleting the folder
|
|
* @param onCreatePrompt - Callback invoked to create a new prompt inside this folder
|
|
* @param onEditPrompt - Callback invoked with a prompt to start editing that prompt
|
|
* @param onDeletePrompt - Callback invoked with a prompt to start deleting that prompt
|
|
* @param canCreate - Whether the current user may create prompts in this folder
|
|
* @param canUpdate - Whether the current user may move/rename prompts or edit the folder
|
|
* @param canDelete - Whether the current user may delete prompts or the folder
|
|
* @returns A JSX element containing the folder row and, when expanded, its nested prompt items
|
|
*/
|
|
function DroppableFolder({
|
|
folder,
|
|
prompts,
|
|
isExpanded,
|
|
isDragOver,
|
|
selectedPromptId,
|
|
onToggle,
|
|
onSelectPrompt,
|
|
onEdit,
|
|
onDelete,
|
|
onCreatePrompt,
|
|
onEditPrompt,
|
|
onDeletePrompt,
|
|
canCreate,
|
|
canUpdate,
|
|
canDelete,
|
|
}: DroppableFolderProps) {
|
|
const { ref } = useDroppable({ id: `folder-${folder.id}` });
|
|
const showActions = canCreate || canUpdate || canDelete;
|
|
|
|
return (
|
|
<div ref={ref} className="mb-1 last:mb-0">
|
|
<div
|
|
className={cn(
|
|
"hover:bg-muted/50 group relative flex h-[30px] cursor-pointer items-center gap-1 rounded-sm px-2 transition-colors",
|
|
isDragOver && "bg-primary/10 ring-primary/30 ring-1",
|
|
)}
|
|
onClick={onToggle}
|
|
data-testid={`sidebar-folder-${folder.id}`}
|
|
>
|
|
<button className="flex shrink-0 items-center" aria-label="Toggle folder">
|
|
{isExpanded ? (
|
|
<ChevronDown className="text-muted-foreground h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="text-muted-foreground h-4 w-4" />
|
|
)}
|
|
</button>
|
|
{isExpanded ? (
|
|
<FolderOpen className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
) : (
|
|
<FolderIcon className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
)}
|
|
<span className="flex-1 truncate text-sm font-medium">{folder.name}</span>
|
|
<span className="text-muted-foreground mr-1 shrink-0 text-xs">{prompts.length}</span>
|
|
{showActions && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()} className="bg-card absolute top-1/2 right-2 -translate-y-1/2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 shrink-0 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100"
|
|
data-testid={`sidebar-folder-actions-${folder.id}`}
|
|
aria-label="Folder actions"
|
|
>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{canCreate && (
|
|
<DropdownMenuItem
|
|
data-testid="folder-create-prompt"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCreatePrompt();
|
|
}}
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
New Prompt
|
|
</DropdownMenuItem>
|
|
)}
|
|
{canCreate && (canUpdate || canDelete) && <DropdownMenuSeparator />}
|
|
{canUpdate && (
|
|
<DropdownMenuItem
|
|
data-testid="folder-action-edit"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onEdit();
|
|
}}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
Edit Folder
|
|
</DropdownMenuItem>
|
|
)}
|
|
{canDelete && (
|
|
<DropdownMenuItem
|
|
variant="destructive"
|
|
data-testid="folder-action-delete"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Delete Folder
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="ml-4 border-l pl-2">
|
|
{prompts.length === 0 ? (
|
|
<div className="text-muted-foreground py-2 pl-4 text-xs">{isDragOver ? "Drop here" : "No prompts"}</div>
|
|
) : (
|
|
prompts.map((prompt) => (
|
|
<DraggablePromptItem
|
|
key={prompt.id}
|
|
prompt={prompt}
|
|
isSelected={selectedPromptId === prompt.id}
|
|
onSelect={() => onSelectPrompt(prompt.id)}
|
|
onEdit={() => onEditPrompt(prompt)}
|
|
onDelete={() => onDeletePrompt(prompt)}
|
|
canUpdate={canUpdate}
|
|
canDelete={canDelete}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DraggablePromptItemProps {
|
|
prompt: Prompt;
|
|
isSelected: boolean;
|
|
onSelect: () => void;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
canUpdate: boolean;
|
|
canDelete: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders a draggable prompt list item that shows the prompt name, selection/drag states, and an actions menu when permitted.
|
|
*
|
|
* Displays a file icon and truncated prompt name, applies visual styles for selection and dragging, prevents selection while dragging, and exposes rename/delete actions via a dropdown when `canUpdate` or `canDelete` are true.
|
|
*
|
|
* @param prompt - The prompt object to render.
|
|
* @param isSelected - Whether this prompt is currently selected; used for styling.
|
|
* @param onSelect - Callback invoked when the item is clicked (not invoked if the item is being dragged).
|
|
* @param onEdit - Callback invoked to start editing/renaming the prompt.
|
|
* @param onDelete - Callback invoked to delete the prompt.
|
|
* @param canUpdate - When true, enables dragging and shows the rename action.
|
|
* @param canDelete - When true, shows the delete action.
|
|
* @returns The rendered prompt item JSX element.
|
|
*/
|
|
function DraggablePromptItem({ prompt, isSelected, onSelect, onEdit, onDelete, canUpdate, canDelete }: DraggablePromptItemProps) {
|
|
const { ref, isDragging } = useDraggable({
|
|
id: `prompt-${prompt.id}`,
|
|
disabled: !canUpdate,
|
|
});
|
|
const showActions = canUpdate || canDelete;
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
data-testid={`sidebar-prompt-${prompt.id}`}
|
|
className={cn(
|
|
"group mb-1 flex h-[30px] cursor-pointer items-center gap-2 rounded-sm px-2 last:mb-0",
|
|
isSelected ? "bg-primary/10 text-primary" : "hover:bg-muted/50",
|
|
isDragging && "opacity-50",
|
|
)}
|
|
onClick={() => {
|
|
// Don't navigate if this was a drag
|
|
if (isDragging) return;
|
|
onSelect();
|
|
}}
|
|
>
|
|
<FileText className="h-4 w-4 shrink-0" />
|
|
<span className="flex-1 truncate text-sm">{prompt.name}</span>
|
|
{showActions && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100"
|
|
data-testid={`sidebar-prompt-actions-${prompt.id}`}
|
|
aria-label="Prompt actions"
|
|
>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{canUpdate && (
|
|
<DropdownMenuItem
|
|
className="cursor-pointer"
|
|
data-testid="prompt-action-rename"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onEdit();
|
|
}}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
Rename
|
|
</DropdownMenuItem>
|
|
)}
|
|
{canDelete && (
|
|
<DropdownMenuItem
|
|
variant="destructive"
|
|
className="cursor-pointer"
|
|
data-testid="prompt-action-delete"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
);
|
|
} |