Improve task management
This commit is contained in:
parent
916dba496a
commit
740bedaf50
4 changed files with 112 additions and 14 deletions
|
|
@ -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(<TabloDetailsPage />, {
|
||||
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(<TabloDetailsPage />, {
|
||||
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(<TabloDetailsPage />, {
|
||||
route: "/tablos/tablo-1",
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ export const TabloDetailsPage = () => {
|
|||
const [taskModalInitialDueDate, setTaskModalInitialDueDate] = useState<Date | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [taskModalDefaultAssigneeId, setTaskModalDefaultAssigneeId] = useState<string | undefined>(
|
||||
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)}
|
||||
|
|
|
|||
|
|
@ -245,9 +245,7 @@ export const TabloTasksSection = ({
|
|||
{orphanedTasks.length} {pluralize("tâche", orphanedTasks.length)} sans Étape
|
||||
</p>
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
{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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="assignee">Assigné à</Label>
|
||||
<Select value={assigneeId} onValueChange={setAssigneeId}>
|
||||
<Select
|
||||
value={assigneeId}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
setAssigneeId(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="assignee" className="w-full">
|
||||
<SelectValue placeholder="Non assigné" />
|
||||
<span className="line-clamp-1">{selectedAssigneeLabel}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="unassigned">Non assigné</SelectItem>
|
||||
|
|
@ -236,9 +256,15 @@ export const TaskModal = ({
|
|||
{providedEtapes.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="etape">Étape</Label>
|
||||
<Select value={etapeId} onValueChange={setEtapeId}>
|
||||
<Select
|
||||
value={etapeId}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
setEtapeId(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="etape" className="w-full">
|
||||
<SelectValue placeholder="Aucune Étape" />
|
||||
<span className="line-clamp-1">{selectedEtapeLabel}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Aucune</SelectItem>
|
||||
|
|
|
|||
Loading…
Reference in a new issue