import pull from "lodash/pull"; import scrollIntoView from "smooth-scroll-into-view-if-needed"; import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types"; import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store"; export type KanbanDropLocation = { columnId: string; groupId: string; subGroupId?: string; id: string | undefined; }; /** * get Kanban Source data from Pragmatic Payload * @param payload * @returns */ export const getSourceFromDropPayload = (payload: IPragmaticDropPayload): KanbanDropLocation | undefined => { const { location, source: sourceIssue } = payload; const sourceIssueData = sourceIssue.data; let sourceColumData; const sourceDropTargets = location?.initial?.dropTargets ?? []; for (const dropTarget of sourceDropTargets) { const dropTargetData = dropTarget?.data; if (!dropTargetData) continue; if (dropTargetData.type === "COLUMN") { sourceColumData = dropTargetData; } } if (sourceIssueData?.id === undefined || !sourceColumData?.groupId) return; return { groupId: sourceColumData.groupId as string, subGroupId: sourceColumData.subGroupId as string, columnId: sourceColumData.columnId as string, id: sourceIssueData.id as string, }; }; /** * get Destination Source data from Pragmatic Payload * @param payload * @returns */ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): KanbanDropLocation | undefined => { const { location } = payload; let destinationIssueData, destinationColumnData; const destDropTargets = location?.current?.dropTargets ?? []; for (const dropTarget of destDropTargets) { const dropTargetData = dropTarget?.data; if (!dropTargetData) continue; if (dropTargetData.type === "COLUMN" || dropTargetData.type === "DELETE") { destinationColumnData = dropTargetData; } if (dropTargetData.type === "ISSUE") { destinationIssueData = dropTargetData; } } if (!destinationColumnData?.groupId) return; return { groupId: destinationColumnData.groupId as string, subGroupId: destinationColumnData.subGroupId as string, columnId: destinationColumnData.columnId as string, id: destinationIssueData?.id as string | undefined, }; }; /** * Returns Sort order of the issue block at the position of drop * @param destinationIssues * @param destinationIssueId * @param getIssueById * @returns */ const handleSortOrder = ( destinationIssues: string[], destinationIssueId: string | undefined, getIssueById: (issueId: string) => TIssue | undefined, shouldAddIssueAtTop = false ) => { const sortOrderDefaultValue = 65535; let currentIssueState = {}; const destinationIndex = destinationIssueId ? destinationIssues.indexOf(destinationIssueId) : shouldAddIssueAtTop ? 0 : destinationIssues.length; if (destinationIssues && destinationIssues.length > 0) { if (destinationIndex === 0) { const destinationIssueId = destinationIssues[0]; const destinationIssue = getIssueById(destinationIssueId); if (!destinationIssue) return currentIssueState; currentIssueState = { ...currentIssueState, sort_order: destinationIssue.sort_order - sortOrderDefaultValue, }; } else if (destinationIndex === destinationIssues.length) { const destinationIssueId = destinationIssues[destinationIssues.length - 1]; const destinationIssue = getIssueById(destinationIssueId); if (!destinationIssue) return currentIssueState; currentIssueState = { ...currentIssueState, sort_order: destinationIssue.sort_order + sortOrderDefaultValue, }; } else { const destinationTopIssueId = destinationIssues[destinationIndex - 1]; const destinationBottomIssueId = destinationIssues[destinationIndex]; const destinationTopIssue = getIssueById(destinationTopIssueId); const destinationBottomIssue = getIssueById(destinationBottomIssueId); if (!destinationTopIssue || !destinationBottomIssue) return currentIssueState; currentIssueState = { ...currentIssueState, sort_order: (destinationTopIssue.sort_order + destinationBottomIssue.sort_order) / 2, }; } } else { currentIssueState = { ...currentIssueState, sort_order: sortOrderDefaultValue, }; } return currentIssueState; }; export const handleDragDrop = async ( source: KanbanDropLocation, destination: KanbanDropLocation, getIssueById: (issueId: string) => TIssue | undefined, getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, groupBy: TIssueGroupByOptions | undefined, subGroupBy: TIssueGroupByOptions | undefined, shouldAddIssueAtTop = false ) => { if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return; let updatedIssue: Partial = {}; const sourceIssues = getIssueIds(source.groupId, source.subGroupId); const destinationIssues = getIssueIds(destination.groupId, destination.subGroupId); const sourceIssue = getIssueById(source.id); if (!sourceIssues || !destinationIssues || !sourceIssue) return; updatedIssue = { id: sourceIssue.id, project_id: sourceIssue.project_id, }; // for both horizontal and vertical dnd updatedIssue = { ...updatedIssue, ...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop), }; if (source.groupId && destination.groupId && source.groupId !== destination.groupId) { const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy]; let groupValue = sourceIssue[groupKey]; if (Array.isArray(groupValue)) { pull(groupValue, source.groupId); groupValue.push(destination.groupId); } else { groupValue = destination.groupId; } updatedIssue = { ...updatedIssue, [groupKey]: groupValue }; } if (subGroupBy && source.subGroupId && destination.subGroupId && source.subGroupId !== destination.subGroupId) { const subGroupKey = ISSUE_FILTER_DEFAULT_DATA[subGroupBy]; let subGroupValue = sourceIssue[subGroupKey]; if (Array.isArray(subGroupValue)) { pull(subGroupValue, source.subGroupId); subGroupValue.push(destination.subGroupId); } else { subGroupValue = destination.subGroupId; } updatedIssue = { ...updatedIssue, [subGroupKey]: subGroupValue }; } if (updatedIssue) { return ( updateIssue && (await updateIssue(sourceIssue.project_id, sourceIssue.id, { ...updatedIssue, id: sourceIssue.id, project_id: sourceIssue.project_id, })) ); } }; /** * This Method finds the DOM element with elementId, scrolls to it and highlights the issue block * @param elementId * @param shouldScrollIntoView */ export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => { setTimeout(async () => { const sourceElementId = elementId ?? ""; const sourceElement = document.getElementById(sourceElementId); sourceElement?.classList?.add("highlight"); if (shouldScrollIntoView && sourceElement) await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 }); }, 200); };