mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-16 08:25:18 +02:00
feat: add google stt and tts. add folders to organize agents
This commit is contained in:
parent
21951eca18
commit
ad2fa07058
52 changed files with 3412 additions and 621 deletions
|
|
@ -1,12 +1,32 @@
|
|||
'use client';
|
||||
|
||||
import { Archive, Pencil, RotateCcw } from 'lucide-react';
|
||||
import {
|
||||
Archive,
|
||||
Check,
|
||||
Folder as FolderIcon,
|
||||
FolderInput,
|
||||
Inbox,
|
||||
Pencil,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useTransition } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { updateWorkflowStatusApiV1WorkflowWorkflowIdStatusPut } from '@/client/sdk.gen';
|
||||
import {
|
||||
moveWorkflowToFolderApiV1WorkflowWorkflowIdFolderPut,
|
||||
updateWorkflowStatusApiV1WorkflowWorkflowIdStatusPut,
|
||||
} from '@/client/sdk.gen';
|
||||
import type { FolderResponse } from '@/client/types.gen';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -21,17 +41,31 @@ interface Workflow {
|
|||
status: string;
|
||||
created_at: string;
|
||||
total_runs?: number | null;
|
||||
folder_id?: number | null;
|
||||
}
|
||||
|
||||
interface WorkflowTableProps {
|
||||
workflows: Workflow[];
|
||||
showArchived: boolean;
|
||||
/**
|
||||
* When provided, each row gets a "Move to folder" action listing these
|
||||
* folders. Omit it (e.g. for the archived list) to hide the control.
|
||||
*/
|
||||
folders?: FolderResponse[];
|
||||
/** The folder this table is rendered under; null means "Uncategorized". */
|
||||
currentFolderId?: number | null;
|
||||
}
|
||||
|
||||
export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) {
|
||||
export function WorkflowTable({
|
||||
workflows,
|
||||
showArchived,
|
||||
folders,
|
||||
currentFolderId = null,
|
||||
}: WorkflowTableProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [loadingWorkflowId, setLoadingWorkflowId] = useState<number | null>(null);
|
||||
const [movingWorkflowId, setMovingWorkflowId] = useState<number | null>(null);
|
||||
|
||||
const handleEdit = (id: number) => {
|
||||
router.push(`/workflow/${id}`);
|
||||
|
|
@ -67,6 +101,30 @@ export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleMove = async (id: number, folderId: number | null) => {
|
||||
setMovingWorkflowId(id);
|
||||
try {
|
||||
const response = await moveWorkflowToFolderApiV1WorkflowWorkflowIdFolderPut({
|
||||
path: { workflow_id: id },
|
||||
body: { folder_id: folderId },
|
||||
});
|
||||
if (response.error) {
|
||||
throw new Error('Failed to move agent');
|
||||
}
|
||||
toast.success(
|
||||
folderId === null ? 'Moved to Uncategorized' : 'Agent moved',
|
||||
);
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error moving workflow:', error);
|
||||
toast.error('Failed to move agent');
|
||||
} finally {
|
||||
setMovingWorkflowId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-card border rounded-lg overflow-hidden shadow-sm">
|
||||
<Table>
|
||||
|
|
@ -114,6 +172,52 @@ export function WorkflowTable({ workflows, showArchived }: WorkflowTableProps) {
|
|||
<Pencil size={16} />
|
||||
Edit
|
||||
</Button>
|
||||
{folders && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={movingWorkflowId === workflow.id || isPending}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{movingWorkflowId === workflow.id ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<FolderInput size={16} />
|
||||
)}
|
||||
Move
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuLabel>Move to folder</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
disabled={currentFolderId === null}
|
||||
onClick={() => handleMove(workflow.id, null)}
|
||||
>
|
||||
<Inbox size={14} className="mr-2" />
|
||||
Uncategorized
|
||||
{currentFolderId === null && (
|
||||
<Check size={14} className="ml-auto" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
{folders.map((folder) => (
|
||||
<DropdownMenuItem
|
||||
key={folder.id}
|
||||
disabled={folder.id === currentFolderId}
|
||||
onClick={() => handleMove(workflow.id, folder.id)}
|
||||
>
|
||||
<FolderIcon size={14} className="mr-2" />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
{folder.id === currentFolderId && (
|
||||
<Check size={14} className="ml-auto shrink-0" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<Button
|
||||
variant={showArchived ? "default" : "outline"}
|
||||
size="sm"
|
||||
|
|
|
|||
63
ui/src/components/workflow/folders/AgentFolderView.tsx
Normal file
63
ui/src/components/workflow/folders/AgentFolderView.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
'use client';
|
||||
|
||||
import type { FolderResponse, WorkflowListResponse } from '@/client/types.gen';
|
||||
|
||||
import { WorkflowTable } from '../WorkflowTable';
|
||||
import { FolderSection } from './FolderSection';
|
||||
|
||||
interface AgentFolderViewProps {
|
||||
/** Active (non-archived) agents only. */
|
||||
workflows: WorkflowListResponse[];
|
||||
folders: FolderResponse[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders active agents grouped into collapsible folder sections.
|
||||
*
|
||||
* When the organization has no folders yet, this falls back to the original
|
||||
* flat table so the feature stays invisible until someone creates a folder.
|
||||
*/
|
||||
export function AgentFolderView({ workflows, folders }: AgentFolderViewProps) {
|
||||
// No folders → keep the original flat list (no folder chrome, nowhere to move to).
|
||||
if (folders.length === 0) {
|
||||
return <WorkflowTable workflows={workflows} showArchived={false} />;
|
||||
}
|
||||
|
||||
// Group agents by folder. Agents whose folder_id is null — or points at a
|
||||
// folder we didn't get back — fall into "Uncategorized".
|
||||
const folderIds = new Set(folders.map((f) => f.id));
|
||||
const byFolder = new Map<number, WorkflowListResponse[]>();
|
||||
const uncategorized: WorkflowListResponse[] = [];
|
||||
|
||||
for (const wf of workflows) {
|
||||
if (wf.folder_id != null && folderIds.has(wf.folder_id)) {
|
||||
const bucket = byFolder.get(wf.folder_id) ?? [];
|
||||
bucket.push(wf);
|
||||
byFolder.set(wf.folder_id, bucket);
|
||||
} else {
|
||||
uncategorized.push(wf);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{folders.map((folder) => (
|
||||
<FolderSection
|
||||
key={folder.id}
|
||||
kind="folder"
|
||||
folder={folder}
|
||||
workflows={byFolder.get(folder.id) ?? []}
|
||||
allFolders={folders}
|
||||
defaultOpen={false}
|
||||
/>
|
||||
))}
|
||||
{uncategorized.length > 0 && (
|
||||
<FolderSection
|
||||
kind="uncategorized"
|
||||
workflows={uncategorized}
|
||||
allFolders={folders}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
ui/src/components/workflow/folders/CreateFolderButton.tsx
Normal file
46
ui/src/components/workflow/folders/CreateFolderButton.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
'use client';
|
||||
|
||||
import { FolderPlus } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { createFolderApiV1FolderPost } from '@/client/sdk.gen';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import { FolderFormDialog } from './FolderFormDialog';
|
||||
|
||||
export function CreateFolderButton() {
|
||||
const router = useRouter();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleCreate = async (name: string) => {
|
||||
const response = await createFolderApiV1FolderPost({ body: { name } });
|
||||
if (response.error) {
|
||||
// 409 = duplicate name; surface the server's message when present.
|
||||
const detail =
|
||||
(response.error as { detail?: string })?.detail ??
|
||||
'Failed to create folder';
|
||||
toast.error(detail);
|
||||
throw new Error(detail);
|
||||
}
|
||||
toast.success(`Folder "${name}" created`);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setIsOpen(true)}>
|
||||
<FolderPlus className="w-4 h-4 mr-2" />
|
||||
New Folder
|
||||
</Button>
|
||||
<FolderFormDialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
title="Create folder"
|
||||
submitLabel="Create"
|
||||
onSubmit={handleCreate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
97
ui/src/components/workflow/folders/FolderFormDialog.tsx
Normal file
97
ui/src/components/workflow/folders/FolderFormDialog.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface FolderFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
/** Pre-fill the input (used when renaming). */
|
||||
initialName?: string;
|
||||
submitLabel: string;
|
||||
/** Resolve to close the dialog; reject/throw to keep it open (e.g. on error). */
|
||||
onSubmit: (name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared single-field dialog used for both creating and renaming a folder.
|
||||
* Keeps name validation and the pending state in one place.
|
||||
*/
|
||||
export function FolderFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
initialName = '',
|
||||
submitLabel,
|
||||
onSubmit,
|
||||
}: FolderFormDialogProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Reset the field whenever the dialog (re)opens.
|
||||
useEffect(() => {
|
||||
if (open) setName(initialName);
|
||||
}, [open, initialName]);
|
||||
|
||||
const trimmed = name.trim();
|
||||
const canSubmit = trimmed.length > 0 && trimmed !== initialName.trim() && !isSubmitting;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canSubmit) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSubmit(trimmed);
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
// onSubmit surfaces its own error toast; keep the dialog open.
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 py-2">
|
||||
<Label htmlFor="folder-name">Folder name</Label>
|
||||
<Input
|
||||
id="folder-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Sales, Support, Onboarding"
|
||||
maxLength={100}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!canSubmit}>
|
||||
{isSubmitting ? 'Saving...' : submitLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
255
ui/src/components/workflow/folders/FolderSection.tsx
Normal file
255
ui/src/components/workflow/folders/FolderSection.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
Archive,
|
||||
ChevronRight,
|
||||
Folder as FolderIcon,
|
||||
FolderOpen,
|
||||
Inbox,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
deleteFolderApiV1FolderFolderIdDelete,
|
||||
renameFolderApiV1FolderFolderIdPut,
|
||||
} from '@/client/sdk.gen';
|
||||
import type { FolderResponse, WorkflowListResponse } from '@/client/types.gen';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import logger from '@/lib/logger';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { WorkflowTable } from '../WorkflowTable';
|
||||
import { FolderFormDialog } from './FolderFormDialog';
|
||||
|
||||
/**
|
||||
* - `folder` — a real, renameable/deletable folder of active agents
|
||||
* - `uncategorized` — active agents with no folder
|
||||
* - `archived` — archived agents (restore-only; not a move target)
|
||||
*/
|
||||
type SectionKind = 'folder' | 'uncategorized' | 'archived';
|
||||
|
||||
interface FolderSectionProps {
|
||||
kind: SectionKind;
|
||||
/** Required when kind === 'folder'; ignored otherwise. */
|
||||
folder?: FolderResponse | null;
|
||||
workflows: WorkflowListResponse[];
|
||||
/** All folders, passed through so each row's "Move to folder" menu has targets. */
|
||||
allFolders?: FolderResponse[];
|
||||
/** Defaults to open only for Uncategorized; folders and Archived start collapsed. */
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export function FolderSection({
|
||||
kind,
|
||||
folder = null,
|
||||
workflows,
|
||||
allFolders = [],
|
||||
defaultOpen,
|
||||
}: FolderSectionProps) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(defaultOpen ?? kind === 'uncategorized');
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const isFolder = kind === 'folder';
|
||||
const isArchived = kind === 'archived';
|
||||
const count = workflows.length;
|
||||
const title = isFolder ? (folder?.name ?? '') : isArchived ? 'Archived' : 'Uncategorized';
|
||||
|
||||
const handleRename = async (name: string) => {
|
||||
if (!folder) return;
|
||||
const response = await renameFolderApiV1FolderFolderIdPut({
|
||||
path: { folder_id: folder.id },
|
||||
body: { name },
|
||||
});
|
||||
if (response.error) {
|
||||
const detail =
|
||||
(response.error as { detail?: string })?.detail ??
|
||||
'Failed to rename folder';
|
||||
toast.error(detail);
|
||||
throw new Error(detail);
|
||||
}
|
||||
toast.success('Folder renamed');
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!folder) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await deleteFolderApiV1FolderFolderIdDelete({
|
||||
path: { folder_id: folder.id },
|
||||
});
|
||||
if (response.error) {
|
||||
throw new Error('Failed to delete folder');
|
||||
}
|
||||
toast.success(`Folder "${folder.name}" deleted`);
|
||||
setConfirmDelete(false);
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
logger.error(`Error deleting folder: ${err}`);
|
||||
toast.error('Failed to delete folder');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div className="flex items-center gap-1">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
className="group flex flex-1 items-center gap-2.5 rounded-md px-2 py-2 text-left transition-colors hover:bg-accent"
|
||||
aria-label={`Toggle ${title}`}
|
||||
>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
className={cn(
|
||||
'shrink-0 text-muted-foreground transition-transform duration-200',
|
||||
open && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
{isFolder ? (
|
||||
open ? (
|
||||
<FolderOpen size={17} className="shrink-0 text-amber-500" />
|
||||
) : (
|
||||
<FolderIcon size={17} className="shrink-0 text-amber-500" />
|
||||
)
|
||||
) : isArchived ? (
|
||||
<Archive size={16} className="shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<Inbox size={17} className="shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span
|
||||
className={cn('font-medium', !isFolder && 'text-muted-foreground')}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<Badge variant="secondary" className="ml-1 font-normal">
|
||||
{count}
|
||||
</Badge>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
{isFolder && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground"
|
||||
aria-label="Folder actions"
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setIsRenaming(true)}>
|
||||
<Pencil size={14} className="mr-2" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 size={14} className="mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CollapsibleContent className="data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-top-1">
|
||||
<div className="pl-7 pt-2">
|
||||
{count > 0 ? (
|
||||
<WorkflowTable
|
||||
workflows={workflows}
|
||||
showArchived={isArchived}
|
||||
// Archived agents are restore-only — not a move target.
|
||||
folders={isArchived ? undefined : allFolders}
|
||||
currentFolderId={folder?.id ?? null}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/30 p-6 text-center text-sm text-muted-foreground">
|
||||
{isArchived
|
||||
? 'No archived agents.'
|
||||
: isFolder
|
||||
? 'This folder is empty. Use “Move to folder” on an agent to add it here.'
|
||||
: 'No uncategorized agents.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{isFolder && folder && (
|
||||
<>
|
||||
<FolderFormDialog
|
||||
open={isRenaming}
|
||||
onOpenChange={setIsRenaming}
|
||||
title="Rename folder"
|
||||
initialName={folder.name}
|
||||
submitLabel="Rename"
|
||||
onSubmit={handleRename}
|
||||
/>
|
||||
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete “{folder.name}”?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
The {count} agent{count === 1 ? '' : 's'} in this folder
|
||||
won’t be deleted — they’ll move to Uncategorized.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDelete();
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete folder'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue