From 4a4eb9c8ade1691867784e97cdcff5e76ee40c1e Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 18 Sep 2025 09:37:46 +0200 Subject: [PATCH] refacto: passing by server actions on taskCard --- TODO.md | 26 +-- clients/tasks-client.ts | 39 +---- components/forms/EditTaskForm.tsx | 13 +- components/kanban/Board.tsx | 8 +- components/kanban/BoardContainer.tsx | 48 +++--- components/kanban/Column.tsx | 6 +- components/kanban/ObjectivesBoard.tsx | 18 -- components/kanban/PrioritySwimlanesBoard.tsx | 6 - components/kanban/SwimlanesBase.tsx | 12 -- components/kanban/SwimlanesBoard.tsx | 6 - components/kanban/TaskCard.tsx | 63 +++++-- hooks/useTasks.ts | 86 ++-------- src/actions/tasks.ts | 165 +++++++++++++++++++ src/app/api/tasks/route.ts | 114 +------------ src/contexts/TasksContext.tsx | 6 +- 15 files changed, 286 insertions(+), 330 deletions(-) create mode 100644 src/actions/tasks.ts diff --git a/TODO.md b/TODO.md index d8f51bd..566bb13 100644 --- a/TODO.md +++ b/TODO.md @@ -138,23 +138,27 @@ - [ ] Graphiques avec Chart.js ou Recharts - [ ] Export des données en CSV/JSON +## Autre Todo +- [ ] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique) + + ## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau) ### 4.1 Migration vers Server Actions - Actions rapides **Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes #### Actions TaskCard (Priorité 1) -- [ ] Créer `actions/tasks.ts` avec server actions de base -- [ ] `updateTaskStatus(taskId, status)` - Changement de statut -- [ ] `updateTaskTitle(taskId, title)` - Édition inline du titre -- [ ] `deleteTask(taskId)` - Suppression de tâche -- [ ] Modifier `TaskCard.tsx` pour utiliser server actions directement -- [ ] Remplacer les props callbacks par calls directs aux actions -- [ ] Intégrer `useTransition` pour les loading states natifs -- [ ] Tester la revalidation automatique du cache -- [ ] **Nettoyage** : Supprimer `PATCH /api/tasks` et `DELETE /api/tasks` -- [ ] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement) -- [ ] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions +- [x] Créer `actions/tasks.ts` avec server actions de base +- [x] `updateTaskStatus(taskId, status)` - Changement de statut +- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre +- [x] `deleteTask(taskId)` - Suppression de tâche +- [x] Modifier `TaskCard.tsx` pour utiliser server actions directement +- [x] Remplacer les props callbacks par calls directs aux actions +- [x] Intégrer `useTransition` pour les loading states natifs +- [x] Tester la revalidation automatique du cache +- [x] **Nettoyage** : Supprimer props obsolètes dans tous les composants Kanban +- [x] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement) +- [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions #### Actions Daily (Priorité 2) - [ ] Créer `actions/daily.ts` pour les checkboxes diff --git a/clients/tasks-client.ts b/clients/tasks-client.ts index f3dfebe..b8d2fd7 100644 --- a/clients/tasks-client.ts +++ b/clients/tasks-client.ts @@ -65,43 +65,8 @@ export class TasksClient { return httpClient.get('/tasks', params); } - /** - * Crée une nouvelle tâche - */ - async createTask(data: CreateTaskData): Promise<{ success: boolean; data: Task; message: string }> { - const payload = { - ...data, - dueDate: data.dueDate?.toISOString() - }; - - return httpClient.post('/tasks', payload); - } - - /** - * Met à jour une tâche - */ - async updateTask(data: UpdateTaskData): Promise<{ success: boolean; data: Task; message: string }> { - const payload = { - ...data, - dueDate: data.dueDate?.toISOString() - }; - - return httpClient.patch('/tasks', payload); - } - - /** - * Supprime une tâche - */ - async deleteTask(taskId: string): Promise<{ success: boolean; message: string }> { - return httpClient.delete('/tasks', { taskId }); - } - - /** - * Met à jour le statut d'une tâche - */ - async updateTaskStatus(taskId: string, status: TaskStatus): Promise<{ success: boolean; data: Task; message: string }> { - return this.updateTask({ taskId, status }); - } + // Note: Les méthodes createTask, updateTask et deleteTask ont été migrées vers Server Actions + // Voir /src/actions/tasks.ts pour createTask, updateTask, updateTaskTitle, updateTaskStatus, deleteTask } // Instance singleton diff --git a/components/forms/EditTaskForm.tsx b/components/forms/EditTaskForm.tsx index bda64e8..c6d4fbf 100644 --- a/components/forms/EditTaskForm.tsx +++ b/components/forms/EditTaskForm.tsx @@ -6,19 +6,26 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { TagInput } from '@/components/ui/TagInput'; import { Task, TaskPriority, TaskStatus } from '@/lib/types'; -import { UpdateTaskData } from '@/clients/tasks-client'; +// UpdateTaskData removed - using Server Actions directly import { getAllStatuses, getAllPriorities } from '@/lib/status-config'; interface EditTaskFormProps { isOpen: boolean; onClose: () => void; - onSubmit: (data: UpdateTaskData) => Promise; + onSubmit: (data: { taskId: string; title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; tags?: string[]; dueDate?: Date; }) => Promise; task: Task | null; loading?: boolean; } export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false }: EditTaskFormProps) { - const [formData, setFormData] = useState>({ + const [formData, setFormData] = useState<{ + title: string; + description: string; + status: TaskStatus; + priority: TaskPriority; + tags: string[]; + dueDate?: Date; + }>({ title: '', description: '', status: 'todo' as TaskStatus, diff --git a/components/kanban/Board.tsx b/components/kanban/Board.tsx index 8efc25f..e2468a1 100644 --- a/components/kanban/Board.tsx +++ b/components/kanban/Board.tsx @@ -18,15 +18,13 @@ import { TaskCard } from './TaskCard'; interface KanbanBoardProps { tasks: Task[]; onCreateTask?: (data: CreateTaskData) => Promise; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; visibleStatuses?: TaskStatus[]; } -export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, onUpdateStatus, compactView = false, visibleStatuses }: KanbanBoardProps) { +export function KanbanBoard({ tasks, onCreateTask, onEditTask, onUpdateStatus, compactView = false, visibleStatuses }: KanbanBoardProps) { const [activeTask, setActiveTask] = useState(null); const { isColumnVisible } = useUserPreferences(); const { isMounted, sensors } = useDragAndDrop(); @@ -95,9 +93,7 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU id={column.id} tasks={column.tasks} onCreateTask={onCreateTask} - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} compactView={compactView} /> ))} @@ -124,9 +120,7 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
) : null} diff --git a/components/kanban/BoardContainer.tsx b/components/kanban/BoardContainer.tsx index 290c9fa..0ef1894 100644 --- a/components/kanban/BoardContainer.tsx +++ b/components/kanban/BoardContainer.tsx @@ -9,8 +9,9 @@ import { KanbanFilters } from './KanbanFilters'; import { EditTaskForm } from '@/components/forms/EditTaskForm'; import { useTasksContext } from '@/contexts/TasksContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; -import { Task, TaskStatus } from '@/lib/types'; -import { UpdateTaskData, CreateTaskData } from '@/clients/tasks-client'; +import { Task, TaskStatus, TaskPriority } from '@/lib/types'; +import { CreateTaskData } from '@/clients/tasks-client'; +import { updateTask, createTask } from '@/actions/tasks'; import { getAllStatuses } from '@/lib/status-config'; interface KanbanBoardContainerProps { @@ -26,13 +27,11 @@ export function KanbanBoardContainer({ filteredTasks, pinnedTasks, loading, - createTask, - deleteTask, - updateTask, updateTaskOptimistic, kanbanFilters, setKanbanFilters, - tags + tags, + refreshTasks } = useTasksContext(); const { preferences, toggleColumnVisibility, isColumnVisible } = useUserPreferences(); @@ -45,24 +44,20 @@ export function KanbanBoardContainer({ setEditingTask(task); }; - const handleUpdateTask = async (data: UpdateTaskData) => { - await updateTask(data); - setEditingTask(null); + const handleUpdateTask = async (data: { taskId: string; title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; tags?: string[]; dueDate?: Date; }) => { + const result = await updateTask(data); + if (result.success) { + await refreshTasks(); // Rafraîchir les données + setEditingTask(null); + } else { + console.error('Error updating task:', result.error); + } }; - const handleUpdateTitle = async (taskId: string, newTitle: string) => { - await updateTask({ - taskId, - title: newTitle - }); - }; const handleUpdateStatus = async (taskId: string, newStatus: TaskStatus) => { // Utiliser la mise à jour optimiste pour le drag & drop - await updateTaskOptimistic({ - taskId, - status: newStatus - }); + await updateTaskOptimistic(taskId, newStatus); }; // Obtenir le nom du tag épinglé pour l'affichage @@ -70,7 +65,12 @@ export function KanbanBoardContainer({ // Wrapper pour adapter le type de createTask const handleCreateTask = async (data: CreateTaskData): Promise => { - await createTask(data); + const result = await createTask(data); + if (result.success) { + await refreshTasks(); // Rafraîchir les données + } else { + console.error('Error creating task:', result.error); + } }; return ( @@ -89,9 +89,7 @@ export function KanbanBoardContainer({ {showObjectives && pinnedTasks.length > 0 && ( Promise; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; compactView?: boolean; } -export function KanbanColumn({ id, tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, compactView = false }: KanbanColumnProps) { +export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView = false }: KanbanColumnProps) { const [showQuickAdd, setShowQuickAdd] = useState(false); // Configuration de la zone droppable @@ -91,7 +89,7 @@ export function KanbanColumn({ id, tasks, onCreateTask, onDeleteTask, onEditTask ) : ( tasks.map((task) => ( - + )) )} diff --git a/components/kanban/ObjectivesBoard.tsx b/components/kanban/ObjectivesBoard.tsx index bedd503..6b81ebc 100644 --- a/components/kanban/ObjectivesBoard.tsx +++ b/components/kanban/ObjectivesBoard.tsx @@ -21,9 +21,7 @@ import { useDroppable } from '@dnd-kit/core'; interface ObjectivesBoardProps { tasks: Task[]; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; pinnedTagName?: string; @@ -36,9 +34,7 @@ function DroppableColumn({ title, color, icon, - onDeleteTask, onEditTask, - onUpdateTitle, compactView }: { status: TaskStatus; @@ -46,9 +42,7 @@ function DroppableColumn({ title: string; color: string; icon: string; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; compactView: boolean; }) { const { setNodeRef } = useDroppable({ @@ -80,9 +74,7 @@ function DroppableColumn({
@@ -96,9 +88,7 @@ function DroppableColumn({ export function ObjectivesBoard({ tasks, - onDeleteTask, onEditTask, - onUpdateTitle, onUpdateStatus, compactView = false, pinnedTagName = "Objectifs" @@ -209,9 +199,7 @@ export function ObjectivesBoard({ title="À faire" color="bg-[var(--primary)]" icon="📋" - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} compactView={compactView} /> @@ -221,9 +209,7 @@ export function ObjectivesBoard({ title="En cours" color="bg-yellow-400" icon="🔄" - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} compactView={compactView} /> @@ -233,9 +219,7 @@ export function ObjectivesBoard({ title="Terminé" color="bg-green-400" icon="✅" - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} compactView={compactView} /> @@ -267,9 +251,7 @@ export function ObjectivesBoard({
diff --git a/components/kanban/PrioritySwimlanesBoard.tsx b/components/kanban/PrioritySwimlanesBoard.tsx index fa81b27..d998a4a 100644 --- a/components/kanban/PrioritySwimlanesBoard.tsx +++ b/components/kanban/PrioritySwimlanesBoard.tsx @@ -10,9 +10,7 @@ interface PrioritySwimlanesBoardProps { loading: boolean; tasks: Task[]; onCreateTask?: (data: CreateTaskData) => Promise; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; visibleStatuses?: TaskStatus[]; @@ -21,9 +19,7 @@ interface PrioritySwimlanesBoardProps { export function PrioritySwimlanesBoard({ tasks, onCreateTask, - onDeleteTask, onEditTask, - onUpdateTitle, onUpdateStatus, compactView = false, visibleStatuses, @@ -66,9 +62,7 @@ export function PrioritySwimlanesBoard({ tasks={tasks} swimlanes={swimlanesData} onCreateTask={onCreateTask} - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} onUpdateStatus={onUpdateStatus} compactView={compactView} visibleStatuses={visibleStatuses} diff --git a/components/kanban/SwimlanesBase.tsx b/components/kanban/SwimlanesBase.tsx index 9e97753..b405b58 100644 --- a/components/kanban/SwimlanesBase.tsx +++ b/components/kanban/SwimlanesBase.tsx @@ -25,9 +25,7 @@ import { useDroppable } from '@dnd-kit/core'; function DroppableColumn({ status, tasks, - onDeleteTask, onEditTask, - onUpdateTitle, compactView, onCreateTask, showQuickAdd, @@ -36,9 +34,7 @@ function DroppableColumn({ }: { status: TaskStatus; tasks: Task[]; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; compactView: boolean; onCreateTask?: (data: CreateTaskData) => Promise; showQuickAdd?: boolean; @@ -60,9 +56,7 @@ function DroppableColumn({ ))} @@ -117,9 +111,7 @@ interface SwimlanesBaseProps { tasks: Task[]; swimlanes: SwimlaneData[]; onCreateTask?: (data: CreateTaskData) => Promise; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; visibleStatuses?: TaskStatus[]; @@ -129,9 +121,7 @@ export function SwimlanesBase({ tasks, swimlanes, onCreateTask, - onDeleteTask, onEditTask, - onUpdateTitle, onUpdateStatus, compactView = false, visibleStatuses @@ -270,9 +260,7 @@ export function SwimlanesBase({ key={columnId} status={status} tasks={statusTasks} - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} compactView={compactView} onCreateTask={onCreateTask ? (data) => handleQuickAdd(data, columnId) : undefined} showQuickAdd={showQuickAdd[columnId] || false} diff --git a/components/kanban/SwimlanesBoard.tsx b/components/kanban/SwimlanesBoard.tsx index 9ff2e15..bf0b2df 100644 --- a/components/kanban/SwimlanesBoard.tsx +++ b/components/kanban/SwimlanesBoard.tsx @@ -9,9 +9,7 @@ import { SwimlanesBase, SwimlaneData } from './SwimlanesBase'; interface SwimlanesboardProps { tasks: Task[]; onCreateTask?: (data: CreateTaskData) => Promise; - onDeleteTask?: (taskId: string) => Promise; onEditTask?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; visibleStatuses?: TaskStatus[]; @@ -21,9 +19,7 @@ interface SwimlanesboardProps { export function SwimlanesBoard({ tasks, onCreateTask, - onDeleteTask, onEditTask, - onUpdateTitle, onUpdateStatus, compactView = false, visibleStatuses, @@ -88,9 +84,7 @@ export function SwimlanesBoard({ tasks={tasks} swimlanes={swimlanesData} onCreateTask={onCreateTask} - onDeleteTask={onDeleteTask} onEditTask={onEditTask} - onUpdateTitle={onUpdateTitle} onUpdateStatus={onUpdateStatus} compactView={compactView} visibleStatuses={visibleStatuses} diff --git a/components/kanban/TaskCard.tsx b/components/kanban/TaskCard.tsx index 6aa01de..252fedc 100644 --- a/components/kanban/TaskCard.tsx +++ b/components/kanban/TaskCard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useTransition } from 'react'; import { Task } from '@/lib/types'; import { formatDistanceToNow } from 'date-fns'; import { fr } from 'date-fns/locale'; @@ -9,21 +9,21 @@ import { useTasksContext } from '@/contexts/TasksContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useDraggable } from '@dnd-kit/core'; import { getPriorityConfig, getPriorityColorHex } from '@/lib/status-config'; +import { updateTaskTitle, deleteTask } from '@/actions/tasks'; interface TaskCardProps { task: Task; - onDelete?: (taskId: string) => Promise; onEdit?: (task: Task) => void; - onUpdateTitle?: (taskId: string, newTitle: string) => Promise; compactView?: boolean; } -export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = false }: TaskCardProps) { +export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) { const [isEditingTitle, setIsEditingTitle] = useState(false); const [editTitle, setEditTitle] = useState(task.title); const [showTooltip, setShowTooltip] = useState(false); + const [isPending, startTransition] = useTransition(); const timeoutRef = useRef(null); - const { tags: availableTags } = useTasksContext(); + const { tags: availableTags, refreshTasks } = useTasksContext(); const { preferences } = useUserPreferences(); // Helper pour construire l'URL Jira @@ -61,8 +61,18 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = const handleDelete = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - if (onDelete) { - await onDelete(task.id); + + if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) { + startTransition(async () => { + const result = await deleteTask(task.id); + if (!result.success) { + console.error('Error deleting task:', result.error); + // TODO: Afficher une notification d'erreur + } else { + // Rafraîchir les données après suppression réussie + await refreshTasks(); + } + }); } }; @@ -77,7 +87,7 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = const handleTitleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - if (onUpdateTitle && !isDragging) { + if (!isDragging && !isPending) { setIsEditingTitle(true); setShowTooltip(false); } @@ -85,8 +95,19 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = const handleTitleSave = async () => { const trimmedTitle = editTitle.trim(); - if (trimmedTitle && trimmedTitle !== task.title && onUpdateTitle) { - await onUpdateTitle(task.id, trimmedTitle); + if (trimmedTitle && trimmedTitle !== task.title) { + startTransition(async () => { + const result = await updateTaskTitle(task.id, trimmedTitle); + if (!result.success) { + console.error('Error updating task title:', result.error); + // Remettre l'ancien titre en cas d'erreur + setEditTitle(task.title); + } else { + // Mettre à jour optimistiquement le titre local + // La Server Action a déjà mis à jour la DB, on synchronise juste l'affichage + task.title = trimmedTitle; + } + }); } setIsEditingTitle(false); setShowTooltip(false); @@ -142,7 +163,7 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = onClick={handleTitleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - title={onUpdateTitle ? "Cliquer pour éditer" : undefined} + title="Cliquer pour éditer" > {titleWithoutEmojis} @@ -190,6 +211,8 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = task.status === 'done' ? 'opacity-60' : '' } ${ isJiraTask ? 'jira-task' : '' + } ${ + isPending ? 'opacity-70 pointer-events-none' : '' }`} {...attributes} {...(isEditingTitle ? {} : listeners)} @@ -231,17 +254,19 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle, compactView = {!isEditingTitle && onEdit && ( )} - {!isEditingTitle && onDelete && ( + {!isEditingTitle && (