fix(boards): keep kanban card drag from locking after a few moves

Reset hung move queues, defer drag visuals past dragstart, and prefer card drop identity so HTML5 DnD no longer dies mid-drag.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-11 16:58:19 +03:30
co-authored by Cursor
parent d87321c7bf
commit 8a4bd70ad4
4 changed files with 180 additions and 91 deletions
+47 -31
View File
@@ -108,6 +108,7 @@ const overlayAccent = ref<PipelineStage['color']>('green');
const overlayPos = ref({ x: 0, y: 0 });
const menuOpenId = ref<string | null>(null);
const moving = ref(false);
let dragSessionId: string | null = null;
const pipelineOptions = computed(() =>
pipelines.value.map((p) => ({
@@ -245,13 +246,8 @@ function onPointerMove(event: PointerEvent | MouseEvent) {
function onDragStart(deal: PipelineDeal, event: DragEvent) {
draggingColumnId.value = null;
draggingId.value = deal.id;
overlayDeal.value = deal;
overlayAccent.value = findAccent(deal.id);
overlayPos.value = {
x: (event.clientX || 0) + 12,
y: (event.clientY || 0) + 12,
};
if (moving.value) moving.value = false;
dragSessionId = deal.id;
event.dataTransfer?.setData('text/plain', deal.id);
event.dataTransfer!.effectAllowed = 'move';
if (event.dataTransfer) {
@@ -260,12 +256,25 @@ function onDragStart(deal: PipelineDeal, event: DragEvent) {
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
event.dataTransfer.setDragImage(img, 0, 0);
}
overlayAccent.value = findAccent(deal.id);
overlayPos.value = {
x: (event.clientX || 0) + 12,
y: (event.clientY || 0) + 12,
};
// DOM را بعد از dragstart عوض کن تا کروم درگ را لغو نکند
window.setTimeout(() => {
if (dragSessionId !== deal.id) return;
draggingId.value = deal.id;
overlayDeal.value = deal;
}, 0);
}
function onDragEnd() {
const endedId = draggingId.value;
const endedId = dragSessionId ?? draggingId.value;
window.setTimeout(() => {
if (draggingId.value !== endedId) return;
if (dragSessionId && dragSessionId !== endedId) return;
if (draggingId.value && draggingId.value !== endedId) return;
dragSessionId = null;
draggingId.value = null;
dropStageId.value = null;
overlayDeal.value = null;
@@ -310,37 +319,44 @@ async function applyUpdateDealStage(
async function onDrop(stageId: string, event: DragEvent, index?: number) {
event.preventDefault();
const transfer = event.dataTransfer?.getData('text/plain') || '';
const draggedCardId = draggingId.value;
const draggedCardId = dragSessionId || draggingId.value;
const columnId = draggingColumnId.value;
dropStageId.value = null;
dragSessionId = null;
draggingId.value = null;
overlayDeal.value = null;
draggingColumnId.value = null;
const isStageDrag = transfer.startsWith('stage:');
if (isStageDrag || (!transfer && !draggedCardId && columnId)) {
const fromId = isStageDrag ? transfer.slice('stage:'.length) : columnId;
if (fromId) await reorderColumn(fromId, stageId);
// کارت بر payload ستون اولویت دارد
const dealId =
draggedCardId || (transfer && !transfer.startsWith('stage:') ? transfer : '');
if (dealId) {
const current = stages.value.find((stage) => stage.deals.some((deal) => deal.id === dealId));
if (current?.id === stageId && index == null) return;
const next = updateDealStage(stages.value, dealId, stageId, index);
const unchanged = next.every((stage, stageIndex) => {
const prev = stages.value[stageIndex];
return (
stage.deals.length === (prev?.deals.length ?? -1) &&
stage.deals.every((deal, dealIndex) => deal.id === prev?.deals[dealIndex]?.id)
);
});
if (unchanged) return;
// اگر جابه‌جایی قبلی هنوز در جریان است، این یکی را صف نکن — فقط رد نکن بی‌صدا با early return قدیمی
if (moving.value) {
moving.value = false;
}
await applyUpdateDealStage(dealId, stageId, index);
return;
}
const dealId = (!isStageDrag && transfer) || draggedCardId || '';
if (!dealId || moving.value) return;
const current = stages.value.find((stage) => stage.deals.some((deal) => deal.id === dealId));
if (current?.id === stageId && index == null) return;
const next = updateDealStage(stages.value, dealId, stageId, index);
const unchanged = next.every((stage, stageIndex) => {
const prev = stages.value[stageIndex];
return (
stage.deals.length === (prev?.deals.length ?? -1) &&
stage.deals.every((deal, dealIndex) => deal.id === prev?.deals[dealIndex]?.id)
);
});
if (unchanged) return;
await applyUpdateDealStage(dealId, stageId, index);
const isStageDrag = transfer.startsWith('stage:');
if (isStageDrag || columnId) {
const fromId = isStageDrag ? transfer.slice('stage:'.length) : columnId;
if (fromId) await reorderColumn(fromId, stageId);
}
}
function apiMessage(e: unknown, fallback: string) {
+33 -23
View File
@@ -56,6 +56,7 @@ const busy = ref(false);
const importMsg = ref('');
const draggingId = ref<string | null>(null);
let dragSessionId: string | null = null;
const draggingColumnId = ref<string | null>(null);
const dropStageId = ref<string | null>(null);
const overlayDeal = ref<PipelineDeal | null>(null);
@@ -178,13 +179,8 @@ function onPointerMove(event: PointerEvent | MouseEvent) {
function onDragStart(deal: PipelineDeal, event: DragEvent) {
draggingColumnId.value = null;
draggingId.value = deal.id;
overlayDeal.value = deal;
overlayAccent.value = findAccent(deal.id);
overlayPos.value = {
x: (event.clientX || 0) + 12,
y: (event.clientY || 0) + 12,
};
if (moving.value) moving.value = false;
dragSessionId = deal.id;
event.dataTransfer?.setData('text/plain', deal.id);
event.dataTransfer!.effectAllowed = 'move';
if (event.dataTransfer) {
@@ -193,12 +189,24 @@ function onDragStart(deal: PipelineDeal, event: DragEvent) {
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
event.dataTransfer.setDragImage(img, 0, 0);
}
overlayAccent.value = findAccent(deal.id);
overlayPos.value = {
x: (event.clientX || 0) + 12,
y: (event.clientY || 0) + 12,
};
window.setTimeout(() => {
if (dragSessionId !== deal.id) return;
draggingId.value = deal.id;
overlayDeal.value = deal;
}, 0);
}
function onDragEnd() {
const endedId = draggingId.value;
const endedId = dragSessionId ?? draggingId.value;
window.setTimeout(() => {
if (draggingId.value !== endedId) return;
if (dragSessionId && dragSessionId !== endedId) return;
if (draggingId.value && draggingId.value !== endedId) return;
dragSessionId = null;
draggingId.value = null;
dropStageId.value = null;
overlayDeal.value = null;
@@ -243,30 +251,32 @@ async function applyUpdateCardStage(
async function onDrop(stageId: string, event: DragEvent, index?: number) {
event.preventDefault();
const transfer = event.dataTransfer?.getData('text/plain') || '';
const draggedCardId = draggingId.value;
const draggedCardId = dragSessionId || draggingId.value;
const columnId = draggingColumnId.value;
dropStageId.value = null;
dragSessionId = null;
draggingId.value = null;
overlayDeal.value = null;
draggingColumnId.value = null;
const cardId =
draggedCardId || (transfer && !transfer.startsWith('stage:') ? transfer : '');
if (cardId) {
const current = stages.value.find((s) => s.deals.some((d) => d.id === cardId));
const currentIndex = current?.deals.findIndex((d) => d.id === cardId) ?? -1;
if (current?.id === stageId && (index == null || index === currentIndex)) {
return;
}
if (moving.value) moving.value = false;
await applyUpdateCardStage(cardId, stageId, index);
return;
}
const isStageDrag = transfer.startsWith('stage:');
if (isStageDrag || (!transfer && !draggedCardId && columnId)) {
if (isStageDrag || columnId) {
const fromId = isStageDrag ? transfer.slice('stage:'.length) : columnId;
if (fromId) await reorderColumn(fromId, stageId);
return;
}
const cardId = (!isStageDrag && transfer) || draggedCardId || '';
if (!cardId || moving.value) return;
const current = stages.value.find((s) => s.deals.some((d) => d.id === cardId));
const currentIndex = current?.deals.findIndex((d) => d.id === cardId) ?? -1;
if (current?.id === stageId && (index == null || index === currentIndex)) {
return;
}
await applyUpdateCardStage(cardId, stageId, index);
}
function onAddCard(stageId: string) {