"use client"; 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 { isNil } from "lodash"; import { observer } from "mobx-react"; import { cn } from "@plane/editor-core"; // plane packages import { IGroupByColumn, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions, TIssue, IIssueDisplayProperties, } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { ListLoaderItemRow } from "@/components/ui"; // constants import { DRAG_ALLOWED_GROUPS } from "@/constants/issue"; // hooks import { useProjectState } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; // components import { GroupDragOverlay } from "../group-drag-overlay"; import { GroupDropLocation, getDestinationFromDropPayload, getIssueBlockId, getSourceFromDropPayload, highlightIssueOnDrop, } from "../utils"; import { IssueBlocksList } from "./blocks-list"; import { HeaderGroupByCard } from "./headers/group-by-card"; import { TRenderQuickActions } from "./list-view-types"; import { ListQuickAddIssueForm } from "./quick-add-issue-form"; interface Props { groupIssueIds: string[] | undefined; group: IGroupByColumn; issuesMap: TIssueMap; group_by: TIssueGroupByOptions | null; orderBy: TIssueOrderByOptions | undefined; getGroupIndex: (groupId: string | undefined) => number; updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; quickActions: TRenderQuickActions; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; canEditProperties: (projectId: string | undefined) => boolean; containerRef: MutableRefObject; quickAddCallback?: ((projectId: string | null | undefined, data: TIssue) => Promise) | undefined; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; disableIssueCreation?: boolean; addIssuesToView?: (issueIds: string[]) => Promise; isCompletedCycle?: boolean; showEmptyGroup?: boolean; loadMoreIssues: (groupId?: string) => void; selectionHelpers: TSelectionHelper; }; export const ListGroup = observer((props: Props) => { const { groupIssueIds, group, issuesMap, group_by, orderBy, getGroupIndex, updateIssue, quickActions, displayProperties, enableIssueQuickAdd, canEditProperties, containerRef, quickAddCallback, handleOnDrop, disableIssueCreation, addIssuesToView, isCompletedCycle, showEmptyGroup, loadMoreIssues, selectionHelpers, } = props; const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [dragColumnOrientation, setDragColumnOrientation] = useState<"justify-start" | "justify-end">("justify-start"); const [isExpanded, setIsExpanded] = useState(true); const groupRef = useRef(null); const projectState = useProjectState(); const { issues: { getGroupIssueCount, getPaginationData, getIssueLoader }, } = useIssuesStore(); const [intersectionElement, setIntersectionElement] = useState(null); useIntersectionObserver(containerRef, intersectionElement, loadMoreIssues, `50% 0% 50% 0%`); const groupIssueCount = getGroupIssueCount(group.id, undefined, false); const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults; const isPaginating = !!getIssueLoader(group.id); const shouldLoadMore = nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds ? groupIssueIds.length < groupIssueCount : !!nextPageResults; const loadMore = isPaginating ? ( ) : (
loadMoreIssues(group.id)} > Load More ↓
); const validateEmptyIssueGroups = (issueCount: number = 0) => { if (!showEmptyGroup && issueCount <= 0) return false; return true; }; const toggleListGroup = () => { setIsExpanded((prevState) => !prevState); }; const prePopulateQuickAddData = (groupByKey: string | null, value: any) => { const defaultState = projectState.projectStates?.find((state) => state.default); let preloadedData: object = { state_id: defaultState?.id }; if (groupByKey === null) { preloadedData = { ...preloadedData }; } else { if (groupByKey === "state") { preloadedData = { ...preloadedData, state_id: value }; } else if (groupByKey === "priority") { preloadedData = { ...preloadedData, priority: value }; } else if (groupByKey === "labels" && value != "None") { preloadedData = { ...preloadedData, label_ids: [value] }; } else if (groupByKey === "assignees" && value != "None") { preloadedData = { ...preloadedData, assignee_ids: [value] }; } else if (groupByKey === "cycle" && value != "None") { preloadedData = { ...preloadedData, cycle_id: value }; } else if (groupByKey === "module" && value != "None") { preloadedData = { ...preloadedData, module_ids: [value] }; } else if (groupByKey === "created_by") { preloadedData = { ...preloadedData }; } else { preloadedData = { ...preloadedData, [groupByKey]: value }; } } return preloadedData; }; useEffect(() => { const element = groupRef.current; if (!element) return; return combine( dropTargetForElements({ element, getData: () => ({ groupId: group.id, type: "COLUMN" }), onDragEnter: () => { setIsDraggingOverColumn(true); }, onDragLeave: () => { setIsDraggingOverColumn(false); }, onDragStart: () => { setIsDraggingOverColumn(true); }, onDrag: ({ source }) => { const sourceGroupId = source?.data?.groupId as string | undefined; const currentGroupId = group.id; const sourceIndex = getGroupIndex(sourceGroupId); const currentIndex = getGroupIndex(currentGroupId); if (sourceIndex > currentIndex) { setDragColumnOrientation("justify-end"); } else { setDragColumnOrientation("justify-start"); } }, onDrop: (payload) => { setIsDraggingOverColumn(false); const source = getSourceFromDropPayload(payload); const destination = getDestinationFromDropPayload(payload); if (!source || !destination) return; if (group.isDropDisabled) { group.dropErrorMessage && setToast({ type: TOAST_TYPE.WARNING, title: "Warning!", message: group.dropErrorMessage, }); return; } handleOnDrop(source, destination); highlightIssueOnDrop(getIssueBlockId(source.id, destination?.groupId), orderBy !== "sort_order"); }, }) ); }, [groupRef?.current, group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn]); const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by); const canOverlayBeVisible = orderBy !== "sort_order" || !!group.isDropDisabled; const isGroupByCreatedBy = group_by === "created_by"; const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by; return groupIssueIds && !isNil(groupIssueCount) && validateEmptyIssueGroups(groupIssueCount) ? (
{shouldExpand && (
{groupIssueIds && ( )} {shouldLoadMore && (group_by ? <>{loadMore} : )} {enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && (
)}
)}
) : null; });