fix: move task deletion to inline actions
This commit is contained in:
parent
e36c536cf2
commit
983dbb01b5
8 changed files with 147 additions and 26 deletions
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
92
apps/main/src/pages/tasks.test.tsx
Normal file
92
apps/main/src/pages/tasks.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ export const TabloTasksSection = ({
|
|||
etapes={etapes}
|
||||
etapeTitles={etapeTitleMap}
|
||||
onTaskClick={handleTaskClick}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onAddTask={handleAddTask}
|
||||
onAddTaskInline={handleCreateTask}
|
||||
onTaskMove={handleTaskMove}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue