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}