From e36c536cf2e74ce1c6140dcc882ae06e1e022ef7 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 16 Apr 2026 11:54:41 +0200 Subject: [PATCH] Allow updates deletions on etapes / tasks --- .../src/pages/tablo-details.layout.test.tsx | 73 ++++++- apps/main/src/pages/tablo-details.tsx | 8 + packages/tablo-views/src/EtapesSection.tsx | 187 +++++++++++++++--- 3 files changed, 239 insertions(+), 29 deletions(-) diff --git a/apps/main/src/pages/tablo-details.layout.test.tsx b/apps/main/src/pages/tablo-details.layout.test.tsx index 17c103e..a327343 100644 --- a/apps/main/src/pages/tablo-details.layout.test.tsx +++ b/apps/main/src/pages/tablo-details.layout.test.tsx @@ -7,6 +7,8 @@ import { TabloDetailsPage } from "./tablo-details"; const mutateUpdateTablo = vi.fn(); const mutateUpdateTask = vi.fn(); const mutateCreateEtape = vi.fn(); +const mutateUpdateEtape = vi.fn(); +const mutateDeleteEtape = vi.fn(); const mutateDeleteTask = vi.fn(); const tablosData = [ @@ -107,7 +109,22 @@ vi.mock("../hooks/tasks", () => ({ ], }), useTabloEtapes: () => ({ - data: [], + data: [ + { + id: "etape-1", + tablo_id: "tablo-1", + title: "Kickoff", + description: null, + status: "todo", + assignee_id: null, + position: 0, + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-01T00:00:00.000Z", + is_parent: true, + parent_task_id: null, + due_date: null, + }, + ], }), useUpdateTask: () => ({ mutate: mutateUpdateTask, @@ -122,6 +139,14 @@ vi.mock("../hooks/tasks", () => ({ mutateAsync: mutateCreateEtape, isPending: false, }), + useUpdateEtape: () => ({ + mutateAsync: mutateUpdateEtape, + isPending: false, + }), + useDeleteEtape: () => ({ + mutateAsync: mutateDeleteEtape, + isPending: false, + }), useCreateTask: () => ({ mutate: vi.fn(), }), @@ -208,10 +233,54 @@ describe("TabloDetailsPage overview layout", () => { expect(mutateCreateEtape).toHaveBeenCalledWith({ tabloId: "tablo-1", title: "Kickoff", - position: 0, + position: 1, }); }); + it("updates an etape from the etapes tab", async () => { + const user = userEvent.setup(); + mutateUpdateEtape.mockResolvedValueOnce(undefined); + + renderWithProviders(, { + route: "/tablos/tablo-1", + path: "/tablos/:tabloId", + }); + + await user.click(screen.getByRole("button", { name: "Étapes" })); + await user.click(screen.getByRole("button", { name: "Modifier l'étape Kickoff" })); + await user.clear(screen.getByDisplayValue("Kickoff")); + await user.type(screen.getByPlaceholderText("Nom de l'étape"), "Planification"); + await user.click(screen.getByRole("button", { name: "Enregistrer l'étape" })); + + expect(mutateUpdateEtape).toHaveBeenCalledWith({ + id: "etape-1", + tabloId: "tablo-1", + title: "Planification", + }); + }); + + it("deletes an etape from the etapes tab", async () => { + const user = userEvent.setup(); + mutateDeleteEtape.mockResolvedValueOnce(undefined); + const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true); + + renderWithProviders(, { + route: "/tablos/tablo-1", + path: "/tablos/:tabloId", + }); + + await user.click(screen.getByRole("button", { name: "Étapes" })); + await user.click(screen.getByRole("button", { name: "Supprimer l'étape Kickoff" })); + + expect(confirmSpy).toHaveBeenCalled(); + expect(mutateDeleteEtape).toHaveBeenCalledWith({ + id: "etape-1", + tabloId: "tablo-1", + }); + + confirmSpy.mockRestore(); + }); + it("deletes a task from the task modal", async () => { const user = userEvent.setup(); diff --git a/apps/main/src/pages/tablo-details.tsx b/apps/main/src/pages/tablo-details.tsx index 50dfa93..824a74e 100644 --- a/apps/main/src/pages/tablo-details.tsx +++ b/apps/main/src/pages/tablo-details.tsx @@ -64,8 +64,10 @@ import { useAllTasks, useCreateEtape, useCreateTask, + useDeleteEtape, useDeleteTask, useTabloEtapes, + useUpdateEtape, useUpdateTask, useUpdateTaskPositions, } from "../hooks/tasks"; @@ -127,6 +129,8 @@ export const TabloDetailsPage = () => { const { mutate: updateTablo, mutateAsync: updateTabloAsync } = useUpdateTablo(); const { mutate: createTask } = useCreateTask(); const { mutateAsync: createEtape, isPending: isCreatingEtape } = useCreateEtape(); + const { mutateAsync: updateEtape, isPending: isUpdatingEtape } = useUpdateEtape(); + const { mutateAsync: deleteEtape, isPending: isDeletingEtape } = useDeleteEtape(); const { mutate: updateTaskPositions } = useUpdateTaskPositions(); // Files & folders hooks @@ -473,7 +477,11 @@ export const TabloDetailsPage = () => { }) } onCreateEtape={(params) => createEtape(params).then(() => undefined)} + onUpdateEtape={(params) => updateEtape(params).then(() => undefined)} + onDeleteEtape={(params) => deleteEtape(params).then(() => undefined)} isCreatingEtape={isCreatingEtape} + isUpdatingEtape={isUpdatingEtape} + isDeletingEtape={isDeletingEtape} /> )} diff --git a/packages/tablo-views/src/EtapesSection.tsx b/packages/tablo-views/src/EtapesSection.tsx index 9f4d0f1..e1b22e8 100644 --- a/packages/tablo-views/src/EtapesSection.tsx +++ b/packages/tablo-views/src/EtapesSection.tsx @@ -4,11 +4,15 @@ import { Button } from "@xtablo/ui/components/button"; import { Input } from "@xtablo/ui/components/input"; import { CalendarIcon, + CheckIcon, ChevronDownIcon, ChevronRightIcon, CircleCheckIcon, + PencilIcon, ListChecksIcon, PlusIcon, + Trash2Icon, + XIcon, } from "lucide-react"; import { useState } from "react"; @@ -26,7 +30,11 @@ interface EtapesSectionProps { position: number; }) => void; onCreateEtape: (params: { tabloId: string; title: string; position: number }) => Promise; + onUpdateEtape?: (params: { id: string; tabloId: string; title: string }) => Promise; + onDeleteEtape?: (params: { id: string; tabloId: string }) => Promise; isCreatingEtape?: boolean; + isUpdatingEtape?: boolean; + isDeletingEtape?: boolean; } export function EtapesSection({ @@ -36,7 +44,11 @@ export function EtapesSection({ isAdmin, onCreateTask, onCreateEtape, + onUpdateEtape, + onDeleteEtape, isCreatingEtape = false, + isUpdatingEtape = false, + isDeletingEtape = false, }: EtapesSectionProps) { const [expandedEtapes, setExpandedEtapes] = useState>( new Set(etapes.map((e) => e.id)) @@ -44,6 +56,8 @@ export function EtapesSection({ const [addingTaskToEtape, setAddingTaskToEtape] = useState(null); const [newEtapeTitle, setNewEtapeTitle] = useState(""); const [newTaskTitle, setNewTaskTitle] = useState(""); + const [editingEtapeId, setEditingEtapeId] = useState(null); + const [editingEtapeTitle, setEditingEtapeTitle] = useState(""); const toggleEtape = (id: string) => { setExpandedEtapes((prev) => { @@ -86,6 +100,46 @@ export function EtapesSection({ setNewEtapeTitle(""); }; + const startEditingEtape = (etapeId: string, title: string) => { + setEditingEtapeId(etapeId); + setEditingEtapeTitle(title); + }; + + const cancelEditingEtape = () => { + setEditingEtapeId(null); + setEditingEtapeTitle(""); + }; + + const handleUpdateEtape = async (etapeId: string) => { + const title = editingEtapeTitle.trim(); + if (!title || !tabloId) { + return; + } + + await onUpdateEtape?.({ + id: etapeId, + tabloId, + title, + }); + + cancelEditingEtape(); + }; + + const handleDeleteEtape = async (etapeId: string, title: string) => { + if (!tabloId) { + return; + } + + if (!window.confirm(`Supprimer l'étape "${title}" ?`)) { + return; + } + + await onDeleteEtape?.({ + id: etapeId, + tabloId, + }); + }; + const statusConfig: Record = { todo: { label: "À faire", @@ -159,6 +213,7 @@ export function EtapesSection({ ? "in_progress" : "todo"; const status = statusConfig[derivedStatus] ?? statusConfig.todo; + const isEditing = editingEtapeId === etape.id; return (
{/* Etape header */} -
+ +
+ + {index + 1} + +
+ +
+ {isEditing ? ( + setEditingEtapeTitle(event.target.value)} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void handleUpdateEtape(etape.id); + } + + if (event.key === "Escape") { + event.preventDefault(); + cancelEditingEtape(); + } + }} + placeholder="Nom de l'étape" + aria-label="Nom de l'étape" + autoFocus + className="h-9" + /> + ) : ( + <> +

+ {etape.title} +

+ {etape.description && ( +

+ {etape.description} +

+ )} + + )} +
+
{etape.due_date && ( @@ -237,8 +319,59 @@ export function EtapesSection({
)} + + {isAdmin && ( +
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+ )} - + {/* Child tasks + add task */} {isExpanded && (