import { KanbanDragAndDropContext } from './KanbanDragAndDropContext'; import { KanbanDraggableCardItem } from './KanbanDraggableCardItem'; import { KanbanDragOverlay } from './KanbanDragOverlay'; import { KanbanState } from '../../state/KanbanState/KanbanState'; import { KanbanDragEndItemEvent } from '../../state/KanbanState/KanbanDragEndEvent'; import { FiltersMap, KeyedItem } from '@wix/bex-core'; import { action, makeObservable, observable } from 'mobx'; import { DragStartEvent, DragEndEvent, } from '@wix/wix-style-react-incubator/dnd-kit/core'; import { v4 as uuid } from 'uuid'; export interface KanbanDragAndDropStateParams { kanbanState: KanbanState; } export class KanbanDragAndDropState { readonly kanbanState: KanbanState; activeId: string | null = null; status: 'start' | 'over' | 'end' | 'cancel' = 'start'; _moveId = ''; constructor(params: KanbanDragAndDropStateParams) { this.kanbanState = params.kanbanState; makeObservable(this, { handleDragEnd: action, setActiveId: action, activeId: observable, status: observable.ref, handleDragStart: action, handleDragCancel: action, }); } setActiveId = (id: string | null) => { this.activeId = id; }; private _parseItemId( combinedId: string, ): { stageKey: string; itemKey: string } | null { // Parse combined ID format: "stageKey-##-itemKey" const [stageKey, itemKey] = combinedId.split('-##-'); if (!stageKey || !itemKey) { return null; } return { stageKey, itemKey }; } private _findItemById(combinedId: string): KeyedItem | null { const parsed = this._parseItemId(combinedId); if (!parsed) { return null; } const stageState = this.kanbanState.kanbanStageStates[parsed.stageKey]; if (!stageState) { return null; } const item = stageState.itemsCollectionState.getKeyedItem(parsed.itemKey); return item || null; } private _findStageByItemId(combinedId: string): string | null { const parsed = this._parseItemId(combinedId); return parsed?.stageKey || null; } handleDragStart = (event: DragStartEvent) => { this._moveId = uuid(); this.status = 'start'; this.activeId = event.active.id.toString(); const keyedItem = this._findItemById(this.activeId); const stageId = this._findStageByItemId(this.activeId); if (!keyedItem || !stageId) { return; } this.kanbanState.kanbanStateBIReporter.reportDragStart({ itemId: keyedItem.id, itemIndex: keyedItem.index, stageId, dragAndDropID: this._moveId, }); }; handleDragCancel = () => { this.status = 'cancel'; this.setActiveId(null); }; handleDragEnd = async (event: DragEndEvent) => { this.status = 'end'; const { active, over } = event; this.setActiveId(null); if (!over) { return; } const activeId = active.id.toString(); const overId = over.id.toString(); const fromStageId = this._findStageByItemId(activeId); // Determine target stage - could be stage key directly or extracted from item ID let toStageId: string | null; if (this.kanbanState.kanbanStageStates[overId]) { // Dropping on a stage background toStageId = overId; } else { // Dropping on an item - extract stage from item ID toStageId = this._findStageByItemId(overId); } // Only return early if we don't have valid source and target stages if (!fromStageId || !toStageId) { return; } // PREVENT MOVEMENT WITHIN THE SAME STAGE if (fromStageId === toStageId) { return; // Don't allow reordering within the same stage } const keyedItem = this._findItemById(activeId); const item = keyedItem?.item; if (!item) { return; } // Create the Kanban drag end event const kanbanDragEndEvent: KanbanDragEndItemEvent = { item, fromStage: this.kanbanState.getStageById(fromStageId), toStage: this.kanbanState.getStageById(toStageId), }; if (this.kanbanState.items?.dragAndDrop?.onSubmit) { const originalItem = item; const updatedItem = { ...item, [this.kanbanState.items.stageField]: toStageId, }; try { this.kanbanState.moveItemBetweenStages( originalItem, updatedItem, fromStageId, toStageId, ); const submittedItem: T = await this.kanbanState.items.dragAndDrop.onSubmit(kanbanDragEndEvent); const submittedItemStageId = submittedItem[this.kanbanState.items.stageField as keyof T]; if (toStageId !== submittedItemStageId) { // move has been cancelled this.kanbanState.moveItemBetweenStages( updatedItem, originalItem, toStageId, fromStageId, ); this.kanbanState.kanbanStateBIReporter.reportDragEnd({ itemId: keyedItem.id, itemIndexBefore: keyedItem.index, itemIndexAfter: keyedItem.index, stageIdBefore: fromStageId, stageIdAfter: fromStageId, dragAndDropID: this._moveId, endType: 'drop back', }); return; } this.kanbanState.updateCard(submittedItem); this.kanbanState.kanbanStateBIReporter.reportDragEnd({ itemId: keyedItem.id, itemIndexBefore: keyedItem.index, itemIndexAfter: 0, stageIdBefore: fromStageId, stageIdAfter: toStageId, dragAndDropID: this._moveId, endType: 'drop succeeded', }); } catch (error) { // rollback the move this.kanbanState.moveItemBetweenStages( updatedItem, originalItem, toStageId, fromStageId, ); this.kanbanState.kanbanStateBIReporter.reportDragEnd({ itemId: keyedItem.id, itemIndexBefore: keyedItem.index, itemIndexAfter: keyedItem.index, stageIdBefore: fromStageId, stageIdAfter: fromStageId, dragAndDropID: this._moveId, endType: 'drag disabled', }); this.kanbanState.showErrorToast(error); } } }; init() { // No initialization needed for native drag and drop } get keyboardCodes() { return { start: ['Space', 'Enter'], cancel: ['Escape'], end: ['Space', 'Enter'], }; } } export const KanbanCardDragAndDrop = { KanbanDragAndDropContext, DraggableCard: KanbanDraggableCardItem, DragOverlay: KanbanDragOverlay, };