import { MutableRefObject, useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { AlertCircle } from "lucide-react"; //types import { TGroupedIssues, TIssue, IIssueDisplayProperties, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssueGroupByOptions, TIssueOrderByOptions, } from "@plane/types"; import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils"; import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue"; // helpers import { cn } from "@/helpers/common.helper"; // hooks import { useProjectState } from "@/hooks/store"; //components import { TRenderQuickActions } from "../list/list-view-types"; import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { groupId: string; issuesMap: IIssueMap; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; displayProperties: IIssueDisplayProperties | undefined; sub_group_by: TIssueGroupByOptions | undefined; group_by: TIssueGroupByOptions | undefined; sub_group_id: string; isDragDisabled: boolean; isDropDisabled: boolean; dropErrorMessage: string | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; quickActions: TRenderQuickActions; enableQuickIssueCreate?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, data: TIssue, viewId?: string ) => Promise<TIssue | undefined>; viewId?: string; disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>; orderBy: TIssueOrderByOptions | undefined; } export const KanbanGroup = (props: IKanbanGroup) => { const { groupId, sub_group_id, group_by, orderBy, sub_group_by, issuesMap, displayProperties, issueIds, isDragDisabled, isDropDisabled, dropErrorMessage, updateIssue, quickActions, canEditProperties, enableQuickIssueCreate, disableIssueCreation, quickAddCallback, viewId, scrollableContainerRef, handleOnDrop, } = props; // hooks const projectState = useProjectState(); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const columnRef = useRef<HTMLDivElement | null>(null); // Enable Kanban Columns as Drop Targets useEffect(() => { const element = columnRef.current; if (!element) return; return combine( dropTargetForElements({ element, getData: () => ({ groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN" }), onDragEnter: () => { setIsDraggingOverColumn(true); }, onDragLeave: () => { setIsDraggingOverColumn(false); }, onDragStart: () => { setIsDraggingOverColumn(true); }, onDrop: (payload) => { setIsDraggingOverColumn(false); const source = getSourceFromDropPayload(payload); const destination = getDestinationFromDropPayload(payload); if (!source || !destination || isDropDisabled) return; handleOnDrop(source, destination); highlightIssueOnDrop( getIssueBlockId(source.id, destination?.groupId, destination?.subGroupId), orderBy !== "sort_order" ); }, }), autoScrollForElements({ element, }) ); }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy, isDropDisabled, handleOnDrop]); const prePopulateQuickAddData = ( groupByKey: string | undefined, subGroupByKey: string | undefined | null, groupValue: string, subGroupValue: string ) => { const defaultState = projectState.projectStates?.find((state) => state.default); let preloadedData: object = { state_id: defaultState?.id }; if (groupByKey) { if (groupByKey === "state") { preloadedData = { ...preloadedData, state_id: groupValue }; } else if (groupByKey === "priority") { preloadedData = { ...preloadedData, priority: groupValue }; } else if (groupByKey === "cycle") { preloadedData = { ...preloadedData, cycle_id: groupValue }; } else if (groupByKey === "module") { preloadedData = { ...preloadedData, module_ids: [groupValue] }; } else if (groupByKey === "labels" && groupValue != "None") { preloadedData = { ...preloadedData, label_ids: [groupValue] }; } else if (groupByKey === "assignees" && groupValue != "None") { preloadedData = { ...preloadedData, assignee_ids: [groupValue] }; } else if (groupByKey === "created_by") { preloadedData = { ...preloadedData }; } else { preloadedData = { ...preloadedData, [groupByKey]: groupValue }; } } if (subGroupByKey) { if (subGroupByKey === "state") { preloadedData = { ...preloadedData, state_id: subGroupValue }; } else if (subGroupByKey === "priority") { preloadedData = { ...preloadedData, priority: subGroupValue }; } else if (groupByKey === "cycle") { preloadedData = { ...preloadedData, cycle_id: subGroupValue }; } else if (groupByKey === "module") { preloadedData = { ...preloadedData, module_ids: [subGroupValue] }; } else if (subGroupByKey === "labels" && subGroupValue != "None") { preloadedData = { ...preloadedData, label_ids: [subGroupValue] }; } else if (subGroupByKey === "assignees" && subGroupValue != "None") { preloadedData = { ...preloadedData, assignee_ids: [subGroupValue] }; } else if (subGroupByKey === "created_by") { preloadedData = { ...preloadedData }; } else { preloadedData = { ...preloadedData, [subGroupByKey]: subGroupValue }; } } return preloadedData; }; const canDropOverIssue = orderBy === "sort_order"; const shouldOverlay = isDraggingOverColumn && (!canDropOverIssue || isDropDisabled); const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.title; return ( <div id={`${groupId}__${sub_group_id}`} className={cn( "relative h-full transition-all min-h-[120px]", { "bg-custom-background-80 rounded": isDraggingOverColumn }, { "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlay } )} ref={columnRef} > <div //column overlay when issues are not sorted by manual className={cn( "absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded bg-custom-background-overlay", { "flex flex-col border-[1px] border-custom-border-300 z-[2]": shouldOverlay, }, { hidden: !shouldOverlay }, { "justify-center": !sub_group_by } )} > <div className={cn( "p-3 mt-8 flex flex-col rounded items-center", { "text-custom-text-200": shouldOverlay, }, { "text-custom-text-error": isDropDisabled, } )} > {dropErrorMessage ? ( <div className="flex items-center"> <AlertCircle width={13} height={13} /> <span>{dropErrorMessage}</span> </div> ) : ( <> {readableOrderBy && ( <span> The layout is ordered by <span className="font-semibold">{readableOrderBy}</span>. </span> )} <span>Drop here to move the issue.</span> </> )} </div> </div> <KanbanIssueBlocksList sub_group_id={sub_group_id} groupId={groupId} issuesMap={issuesMap} issueIds={(issueIds as TGroupedIssues)?.[groupId] || []} displayProperties={displayProperties} isDragDisabled={isDragDisabled} updateIssue={updateIssue} quickActions={quickActions} canEditProperties={canEditProperties} scrollableContainerRef={sub_group_by ? scrollableContainerRef : columnRef} canDropOverIssue={canDropOverIssue} /> {enableQuickIssueCreate && !disableIssueCreation && ( <div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0"> <KanBanQuickAddIssueForm formKey="name" groupId={groupId} subGroupId={sub_group_id} prePopulatedData={{ ...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)), }} quickAddCallback={quickAddCallback} viewId={viewId} /> </div> )} </div> ); };