fix: ux consistency on archiving and deleting tasks (#627)
This commit is contained in:
committed by
GitHub
parent
73e58729ba
commit
cd46516cef
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { Row, ColumnDef } from "@tanstack/react-table";
|
||||
import type { Task, Workflow, WorkflowStep, Repository } from "@/lib/types/http";
|
||||
import Link from "next/link";
|
||||
@@ -8,6 +9,8 @@ import { Button } from "@kandev/ui/button";
|
||||
import { Badge } from "@kandev/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||
import { TaskArchiveConfirmDialog } from "@/components/task/task-archive-confirm-dialog";
|
||||
|
||||
type TaskWithResolution = Task & {
|
||||
workflowName?: string;
|
||||
@@ -66,6 +69,8 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
const task = row.original;
|
||||
const isDeleting = ctx.deletingTaskId === task.id;
|
||||
const isArchived = !!task.archived_at;
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showArchiveConfirm, setShowArchiveConfirm] = useState(false);
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-0.5">
|
||||
{!isArchived && (
|
||||
@@ -77,7 +82,7 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
className="cursor-pointer h-7 w-7 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
ctx.onArchive(task.id);
|
||||
setShowArchiveConfirm(true);
|
||||
}}
|
||||
>
|
||||
<IconArchive className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
@@ -95,7 +100,7 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
disabled={isDeleting}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
ctx.onDelete(task.id);
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
>
|
||||
{isDeleting ? (
|
||||
@@ -107,6 +112,19 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
<TaskDeleteConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onOpenChange={setShowDeleteConfirm}
|
||||
taskTitle={task.title}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={() => ctx.onDelete(task.id)}
|
||||
/>
|
||||
<TaskArchiveConfirmDialog
|
||||
open={showArchiveConfirm}
|
||||
onOpenChange={setShowArchiveConfirm}
|
||||
taskTitle={task.title}
|
||||
onConfirm={() => ctx.onArchive(task.id)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@ function useTaskOperations({
|
||||
setDeletingTaskId(taskId);
|
||||
try {
|
||||
await deleteTask(taskId);
|
||||
toast({ title: "Task deleted", description: "The task has been deleted successfully." });
|
||||
fetchTasks();
|
||||
} catch (err) {
|
||||
toast({
|
||||
|
||||
@@ -13,16 +13,7 @@ import {
|
||||
import { Checkbox } from "@kandev/ui/checkbox";
|
||||
import { Card, CardContent } from "@kandev/ui/card";
|
||||
import { Badge } from "@kandev/ui/badge";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@kandev/ui/alert-dialog";
|
||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -41,7 +32,7 @@ import { needsAction } from "@/lib/utils/needs-action";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { PRTaskIcon } from "@/components/github/pr-task-icon";
|
||||
import { RemoteCloudTooltip } from "@/components/task/remote-cloud-tooltip";
|
||||
import { ArchiveConfirmDialog } from "@/components/task/archive-confirm-dialog";
|
||||
import { TaskArchiveConfirmDialog } from "@/components/task/task-archive-confirm-dialog";
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
@@ -258,48 +249,6 @@ function KanbanCardActions({
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
taskTitle,
|
||||
isDeleting,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
taskTitle: string;
|
||||
isDeleting?: boolean;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete task</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{taskTitle}"? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isDeleting}
|
||||
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => {
|
||||
if (isDeleting) return;
|
||||
onConfirm();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{isDeleting ? <IconLoader className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function MoveToSubmenu({
|
||||
task,
|
||||
steps,
|
||||
@@ -420,14 +369,14 @@ function KanbanCardMenu(props: KanbanCardMenuProps) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DeleteConfirmDialog
|
||||
<TaskDeleteConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onOpenChange={setShowDeleteConfirm}
|
||||
taskTitle={task.title}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={() => onDelete?.(task)}
|
||||
/>
|
||||
<ArchiveConfirmDialog
|
||||
<TaskArchiveConfirmDialog
|
||||
open={showArchiveConfirm}
|
||||
onOpenChange={setShowArchiveConfirm}
|
||||
taskTitle={task.title}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { IconDots, IconTrash } from "@tabler/icons-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { IconArchive, IconDots, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@kandev/ui/dropdown-menu";
|
||||
import { cn } from "@kandev/ui/lib/utils";
|
||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||
import { TaskArchiveConfirmDialog } from "@/components/task/task-archive-confirm-dialog";
|
||||
import { needsAction } from "@/lib/utils/needs-action";
|
||||
import { Graph2StepNode } from "./graph2-step-node";
|
||||
import { Graph2Connector } from "./graph2-connector";
|
||||
@@ -40,8 +42,10 @@ export type Graph2TaskPipelineProps = {
|
||||
onPreviewTask: (task: Task) => void;
|
||||
onOpenTask: (task: Task) => void;
|
||||
onDeleteTask: (task: Task) => void;
|
||||
onArchiveTask?: (task: Task) => void;
|
||||
isMoving?: boolean;
|
||||
isDeleting?: boolean;
|
||||
isArchiving?: boolean;
|
||||
};
|
||||
|
||||
function getStepPhase(index: number, currentStepIndex: number): "past" | "current" | "future" {
|
||||
@@ -97,6 +101,7 @@ function PipelineStepNodes({
|
||||
onPreviewTask={onPreviewTask}
|
||||
isMoving={isMoving}
|
||||
/>
|
||||
|
||||
{connectorType && <Graph2Connector type={connectorType} />}
|
||||
</div>
|
||||
);
|
||||
@@ -105,6 +110,69 @@ function PipelineStepNodes({
|
||||
);
|
||||
}
|
||||
|
||||
function TaskActions({
|
||||
task,
|
||||
onDeleteTask,
|
||||
onArchiveTask,
|
||||
isDeleting,
|
||||
isArchiving,
|
||||
}: Pick<
|
||||
Graph2TaskPipelineProps,
|
||||
"task" | "onDeleteTask" | "onArchiveTask" | "isDeleting" | "isArchiving"
|
||||
>) {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showArchiveConfirm, setShowArchiveConfirm] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
|
||||
>
|
||||
<IconDots className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
{onArchiveTask && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowArchiveConfirm(true)}
|
||||
disabled={isArchiving}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconArchive className="h-3.5 w-3.5 mr-2" />
|
||||
Archive task
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isDeleting}
|
||||
className="text-destructive focus:text-destructive cursor-pointer"
|
||||
>
|
||||
<IconTrash className="h-3.5 w-3.5 mr-2" />
|
||||
Delete task
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TaskDeleteConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onOpenChange={setShowDeleteConfirm}
|
||||
taskTitle={task.title}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={() => onDeleteTask(task)}
|
||||
/>
|
||||
<TaskArchiveConfirmDialog
|
||||
open={showArchiveConfirm}
|
||||
onOpenChange={setShowArchiveConfirm}
|
||||
taskTitle={task.title}
|
||||
isArchiving={isArchiving}
|
||||
onConfirm={() => onArchiveTask?.(task)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Graph2TaskPipeline({
|
||||
task,
|
||||
steps,
|
||||
@@ -112,14 +180,15 @@ export function Graph2TaskPipeline({
|
||||
onPreviewTask,
|
||||
onOpenTask,
|
||||
onDeleteTask,
|
||||
onArchiveTask,
|
||||
isMoving,
|
||||
isDeleting,
|
||||
isArchiving,
|
||||
}: Graph2TaskPipelineProps) {
|
||||
const currentStepIndex = useMemo(
|
||||
() => steps.findIndex((s) => s.id === task.workflowStepId),
|
||||
[steps, task.workflowStepId],
|
||||
);
|
||||
|
||||
const hasAction = needsAction(task);
|
||||
const sessionCount = task.sessionCount ?? 0;
|
||||
|
||||
@@ -152,7 +221,6 @@ export function Graph2TaskPipeline({
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<PipelineStepNodes
|
||||
steps={steps}
|
||||
currentStepIndex={currentStepIndex}
|
||||
@@ -161,27 +229,13 @@ export function Graph2TaskPipeline({
|
||||
onPreviewTask={onPreviewTask}
|
||||
isMoving={isMoving}
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
|
||||
>
|
||||
<IconDots className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDeleteTask(task)}
|
||||
disabled={isDeleting}
|
||||
className="text-destructive focus:text-destructive cursor-pointer"
|
||||
>
|
||||
<IconTrash className="h-3.5 w-3.5 mr-2" />
|
||||
Delete task
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TaskActions
|
||||
task={task}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onArchiveTask={onArchiveTask}
|
||||
isDeleting={isDeleting}
|
||||
isArchiving={isArchiving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,8 +12,10 @@ export function SwimlaneGraph2Content({
|
||||
onPreviewTask,
|
||||
onOpenTask,
|
||||
onDeleteTask,
|
||||
onArchiveTask,
|
||||
onMoveError,
|
||||
deletingTaskId,
|
||||
archivingTaskId,
|
||||
}: ViewContentProps) {
|
||||
const { moveTask } = useSwimlaneMove(workflowId, {
|
||||
onMoveError,
|
||||
@@ -60,8 +62,10 @@ export function SwimlaneGraph2Content({
|
||||
onPreviewTask={onPreviewTask}
|
||||
onOpenTask={onOpenTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onArchiveTask={onArchiveTask}
|
||||
isMoving={movingTaskId === task.id}
|
||||
isDeleting={deletingTaskId === task.id}
|
||||
isArchiving={archivingTaskId === task.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { IconLoader, IconTrash, IconArchive, IconChevronRight, IconX } from "@tabler/icons-react";
|
||||
import { IconTrash, IconArchive, IconChevronRight, IconX } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@kandev/ui/alert-dialog";
|
||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||
import { TaskArchiveConfirmDialog } from "@/components/task/task-archive-confirm-dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -33,6 +25,43 @@ interface TaskMultiSelectToolbarProps {
|
||||
onBulkMove: (targetStepId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function BulkArchiveDialog({
|
||||
count,
|
||||
isProcessing,
|
||||
onConfirm,
|
||||
}: {
|
||||
count: number;
|
||||
isProcessing: boolean;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="cursor-pointer gap-1.5"
|
||||
disabled={isProcessing}
|
||||
onClick={() => setOpen(true)}
|
||||
data-testid="bulk-archive-button"
|
||||
>
|
||||
<IconArchive className="h-4 w-4" />
|
||||
Archive {count}
|
||||
</Button>
|
||||
<TaskArchiveConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
isBulkOperation
|
||||
count={count}
|
||||
isArchiving={isProcessing}
|
||||
onConfirm={onConfirm}
|
||||
confirmTestId="bulk-archive-confirm"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function BulkDeleteDialog({
|
||||
count,
|
||||
isProcessing,
|
||||
@@ -57,32 +86,15 @@ function BulkDeleteDialog({
|
||||
<IconTrash className="h-4 w-4" />
|
||||
Delete {count}
|
||||
</Button>
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete {count} tasks</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {count} task{count !== 1 ? "s" : ""}? This action
|
||||
cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isProcessing}
|
||||
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
setOpen(false);
|
||||
}}
|
||||
data-testid="bulk-delete-confirm"
|
||||
>
|
||||
{isProcessing ? <IconLoader className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<TaskDeleteConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
isBulkOperation
|
||||
count={count}
|
||||
isDeleting={isProcessing}
|
||||
onConfirm={onConfirm}
|
||||
confirmTestId="bulk-delete-confirm"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -143,17 +155,11 @@ export function TaskMultiSelectToolbar({
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="cursor-pointer gap-1.5"
|
||||
disabled={isProcessing}
|
||||
onClick={() => onBulkArchive()}
|
||||
data-testid="bulk-archive-button"
|
||||
>
|
||||
<IconArchive className="h-4 w-4" />
|
||||
Archive {count}
|
||||
</Button>
|
||||
<BulkArchiveDialog
|
||||
count={count}
|
||||
isProcessing={isProcessing}
|
||||
onConfirm={() => onBulkArchive()}
|
||||
/>
|
||||
|
||||
<BulkDeleteDialog
|
||||
count={count}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import { TaskSwitcher } from "../task-switcher";
|
||||
import { WorkspaceSwitcher } from "../workspace-switcher";
|
||||
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
||||
import { ArchiveConfirmDialog } from "../archive-confirm-dialog";
|
||||
import { TaskArchiveConfirmDialog } from "../task-archive-confirm-dialog";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { replaceTaskUrl } from "@/lib/links";
|
||||
import { fetchWorkflowSnapshot, listWorkflows } from "@/lib/api";
|
||||
@@ -467,7 +467,7 @@ export const SessionTaskSwitcherSheet = memo(function SessionTaskSwitcherSheet({
|
||||
onSuccess={handleTaskCreated}
|
||||
/>
|
||||
|
||||
<ArchiveConfirmDialog
|
||||
<TaskArchiveConfirmDialog
|
||||
open={archivingTask !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setArchivingTask(null);
|
||||
|
||||
+22
-8
@@ -12,29 +12,42 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@kandev/ui/alert-dialog";
|
||||
|
||||
type ArchiveConfirmDialogProps = {
|
||||
type TaskArchiveConfirmDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
taskTitle: string;
|
||||
taskTitle?: string;
|
||||
isBulkOperation?: boolean;
|
||||
count?: number;
|
||||
isArchiving?: boolean;
|
||||
onConfirm: () => void;
|
||||
confirmTestId?: string;
|
||||
};
|
||||
|
||||
export function ArchiveConfirmDialog({
|
||||
export function TaskArchiveConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
taskTitle,
|
||||
isBulkOperation,
|
||||
count,
|
||||
isArchiving,
|
||||
onConfirm,
|
||||
}: ArchiveConfirmDialogProps) {
|
||||
confirmTestId,
|
||||
}: TaskArchiveConfirmDialogProps) {
|
||||
const safeCount = count ?? 0;
|
||||
const label = isBulkOperation ? `task${safeCount !== 1 ? "s" : ""}` : "task";
|
||||
const title = isBulkOperation ? `Archive ${safeCount} ${label}?` : "Archive task?";
|
||||
const firstLine = isBulkOperation
|
||||
? `Are you sure you want to archive ${safeCount} ${label}?`
|
||||
: `Are you sure you want to archive "${taskTitle}"?`;
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Archive task?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>
|
||||
<p>Are you sure you want to archive "{taskTitle}"?</p>
|
||||
<p>{firstLine}</p>
|
||||
<p className="mt-2">
|
||||
This will delete the task's worktree and stop any running agent sessions.
|
||||
</p>
|
||||
@@ -46,10 +59,11 @@ export function ArchiveConfirmDialog({
|
||||
<AlertDialogAction
|
||||
disabled={isArchiving}
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
data-testid={confirmTestId}
|
||||
onClick={() => {
|
||||
if (isArchiving) return;
|
||||
onConfirm();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{isArchiving ? <IconLoader className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { IconLoader } from "@tabler/icons-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@kandev/ui/alert-dialog";
|
||||
|
||||
export function TaskDeleteConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
taskTitle,
|
||||
isBulkOperation,
|
||||
count,
|
||||
isDeleting,
|
||||
onConfirm,
|
||||
confirmTestId,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
taskTitle?: string;
|
||||
isBulkOperation?: boolean;
|
||||
count?: number;
|
||||
isDeleting?: boolean;
|
||||
onConfirm: () => void;
|
||||
confirmTestId?: string;
|
||||
}) {
|
||||
const safeCount = count ?? 0;
|
||||
const label = isBulkOperation ? `task${safeCount !== 1 ? "s" : ""}` : "task";
|
||||
const title = isBulkOperation ? `Delete ${safeCount} ${label}` : "Delete task";
|
||||
const description = isBulkOperation
|
||||
? `Are you sure you want to delete ${safeCount} ${label}? This action cannot be undone.`
|
||||
: `Are you sure you want to delete "${taskTitle}"? This action cannot be undone.`;
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isDeleting}
|
||||
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
data-testid={confirmTestId}
|
||||
onClick={() => {
|
||||
if (isDeleting) return;
|
||||
onConfirm();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{isDeleting ? <IconLoader className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { GitStatusEntry } from "@/lib/state/slices/session-runtime/types";
|
||||
import { TaskSwitcher } from "./task-switcher";
|
||||
import type { TaskSwitcherItem } from "./task-switcher";
|
||||
import { TaskRenameDialog } from "./task-rename-dialog";
|
||||
import { ArchiveConfirmDialog } from "./archive-confirm-dialog";
|
||||
import { TaskArchiveConfirmDialog } from "./task-archive-confirm-dialog";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { PanelRoot, PanelBody } from "./panel-primitives";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
@@ -662,7 +662,7 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({
|
||||
currentTitle={renamingTask?.title ?? ""}
|
||||
onSubmit={handleRenameSubmit}
|
||||
/>
|
||||
<ArchiveConfirmDialog
|
||||
<TaskArchiveConfirmDialog
|
||||
open={archivingTask !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setArchivingTask(null);
|
||||
|
||||
@@ -9,6 +9,7 @@ export class KanbanPage {
|
||||
readonly bulkMoveButton: Locator;
|
||||
readonly bulkClearButton: Locator;
|
||||
readonly bulkDeleteConfirm: Locator;
|
||||
readonly bulkArchiveConfirm: Locator;
|
||||
|
||||
readonly multiSelectToggle: Locator;
|
||||
|
||||
@@ -22,6 +23,7 @@ export class KanbanPage {
|
||||
this.bulkMoveButton = page.getByTestId("bulk-move-button");
|
||||
this.bulkClearButton = page.getByTestId("bulk-clear-selection");
|
||||
this.bulkDeleteConfirm = page.getByTestId("bulk-delete-confirm");
|
||||
this.bulkArchiveConfirm = page.getByTestId("bulk-archive-confirm");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
|
||||
@@ -111,6 +111,7 @@ test.describe("Multi-select bulk actions", () => {
|
||||
await kanban.selectTask(t2.id);
|
||||
|
||||
await kanban.bulkArchiveButton.click();
|
||||
await kanban.bulkArchiveConfirm.click();
|
||||
|
||||
await expect(kanban.taskCard(t1.id)).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(kanban.taskCard(t2.id)).not.toBeVisible();
|
||||
@@ -142,6 +143,7 @@ test.describe("Multi-select bulk actions", () => {
|
||||
await expect(testPage.locator('[data-multi-select-active="true"]').first()).toBeVisible();
|
||||
|
||||
await kanban.bulkArchiveButton.click();
|
||||
await kanban.bulkArchiveConfirm.click();
|
||||
await expect(kanban.taskCard(t1.id)).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(kanban.taskCard(t2.id)).not.toBeVisible();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user