fix: move task deletion to inline actions

This commit is contained in:
Arthur Belleville 2026-04-16 12:02:18 +02:00
parent e36c536cf2
commit 983dbb01b5
No known key found for this signature in database
8 changed files with 147 additions and 26 deletions

View file

@ -281,7 +281,7 @@ describe("TabloDetailsPage overview layout", () => {
confirmSpy.mockRestore();
});
it("deletes a task from the task modal", async () => {
it("deletes a task from the inline kanban action", async () => {
const user = userEvent.setup();
renderWithProviders(<TabloDetailsPage />, {
@ -290,8 +290,7 @@ describe("TabloDetailsPage overview layout", () => {
});
await user.click(screen.getByRole("button", { name: "Tâches" }));
await user.click(screen.getByText("Task A"));
await user.click(screen.getByRole("button", { name: "Supprimer la tâche" }));
await user.click(screen.getByRole("button", { name: "Supprimer la tâche Task A" }));
expect(mutateDeleteTask).toHaveBeenCalledWith("task-1");
});

View file

@ -0,0 +1,92 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TasksPage } from "./tasks";
import { renderWithProviders } from "../utils/testHelpers";
const mutateUpdateTask = vi.fn();
const mutateDeleteTask = vi.fn();
vi.mock("../hooks/tablos", () => ({
useTablosList: () => ({
data: [
{
id: "tablo-1",
name: "Projet Alpha",
color: "bg-blue-500",
},
],
isLoading: false,
}),
}));
vi.mock("../hooks/tasks", () => ({
useAllTasks: () => ({
data: [
{
id: "task-1",
tablo_id: "tablo-1",
title: "Task A",
description: "Description",
status: "todo",
due_date: null,
assignee_id: "123",
assignee_name: "John Doe",
assignee_avatar: null,
tablos: {
id: "tablo-1",
name: "Projet Alpha",
color: "bg-blue-500",
},
},
],
isLoading: false,
}),
useUpdateTask: () => ({
mutate: mutateUpdateTask,
}),
useDeleteTask: () => ({
mutate: mutateDeleteTask,
}),
}));
vi.mock("@xtablo/tablo-views", async (importOriginal) => {
const actual = await importOriginal<typeof import("@xtablo/tablo-views")>();
return {
...actual,
GanttChart: () => <div data-testid="gantt-chart" />,
TaskModal: () => null,
};
});
describe("TasksPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("deletes a task from the inline kanban action", async () => {
const user = userEvent.setup();
renderWithProviders(<TasksPage />, {
route: "/tasks?view=kanban",
path: "/tasks",
});
await user.click(screen.getByRole("button", { name: "Supprimer la tâche Task A" }));
expect(mutateDeleteTask).toHaveBeenCalledWith("task-1");
});
it("deletes a task from the inline list action", async () => {
const user = userEvent.setup();
renderWithProviders(<TasksPage />, {
route: "/tasks?view=aggregated",
path: "/tasks",
});
await user.click(screen.getByRole("button", { name: "Supprimer la tâche Task A" }));
expect(mutateDeleteTask).toHaveBeenCalledWith("task-1");
});
});

View file

@ -33,6 +33,7 @@ import {
Sparkles,
Star,
Sun,
Trash2Icon,
UserIcon,
Waves,
Zap,
@ -42,7 +43,7 @@ import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { useTablosList } from "../hooks/tablos";
import { useAllTasks, useUpdateTask } from "../hooks/tasks";
import { useAllTasks, useDeleteTask, useUpdateTask } from "../hooks/tasks";
import { useUser } from "../providers/UserStoreProvider";
type TaskStatus = "all" | "todo" | "in_progress" | "in_review" | "done";
@ -134,6 +135,7 @@ export function TasksPage() {
// Mutation for updating task status
const updateTaskMutation = useUpdateTask();
const deleteTaskMutation = useDeleteTask();
const openTaskModal = (dueDate?: Date) => {
setTaskModalInitialDueDate(dueDate ? new Date(dueDate) : undefined);
@ -556,6 +558,17 @@ export function TasksPage() {
))}
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
aria-label={`Supprimer la tâche ${task.title}`}
onClick={(e) => {
e.stopPropagation();
deleteTaskMutation.mutate(task.id);
}}
className="text-gray-400 hover:text-red-500 shrink-0 p-2 -m-1 min-h-[44px] min-w-[44px] flex items-center justify-center"
>
<Trash2Icon className="w-4 h-4" />
</button>
</div>
{formattedDate && (
@ -878,6 +891,17 @@ export function TasksPage() {
))}
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
aria-label={`Supprimer la tâche ${task.title}`}
onClick={(e) => {
e.stopPropagation();
deleteTaskMutation.mutate(task.id);
}}
className="inline-flex items-center justify-center h-8 w-8 min-h-[44px] min-w-[44px] rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<Trash2Icon className="w-4 h-4 text-gray-400 hover:text-red-500" />
</button>
</td>
</tr>
);

