From 740bedaf50a69f27e6ee0372c7524acaa24104d9 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 16 Apr 2026 22:21:16 +0200 Subject: [PATCH] Improve task management --- .../src/pages/tablo-details.layout.test.tsx | 72 ++++++++++++++++++- apps/main/src/pages/tablo-details.tsx | 12 +++- .../tablo-views/src/TabloTasksSection.tsx | 4 +- .../src/components/kanban/TaskModal.tsx | 38 ++++++++-- 4 files changed, 112 insertions(+), 14 deletions(-) diff --git a/apps/main/src/pages/tablo-details.layout.test.tsx b/apps/main/src/pages/tablo-details.layout.test.tsx index 43c80a9..e30a059 100644 --- a/apps/main/src/pages/tablo-details.layout.test.tsx +++ b/apps/main/src/pages/tablo-details.layout.test.tsx @@ -1,11 +1,24 @@ -import { screen } from "@testing-library/react"; +import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "../utils/testHelpers"; import { TabloDetailsPage } from "./tablo-details"; +if (!HTMLElement.prototype.hasPointerCapture) { + HTMLElement.prototype.hasPointerCapture = () => false; +} + +if (!HTMLElement.prototype.setPointerCapture) { + HTMLElement.prototype.setPointerCapture = () => undefined; +} + +if (!HTMLElement.prototype.releasePointerCapture) { + HTMLElement.prototype.releasePointerCapture = () => undefined; +} + const mutateUpdateTablo = vi.fn(); const mutateUpdateTask = vi.fn(); +const mutateCreateTask = vi.fn(); const mutateCreateEtape = vi.fn(); const mutateUpdateEtape = vi.fn(); const mutateDeleteEtape = vi.fn(); @@ -47,7 +60,15 @@ vi.mock("../hooks/tablos", () => ({ isLoading: false, }), useTabloMembers: () => ({ - data: [], + data: [ + { + id: "user-1", + name: "Test User", + email: "john@example.com", + avatar_url: null, + is_admin: true, + }, + ], }), useTabloOverviewLayout: () => ({ data: layoutData, @@ -105,6 +126,7 @@ vi.mock("../hooks/tasks", () => ({ assignee_id: "user-1", title: "Task A", status: "todo", + parent_task_id: "etape-1", }, ], }), @@ -148,7 +170,7 @@ vi.mock("../hooks/tasks", () => ({ isPending: false, }), useCreateTask: () => ({ - mutate: vi.fn(), + mutate: mutateCreateTask, }), useUpdateTaskPositions: () => ({ mutate: vi.fn(), @@ -295,6 +317,50 @@ describe("TabloDetailsPage overview layout", () => { expect(mutateDeleteTask).toHaveBeenCalledWith("task-1"); }); + it("restores the task etape when reopening the same edit modal", async () => { + const user = userEvent.setup(); + + renderWithProviders(, { + route: "/tablos/tablo-1?section=tasks", + path: "/tablos/:tabloId", + }); + + await user.click(screen.getByText("Task A")); + await user.click(screen.getByRole("combobox", { name: "Étape" })); + await user.click(screen.getByRole("option", { name: "Aucune" })); + await user.click(screen.getByRole("button", { name: "Annuler" })); + + await user.click(screen.getByText("Task A")); + + expect(screen.getByRole("combobox", { name: "Étape" })).toHaveTextContent("Kickoff"); + }); + + it("assigns the current user when creating from mes tâches in overview", async () => { + const user = userEvent.setup(); + + renderWithProviders(, { + route: "/tablos/tablo-1", + path: "/tablos/:tabloId", + }); + + await user.click(screen.getByRole("button", { name: "Ajouter" })); + + await waitFor(() => { + expect(screen.getByRole("combobox", { name: "Assigné à" })).toHaveTextContent("Test User"); + }); + + await user.type(screen.getByLabelText("Titre *"), "Nouvelle tâche"); + await user.click(screen.getByRole("button", { name: "Créer" })); + + expect(mutateCreateTask).toHaveBeenCalledWith( + expect.objectContaining({ + tablo_id: "tablo-1", + title: "Nouvelle tâche", + assignee_id: "user-1", + }) + ); + }); + it("renders overview cards in persisted left-zone order", () => { renderWithProviders(, { route: "/tablos/tablo-1", diff --git a/apps/main/src/pages/tablo-details.tsx b/apps/main/src/pages/tablo-details.tsx index 824a74e..4816bb3 100644 --- a/apps/main/src/pages/tablo-details.tsx +++ b/apps/main/src/pages/tablo-details.tsx @@ -103,6 +103,9 @@ export const TabloDetailsPage = () => { const [taskModalInitialDueDate, setTaskModalInitialDueDate] = useState( undefined ); + const [taskModalDefaultAssigneeId, setTaskModalDefaultAssigneeId] = useState( + undefined + ); const [showAllOverviewTasks, setShowAllOverviewTasks] = useState(false); const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); @@ -169,14 +172,16 @@ export const TabloDetailsPage = () => { setInviteEmail(""); }; - const openTaskModal = (dueDate?: Date) => { + const openTaskModal = (dueDate?: Date, defaultAssigneeId?: string) => { setTaskModalInitialDueDate(dueDate ? new Date(dueDate) : undefined); + setTaskModalDefaultAssigneeId(defaultAssigneeId); setIsTaskModalOpen(true); }; const closeTaskModal = () => { setIsTaskModalOpen(false); setTaskModalInitialDueDate(undefined); + setTaskModalDefaultAssigneeId(undefined); }; const sectionParam = searchParams.get("section") as TabSection | null; @@ -357,7 +362,7 @@ export const TabloDetailsPage = () => { showFileMenu={true} onOpenTasks={() => setSearchParams({ section: "tasks" })} onOpenFiles={() => setSearchParams({ section: "files" })} - onCreateTask={() => openTaskModal()} + onCreateTask={() => openTaskModal(undefined, currentUser.id)} onToggleTaskDone={(taskId) => updateTask({ id: taskId, status: "done" })} showAllTasks={showAllOverviewTasks} onToggleShowAllTasks={() => setShowAllOverviewTasks((prev) => !prev)} @@ -500,8 +505,11 @@ export const TabloDetailsPage = () => { tabloId={tabloId} isOpen={isTaskModalOpen} onClose={closeTaskModal} + members={members} + etapes={etapes} initialStatus="todo" initialDueDate={taskModalInitialDueDate} + defaultAssigneeId={taskModalDefaultAssigneeId} onCreateTask={(task) => createTask(task)} onUpdateTask={(task) => updateTask(task)} onDeleteTask={(taskId) => deleteTask(taskId)} diff --git a/packages/tablo-views/src/TabloTasksSection.tsx b/packages/tablo-views/src/TabloTasksSection.tsx index 90eef07..1839fe9 100644 --- a/packages/tablo-views/src/TabloTasksSection.tsx +++ b/packages/tablo-views/src/TabloTasksSection.tsx @@ -245,9 +245,7 @@ export const TabloTasksSection = ({ {orphanedTasks.length} {pluralize("tâche", orphanedTasks.length)} sans Étape

- {orphanedTasks.length === 1 - ? "Cette tâche n'est associée à aucune Étape. Modifiez-la pour l'associer à une Étape." - : "Ces tâches ne sont associées à aucune Étape. Modifiez-les pour les associer à une Étape."} + Associez {orphanedTasks.length > 1 ? "ces" : "cette"} {pluralize("tâche", orphanedTasks.length)} à une étape.

diff --git a/packages/tablo-views/src/components/kanban/TaskModal.tsx b/packages/tablo-views/src/components/kanban/TaskModal.tsx index aebfe4f..df1dd2b 100644 --- a/packages/tablo-views/src/components/kanban/TaskModal.tsx +++ b/packages/tablo-views/src/components/kanban/TaskModal.tsx @@ -36,6 +36,7 @@ interface TaskModalProps { tablos?: MinimalTablo[]; allowTabloSelection?: boolean; initialDueDate?: Date; + defaultAssigneeId?: string; /** Called when creating a new task. */ onCreateTask?: (task: KanbanTaskInsert) => void; /** Called when updating an existing task. */ @@ -56,6 +57,7 @@ export const TaskModal = ({ tablos, allowTabloSelection = false, initialDueDate, + defaultAssigneeId, onCreateTask, onUpdateTask, }: TaskModalProps) => { @@ -69,8 +71,20 @@ export const TaskModal = ({ ); const currentTabloId = allowTabloSelection ? selectedTabloId : initialTabloId || ""; + const selectedAssigneeLabel = + assigneeId === "unassigned" + ? "Non assigné" + : providedMembers.find((member) => member.id === assigneeId)?.name ?? "Non assigné"; + const selectedEtapeLabel = + etapeId === "none" + ? "Aucune Étape" + : providedEtapes.find((etape) => etape.id === etapeId)?.title ?? "Aucune Étape"; useEffect(() => { + if (!isOpen) { + return; + } + if (task) { setTitle(task.title ?? ""); setDescription(task.description ?? ""); @@ -83,14 +97,14 @@ export const TaskModal = ({ } else { setTitle(""); setDescription(""); - setAssigneeId("unassigned"); + setAssigneeId(defaultAssigneeId ?? "unassigned"); setEtapeId("none"); setDueDate(initialDueDate ? new Date(initialDueDate) : undefined); if (allowTabloSelection && tablos && tablos.length > 0) { setSelectedTabloId(tablos[0].id); } } - }, [task, initialTabloId, allowTabloSelection, tablos, initialDueDate]); + }, [isOpen, task, initialTabloId, allowTabloSelection, tablos, initialDueDate, defaultAssigneeId]); // Format Date to YYYY-MM-DD string for database storage const formatDateForDb = (date: Date | undefined): string | null => { @@ -217,9 +231,15 @@ export const TaskModal = ({ {/* Assignee */}
- { + if (!value) return; + setAssigneeId(value); + }} + > - + {selectedAssigneeLabel} Non assigné @@ -236,9 +256,15 @@ export const TaskModal = ({ {providedEtapes.length > 0 && (
- { + if (!value) return; + setEtapeId(value); + }} + > - + {selectedEtapeLabel} Aucune