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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "1.15.1",
|
||||
"version": "1.15.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "1.15.1",
|
||||
"version": "1.15.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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 && (
|
||||
<td className="drag-handle-cell" onClick={(e) => e.stopPropagation()}>
|
||||
<td className="drag-handle-cell" onClick={(e) => e.stopPropagation()} {...getHandleProps(row)}>
|
||||
<GripVertical size={15} />
|
||||
</td>
|
||||
)}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GripVertical size={15} className="drag-handle" style={{ opacity: .6, cursor: 'grab', flexShrink: 0 }} />
|
||||
<GripVertical
|
||||
size={15} className="drag-handle" style={{ opacity: .6, cursor: 'grab', flexShrink: 0 }}
|
||||
{...getHandleProps(task)}
|
||||
/>
|
||||
<input type="checkbox" checked={!!task.is_done} onChange={() => toggleTask(task)} />
|
||||
<span style={{ flex: 1, fontSize: '.8rem', textDecoration: task.is_done ? 'line-through' : 'none', opacity: task.is_done ? .6 : 1 }}>{task.name}</span>
|
||||
<button className="icon-btn" title={t('common_edit')} onClick={() => startEditTask(task)}><Pencil size={15} /></button>
|
||||
|
||||
Reference in New Issue
Block a user