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}