From 848ede2008c53c70358d88ee74e6d38bea02d59d Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Thu, 5 Feb 2026 23:47:52 -0300 Subject: [PATCH] fix: drag and drop usability, removing the flick view --- apps/web/src/components/board/task-board.tsx | 32 +++++---- .../src/hooks/board/use-column-mutations.ts | 25 +++++-- .../web/src/hooks/board/use-task-mutations.ts | 66 ++++++++++++++++--- 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/apps/web/src/components/board/task-board.tsx b/apps/web/src/components/board/task-board.tsx index b9ebd76..9a8ce5f 100644 --- a/apps/web/src/components/board/task-board.tsx +++ b/apps/web/src/components/board/task-board.tsx @@ -360,7 +360,7 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { ); const handleDragEnd = useCallback( - async (event: DragEndEvent) => { + (event: DragEndEvent) => { const { active } = event; const activeData = active.data.current; @@ -382,7 +382,6 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { const currentIndex = localColumns.findIndex((c) => c.id === column.id); if (currentIndex !== startIndex) { - // Reorder columns - primeiro HTTP, depois WebSocket const reorderedColumns = localColumns.map((col, index) => ({ id: col.id, order: index, @@ -396,10 +395,14 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { toast.error("Failed to reorder columns"); setLocalColumns(columns); }, + onSettled: () => { + isPendingMutation.current = false; + }, }); + } else { + isPendingMutation.current = false; } - isPendingMutation.current = false; return; } @@ -454,6 +457,10 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { toast.error("Failed to move task"); setLocalColumns(columns); }, + onSettled: () => { + isPendingMutation.current = false; + lastMoveRef.current = null; + }, }, ); } else if (indexChanged) { @@ -463,16 +470,17 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { columnId: currentColumn.id, })); - try { - await reorderTasksMutation.mutateAsync(reorderedTasks); - } catch { - toast.error("Failed to reorder tasks"); - setLocalColumns(columns); - } + reorderTasksMutation.mutate(reorderedTasks, { + onError: () => { + toast.error("Failed to reorder tasks"); + setLocalColumns(columns); + }, + onSettled: () => { + isPendingMutation.current = false; + lastMoveRef.current = null; + }, + }); } - - isPendingMutation.current = false; - lastMoveRef.current = null; }, [ columns, diff --git a/apps/web/src/hooks/board/use-column-mutations.ts b/apps/web/src/hooks/board/use-column-mutations.ts index a056b2d..9b45bc9 100644 --- a/apps/web/src/hooks/board/use-column-mutations.ts +++ b/apps/web/src/hooks/board/use-column-mutations.ts @@ -114,11 +114,26 @@ export function useReorderColumns( return columns; }, - onSuccess: (columns) => { - queryClient.invalidateQueries({ - queryKey: boardKeys.columns(organizationId), - }); - options?.onSuccess?.(columns); + onSuccess: (reorderedColumns) => { + // Update cache directly to avoid refetch flicker + queryClient.setQueryData( + boardKeys.columns(organizationId), + (oldData: Column[] | undefined) => { + if (!oldData) return oldData; + + const orderMap = new Map( + reorderedColumns.map((c) => [c.id, c.order]), + ); + + return [...oldData] + .map((col) => ({ + ...col, + order: orderMap.get(col.id) ?? col.order, + })) + .sort((a, b) => a.order - b.order); + }, + ); + options?.onSuccess?.(reorderedColumns); }, }); } diff --git a/apps/web/src/hooks/board/use-task-mutations.ts b/apps/web/src/hooks/board/use-task-mutations.ts index fa26df2..c708977 100644 --- a/apps/web/src/hooks/board/use-task-mutations.ts +++ b/apps/web/src/hooks/board/use-task-mutations.ts @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { api } from "~/lib/api"; import type { + Column, CreateTaskInput, MoveTaskInput, Task, @@ -124,9 +125,34 @@ export function useMoveTask( return { task: data as Task, input }; }, onSuccess: ({ task, input }) => { - queryClient.invalidateQueries({ - queryKey: boardKeys.columns(organizationId), - }); + // Update cache directly to avoid refetch flicker + queryClient.setQueryData( + boardKeys.columns(organizationId), + (oldData: Column[] | undefined) => { + if (!oldData) return oldData; + + return oldData.map((col) => { + // Remove task from source column + if (col.tasks.some((t) => t.id === input.taskId)) { + return { + ...col, + tasks: col.tasks.filter((t) => t.id !== input.taskId), + }; + } + // Add task to target column + if (col.id === input.columnId) { + const newTasks = [...col.tasks]; + const insertIndex = Math.min(input.order, newTasks.length); + newTasks.splice(insertIndex, 0, task); + return { + ...col, + tasks: newTasks.map((t, idx) => ({ ...t, order: idx })), + }; + } + return col; + }); + }, + ); options?.onSuccess?.(task, input); }, }); @@ -145,12 +171,36 @@ export function useReorderTasks(organizationId: string) { throw new Error("Failed to reorder tasks"); } - return { success: true }; + return tasks; }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: boardKeys.columns(organizationId), - }); + onSuccess: (reorderedTasks) => { + // Update cache directly to avoid refetch flicker + queryClient.setQueryData( + boardKeys.columns(organizationId), + (oldData: Column[] | undefined) => { + if (!oldData) return oldData; + + const taskOrderMap = new Map( + reorderedTasks.map((t) => [t.id, t.order]), + ); + const columnId = reorderedTasks[0]?.columnId; + + return oldData.map((col) => { + if (col.id === columnId) { + const sortedTasks = [...col.tasks].sort((a, b) => { + const orderA = taskOrderMap.get(a.id) ?? a.order; + const orderB = taskOrderMap.get(b.id) ?? b.order; + return orderA - orderB; + }); + return { + ...col, + tasks: sortedTasks.map((t, idx) => ({ ...t, order: idx })), + }; + } + return col; + }); + }, + ); }, }); } \ No newline at end of file