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 */}
Assigné à
-
+ {
+ if (!value) return;
+ setAssigneeId(value);
+ }}
+ >
-
+ {selectedAssigneeLabel}
Non assigné
@@ -236,9 +256,15 @@ export const TaskModal = ({
{providedEtapes.length > 0 && (
Étape
-
+ {
+ if (!value) return;
+ setEtapeId(value);
+ }}
+ >
-
+ {selectedEtapeLabel}
Aucune