From c84a07968f18214d59bd6cadd587a1aeab51ca37 Mon Sep 17 00:00:00 2001 From: Milton Gonzalez Date: Wed, 16 Sep 2026 12:33:35 -0500 Subject: [PATCH] fix: checkbox de tarea (hecho) no se actualizaba tras guardar useDragReorder resincronizaba su copia local solo cuando cambiaba el conjunto de ids, no cuando se editaba un campo de una tarea existente (marcar hecho, renombrar) -- la fila se quedaba mostrando el snapshot viejo para siempre despues de guardar. Ahora compara el contenido completo. De paso: draggable/onDragStart pasan a vivir solo en el icono de agarre, nunca en toda la fila -- un ancestro draggable=true le gana el mousedown a los controles interactivos que tenga adentro en algunos navegadores (no era la causa de este bug puntual, pero es un problema real del mismo patron que conviene cerrar ya que se reutiliza en cualquier lista futura). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 15 ++++++++ VERSION | 2 +- backend/package.json | 2 +- frontend/package.json | 2 +- frontend/src/components/ListView.jsx | 6 ++-- frontend/src/hooks/useDragReorder.js | 35 +++++++++++++------ .../pages/Proyectos/ActivityDetailPanel.jsx | 9 +++-- 7 files changed, 52 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddcaa5..454ffd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog - project.linkedcorp.net +## [1.15.2] - 2026-09-16 +### Fixed +- El checkbox de "hecho" de una tarea (y el renombrado en linea) dejaba de reflejarse tras + guardar: `useDragReorder` (hook de arrastrar-para-reordenar, introducido en v1.14.0) resincronizaba + su copia local solo cuando cambiaba el CONJUNTO de ids, no cuando se editaba un campo de una tarea + ya existente (marcar hecho, renombrar) — la fila se quedaba mostrando el snapshot viejo para + siempre. Reportado por el usuario ("no se marca, debe ser por el drag de mover tareas") — tenia + razon en que era el drag, aunque la causa real no era la interferencia del arrastre con el click + sino esta resincronizacion incompleta. Corregido comparando el contenido completo, no solo los ids. +- De paso, separado `draggable`/`onDragStart` (antes en toda la fila) para que vivan SOLO en el + icono de agarre (`GripVertical`), nunca en la fila completa: un ancestro `draggable=true` le gana + el mousedown a los controles interactivos que tenga adentro en algunos navegadores. No era la + causa de este bug puntual, pero es una corrección real y necesaria para que el patrón sea seguro + de reutilizar en cualquier lista futura con controles interactivos en la fila. + ## [1.15.1] - 2026-09-15 ### Added - `scripts/install.sh`: instalador para desplegar en un servidor nuevo. Verifica requisitos diff --git a/VERSION b/VERSION index ace4423..42cf067 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.1 +1.15.2 diff --git a/backend/package.json b/backend/package.json index 800238b..46ee020 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "project-linkedcorp-backend", - "version": "1.15.1", + "version": "1.15.2", "private": true, "type": "module", "main": "src/server.js", diff --git a/frontend/package.json b/frontend/package.json index e12a1b9..8c8b7eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "project-linkedcorp-frontend", "private": true, - "version": "1.15.1", + "version": "1.15.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/ListView.jsx b/frontend/src/components/ListView.jsx index c47a296..7aa61d8 100644 --- a/frontend/src/components/ListView.jsx +++ b/frontend/src/components/ListView.jsx @@ -31,7 +31,7 @@ export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowC }, [filtered, sort, columns]); const dragEnabled = !!onReorder && !sort.key && !searchValue; - const { order, getDragHandlers, isDragging } = useDragReorder(sorted, (ids) => onReorder?.(ids), getRowKey); + const { order, getHandleProps, getRowProps, isDragging } = useDragReorder(sorted, (ids) => onReorder?.(ids), getRowKey); const displayRows = dragEnabled ? order : sorted; function toggleSort(key) { @@ -58,10 +58,10 @@ export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowC key={getRowKey(row)} onClick={() => onRowClick?.(row)} className={dragEnabled && isDragging(getRowKey(row)) ? 'dragging' : ''} - {...(dragEnabled ? getDragHandlers(row) : {})} + {...(dragEnabled ? getRowProps(row) : {})} > {dragEnabled && ( - e.stopPropagation()}> + e.stopPropagation()} {...getHandleProps(row)}> )} diff --git a/frontend/src/hooks/useDragReorder.js b/frontend/src/hooks/useDragReorder.js index aa7f7bf..0f8728e 100644 --- a/frontend/src/hooks/useDragReorder.js +++ b/frontend/src/hooks/useDragReorder.js @@ -5,15 +5,24 @@ import { useEffect, useRef, useState } from 'react'; // mientras se arrastra (estado local optimista); el llamador persiste el orden final. // Reutilizado por ListView (actividades, y cualquier listado que pase onReorder) y por el listado // de tareas de una actividad. +// +// `getHandleProps` (draggable + onDragStart/onDragEnd) va SOLO en el icono de arrastre, nunca en +// la fila completa: un ancestro `draggable=true` le gana el mousedown a los controles interactivos +// que tenga adentro (checkboxes, botones) y el navegador deja de dispararles click/change. Los que +// llaman a este hook siempre deben combinar `getHandleProps` en el handle con `getRowProps` +// (onDragOver/onDrop, para poder soltar en cualquier punto de la fila) en la fila. export function useDragReorder(items, onReorder, getId = (x) => x.id) { const [order, setOrder] = useState(items); const orderRef = useRef(items); const draggingId = useRef(null); - // Se sincroniza por el CONTENIDO (ids), no por la referencia del arreglo: un llamador que pase - // `algo || []` (comun) crea un arreglo nuevo en cada render aunque el contenido no cambie, y - // sincronizar por referencia ahi entra en loop infinito de renders. - const itemsKey = items.map(getId).join(','); + // Se sincroniza por el CONTENIDO completo (JSON), no por la referencia del arreglo ni solo por + // los ids: un llamador que pase `algo || []` (comun) crea un arreglo nuevo en cada render aunque + // el contenido no cambie (sincronizar por referencia ahi entra en loop infinito de renders), pero + // un key basado solo en ids tampoco alcanza — no cambia cuando se edita un campo de un item ya + // existente (ej. marcar una tarea como hecha, renombrarla), y el checkbox/nombre se queda + // mostrando el snapshot viejo para siempre tras guardar. + const itemsKey = JSON.stringify(items); useEffect(() => { if (!draggingId.current) { setOrder(items); orderRef.current = items; } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -21,11 +30,21 @@ export function useDragReorder(items, onReorder, getId = (x) => x.id) { useEffect(() => { orderRef.current = order; }, [order]); - function getDragHandlers(item) { + function getHandleProps(item) { const itemId = getId(item); return { draggable: true, onDragStart: (e) => { draggingId.current = itemId; e.dataTransfer.effectAllowed = 'move'; }, + onDragEnd: () => { + draggingId.current = null; + onReorder(orderRef.current.map(getId)); + }, + }; + } + + function getRowProps(item) { + const itemId = getId(item); + return { onDragOver: (e) => { e.preventDefault(); if (draggingId.current === null || draggingId.current === itemId) return; @@ -40,12 +59,8 @@ export function useDragReorder(items, onReorder, getId = (x) => x.id) { }); }, onDrop: (e) => e.preventDefault(), - onDragEnd: () => { - draggingId.current = null; - onReorder(orderRef.current.map(getId)); - }, }; } - return { order, getDragHandlers, isDragging: (itemId) => draggingId.current === itemId }; + return { order, getHandleProps, getRowProps, isDragging: (itemId) => draggingId.current === itemId }; } diff --git a/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx b/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx index 0f73ac2..f697059 100644 --- a/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx +++ b/frontend/src/pages/Proyectos/ActivityDetailPanel.jsx @@ -35,7 +35,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi const { data: statuses } = useAsync(() => listCatalog('activity-statuses'), []); const { data: users } = useAsync(listUsers, []); const { data: tasks, reload: reloadTasks } = useAsync(() => listTasks(activity.id), [activity.id]); - const { order: orderedTasks, getDragHandlers, isDragging } = useDragReorder(tasks || [], async (ids) => { + const { order: orderedTasks, getHandleProps, getRowProps, isDragging } = useDragReorder(tasks || [], async (ids) => { await reorderTasks(activity.id, ids); await reloadTasks(); }); @@ -238,7 +238,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi key={task.id} className={isDragging(task.id) ? 'drag-row dragging' : 'drag-row'} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }} - {...(editingTaskId === task.id ? {} : getDragHandlers(task))} + {...(editingTaskId === task.id ? {} : getRowProps(task))} > {editingTaskId === task.id ? ( <> @@ -252,7 +252,10 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi ) : ( <> - + toggleTask(task)} /> {task.name}