From b608a16b5433c7e6c37e535f15b0a9613d8adab3 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Wed, 10 Apr 2024 20:14:00 +0530 Subject: [PATCH 1/3] Pragmatic drag and drop implmentation of Kanban --- .../issue-layouts/kanban/base-kanban-root.tsx | 122 ++++++---- .../issues/issue-layouts/kanban/block.tsx | 78 +++--- .../issue-layouts/kanban/blocks-list.tsx | 6 +- .../issues/issue-layouts/kanban/default.tsx | 13 +- .../issue-layouts/kanban/kanban-group.tsx | 63 +++-- .../issues/issue-layouts/kanban/swimlanes.tsx | 13 +- .../issues/issue-layouts/kanban/utils.ts | 224 +++++++++++------- web/package.json | 4 +- web/store/issue/issue_kanban_view.store.ts | 9 + web/store/issue/project/issue.store.ts | 25 ++ yarn.lock | 36 ++- 11 files changed, 389 insertions(+), 204 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 41043085315..2ee0b35a365 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -1,5 +1,9 @@ -import { FC, useCallback, useRef, useState } from "react"; -import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd"; +import { FC, useCallback, 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 { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { TIssue } from "@plane/types"; @@ -9,15 +13,16 @@ import { DeleteIssueModal } from "@/components/issues"; import { ISSUE_DELETED } from "@/constants/event-tracker"; import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; -import { useEventTracker, useIssues, useUser } from "@/hooks/store"; +import { useEventTracker, useIssues, useKanbanView, useUser } from "@/hooks/store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; +import { IProjectIssues } from "@/store/issue/project"; // ui // types import { IQuickActionProps } from "../list/list-view-types"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; -import { handleDragDrop } from "./utils"; +import { DropLocation, handleDragDrop, processPragmaticDropPayload } from "./utils"; export type KanbanStoreType = | EIssuesStoreType.PROJECT @@ -38,8 +43,8 @@ export interface IBaseKanBanLayout { type KanbanDragState = { draggedIssueId?: string | null; - source?: DraggableLocation | null; - destination?: DraggableLocation | null; + source?: DropLocation | null; + destination?: DropLocation | null; }; export const BaseKanBanRoot: React.FC = observer((props: IBaseKanBanLayout) => { @@ -60,10 +65,15 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas membership: { currentProjectRole }, } = useUser(); const { captureIssueEvent } = useEventTracker(); - const { issueMap, issuesFilter, issues } = useIssues(storeType); + const { issueMap, issuesFilter, issues, } = useIssues(storeType); const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = useIssuesActions(storeType); + const deleteAreaRef = useRef(null); + const [isDragOverDelete, setIsDragOverDelete] = useState(false); + + const { isDragging} = useKanbanView(); + const issueIds = issues?.groupedIssueIds || []; const displayFilters = issuesFilter?.issueFilters?.displayFilters; @@ -81,7 +91,6 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const scrollableContainerRef = useRef(null); // states - const [isDragStarted, setIsDragStarted] = useState(false); const [dragState, setDragState] = useState({}); const [deleteIssueModal, setDeleteIssueModal] = useState(false); @@ -97,46 +106,71 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); - const onDragStart = (dragStart: DragStart) => { - setDragState({ - draggedIssueId: dragStart.draggableId.split("__")[0], - }); - setIsDragStarted(true); - }; + useEffect(() => { + const element = scrollableContainerRef.current; + + if(!element) return; + + return combine( + autoScrollForElements({ + element + }) + ) + }, [scrollableContainerRef?.current]) - const onDragEnd = async (result: DropResult) => { - setIsDragStarted(false); + useEffect(() => { + const element = deleteAreaRef.current; - if (!result) return; + if(!element) return; + + return combine(dropTargetForElements({ + element, + getData: () => ({columnId: "issue-trash-box", groupId: "issue-trash-box", type: "DELETE"}), + onDragEnter: () => {setIsDragOverDelete(true)}, + onDragLeave: () => {setIsDragOverDelete(false)}, + onDrop: (payload) => { + setIsDragOverDelete(false); + const { source, destination} = processPragmaticDropPayload(payload) ?? {}; + + console.log("on drop delete", source, destination) + + if(!source || !destination) return; + + handleOnDrop(source, destination) + }, + } + ),) + }, [deleteAreaRef?.current, setIsDragOverDelete]) + + const handleOnDrop = async (source: DropLocation, destination: DropLocation) => { if ( - result.destination && - result.source && - result.source.droppableId && - result.destination.droppableId && - result.destination.droppableId === result.source.droppableId && - result.destination.index === result.source.index + source.columnId && + destination.columnId && + destination.columnId === source.columnId && + destination.id === source.id ) return; if (handleDragDrop) { - if (result.destination?.droppableId && result.destination?.droppableId.split("__")[0] === "issue-trash-box") { + if (destination.columnId && destination.columnId === "issue-trash-box") { setDragState({ ...dragState, - source: result.source, - destination: result.destination, + draggedIssueId: source?.id, + source, + destination, }); setDeleteIssueModal(true); } else { await handleDragDrop( - result.source, - result.destination, + source, + destination, workspaceSlug?.toString(), projectId?.toString(), sub_group_by, group_by, issueMap, - issueIds, + (issues as IProjectIssues).getIssueIds, updateIssue, removeIssue ).catch((err) => { @@ -168,7 +202,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas ); const handleDeleteIssue = async () => { - if (!handleDragDrop || !dragState.draggedIssueId) return; + if (!handleDragDrop) return; await handleDragDrop( dragState.source, dragState.destination, @@ -177,12 +211,10 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas sub_group_by, group_by, issueMap, - issueIds, + (issues as IProjectIssues).getIssueIds, updateIssue, removeIssue ).finally(() => { - const draggedIssue = issueMap[dragState.draggedIssueId!]; - removeIssue(draggedIssue.project_id, draggedIssue.id); setDeleteIssueModal(false); setDragState({}); captureIssueEvent({ @@ -222,35 +254,29 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas )}
-
- +
{/* drag and delete component */}
- - {(provided, snapshot) => (
Drop here to delete the issue.
- )} -
-
+
= observer((props: IBas storeType={storeType} addIssuesToView={addIssuesToView} scrollableContainerRef={scrollableContainerRef} - isDragStarted={isDragStarted} + handleOnDrop={handleOnDrop} />
-
); }); + diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 1e7bbcb6cc3..25c066a846e 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,12 +1,16 @@ -import { MutableRefObject, memo } from "react"; -import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { MutableRefObject, memo, useEffect, useRef, useState } from "react"; +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { + draggable, + dropTargetForElements, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { observer } from "mobx-react-lite"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; // hooks import { Tooltip, ControlLink } from "@plane/ui"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import { cn } from "@/helpers/common.helper"; -import { useApplication, useIssueDetail, useProject } from "@/hooks/store"; +import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // components import { IssueProperties } from "../properties/all-properties"; @@ -22,12 +26,10 @@ interface IssueBlockProps { displayProperties: IIssueDisplayProperties | undefined; isDragDisabled: boolean; draggableId: string; - index: number; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; - isDragStarted?: boolean; issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization } @@ -108,51 +110,73 @@ export const KanbanIssueBlock: React.FC = memo((props) => { issuesMap, displayProperties, isDragDisabled, - draggableId, - index, updateIssue, quickActions, canEditProperties, scrollableContainerRef, - isDragStarted, issueIds, } = props; + const cardRef = useRef(null); const issue = issuesMap[issueId]; + const {setIsDragging: setIsKanbanDragging} =useKanbanView(); + + const [isDraggingOver, setIsDraggingOver] = useState(false); + const [isDragging, setIsDragging] = useState(false); + + useEffect(() => { + const element = cardRef.current; + + if(!element) return; + + return combine(draggable({ + element, + getInitialData: () => ({id: issue.id, type: "ISSUE"}), + onDragStart: () => {setIsKanbanDragging(true); + setIsDragging(true)}, + onDrop: () => {setIsKanbanDragging(false); setIsDragging(false)}, + } + ), + dropTargetForElements( + { + element, + canDrop: (payload) => payload.source.data.id !== issue.id, + getData: () => ({id: issue.id, type: "ISSUE"}), + onDragEnter: () =>{ + setIsDraggingOver(true) + }, + onDragLeave: () => { + setIsDraggingOver(false) + }, + onDrop: () => { + setIsDraggingOver(false) + }, + }) + ) + }, [cardRef?.current, issue.id, setIsDragging, setIsDraggingOver]) + if (!issue) return null; const canEditIssueProperties = canEditProperties(issue.project_id); return ( - - {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( -
+
+
= memo((props) => { />
-
- )} - +
); }); diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index a906a329f14..c352e6b5c34 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -16,7 +16,6 @@ interface IssueBlocksListProps { quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; - isDragStarted?: boolean; } const KanbanIssueBlocksListMemo: React.FC = (props) => { @@ -32,14 +31,13 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { quickActions, canEditProperties, scrollableContainerRef, - isDragStarted, } = props; return ( <> {issueIds && issueIds.length > 0 ? ( <> - {issueIds.map((issueId, index) => { + {issueIds.map((issueId) => { if (!issueId) return null; let draggableId = issueId; @@ -56,11 +54,9 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { updateIssue={updateIssue} quickActions={quickActions} draggableId={draggableId} - index={index} isDragDisabled={isDragDisabled} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} - isDragStarted={isDragStarted} issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders /> ); diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index ee1ceef53c8..1ef854cdbc9 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -30,6 +30,7 @@ import { getGroupByColumns, isWorkspaceLevel } from "../utils"; import { KanbanStoreType } from "./base-kanban-root"; import { HeaderGroupByCard } from "./headers/group-by-card"; import { KanbanGroup } from "./kanban-group"; +import { DropLocation } from "./utils"; export interface IGroupByKanBan { issuesMap: IIssueMap; @@ -56,7 +57,7 @@ export interface IGroupByKanBan { addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; - isDragStarted?: boolean; + handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; showEmptyGroup?: boolean; subGroupIssueHeaderCount?: (listId: string) => number; } @@ -82,7 +83,7 @@ const GroupByKanBan: React.FC = observer((props) => { addIssuesToView, canEditProperties, scrollableContainerRef, - isDragStarted, + handleOnDrop, showEmptyGroup = true, subGroupIssueHeaderCount, } = props; @@ -188,7 +189,7 @@ const GroupByKanBan: React.FC = observer((props) => { disableIssueCreation={disableIssueCreation} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} - isDragStarted={isDragStarted} + handleOnDrop={handleOnDrop} /> )}
@@ -223,7 +224,7 @@ export interface IKanBan { addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; - isDragStarted?: boolean; + handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; subGroupIssueHeaderCount?: (listId: string) => number; } @@ -247,7 +248,7 @@ export const KanBan: React.FC = observer((props) => { addIssuesToView, canEditProperties, scrollableContainerRef, - isDragStarted, + handleOnDrop, showEmptyGroup, subGroupIssueHeaderCount, } = props; @@ -275,7 +276,7 @@ export const KanBan: React.FC = observer((props) => { addIssuesToView={addIssuesToView} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} - isDragStarted={isDragStarted} + handleOnDrop={handleOnDrop} showEmptyGroup={showEmptyGroup} subGroupIssueHeaderCount={subGroupIssueHeaderCount} /> diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index b3e24dc2363..a98a05005d4 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -1,6 +1,10 @@ -import { MutableRefObject } from "react"; -import { Droppable } from "@hello-pangea/dnd"; -// hooks +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'; +//types import { TGroupedIssues, TIssue, @@ -9,9 +13,10 @@ import { TSubGroupedIssues, TUnGroupedIssues, } from "@plane/types"; +// hooks import { useProjectState } from "@/hooks/store"; //components -//types +import { DropLocation, processPragmaticDropPayload } from "./utils"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { @@ -38,7 +43,7 @@ interface IKanbanGroup { canEditProperties: (projectId: string | undefined) => boolean; groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; - isDragStarted?: boolean; + handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; } export const KanbanGroup = (props: IKanbanGroup) => { @@ -60,11 +65,41 @@ export const KanbanGroup = (props: IKanbanGroup) => { quickAddCallback, viewId, scrollableContainerRef, - isDragStarted, + handleOnDrop, } = props; // hooks const projectState = useProjectState(); + const [isDragging, setIsDragging] = useState(false); + + const columnRef = useRef(null); + + 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: () => {setIsDragging(true)}, + onDragLeave: () => {setIsDragging(false)}, + onDragStart: () => {setIsDragging(true)}, + onDrop: (payload) => { + setIsDragging(false); + const { source, destination} = processPragmaticDropPayload(payload) ?? {}; + + if(!source || !destination) return; + + handleOnDrop(source, destination) + }, + } + ), + autoScrollForElements({ + element + })) + }, [columnRef?.current, groupId, sub_group_id, setIsDragging]) + const prePopulateQuickAddData = ( groupByKey: string | null, subGroupByKey: string | null, @@ -118,13 +153,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { }; return ( -
- - {(provided: any, snapshot: any) => (
{ quickActions={quickActions} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} - isDragStarted={isDragStarted} /> - {provided.placeholder} - {enableQuickIssueCreate && !disableIssueCreation && (
{
)}
- )} -
-
); }; + diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 197d8d2934a..c61a622ee57 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -18,6 +18,7 @@ import { KanbanStoreType } from "./base-kanban-root"; import { KanBan } from "./default"; import { HeaderGroupByCard } from "./headers/group-by-card"; import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; +import { DropLocation } from "./utils"; // types // constants @@ -107,7 +108,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; - isDragStarted?: boolean; + handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; disableIssueCreation?: boolean; storeType: KanbanStoreType; enableQuickIssueCreate: boolean; @@ -142,7 +143,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { quickAddCallback, viewId, scrollableContainerRef, - isDragStarted, + handleOnDrop, } = props; const calculateIssueCount = (column_id: string) => { @@ -213,7 +214,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { quickAddCallback={quickAddCallback} viewId={viewId} scrollableContainerRef={scrollableContainerRef} - isDragStarted={isDragStarted} + handleOnDrop={handleOnDrop} subGroupIssueHeaderCount={(groupByListId: string) => getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) } @@ -238,7 +239,7 @@ export interface IKanBanSwimLanes { kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; - isDragStarted?: boolean; + handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; disableIssueCreation?: boolean; storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; @@ -267,7 +268,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters, handleKanbanFilters, showEmptyGroup, - isDragStarted, + handleOnDrop, disableIssueCreation, enableQuickIssueCreate, canEditProperties, @@ -337,7 +338,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} showEmptyGroup={showEmptyGroup} - isDragStarted={isDragStarted} + handleOnDrop={handleOnDrop} disableIssueCreation={disableIssueCreation} enableQuickIssueCreate={enableQuickIssueCreate} addIssuesToView={addIssuesToView} diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index 855f096e655..b83f0451c7a 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -1,19 +1,77 @@ -import { DraggableLocation } from "@hello-pangea/dnd"; -import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssue } from "@plane/types"; +import { IIssueMap, TIssue } from "@plane/types"; -const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { +export const processPragmaticDropPayload = ( + payload: any +): { source: DropLocation; destination: DropLocation } | undefined => { + const { location, source: sourceIssue } = payload; + + const sourceIssueData = sourceIssue.data; + let sourceColumData; + + let destinationIssueData, destinationColumnData; + + const destDropTargets = location?.current?.dropTargets ?? []; + + const sourceDropTargets = location?.initial?.dropTargets ?? []; + for (const dropTarget of sourceDropTargets) { + const dropTargetData = dropTarget?.data; + + if (!dropTargetData) continue; + + if (dropTargetData.type === "COLUMN") { + sourceColumData = dropTargetData; + } + } + + 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 (sourceIssueData?.id === undefined || !sourceColumData?.groupId || !destinationColumnData?.groupId) return; + + return { + source: { + groupId: sourceColumData.groupId as string, + subGroupId: sourceColumData.subGroupId as string, + columnId: sourceColumData.columnId as string, + id: sourceIssueData.id as string, + }, + destination: { + groupId: destinationColumnData.groupId as string, + subGroupId: destinationColumnData.subGroupId as string, + columnId: destinationColumnData.columnId as string, + id: destinationIssueData?.id as string | undefined, + }, + }; +}; + +const handleSortOrder = (destinationIssues: string[], destinationIssueId: string | undefined, issueMap: IIssueMap) => { const sortOrderDefaultValue = 65535; let currentIssueState = {}; + const destinationIndex = destinationIssueId + ? destinationIssues.indexOf(destinationIssueId) + : destinationIssues.length; + if (destinationIssues && destinationIssues.length > 0) { if (destinationIndex === 0) { - const destinationIssueId = destinationIssues[destinationIndex]; + const destinationIssueId = destinationIssues[0]; currentIssueState = { ...currentIssueState, sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue, }; } else if (destinationIndex === destinationIssues.length) { - const destinationIssueId = destinationIssues[destinationIndex - 1]; + const destinationIssueId = destinationIssues[destinationIssues.length - 1]; currentIssueState = { ...currentIssueState, sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue, @@ -36,117 +94,101 @@ const handleSortOrder = (destinationIssues: string[], destinationIndex: number, return currentIssueState; }; +export type DropLocation = { + columnId: string; + groupId: string; + subGroupId?: string; + id: string | undefined; +}; + export const handleDragDrop = async ( - source: DraggableLocation | null | undefined, - destination: DraggableLocation | null | undefined, + source: DropLocation | null | undefined, + destination: DropLocation | null | undefined, workspaceSlug: string | undefined, projectId: string | undefined, // projectId for all views or user id in profile issues subGroupBy: string | null, groupBy: string | null, issueMap: IIssueMap, - issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined, + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, removeIssue: (projectId: string, issueId: string) => Promise | undefined ) => { - if (!issueMap || !issueWithIds || !source || !destination || !workspaceSlug || !projectId) return; + if (!issueMap || !source || !destination || !source.id || !workspaceSlug || !projectId) return; let updatedIssue: any = {}; - const sourceDroppableId = source?.droppableId; - const destinationDroppableId = destination?.droppableId; + if (!workspaceSlug || !projectId || !groupBy) return; - const sourceColumnId = (sourceDroppableId && sourceDroppableId.split("__")) || null; - const destinationColumnId = (destinationDroppableId && destinationDroppableId.split("__")) || null; + if (subGroupBy && (!source.subGroupId || !destination.subGroupId)) return; - if (!sourceColumnId || !destinationColumnId || !sourceDroppableId || !destinationDroppableId) return; + if (destination.columnId === "issue-trash-box") { + const sourceIssues = getIssueIds(source.groupId, source.subGroupId); - const sourceGroupByColumnId = sourceColumnId[0] || null; - const destinationGroupByColumnId = destinationColumnId[0] || null; + if (!sourceIssues) return; - const sourceSubGroupByColumnId = sourceColumnId[1] || null; - const destinationSubGroupByColumnId = destinationColumnId[1] || null; - - if ( - !workspaceSlug || - !projectId || - !groupBy || - !sourceGroupByColumnId || - !destinationGroupByColumnId || - !sourceSubGroupByColumnId || - !destinationSubGroupByColumnId - ) - return; - - if (destinationGroupByColumnId === "issue-trash-box") { - const sourceIssues: string[] = subGroupBy - ? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId] - : (issueWithIds as TGroupedIssues)[sourceGroupByColumnId]; - - const [removed] = sourceIssues.splice(source.index, 1); + const sourceIndex = sourceIssues.indexOf(source.id); + const [removed] = sourceIssues.splice(sourceIndex, 1); if (removed) { return await removeIssue(projectId, removed); } - } else { - //spreading the array to stop changing the original reference - //since we are removing an id from array further down - const sourceIssues = [ - ...(subGroupBy - ? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId] - : (issueWithIds as TGroupedIssues)[sourceGroupByColumnId]), - ]; - const destinationIssues = subGroupBy - ? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][destinationGroupByColumnId] - : (issueWithIds as TGroupedIssues)[destinationGroupByColumnId]; - - const [removed] = sourceIssues.splice(source.index, 1); - const removedIssueDetail = issueMap[removed]; - - updatedIssue = { - id: removedIssueDetail?.id, - project_id: removedIssueDetail?.project_id, - }; - // for both horizontal and vertical dnd - updatedIssue = { - ...updatedIssue, - ...handleSortOrder( - sourceDroppableId === destinationDroppableId ? sourceIssues : destinationIssues, - destination.index, - issueMap - ), - }; + return; + } - if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) { - if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) { - if (sourceGroupByColumnId != destinationGroupByColumnId) { - if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; - } - } else { - if (subGroupBy === "state") - updatedIssue = { - ...updatedIssue, - state_id: destinationSubGroupByColumnId, - priority: destinationGroupByColumnId, - }; - if (subGroupBy === "priority") - updatedIssue = { - ...updatedIssue, - state_id: destinationGroupByColumnId, - priority: destinationSubGroupByColumnId, - }; + //spreading the array to stop changing the original reference + //since we are removing an id from array further down + const sourceIssues = getIssueIds(source.groupId, source.subGroupId); + const destinationIssues = getIssueIds(destination.groupId, destination.subGroupId); + + if (!sourceIssues || !destinationIssues) return; + + const sourceIssue = issueMap[source.id]; + + updatedIssue = { + id: sourceIssue?.id, + project_id: sourceIssue?.project_id, + }; + + // for both horizontal and vertical dnd + updatedIssue = { + ...updatedIssue, + ...handleSortOrder( + source.columnId === destination.columnId ? sourceIssues : destinationIssues, + destination.id, + issueMap + ), + }; + + if (subGroupBy && source.subGroupId && destination.subGroupId) { + if (source.subGroupId === destination.subGroupId) { + if (source.groupId != destination.groupId) { + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destination.groupId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destination.groupId }; } } else { - // for horizontal dnd - if (sourceColumnId != destinationColumnId) { - if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; - } + if (subGroupBy === "state") + updatedIssue = { + ...updatedIssue, + state_id: destination.subGroupId, + priority: destination.groupId, + }; + if (subGroupBy === "priority") + updatedIssue = { + ...updatedIssue, + state_id: destination.groupId, + priority: destination.subGroupId, + }; } - - if (updatedIssue && updatedIssue?.id) { - return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); + } else { + // for horizontal dnd + if (source.columnId != destination.columnId) { + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destination.groupId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destination.groupId }; } } + + if (updatedIssue && updatedIssue?.id) { + return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); + } }; diff --git a/web/package.json b/web/package.json index de73ad407ff..eb0be63441f 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,8 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.3", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.0.3", "@blueprintjs/popover2": "^1.13.3", "@headlessui/react": "^1.7.3", "@hello-pangea/dnd": "^16.3.0", @@ -61,6 +63,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "prettier": "^3.2.5", "@types/dompurify": "^3.0.5", "@types/js-cookie": "^3.0.2", "@types/lodash": "^4.14.202", @@ -71,7 +74,6 @@ "@types/react-dom": "^18.2.17", "@types/uuid": "^8.3.4", "eslint-config-custom": "*", - "prettier": "^2.8.7", "tailwind-config-custom": "*", "tsconfig": "*", "typescript": "4.7.4" diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts index fa25038387d..36e65e47684 100644 --- a/web/store/issue/issue_kanban_view.store.ts +++ b/web/store/issue/issue_kanban_view.store.ts @@ -8,12 +8,14 @@ export interface IIssueKanBanViewStore { groupByHeaderMinMax: string[]; subgroupByIssuesVisibility: string[]; }; + isDragging: boolean, // computed getCanUserDragDrop: (group_by: string | null, sub_group_by: string | null) => boolean; canUserDragDropVertically: boolean; canUserDragDropHorizontally: boolean; // actions handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void; + setIsDragging: (isDragging: boolean) => void; } export class IssueKanBanViewStore implements IIssueKanBanViewStore { @@ -21,23 +23,30 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { groupByHeaderMinMax: string[]; subgroupByIssuesVisibility: string[]; } = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] }; + isDragging = false; // root store rootStore; constructor(_rootStore: IssueRootStore) { makeObservable(this, { kanBanToggle: observable, + isDragging: observable.ref, // computed canUserDragDropVertically: computed, canUserDragDropHorizontally: computed, // actions handleKanBanToggle: action, + setIsDragging: action.bound, }); this.rootStore = _rootStore; } + setIsDragging = (isDragging: boolean) => { + this.isDragging = isDragging; + } + getCanUserDragDrop = computedFn((group_by: string | null, sub_group_by: string | null) => { if (group_by && ["state", "priority"].includes(group_by)) { if (!sub_group_by) return true; diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 5c329749aa3..1de1bfc1ae0 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -18,6 +18,7 @@ export interface IProjectIssues { viewFlags: ViewFlags; // computed groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; // action fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -100,6 +101,30 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { return issues; } + getIssueIds = (groupId?: string, subGroupId?: string) => { + const groupedIssueIds = this.groupedIssueIds; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + + if(!groupBy && !subGroupBy) { + return groupedIssueIds as string[] + } + + if(groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[] + } + + if(groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[] + } + + return undefined; + } + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { try { this.loader = loadType; diff --git a/yarn.lock b/yarn.lock index 88f5d4e5533..826fa4f53d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,6 +29,23 @@ jsonpointer "^5.0.0" leven "^3.1.0" +"@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.0.3.tgz#bb088a3d8eb77d9454dfdead433e594c5f793dae" + integrity sha512-gfXwzQZRsWSLd/88IRNKwf2e5r3smZlDGLopg5tFkSqsL03VDHgd6wch6OfPhRK2kSCrT1uXv5n9hOpVBu678g== + dependencies: + "@atlaskit/pragmatic-drag-and-drop" "^1.1.0" + "@babel/runtime" "^7.0.0" + +"@atlaskit/pragmatic-drag-and-drop@^1.1.0", "@atlaskit/pragmatic-drag-and-drop@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.3.tgz#ecbfa4dcd2f9bf9b87f3d1565cedb2661d1fae0a" + integrity sha512-lx6ZMPSU8zPhUfAkdKajNAFWDDIqdtM8eQzCsqCRalXWumpclcvqeN8VCLkmclcQDEUhV8c2utKbcuhm7hvRIw== + dependencies: + "@babel/runtime" "^7.0.0" + bind-event-listener "^2.1.1" + raf-schd "^4.0.3" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -921,6 +938,13 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== +"@babel/runtime@^7.0.0": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.5", "@babel/runtime@^7.23.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" @@ -3354,6 +3378,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bind-event-listener@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bind-event-listener/-/bind-event-listener-2.1.1.tgz#5e57290181af3027ff53ba6109e417a1e3cbb6f3" + integrity sha512-O+a5c0D2se/u2VlBJmPRn45IB6R4mYMh1ok3dWxrIZ2pmLqzggBhb875mbq73508ylzofc0+hT9W41x4Y2s8lg== + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -6866,11 +6895,16 @@ prettier-plugin-tailwindcss@^0.5.4: resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.9.tgz#fdc2bd95a02b64702ebd2d6c7ddd300198de3cc6" integrity sha512-9x3t1s2Cjbut2QiP+O0mDqV3gLXTe2CgRlQDgucopVkUdw26sQi53p/q4qvGxMLBDfk/dcTV57Aa/zYwz9l8Ew== -prettier@^2.8.7, prettier@^2.8.8: +prettier@^2.8.8: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + prettier@latest: version "3.1.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" From 9e21ae5e205d4d91dfb297a2b70c00958b5800b8 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Fri, 12 Apr 2024 17:45:27 +0530 Subject: [PATCH 2/3] refactor pragmatic dnd implementation and fix bugs --- packages/types/src/index.d.ts | 1 + packages/types/src/pragmatic.d.ts | 25 ++ packages/ui/src/drop-indicator.tsx | 21 ++ packages/ui/src/index.ts | 1 + .../issue-layouts/kanban/base-kanban-root.tsx | 227 ++++++++---------- .../issues/issue-layouts/kanban/block.tsx | 110 +++++---- .../issues/issue-layouts/kanban/default.tsx | 15 +- .../kanban/headers/group-by-card.tsx | 6 +- .../issue-layouts/kanban/kanban-group.tsx | 141 ++++++----- .../issues/issue-layouts/kanban/swimlanes.tsx | 15 +- .../issues/issue-layouts/kanban/utils.ts | 211 ++++++++-------- web/store/issue/cycle/issue.store.ts | 25 ++ web/store/issue/draft/issue.store.ts | 25 ++ web/store/issue/helpers/issue-helper.store.ts | 2 +- web/store/issue/issue_kanban_view.store.ts | 24 +- web/store/issue/module/issue.store.ts | 25 ++ web/store/issue/profile/issue.store.ts | 25 ++ web/store/issue/project-views/issue.store.ts | 25 ++ web/store/issue/project/issue.store.ts | 16 +- 19 files changed, 567 insertions(+), 373 deletions(-) create mode 100644 packages/types/src/pragmatic.d.ts create mode 100644 packages/ui/src/drop-indicator.tsx diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 52ea3771edb..4d98b8f7a24 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -27,3 +27,4 @@ export * from "./api_token"; export * from "./instance"; export * from "./app"; export * from "./common"; +export * from "./pragmatic"; diff --git a/packages/types/src/pragmatic.d.ts b/packages/types/src/pragmatic.d.ts new file mode 100644 index 00000000000..ca47e2d37a7 --- /dev/null +++ b/packages/types/src/pragmatic.d.ts @@ -0,0 +1,25 @@ +export type TDropTarget = { + element: Element; + data: Record; +}; + +export type TDropTargetMiscellaneousData = { + dropEffect: string; + isActiveDueToStickiness: boolean; +}; + +export interface IPragmaticDropPayload { + location: { + initial: { + dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[]; + }; + current: { + dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[]; + }; + previous: { + dropTargets: (TDropTarget & TDropTargetMiscellaneousData)[]; + }; + }; + source: TDropTarget; + self: TDropTarget & TDropTargetMiscellaneousData; +} diff --git a/packages/ui/src/drop-indicator.tsx b/packages/ui/src/drop-indicator.tsx new file mode 100644 index 00000000000..228c1a33012 --- /dev/null +++ b/packages/ui/src/drop-indicator.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { cn } from "../helpers"; + +type Props = { + isVisible: boolean; +}; + +export const DropIndicator = (props: Props) => { + return ( +
+ ); +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 24b76c3e0e4..78013962cc4 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -12,3 +12,4 @@ export * from "./tooltip"; export * from "./loader"; export * from "./control-link"; export * from "./toast"; +export * from "./drop-indicator"; \ No newline at end of file diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 2ee0b35a365..be9b94cdc52 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -1,9 +1,7 @@ import { FC, useCallback, 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 { 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 { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { TIssue } from "@plane/types"; @@ -13,16 +11,15 @@ import { DeleteIssueModal } from "@/components/issues"; import { ISSUE_DELETED } from "@/constants/event-tracker"; import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; -import { useEventTracker, useIssues, useKanbanView, useUser } from "@/hooks/store"; +import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUser } from "@/hooks/store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; -import { IProjectIssues } from "@/store/issue/project"; // ui // types import { IQuickActionProps } from "../list/list-view-types"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; -import { DropLocation, handleDragDrop, processPragmaticDropPayload } from "./utils"; +import { KanbanDropLocation, handleDragDrop, getSourceFromDropPayload } from "./utils"; export type KanbanStoreType = | EIssuesStoreType.PROJECT @@ -41,12 +38,6 @@ export interface IBaseKanBanLayout { isCompletedCycle?: boolean; } -type KanbanDragState = { - draggedIssueId?: string | null; - source?: DropLocation | null; - destination?: DropLocation | null; -}; - export const BaseKanBanRoot: React.FC = observer((props: IBaseKanBanLayout) => { const { QuickActions, @@ -65,22 +56,25 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas membership: { currentProjectRole }, } = useUser(); const { captureIssueEvent } = useEventTracker(); - const { issueMap, issuesFilter, issues, } = useIssues(storeType); + const { issueMap, issuesFilter, issues } = useIssues(storeType); + const { + issue: { getIssueById }, + } = useIssueDetail(); const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = useIssuesActions(storeType); const deleteAreaRef = useRef(null); const [isDragOverDelete, setIsDragOverDelete] = useState(false); - const { isDragging} = useKanbanView(); + const { isDragging } = useKanbanView(); const issueIds = issues?.groupedIssueIds || []; const displayFilters = issuesFilter?.issueFilters?.displayFilters; const displayProperties = issuesFilter?.issueFilters?.displayProperties; - const sub_group_by: string | null = displayFilters?.sub_group_by || null; - const group_by: string | null = displayFilters?.group_by || null; + const sub_group_by = displayFilters?.sub_group_by; + const group_by = displayFilters?.group_by; const userDisplayFilters = displayFilters || null; @@ -91,7 +85,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const scrollableContainerRef = useRef(null); // states - const [dragState, setDragState] = useState({}); + const [draggedIssueId, setDraggedIssueId] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -106,82 +100,72 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); + // Enable Auto Scroll for Main Kanban useEffect(() => { const element = scrollableContainerRef.current; - if(!element) return; + if (!element) return; return combine( autoScrollForElements({ - element + element, }) - ) - }, [scrollableContainerRef?.current]) + ); + }, [scrollableContainerRef?.current]); + // Make the Issue Delete Box a Drop Target useEffect(() => { const element = deleteAreaRef.current; - if(!element) return; + if (!element) return; - return combine(dropTargetForElements({ - element, - getData: () => ({columnId: "issue-trash-box", groupId: "issue-trash-box", type: "DELETE"}), - onDragEnter: () => {setIsDragOverDelete(true)}, - onDragLeave: () => {setIsDragOverDelete(false)}, + return combine( + dropTargetForElements({ + element, + getData: () => ({ columnId: "issue-trash-box", groupId: "issue-trash-box", type: "DELETE" }), + onDragEnter: () => { + setIsDragOverDelete(true); + }, + onDragLeave: () => { + setIsDragOverDelete(false); + }, onDrop: (payload) => { setIsDragOverDelete(false); - const { source, destination} = processPragmaticDropPayload(payload) ?? {}; + const source = getSourceFromDropPayload(payload); - console.log("on drop delete", source, destination) + if (!source) return; - if(!source || !destination) return; - - handleOnDrop(source, destination) + setDraggedIssueId(source.id); + setDeleteIssueModal(true); }, - } - ),) - }, [deleteAreaRef?.current, setIsDragOverDelete]) - - const handleOnDrop = async (source: DropLocation, destination: DropLocation) => { + }) + ); + }, [deleteAreaRef?.current, setIsDragOverDelete, setDraggedIssueId, setDeleteIssueModal]); + const handleOnDrop = async (source: KanbanDropLocation, destination: KanbanDropLocation) => { if ( source.columnId && destination.columnId && destination.columnId === source.columnId && - destination.id === source.id + destination.id === source.id ) return; - if (handleDragDrop) { - if (destination.columnId && destination.columnId === "issue-trash-box") { - setDragState({ - ...dragState, - draggedIssueId: source?.id, - source, - destination, - }); - setDeleteIssueModal(true); - } else { - await handleDragDrop( - source, - destination, - workspaceSlug?.toString(), - projectId?.toString(), - sub_group_by, - group_by, - issueMap, - (issues as IProjectIssues).getIssueIds, - updateIssue, - removeIssue - ).catch((err) => { - setToast({ - title: "Error", - type: TOAST_TYPE.ERROR, - message: err?.detail ?? "Failed to perform this action", - }); - }); - } - } + await handleDragDrop( + source, + destination, + getIssueById, + issues.getIssueIds, + updateIssue, + group_by, + sub_group_by + ).catch((err) => { + setToast({ + title: "Error", + type: TOAST_TYPE.ERROR, + message: err?.detail ?? "Failed to perform this action", + }); + }); }; const renderQuickActions = useCallback( @@ -202,24 +186,16 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas ); const handleDeleteIssue = async () => { - if (!handleDragDrop) return; - await handleDragDrop( - dragState.source, - dragState.destination, - workspaceSlug?.toString(), - projectId?.toString(), - sub_group_by, - group_by, - issueMap, - (issues as IProjectIssues).getIssueIds, - updateIssue, - removeIssue - ).finally(() => { + const draggedIssue = getIssueById(draggedIssueId ?? ""); + + if (!draggedIssueId || !draggedIssue) return; + + await removeIssue(draggedIssue.project_id, draggedIssueId).finally(() => { setDeleteIssueModal(false); - setDragState({}); + setDraggedIssueId(undefined); captureIssueEvent({ eventName: ISSUE_DELETED, - payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" }, + payload: { id: draggedIssueId, state: "FAILED", element: "Kanban layout drag & drop" }, path: router.asPath, }); }); @@ -241,7 +217,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas return ( <> setDeleteIssueModal(false)} onSubmit={handleDeleteIssue} @@ -254,54 +230,53 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas )}
- {/* drag and delete component */} + {/* drag and delete component */} +
-
- Drop here to delete the issue. -
-
- -
- + Drop here to delete the issue.
+
+ +
+ +
); }); - diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 43c2522175e..0d424dbfb22 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,13 +1,10 @@ import { MutableRefObject, memo, useEffect, useRef, useState } from "react"; -import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; -import { - draggable, - dropTargetForElements, -} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { observer } from "mobx-react-lite"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; // hooks -import { ControlLink, Tooltip } from "@plane/ui"; +import { ControlLink, DropIndicator, Tooltip } from "@plane/ui"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import { cn } from "@/helpers/common.helper"; import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store"; @@ -115,64 +112,76 @@ export const KanbanIssueBlock: React.FC = memo((props) => { const issue = issuesMap[issueId]; - const {setIsDragging: setIsKanbanDragging} =useKanbanView(); + const { setIsDragging: setIsKanbanDragging } = useKanbanView(); - const [isDraggingOver, setIsDraggingOver] = useState(false); - const [isDragging, setIsDragging] = useState(false); + const [isDraggingOverBlock, setIsDraggingOverBlock] = useState(false); + const [isCurrentBlockDragging, setIsCurrentBlockDragging] = useState(false); + // Make Issue block both as as Draggable and, + // as a DropTarget for other issues being dragged to get the location of drop useEffect(() => { const element = cardRef.current; - if(!element) return; - - return combine(draggable({ - element, - getInitialData: () => ({id: issue.id, type: "ISSUE"}), - onDragStart: () => {setIsKanbanDragging(true); - setIsDragging(true)}, - onDrop: () => {setIsKanbanDragging(false); setIsDragging(false)}, - } - ), - dropTargetForElements( - { - element, - canDrop: (payload) => payload.source.data.id !== issue.id, - getData: () => ({id: issue.id, type: "ISSUE"}), - onDragEnter: () =>{ - setIsDraggingOver(true) - }, - onDragLeave: () => { - setIsDraggingOver(false) - }, - onDrop: () => { - setIsDraggingOver(false) - }, - }) - ) - }, [cardRef?.current, issue.id, setIsDragging, setIsDraggingOver]) + if (!element) return; + + return combine( + draggable({ + element, + canDrag: () => !isDragDisabled, + getInitialData: () => ({ id: issue?.id, type: "ISSUE" }), + onDragStart: () => { + setIsCurrentBlockDragging(true); + setIsKanbanDragging(true); + }, + onDrop: () => { + setIsKanbanDragging(false); + setIsCurrentBlockDragging(false); + }, + }), + dropTargetForElements({ + element, + canDrop: (payload) => payload.source?.data?.id !== issue?.id, + getData: () => ({ id: issue?.id, type: "ISSUE" }), + onDragEnter: () => { + setIsDraggingOverBlock(true); + }, + onDragLeave: () => { + setIsDraggingOverBlock(false); + }, + onDrop: () => { + setIsDraggingOverBlock(false); + }, + }) + ); + }, [cardRef?.current, issue?.id, setIsCurrentBlockDragging, setIsDraggingOverBlock]); if (!issue) return null; const canEditIssueProperties = canEditProperties(issue.project_id); return ( -
-
- handleIssuePeekOverview(issue)} - disabled={!!issue?.tempId} - > + <> + +
!isDragDisabled && setIsCurrentBlockDragging(true)} + > + handleIssuePeekOverview(issue)} + disabled={!!issue?.tempId} + >
@@ -192,8 +201,9 @@ export const KanbanIssueBlock: React.FC = memo((props) => { />
-
-
+
+
+ ); }); diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 1ef854cdbc9..cb40accf5f0 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -10,6 +10,7 @@ import { TSubGroupedIssues, TUnGroupedIssues, TIssueKanbanFilters, + TIssueGroupByOptions, } from "@plane/types"; // constants // hooks @@ -30,14 +31,14 @@ import { getGroupByColumns, isWorkspaceLevel } from "../utils"; import { KanbanStoreType } from "./base-kanban-root"; import { HeaderGroupByCard } from "./headers/group-by-card"; import { KanbanGroup } from "./kanban-group"; -import { DropLocation } from "./utils"; +import { KanbanDropLocation } from "./utils"; export interface IGroupByKanBan { issuesMap: IIssueMap; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; displayProperties: IIssueDisplayProperties | undefined; - sub_group_by: string | null; - group_by: string | null; + sub_group_by: TIssueGroupByOptions | undefined; + group_by: TIssueGroupByOptions | undefined; sub_group_id: string; isDragDisabled: boolean; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; @@ -57,7 +58,7 @@ export interface IGroupByKanBan { addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; - handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; + handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise; showEmptyGroup?: boolean; subGroupIssueHeaderCount?: (listId: string) => number; } @@ -203,8 +204,8 @@ export interface IKanBan { issuesMap: IIssueMap; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; displayProperties: IIssueDisplayProperties | undefined; - sub_group_by: string | null; - group_by: string | null; + sub_group_by: TIssueGroupByOptions | undefined; + group_by: TIssueGroupByOptions | undefined; sub_group_id?: string; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; @@ -224,7 +225,7 @@ export interface IKanBan { addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; - handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; + handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise; subGroupIssueHeaderCount?: (listId: string) => number; } diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 44df59d9ac3..afdd3351585 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; -import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types"; +import { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types"; // ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components @@ -16,8 +16,8 @@ import { useEventTracker } from "@/hooks/store"; import { KanbanStoreType } from "../base-kanban-root"; interface IHeaderGroupByCard { - sub_group_by: string | null; - group_by: string | null; + sub_group_by: TIssueGroupByOptions | undefined; + group_by: TIssueGroupByOptions | undefined; column_id: string; icon?: React.ReactNode; title: string; diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index a98a05005d4..e913e0e31ae 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -1,9 +1,7 @@ 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 { 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"; //types import { TGroupedIssues, @@ -12,11 +10,14 @@ import { IIssueMap, TSubGroupedIssues, TUnGroupedIssues, + TIssueGroupByOptions, } from "@plane/types"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useProjectState } from "@/hooks/store"; //components -import { DropLocation, processPragmaticDropPayload } from "./utils"; +import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { @@ -25,8 +26,8 @@ interface IKanbanGroup { peekIssueId?: string; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; displayProperties: IIssueDisplayProperties | undefined; - sub_group_by: string | null; - group_by: string | null; + sub_group_by: TIssueGroupByOptions | undefined; + group_by: TIssueGroupByOptions | undefined; sub_group_id: string; isDragDisabled: boolean; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; @@ -43,7 +44,7 @@ interface IKanbanGroup { canEditProperties: (projectId: string | undefined) => boolean; groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; - handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; + handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise; } export const KanbanGroup = (props: IKanbanGroup) => { @@ -70,39 +71,48 @@ export const KanbanGroup = (props: IKanbanGroup) => { // hooks const projectState = useProjectState(); - const [isDragging, setIsDragging] = useState(false); + const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const columnRef = useRef(null); + // Enable Kanban Columns as Drop Targets useEffect(() => { const element = columnRef.current; - if(!element) return; + if (!element) return; - return combine(dropTargetForElements({ - element, - getData: () => ({groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN"}), - onDragEnter: () => {setIsDragging(true)}, - onDragLeave: () => {setIsDragging(false)}, - onDragStart: () => {setIsDragging(true)}, + 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) => { - setIsDragging(false); - const { source, destination} = processPragmaticDropPayload(payload) ?? {}; + setIsDraggingOverColumn(false); + const source = getSourceFromDropPayload(payload); + const destination = getDestinationFromDropPayload(payload); - if(!source || !destination) return; + if (!source || !destination) return; - handleOnDrop(source, destination) + handleOnDrop(source, destination); }, - } - ), - autoScrollForElements({ - element - })) - }, [columnRef?.current, groupId, sub_group_id, setIsDragging]) + }), + autoScrollForElements({ + element, + }) + ); + }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]); const prePopulateQuickAddData = ( - groupByKey: string | null, - subGroupByKey: string | null, + groupByKey: string | undefined, + subGroupByKey: string | undefined | null, groupValue: string, subGroupValue: string ) => { @@ -153,40 +163,43 @@ export const KanbanGroup = (props: IKanbanGroup) => { }; return ( -
- - - {enableQuickIssueCreate && !disableIssueCreation && ( -
- -
- )} -
+
+ + + {enableQuickIssueCreate && !disableIssueCreation && ( +
+ +
+ )} +
); }; - diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index c61a622ee57..ae89664b529 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -10,6 +10,7 @@ import { TSubGroupedIssues, TUnGroupedIssues, TIssueKanbanFilters, + TIssueGroupByOptions, } from "@plane/types"; // components import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; @@ -18,14 +19,14 @@ import { KanbanStoreType } from "./base-kanban-root"; import { KanBan } from "./default"; import { HeaderGroupByCard } from "./headers/group-by-card"; import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; -import { DropLocation } from "./utils"; +import { KanbanDropLocation } from "./utils"; // types // constants interface ISubGroupSwimlaneHeader { issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; - sub_group_by: string | null; - group_by: string | null; + sub_group_by: TIssueGroupByOptions | undefined; + group_by: TIssueGroupByOptions | undefined; list: IGroupByColumn[]; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; @@ -108,7 +109,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; - handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; + handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise; disableIssueCreation?: boolean; storeType: KanbanStoreType; enableQuickIssueCreate: boolean; @@ -232,14 +233,14 @@ export interface IKanBanSwimLanes { issuesMap: IIssueMap; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; displayProperties: IIssueDisplayProperties | undefined; - sub_group_by: string | null; - group_by: string | null; + sub_group_by: TIssueGroupByOptions | undefined; + group_by: TIssueGroupByOptions | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; - handleOnDrop: (source: DropLocation, destination: DropLocation) => Promise; + handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise; disableIssueCreation?: boolean; storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index b83f0451c7a..3981455dbc0 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -1,17 +1,25 @@ -import { IIssueMap, TIssue } from "@plane/types"; +import pull from "lodash/pull"; +import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types"; +import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store"; -export const processPragmaticDropPayload = ( - payload: any -): { source: DropLocation; destination: DropLocation } | undefined => { +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; - let destinationIssueData, destinationColumnData; - - const destDropTargets = location?.current?.dropTargets ?? []; - const sourceDropTargets = location?.initial?.dropTargets ?? []; for (const dropTarget of sourceDropTargets) { const dropTargetData = dropTarget?.data; @@ -23,6 +31,28 @@ export const processPragmaticDropPayload = ( } } + 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; @@ -37,25 +67,28 @@ export const processPragmaticDropPayload = ( } } - if (sourceIssueData?.id === undefined || !sourceColumData?.groupId || !destinationColumnData?.groupId) return; + if (!destinationColumnData?.groupId) return; return { - source: { - groupId: sourceColumData.groupId as string, - subGroupId: sourceColumData.subGroupId as string, - columnId: sourceColumData.columnId as string, - id: sourceIssueData.id as string, - }, - destination: { - groupId: destinationColumnData.groupId as string, - subGroupId: destinationColumnData.subGroupId as string, - columnId: destinationColumnData.columnId as string, - id: destinationIssueData?.id as string | undefined, - }, + groupId: destinationColumnData.groupId as string, + subGroupId: destinationColumnData.subGroupId as string, + columnId: destinationColumnData.columnId as string, + id: destinationIssueData?.id as string | undefined, }; }; -const handleSortOrder = (destinationIssues: string[], destinationIssueId: string | undefined, issueMap: IIssueMap) => { +/** + * 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 +) => { const sortOrderDefaultValue = 65535; let currentIssueState = {}; @@ -66,22 +99,33 @@ const handleSortOrder = (destinationIssues: string[], destinationIssueId: string if (destinationIssues && destinationIssues.length > 0) { if (destinationIndex === 0) { const destinationIssueId = destinationIssues[0]; + const destinationIssue = getIssueById(destinationIssueId); + if (!destinationIssue) return currentIssueState; + currentIssueState = { ...currentIssueState, - sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue, + 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: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue, + 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: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2, + sort_order: (destinationTopIssue.sort_order + destinationBottomIssue.sort_order) / 2, }; } } else { @@ -94,101 +138,72 @@ const handleSortOrder = (destinationIssues: string[], destinationIssueId: string return currentIssueState; }; -export type DropLocation = { - columnId: string; - groupId: string; - subGroupId?: string; - id: string | undefined; -}; - export const handleDragDrop = async ( - source: DropLocation | null | undefined, - destination: DropLocation | null | undefined, - workspaceSlug: string | undefined, - projectId: string | undefined, // projectId for all views or user id in profile issues - subGroupBy: string | null, - groupBy: string | null, - issueMap: IIssueMap, + 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, - removeIssue: (projectId: string, issueId: string) => Promise | undefined + groupBy: TIssueGroupByOptions | undefined, + subGroupBy: TIssueGroupByOptions | undefined ) => { - if (!issueMap || !source || !destination || !source.id || !workspaceSlug || !projectId) return; - - let updatedIssue: any = {}; - - if (!workspaceSlug || !projectId || !groupBy) return; - - if (subGroupBy && (!source.subGroupId || !destination.subGroupId)) return; - - if (destination.columnId === "issue-trash-box") { - const sourceIssues = getIssueIds(source.groupId, source.subGroupId); - - if (!sourceIssues) return; - - const sourceIndex = sourceIssues.indexOf(source.id); - const [removed] = sourceIssues.splice(sourceIndex, 1); + if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return; - if (removed) { - return await removeIssue(projectId, removed); - } - - return; - } - - //spreading the array to stop changing the original reference - //since we are removing an id from array further down + let updatedIssue: Partial = {}; const sourceIssues = getIssueIds(source.groupId, source.subGroupId); const destinationIssues = getIssueIds(destination.groupId, destination.subGroupId); - if (!sourceIssues || !destinationIssues) return; + const sourceIssue = getIssueById(source.id); - const sourceIssue = issueMap[source.id]; + if (!sourceIssues || !destinationIssues || !sourceIssue) return; updatedIssue = { - id: sourceIssue?.id, - project_id: sourceIssue?.project_id, + id: sourceIssue.id, + project_id: sourceIssue.project_id, }; // for both horizontal and vertical dnd updatedIssue = { ...updatedIssue, - ...handleSortOrder( - source.columnId === destination.columnId ? sourceIssues : destinationIssues, - destination.id, - issueMap - ), + ...handleSortOrder(destinationIssues, destination.id, getIssueById), }; - if (subGroupBy && source.subGroupId && destination.subGroupId) { - if (source.subGroupId === destination.subGroupId) { - if (source.groupId != destination.groupId) { - if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destination.groupId }; - if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destination.groupId }; - } + 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 { - if (subGroupBy === "state") - updatedIssue = { - ...updatedIssue, - state_id: destination.subGroupId, - priority: destination.groupId, - }; - if (subGroupBy === "priority") - updatedIssue = { - ...updatedIssue, - state_id: destination.groupId, - priority: destination.subGroupId, - }; + groupValue = destination.groupId; } - } else { - // for horizontal dnd - if (source.columnId != destination.columnId) { - if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destination.groupId }; - if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: 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 && updatedIssue?.id) { - return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); + if (updatedIssue) { + return ( + updateIssue && + (await updateIssue(sourceIssue.project_id, sourceIssue.id, { + ...updatedIssue, + id: sourceIssue.id, + project_id: sourceIssue.project_id, + })) + ); } }; diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index a5d7e0670df..44586fa5459 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -23,6 +23,7 @@ export interface ICycleIssues { // computed groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; fetchIssues: ( workspaceSlug: string, projectId: string, @@ -142,6 +143,30 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { return issues; } + getIssueIds = (groupId?: string, subGroupId?: string) => { + const groupedIssueIds = this.groupedIssueIds; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters || !groupedIssueIds) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + + if (!groupBy && !subGroupBy) { + return groupedIssueIds as string[]; + } + + if (groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; + } + + if (groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[]; + } + + return undefined; + }; + fetchIssues = async ( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index d755a4549bb..43a3121d0ff 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -20,6 +20,7 @@ export interface IDraftIssues { // computed groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -97,6 +98,30 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { return issues; } + getIssueIds = (groupId?: string, subGroupId?: string) => { + const groupedIssueIds = this.groupedIssueIds; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters || !groupedIssueIds) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + + if (!groupBy && !subGroupBy) { + return groupedIssueIds as string[]; + } + + if (groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; + } + + if (groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[]; + } + + return undefined; + }; + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { try { this.loader = loadType; diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 8dafa2694c7..3876d675cff 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -35,7 +35,7 @@ export type TIssueHelperStore = { getGroupArray(value: boolean | number | string | string[] | null, isDate?: boolean): string[]; }; -const ISSUE_FILTER_DEFAULT_DATA: Record = { +export const ISSUE_FILTER_DEFAULT_DATA: Record = { project: "project_id", cycle: "cycle_id", module: "module_ids", diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts index 36e65e47684..12f5306cba1 100644 --- a/web/store/issue/issue_kanban_view.store.ts +++ b/web/store/issue/issue_kanban_view.store.ts @@ -1,6 +1,7 @@ import { action, computed, makeObservable, observable } from "mobx"; import { computedFn } from "mobx-utils"; import { IssueRootStore } from "./root.store"; +import { TIssueGroupByOptions } from "@plane/types"; // types export interface IIssueKanBanViewStore { @@ -8,9 +9,12 @@ export interface IIssueKanBanViewStore { groupByHeaderMinMax: string[]; subgroupByIssuesVisibility: string[]; }; - isDragging: boolean, + isDragging: boolean; // computed - getCanUserDragDrop: (group_by: string | null, sub_group_by: string | null) => boolean; + getCanUserDragDrop: ( + group_by: TIssueGroupByOptions | undefined, + sub_group_by: TIssueGroupByOptions | undefined + ) => boolean; canUserDragDropVertically: boolean; canUserDragDropHorizontally: boolean; // actions @@ -45,15 +49,17 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { setIsDragging = (isDragging: boolean) => { this.isDragging = isDragging; - } + }; - getCanUserDragDrop = computedFn((group_by: string | null, sub_group_by: string | null) => { - if (group_by && ["state", "priority"].includes(group_by)) { - if (!sub_group_by) return true; - if (sub_group_by && ["state", "priority"].includes(sub_group_by)) return true; + getCanUserDragDrop = computedFn( + (group_by: TIssueGroupByOptions | undefined, sub_group_by: TIssueGroupByOptions | undefined) => { + if (group_by && ["state", "priority"].includes(group_by)) { + if (!sub_group_by) return true; + if (sub_group_by && ["state", "priority"].includes(sub_group_by)) return true; + } + return false; } - return false; - }); + ); get canUserDragDropVertically() { return false; diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index e1af2c256b0..e5e44036bb0 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -21,6 +21,7 @@ export interface IModuleIssues { // computed groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; fetchIssues: ( workspaceSlug: string, projectId: string, @@ -146,6 +147,30 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { return issues; } + getIssueIds = (groupId?: string, subGroupId?: string) => { + const groupedIssueIds = this.groupedIssueIds; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters || !groupedIssueIds) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + + if (!groupBy && !subGroupBy) { + return groupedIssueIds as string[]; + } + + if (groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; + } + + if (groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[]; + } + + return undefined; + }; + fetchIssues = async ( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index 6da65634211..9a754b4f559 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -23,6 +23,7 @@ export interface IProfileIssues { viewFlags: ViewFlags; // actions setViewId: (viewId: "assigned" | "created" | "subscribed") => void; + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; fetchIssues: ( workspaceSlug: string, projectId: string | undefined, @@ -118,6 +119,30 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { return issues; } + getIssueIds = (groupId?: string, subGroupId?: string) => { + const groupedIssueIds = this.groupedIssueIds; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters || !groupedIssueIds) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + + if (!groupBy && !subGroupBy) { + return groupedIssueIds as string[]; + } + + if (groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; + } + + if (groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[]; + } + + return undefined; + }; + get viewFlags() { if (this.currentView === "subscribed") return { diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index 4fc365daabd..082534b90e9 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -17,6 +17,7 @@ export interface IProjectViewIssues { // computed groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions + getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; fetchIssues: ( workspaceSlug: string, projectId: string, @@ -114,6 +115,30 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI return issues; } + getIssueIds = (groupId?: string, subGroupId?: string) => { + const groupedIssueIds = this.groupedIssueIds; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters || !groupedIssueIds) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + + if (!groupBy && !subGroupBy) { + return groupedIssueIds as string[]; + } + + if (groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; + } + + if (groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[]; + } + + return undefined; + }; + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader", viewId: string) => { try { this.loader = loadType; diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 1de1bfc1ae0..123c943e88a 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -105,25 +105,25 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { const groupedIssueIds = this.groupedIssueIds; const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; - if (!displayFilters) return undefined; + if (!displayFilters || !groupedIssueIds) return undefined; const subGroupBy = displayFilters?.sub_group_by; const groupBy = displayFilters?.group_by; - if(!groupBy && !subGroupBy) { - return groupedIssueIds as string[] + if (!groupBy && !subGroupBy) { + return groupedIssueIds as string[]; } - if(groupBy && subGroupBy && groupId && subGroupId) { - return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[] + if (groupBy && subGroupBy && groupId && subGroupId) { + return (groupedIssueIds as TSubGroupedIssues)?.[subGroupId]?.[groupId] as string[]; } - if(groupBy && groupId) { - return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[] + if (groupBy && groupId) { + return (groupedIssueIds as TGroupedIssues)?.[groupId] as string[]; } return undefined; - } + }; fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { try { From 482112617181907fb3dcfe054caed2293490c167 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Fri, 12 Apr 2024 18:08:47 +0530 Subject: [PATCH 3/3] fix dnd for modules, cycles, draft and project views --- web/store/issue/cycle/issue.store.ts | 2 +- web/store/issue/draft/issue.store.ts | 6 +++--- web/store/issue/module/issue.store.ts | 2 +- web/store/issue/project-views/issue.store.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 44586fa5459..abdd141419b 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -146,7 +146,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { getIssueIds = (groupId?: string, subGroupId?: string) => { const groupedIssueIds = this.groupedIssueIds; - const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + const displayFilters = this.rootIssueStore?.cycleIssuesFilter?.issueFilters?.displayFilters; if (!displayFilters || !groupedIssueIds) return undefined; const subGroupBy = displayFilters?.sub_group_by; diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index 43a3121d0ff..c7d94e85c23 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -101,7 +101,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { getIssueIds = (groupId?: string, subGroupId?: string) => { const groupedIssueIds = this.groupedIssueIds; - const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + const displayFilters = this.rootIssueStore?.draftIssuesFilter?.issueFilters?.displayFilters; if (!displayFilters || !groupedIssueIds) return undefined; const subGroupBy = displayFilters?.sub_group_by; @@ -166,8 +166,6 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { - await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data); - this.rootStore.issues.updateIssue(issueId, data); if (data.hasOwnProperty("is_draft") && data?.is_draft === false) { @@ -178,6 +176,8 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { }); }); } + + await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data); } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation"); throw error; diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index e5e44036bb0..6e44e95a2fd 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -150,7 +150,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { getIssueIds = (groupId?: string, subGroupId?: string) => { const groupedIssueIds = this.groupedIssueIds; - const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + const displayFilters = this.rootIssueStore?.moduleIssuesFilter?.issueFilters?.displayFilters; if (!displayFilters || !groupedIssueIds) return undefined; const subGroupBy = displayFilters?.sub_group_by; diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index 082534b90e9..2166f254f1b 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -118,7 +118,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI getIssueIds = (groupId?: string, subGroupId?: string) => { const groupedIssueIds = this.groupedIssueIds; - const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + const displayFilters = this.rootIssueStore?.projectViewIssuesFilter?.issueFilters?.displayFilters; if (!displayFilters || !groupedIssueIds) return undefined; const subGroupBy = displayFilters?.sub_group_by;