View file

@ -262,6 +262,7 @@ export const TabloTasksSection = ({
etapes={etapes}
etapeTitles={etapeTitleMap}
onTaskClick={handleTaskClick}
onDeleteTask={onDeleteTask}
onAddTask={handleAddTask}
onAddTaskInline={handleCreateTask}
onTaskMove={handleTaskMove}

View file

@ -14,6 +14,7 @@ interface KanbanBoardProps {
etapes: Etape[];
etapeTitles: Record<string, string>;
onTaskClick: (task: KanbanTask) => void;
onDeleteTask?: (taskId: string) => void;
onAddTask: (status: TaskStatus) => void;
onAddTaskInline: (task: {
title: string;
@ -30,6 +31,7 @@ export const KanbanBoard = ({
members,
etapes,
onTaskClick,
onDeleteTask,
onAddTask,
onAddTaskInline,
onTaskMove,
@ -63,6 +65,7 @@ export const KanbanBoard = ({
members={members}
etapes={etapes}
onTaskClick={onTaskClick}
onDeleteTask={onDeleteTask}
onAddTask={onAddTask}
onAddTaskInline={onAddTaskInline}
onDragStart={handleDragStart}

View file

@ -15,6 +15,7 @@ interface KanbanColumnProps {
members: TabloMember[];
etapes: Etape[];
onTaskClick: (task: KanbanTask) => void;
onDeleteTask?: (taskId: string) => void;
onAddTask: (status: KanbanColumnType["status"]) => void;
onAddTaskInline: (task: {
title: string;
@ -33,6 +34,7 @@ export const KanbanColumn = ({
members,
etapes,
onTaskClick,
onDeleteTask,
onAddTask,
onAddTaskInline,
onDragStart,
@ -84,6 +86,7 @@ export const KanbanColumn = ({
task={task}
etapeTitle={etape?.title}
onClick={() => onTaskClick(task)}
onDelete={onDeleteTask ? () => onDeleteTask(task.id) : undefined}
/>
</div>
);

View file

@ -1,11 +1,12 @@
import type { KanbanTask } from "@xtablo/shared-types";
import { TypographyH4, TypographyMuted } from "@xtablo/ui/components/typography";
import { CalendarIcon, User } from "lucide-react";
import { CalendarIcon, Trash2Icon, User } from "lucide-react";
interface KanbanTaskCardProps {
task: KanbanTask;
etapeTitle?: string;
onClick: () => void;
onDelete?: () => void;
}
function formatDueDate(dateStr: string): string {
@ -23,7 +24,7 @@ function isOverdue(dateStr: string): boolean {
return due < today;
}
export const KanbanTaskCard = ({ task, etapeTitle, onClick }: KanbanTaskCardProps) => {
export const KanbanTaskCard = ({ task, etapeTitle, onClick, onDelete }: KanbanTaskCardProps) => {
const overdue = task.due_date && task.status !== "done" && isOverdue(task.due_date);
return (
@ -31,9 +32,24 @@ export const KanbanTaskCard = ({ task, etapeTitle, onClick }: KanbanTaskCardProp
onClick={onClick}
className="bg-card border border-border rounded-lg p-3 hover:shadow-md transition-shadow cursor-pointer group"
>
<TypographyH4 className="font-medium text-foregroud mb-1 line-clamp-2">
{task.title}
</TypographyH4>
<div className="mb-1 flex items-start justify-between gap-2">
<TypographyH4 className="font-medium text-foregroud line-clamp-2 flex-1">
{task.title}
</TypographyH4>
{onDelete && (
<button
type="button"
aria-label={`Supprimer la tâche ${task.title}`}
className="shrink-0 rounded p-1 text-muted-foreground hover:text-destructive"
onClick={(event) => {
event.stopPropagation();
onDelete();
}}
>
<Trash2Icon className="h-4 w-4" />
</button>
)}
</div>
{task.description && (
<TypographyMuted className=" line-clamp-2 mt-1 mb-2 text-ellipsis overflow-hidden">

View file

@ -58,7 +58,6 @@ export const TaskModal = ({
initialDueDate,
onCreateTask,
onUpdateTask,
onDeleteTask,
}: TaskModalProps) => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
@ -140,12 +139,6 @@ export const TaskModal = ({
onClose();
};
const handleDelete = () => {
if (!taskId || !task) return;
onDeleteTask?.(task.id);
onClose();
};
if (!isOpen) return null;
return (
@ -261,16 +254,6 @@ export const TaskModal = ({
{/* Actions */}
<div className="flex justify-end gap-2 pt-4">
{taskId && task && (
<Button
type="button"
variant="destructive"
className="mr-auto"
onClick={handleDelete}
>
Supprimer la tâche
</Button>
)}
<Button type="button" variant="ghost" onClick={onClose}>
Annuler
</Button>