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:
Milton Gonzalez
2026-09-15 12:41:19 -05:00
parent 9bb8fecf1c
commit 80981e9063
22 changed files with 183 additions and 20 deletions
@@ -3,7 +3,7 @@ import { Plus, Save, Trash2 } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAuth } from '../../context/auth-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { listActivitiesByProject, createActivity, getActivity, deleteActivity } from '../../services/activities.js';
import { listActivitiesByProject, createActivity, getActivity, deleteActivity, reorderActivities } from '../../services/activities.js';
import { listCatalog } from '../../services/catalogs.js';
import { listUsers } from '../../services/users.js';
import { ListView } from '../../components/ListView.jsx';
@@ -57,6 +57,11 @@ export default function ProjectActivitiesTab({ projectId }) {
if (selected) setSelected(await getActivity(selected.id));
}
async function handleReorder(ids) {
await reorderActivities(projectId, ids);
await reload();
}
function confirmDelete(row) {
openDialog({
tone: 'warning',
@@ -107,6 +112,7 @@ export default function ProjectActivitiesTab({ projectId }) {
getSearchHaystack={(a) => `${a.code} ${a.name} ${a.responsible_name || ''}`}
getRowKey={(a) => a.id}
onRowClick={openActivity}
onReorder={currentUser?.roleCode !== 'viewer' ? handleReorder : undefined}
rowActions={currentUser?.roleCode !== 'viewer' ? (a) => [
{ label: t('common_delete'), icon: Trash2, danger: true, onClick: () => confirmDelete(a) },
] : undefined}