feat: arrastrar con el mouse para reordenar actividades y tareas
Hook reutilizable useDragReorder (drag & drop nativo HTML5, sin libreria nueva) usado en dos lugares: ListView (opt-in via onReorder, deshabilitado mientras haya un ordenamiento por columna activo) para el listado de actividades de un proyecto, y el listado de tareas de una actividad. Orden persistido en sort_order (activities y tasks, migracion 012, posiciones siempre 1..N). Fix real encontrado durante las pruebas: sincronizar el estado interno del hook por la REFERENCIA del arreglo de entrada entraba en loop infinito de renders cuando el llamador pasa "algo || []" (un arreglo nuevo en cada render mientras los datos cargan). Corregido para sincronizar por el contenido (ids), no por referencia. Probado en dev con curl (persistencia del orden) y Playwright (arrastrar filas en el navegador, orden visible y guardado correctamente, sin errores de consola tras el fix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Arrastrar-para-reordenar generico con drag & drop nativo (sin libreria nueva). `items` debe
|
||||
// tener `.id`; `onReorder(idsEnNuevoOrden)` se llama al soltar. La reubicacion se ve al instante
|
||||
// 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.
|
||||
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(',');
|
||||
useEffect(() => {
|
||||
if (!draggingId.current) { setOrder(items); orderRef.current = items; }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemsKey]);
|
||||
|
||||
useEffect(() => { orderRef.current = order; }, [order]);
|
||||
|
||||
function getDragHandlers(item) {
|
||||
const itemId = getId(item);
|
||||
return {
|
||||
draggable: true,
|
||||
onDragStart: (e) => { draggingId.current = itemId; e.dataTransfer.effectAllowed = 'move'; },
|
||||
onDragOver: (e) => {
|
||||
e.preventDefault();
|
||||
if (draggingId.current === null || draggingId.current === itemId) return;
|
||||
setOrder((current) => {
|
||||
const from = current.findIndex((x) => getId(x) === draggingId.current);
|
||||
const to = current.findIndex((x) => getId(x) === itemId);
|
||||
if (from === -1 || to === -1 || from === to) return current;
|
||||
const next = current.slice();
|
||||
const [moved] = next.splice(from, 1);
|
||||
next.splice(to, 0, moved);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
onDrop: (e) => e.preventDefault(),
|
||||
onDragEnd: () => {
|
||||
draggingId.current = null;
|
||||
onReorder(orderRef.current.map(getId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { order, getDragHandlers, isDragging: (itemId) => draggingId.current === itemId };
|
||||
}
|
||||
Reference in New Issue
Block a user