fix: drag-and-drop en tablero de tareas (SortableJS + Alpine x-for)

SortableJS mueve el nodo DOM físicamente antes de que Alpine re-renderice,
causando conflicto: la tarjeta desaparecía o volvía a su columna original.
Se revierte el movimiento DOM de SortableJS en onEnd y se deja que Alpine
re-renderice solo al actualizar t.estado; luego se re-inicia Sortable.
Se agrega cursor grab/grabbing como indicador visual de arrastre.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-09 20:04:48 -05:00
co-authored by Claude Sonnet 4.6
parent 65c988dc70
commit 9305cd6eb2
+24 -9
View File
@@ -69,7 +69,7 @@
data-estado="">
<template x-for="t in tareasFiltradas(col.id)" :key="t.ID">
<div :data-id="t.ID" @click="abrirDetalle(t)"
class="bg-white rounded-xl border border-slate-200 shadow-sm p-3 cursor-pointer hover:shadow-md transition-all group">
class="bg-white rounded-xl border border-slate-200 shadow-sm p-3 cursor-grab active:cursor-grabbing hover:shadow-md transition-all group">
<!-- Prioridad strip -->
<div class="h-1 rounded-full mb-2 -mt-1 -mx-1"
@@ -470,15 +470,30 @@
onEnd: (evt) => {
const id = parseInt(evt.item.dataset.id);
const nuevoEstado = evt.to.id.replace('col-', '');
const estadoAnterior = evt.from.id.replace('col-', '');
if (nuevoEstado === estadoAnterior) return;
// Revertir el movimiento DOM de SortableJS para que Alpine
// pueda re-renderizar el x-for sin conflicto de nodos.
evt.from.insertBefore(evt.item, evt.from.children[evt.oldIndex] || null);
const t = this.tareas.find(t => t.ID === id);
if (t && t.estado !== nuevoEstado) {
t.estado = nuevoEstado;
fetch(`/app/tarea/${id}/estado`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ estado: nuevoEstado }),
});
}
if (!t) return;
t.estado = nuevoEstado; // dispara el re-render de Alpine
// Re-iniciar Sortable después de que Alpine actualice el DOM
this.$nextTick(() => this.iniciarSortable());
fetch(`/app/tarea/${id}/estado`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ estado: nuevoEstado }),
}).catch(() => {
// Revertir estado local si la API falla
const tRev = this.tareas.find(t => t.ID === id);
if (tRev) tRev.estado = estadoAnterior;
this.$nextTick(() => this.iniciarSortable());
});
},
});
this.sortables.push(s);