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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user