Add inline task creation in étapes view

- Each étape shows an "Ajouter une tâche" button at the bottom when expanded
- Clicking opens an inline text input with Enter to submit, Escape to cancel
- Created tasks are linked to the étape via parent_task_id
- Purple "Ajouter" button with instant feedback
- Uses existing useCreateTask hook with correct tablo_id and parent_task_id

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-02-22 09:42:22 +01:00
parent 3f97478977
commit bbf9d695ba
No known key found for this signature in database

View file

@ -29,7 +29,7 @@ import { TabloDiscussionSection } from "../components/TabloDiscussionSection";
import { TabloEventsSection } from "../components/TabloEventsSection";
import { TabloFilesSection } from "../components/TabloFilesSection";
import { TabloTasksSection } from "../components/TabloTasksSection";
import { useAllTasks, useTabloEtapes } from "../hooks/tasks";
import { useAllTasks, useCreateTask, useTabloEtapes } from "../hooks/tasks";
import { useTabloFileNames } from "../hooks/tablo_data";
import { useTablosList } from "../hooks/tablos";
@ -459,7 +459,7 @@ export const TabloDetailsPage = () => {
)}
{activeSection === "etapes" && (
<EtapesSection etapes={etapes} tabloTasks={tabloTasks} />
<EtapesSection etapes={etapes} tabloTasks={tabloTasks} tabloId={tabloId ?? ""} />
)}
</div>
</div>
@ -471,13 +471,18 @@ export const TabloDetailsPage = () => {
function EtapesSection({
etapes,
tabloTasks,
tabloId,
}: {
etapes: Etape[];
tabloTasks: KanbanTask[];
tabloId: string;
}) {
const [expandedEtapes, setExpandedEtapes] = useState<Set<string>>(
new Set(etapes.map((e) => e.id)),
);
const [addingTaskToEtape, setAddingTaskToEtape] = useState<string | null>(null);
const [newTaskTitle, setNewTaskTitle] = useState("");
const { mutate: createTask } = useCreateTask();
const toggleEtape = (id: string) => {
setExpandedEtapes((prev) => {
@ -488,6 +493,21 @@ function EtapesSection({
});
};
const handleAddTask = (etapeId: string) => {
const title = newTaskTitle.trim();
if (!title || !tabloId) return;
createTask({
tablo_id: tabloId,
title,
status: "todo",
parent_task_id: etapeId,
is_parent: false,
position: tabloTasks.filter((t) => t.parent_task_id === etapeId).length,
});
setNewTaskTitle("");
setAddingTaskToEtape(null);
};
const statusConfig: Record<string, { label: string; color: string }> = {
todo: { label: "À faire", color: "bg-blue-100 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400" },
in_progress: { label: "En cours", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-400" },
@ -575,40 +595,87 @@ function EtapesSection({
)}
</button>
{/* Child tasks */}
{isExpanded && childTasks.length > 0 && (
<div className="border-t border-gray-100 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-700">
{childTasks.map((task) => (
<div
key={task.id}
className="flex items-center gap-3 px-5 py-3 pl-16 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
{task.status === "done" ? (
<CircleCheckIcon className="w-4 h-4 text-green-500 shrink-0" />
) : (
<div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
)}
<span
className={cn(
"text-sm flex-1 truncate",
task.status === "done" ? "line-through text-gray-400" : "text-gray-900 dark:text-gray-100",
)}
>
{task.title}
</span>
{task.status && (
<span className={cn("px-2 py-0.5 rounded-full text-[10px] font-medium shrink-0", (statusConfig[task.status] ?? statusConfig.todo).color)}>
{(statusConfig[task.status] ?? statusConfig.todo).label}
</span>
)}
{/* Child tasks + add task */}
{isExpanded && (
<div className="border-t border-gray-100 dark:border-gray-700">
{childTasks.length > 0 && (
<div className="divide-y divide-gray-100 dark:divide-gray-700">
{childTasks.map((task) => (
<div
key={task.id}
className="flex items-center gap-3 px-5 py-3 pl-16 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
{task.status === "done" ? (
<CircleCheckIcon className="w-4 h-4 text-green-500 shrink-0" />
) : (
<div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
)}
<span
className={cn(
"text-sm flex-1 truncate",
task.status === "done" ? "line-through text-gray-400" : "text-gray-900 dark:text-gray-100",
)}
>
{task.title}
</span>
{task.status && (
<span className={cn("px-2 py-0.5 rounded-full text-[10px] font-medium shrink-0", (statusConfig[task.status] ?? statusConfig.todo).color)}>
{(statusConfig[task.status] ?? statusConfig.todo).label}
</span>
)}
</div>
))}
</div>
))}
</div>
)}
)}
{isExpanded && childTasks.length === 0 && (
<div className="border-t border-gray-100 dark:border-gray-700 px-5 py-4 pl-16 text-sm text-muted-foreground">
Aucune tâche dans cette étape
{childTasks.length === 0 && addingTaskToEtape !== etape.id && (
<div className="px-5 py-4 pl-16 text-sm text-muted-foreground">
Aucune tâche dans cette étape
</div>
)}
{/* Inline add task */}
{addingTaskToEtape === etape.id ? (
<div className="flex items-center gap-2 px-5 py-3 pl-16 border-t border-gray-100 dark:border-gray-700">
<div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
<input
autoFocus
type="text"
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleAddTask(etape.id);
if (e.key === "Escape") { setAddingTaskToEtape(null); setNewTaskTitle(""); }
}}
placeholder="Nom de la tâche..."
className="flex-1 text-sm bg-transparent border-none outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400"
/>
<button
type="button"
onClick={() => handleAddTask(etape.id)}
disabled={!newTaskTitle.trim()}
className="text-xs font-medium px-3 py-1 rounded-md bg-[#804EEC] text-white hover:bg-[#6f3fd4] disabled:opacity-40 transition-colors"
>
Ajouter
</button>
<button
type="button"
onClick={() => { setAddingTaskToEtape(null); setNewTaskTitle(""); }}
className="text-xs text-muted-foreground hover:text-foreground px-2 py-1"
>
Annuler
</button>
</div>
) : (
<button
type="button"
onClick={(e) => { e.stopPropagation(); setAddingTaskToEtape(etape.id); setNewTaskTitle(""); }}
className="flex items-center gap-2 px-5 py-2.5 pl-16 text-sm text-muted-foreground hover:text-[#804EEC] hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors w-full text-left border-t border-gray-100 dark:border-gray-700"
>
<PlusIcon className="w-4 h-4" />
Ajouter une tâche
</button>
)}
</div>
)}
</div>