forked from github/plane
1b55411919
* Kanban DnD improvement * minor fixes for kanban dnd improvement * change scroll duration * fix feedback on the UX * add highlight before drop * add toast message explain drag and drop is currently disabled * Change warning dnd message * add comments * fix minor build error
230 lines
7.2 KiB
TypeScript
230 lines
7.2 KiB
TypeScript
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<TIssue>) => Promise<void>) | undefined,
|
|
groupBy: TIssueGroupByOptions | undefined,
|
|
subGroupBy: TIssueGroupByOptions | undefined,
|
|
shouldAddIssueAtTop = false
|
|
) => {
|
|
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
|
|
|
let updatedIssue: Partial<TIssue> = {};
|
|
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);
|
|
};
|