[WEB-1004] feat: Pragmatic dnd implementation for Kanban (#4189)

* Pragmatic drag and drop implmentation of Kanban

* refactor pragmatic dnd implementation and fix bugs

* fix dnd for modules, cycles, draft and project views
This commit is contained in:
rahulramesha 2024-04-15 17:02:53 +05:30 committed by GitHub
parent 384624a21b
commit 7a21855ab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 756 additions and 377 deletions

View File

@ -27,3 +27,4 @@ export * from "./api_token";
export * from "./instance"; export * from "./instance";
export * from "./app"; export * from "./app";
export * from "./common"; export * from "./common";
export * from "./pragmatic";

25
packages/types/src/pragmatic.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
export type TDropTarget = {
element: Element;
data: Record<string | symbol, unknown>;
};
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;
}

View File

@ -0,0 +1,21 @@
import React from "react";
import { cn } from "../helpers";
type Props = {
isVisible: boolean;
};
export const DropIndicator = (props: Props) => {
return (
<div
className={cn(
`block relative h-[2px] w-full
before:left-0 before:relative before:block before:top-[-2px] before:h-[6px] before:w-[6px] before:rounded
after:left-[calc(100%-6px)] after:relative after:block after:top-[-8px] after:h-[6px] after:w-[6px] after:rounded`,
{
"bg-custom-primary-100 before:bg-custom-primary-100 after:bg-custom-primary-100": props.isVisible,
}
)}
/>
);
};

View File

@ -12,3 +12,4 @@ export * from "./tooltip";
export * from "./loader"; export * from "./loader";
export * from "./control-link"; export * from "./control-link";
export * from "./toast"; export * from "./toast";
export * from "./drop-indicator";

View File

@ -1,5 +1,7 @@
import { FC, useCallback, useRef, useState } from "react"; import { FC, useCallback, useEffect, useRef, useState } from "react";
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd"; 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 { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
@ -9,7 +11,7 @@ import { DeleteIssueModal } from "@/components/issues";
import { ISSUE_DELETED } from "@/constants/event-tracker"; import { ISSUE_DELETED } from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project"; import { EUserProjectRoles } from "@/constants/project";
import { useEventTracker, useIssues, useUser } from "@/hooks/store"; import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUser } from "@/hooks/store";
import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useIssuesActions } from "@/hooks/use-issues-actions";
// ui // ui
// types // types
@ -17,7 +19,7 @@ import { IQuickActionProps } from "../list/list-view-types";
//components //components
import { KanBan } from "./default"; import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { handleDragDrop } from "./utils"; import { KanbanDropLocation, handleDragDrop, getSourceFromDropPayload } from "./utils";
export type KanbanStoreType = export type KanbanStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -36,12 +38,6 @@ export interface IBaseKanBanLayout {
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
} }
type KanbanDragState = {
draggedIssueId?: string | null;
source?: DraggableLocation | null;
destination?: DraggableLocation | null;
};
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => { export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
const { const {
QuickActions, QuickActions,
@ -61,16 +57,24 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
} = useUser(); } = useUser();
const { captureIssueEvent } = useEventTracker(); 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 } = const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
useIssuesActions(storeType); useIssuesActions(storeType);
const deleteAreaRef = useRef<HTMLDivElement | null>(null);
const [isDragOverDelete, setIsDragOverDelete] = useState(false);
const { isDragging } = useKanbanView();
const issueIds = issues?.groupedIssueIds || []; const issueIds = issues?.groupedIssueIds || [];
const displayFilters = issuesFilter?.issueFilters?.displayFilters; const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const displayProperties = issuesFilter?.issueFilters?.displayProperties; const displayProperties = issuesFilter?.issueFilters?.displayProperties;
const sub_group_by: string | null = displayFilters?.sub_group_by || null; const sub_group_by = displayFilters?.sub_group_by;
const group_by: string | null = displayFilters?.group_by || null; const group_by = displayFilters?.group_by;
const userDisplayFilters = displayFilters || null; const userDisplayFilters = displayFilters || null;
@ -81,8 +85,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const scrollableContainerRef = useRef<HTMLDivElement | null>(null); const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
// states // states
const [isDragStarted, setIsDragStarted] = useState<boolean>(false); const [draggedIssueId, setDraggedIssueId] = useState<string | undefined>(undefined);
const [dragState, setDragState] = useState<KanbanDragState>({});
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@ -97,57 +100,72 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
); );
const onDragStart = (dragStart: DragStart) => { // Enable Auto Scroll for Main Kanban
setDragState({ useEffect(() => {
draggedIssueId: dragStart.draggableId.split("__")[0], const element = scrollableContainerRef.current;
});
setIsDragStarted(true);
};
const onDragEnd = async (result: DropResult) => { if (!element) return;
setIsDragStarted(false);
if (!result) return; return combine(
autoScrollForElements({
element,
})
);
}, [scrollableContainerRef?.current]);
// Make the Issue Delete Box a Drop Target
useEffect(() => {
const element = deleteAreaRef.current;
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 = getSourceFromDropPayload(payload);
if (!source) return;
setDraggedIssueId(source.id);
setDeleteIssueModal(true);
},
})
);
}, [deleteAreaRef?.current, setIsDragOverDelete, setDraggedIssueId, setDeleteIssueModal]);
const handleOnDrop = async (source: KanbanDropLocation, destination: KanbanDropLocation) => {
if ( if (
result.destination && source.columnId &&
result.source && destination.columnId &&
result.source.droppableId && destination.columnId === source.columnId &&
result.destination.droppableId && destination.id === source.id
result.destination.droppableId === result.source.droppableId &&
result.destination.index === result.source.index
) )
return; return;
if (handleDragDrop) { await handleDragDrop(
if (result.destination?.droppableId && result.destination?.droppableId.split("__")[0] === "issue-trash-box") { source,
setDragState({ destination,
...dragState, getIssueById,
source: result.source, issues.getIssueIds,
destination: result.destination, updateIssue,
}); group_by,
setDeleteIssueModal(true); sub_group_by
} else { ).catch((err) => {
await handleDragDrop( setToast({
result.source, title: "Error",
result.destination, type: TOAST_TYPE.ERROR,
workspaceSlug?.toString(), message: err?.detail ?? "Failed to perform this action",
projectId?.toString(), });
sub_group_by, });
group_by,
issueMap,
issueIds,
updateIssue,
removeIssue
).catch((err) => {
setToast({
title: "Error",
type: TOAST_TYPE.ERROR,
message: err?.detail ?? "Failed to perform this action",
});
});
}
}
}; };
const renderQuickActions = useCallback( const renderQuickActions = useCallback(
@ -168,26 +186,16 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
); );
const handleDeleteIssue = async () => { const handleDeleteIssue = async () => {
if (!handleDragDrop || !dragState.draggedIssueId) return; const draggedIssue = getIssueById(draggedIssueId ?? "");
await handleDragDrop(
dragState.source, if (!draggedIssueId || !draggedIssue) return;
dragState.destination,
workspaceSlug?.toString(), await removeIssue(draggedIssue.project_id, draggedIssueId).finally(() => {
projectId?.toString(),
sub_group_by,
group_by,
issueMap,
issueIds,
updateIssue,
removeIssue
).finally(() => {
const draggedIssue = issueMap[dragState.draggedIssueId!];
removeIssue(draggedIssue.project_id, draggedIssue.id);
setDeleteIssueModal(false); setDeleteIssueModal(false);
setDragState({}); setDraggedIssueId(undefined);
captureIssueEvent({ captureIssueEvent({
eventName: ISSUE_DELETED, 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, path: router.asPath,
}); });
}); });
@ -209,7 +217,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
return ( return (
<> <>
<DeleteIssueModal <DeleteIssueModal
dataId={dragState.draggedIssueId} dataId={draggedIssueId}
isOpen={deleteIssueModal} isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)} handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDeleteIssue} onSubmit={handleDeleteIssue}
@ -222,58 +230,51 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
)} )}
<div <div
className="vertical-scrollbar horizontal-scrollbar scrollbar-lg relative flex h-full w-full overflow-auto bg-custom-background-90" className={`horizontal-scrollbar scrollbar-lg relative flex h-full w-full bg-custom-background-90 ${sub_group_by ? "vertical-scrollbar overflow-y-auto" : "overflow-x-auto overflow-y-hidden"}`}
ref={scrollableContainerRef} ref={scrollableContainerRef}
> >
<div className="relative h-max w-max min-w-full bg-custom-background-90 px-2"> <div className="relative h-full w-max min-w-full bg-custom-background-90 px-2">
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}> {/* drag and delete component */}
{/* drag and delete component */} <div
className={`fixed left-1/2 -translate-x-1/2 ${
isDragging ? "z-40" : ""
} top-3 mx-3 flex w-72 items-center justify-center`}
ref={deleteAreaRef}
>
<div <div
className={`fixed left-1/2 -translate-x-1/2 ${ className={`${
isDragStarted ? "z-40" : "" isDragging ? `opacity-100` : `opacity-0`
} top-3 mx-3 flex w-72 items-center justify-center`} } flex w-full items-center justify-center rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
isDragOverDelete ? "bg-red-500 opacity-70 blur-2xl" : ""
} transition duration-300`}
> >
<Droppable droppableId="issue-trash-box" isDropDisabled={!isDragStarted}> Drop here to delete the issue.
{(provided, snapshot) => (
<div
className={`${
isDragStarted ? `opacity-100` : `opacity-0`
} flex w-full items-center justify-center rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 opacity-70 blur-2xl" : ""
} transition duration-300`}
ref={provided.innerRef}
{...provided.droppableProps}
>
Drop here to delete the issue.
</div>
)}
</Droppable>
</div> </div>
</div>
<div className="h-max w-max"> <div className="h-full w-max">
<KanBanView <KanBanView
issuesMap={issueMap} issuesMap={issueMap}
issueIds={issueIds} issueIds={issueIds}
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={renderQuickActions} quickActions={renderQuickActions}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
enableQuickIssueCreate={enableQuickAdd} enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true} showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
quickAddCallback={issues?.quickAddIssue} quickAddCallback={issues?.quickAddIssue}
viewId={viewId} viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
storeType={storeType} storeType={storeType}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} handleOnDrop={handleOnDrop}
/> />
</div> </div>
</DragDropContext>
</div> </div>
</div> </div>
</> </>

View File

@ -1,12 +1,13 @@
import { MutableRefObject, memo } from "react"; import { MutableRefObject, memo, useEffect, useRef, useState } from "react";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; 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 { observer } from "mobx-react-lite";
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
// hooks // hooks
import { ControlLink, Tooltip } from "@plane/ui"; import { ControlLink, DropIndicator, Tooltip } from "@plane/ui";
import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import RenderIfVisible from "@/components/core/render-if-visible-HOC";
import { cn } from "@/helpers/common.helper"; 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"; import { usePlatformOS } from "@/hooks/use-platform-os";
// components // components
import { IssueProperties } from "../properties/all-properties"; import { IssueProperties } from "../properties/all-properties";
@ -22,12 +23,10 @@ interface IssueBlockProps {
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean; isDragDisabled: boolean;
draggableId: string; draggableId: string;
index: number;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
} }
@ -97,16 +96,14 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
issuesMap, issuesMap,
displayProperties, displayProperties,
isDragDisabled, isDragDisabled,
draggableId,
index,
updateIssue, updateIssue,
quickActions, quickActions,
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isDragStarted,
issueIds, issueIds,
} = props; } = props;
const cardRef = useRef<HTMLDivElement | null>(null);
const { const {
router: { workspaceSlug }, router: { workspaceSlug },
} = useApplication(); } = useApplication();
@ -122,63 +119,98 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
const issue = issuesMap[issueId]; const issue = issuesMap[issueId];
const { setIsDragging: setIsKanbanDragging } = useKanbanView();
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,
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; if (!issue) return null;
const canEditIssueProperties = canEditProperties(issue.project_id); const canEditIssueProperties = canEditProperties(issue.project_id);
return ( return (
<Draggable <>
key={draggableId} <DropIndicator isVisible={!isCurrentBlockDragging && isDraggingOverBlock} />
draggableId={draggableId} <div
index={index} // make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
isDragDisabled={!canEditIssueProperties || isDragDisabled} className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
> onDragStart={() => !isDragDisabled && setIsCurrentBlockDragging(true)}
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( >
<div <ControlLink
className="group/kanban-block relative p-1.5 hover:cursor-default" id={`issue-${issue.id}`}
{...provided.draggableProps} href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${
{...provided.dragHandleProps} issue.id
ref={provided.innerRef} }`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
disabled={!!issue?.tempId}
> >
<ControlLink <div
id={`issue-${issue.id}`} className={cn(
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${ "rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
issue.id { "hover:cursor-grab": !isDragDisabled },
}`} { "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id },
target="_blank" { "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
onClick={() => handleIssuePeekOverview(issue)} )}
disabled={!!issue?.tempId} ref={cardRef}
> >
<div <RenderIfVisible
className={cn( classNames="space-y-2 px-3 py-2"
"rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400", root={scrollableContainerRef}
{ "hover:cursor-pointer": !isDragDisabled }, defaultHeight="100px"
{ "border-custom-primary-100": snapshot.isDragging }, horizontalOffset={50}
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id } changingReference={issueIds}
)}
> >
<RenderIfVisible <KanbanIssueDetailsBlock
classNames="space-y-2 px-3 py-2" issue={issue}
root={scrollableContainerRef} displayProperties={displayProperties}
defaultHeight="100px" updateIssue={updateIssue}
horizontalOffset={50} quickActions={quickActions}
alwaysRender={snapshot.isDragging} isReadOnly={!canEditIssueProperties}
pauseHeightUpdateWhileRendering={isDragStarted} />
changingReference={issueIds} </RenderIfVisible>
> </div>
<KanbanIssueDetailsBlock </ControlLink>
issue={issue} </div>
displayProperties={displayProperties} </>
updateIssue={updateIssue}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</RenderIfVisible>
</div>
</ControlLink>
</div>
)}
</Draggable>
); );
}); });

View File

@ -16,7 +16,6 @@ interface IssueBlocksListProps {
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
} }
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => { const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
@ -32,14 +31,13 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
quickActions, quickActions,
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isDragStarted,
} = props; } = props;
return ( return (
<> <>
{issueIds && issueIds.length > 0 ? ( {issueIds && issueIds.length > 0 ? (
<> <>
{issueIds.map((issueId, index) => { {issueIds.map((issueId) => {
if (!issueId) return null; if (!issueId) return null;
let draggableId = issueId; let draggableId = issueId;
@ -56,11 +54,9 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
draggableId={draggableId} draggableId={draggableId}
index={index}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
/> />
); );

View File

@ -10,6 +10,7 @@ import {
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues, TUnGroupedIssues,
TIssueKanbanFilters, TIssueKanbanFilters,
TIssueGroupByOptions,
} from "@plane/types"; } from "@plane/types";
// constants // constants
// hooks // hooks
@ -30,13 +31,14 @@ import { getGroupByColumns, isWorkspaceLevel } from "../utils";
import { KanbanStoreType } from "./base-kanban-root"; import { KanbanStoreType } from "./base-kanban-root";
import { HeaderGroupByCard } from "./headers/group-by-card"; import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group"; import { KanbanGroup } from "./kanban-group";
import { KanbanDropLocation } from "./utils";
export interface IGroupByKanBan { export interface IGroupByKanBan {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: TIssueGroupByOptions | undefined;
group_by: string | null; group_by: TIssueGroupByOptions | undefined;
sub_group_id: string; sub_group_id: string;
isDragDisabled: boolean; isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
@ -56,7 +58,7 @@ export interface IGroupByKanBan {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
showEmptyGroup?: boolean; showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number; subGroupIssueHeaderCount?: (listId: string) => number;
} }
@ -82,7 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
addIssuesToView, addIssuesToView,
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isDragStarted, handleOnDrop,
showEmptyGroup = true, showEmptyGroup = true,
subGroupIssueHeaderCount, subGroupIssueHeaderCount,
} = props; } = props;
@ -188,7 +190,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} handleOnDrop={handleOnDrop}
/> />
)} )}
</div> </div>
@ -202,8 +204,8 @@ export interface IKanBan {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: TIssueGroupByOptions | undefined;
group_by: string | null; group_by: TIssueGroupByOptions | undefined;
sub_group_id?: string; sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
@ -223,7 +225,7 @@ export interface IKanBan {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
subGroupIssueHeaderCount?: (listId: string) => number; subGroupIssueHeaderCount?: (listId: string) => number;
} }
@ -247,7 +249,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
addIssuesToView, addIssuesToView,
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isDragStarted, handleOnDrop,
showEmptyGroup, showEmptyGroup,
subGroupIssueHeaderCount, subGroupIssueHeaderCount,
} = props; } = props;
@ -275,7 +277,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} handleOnDrop={handleOnDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount} subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/> />

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// lucide icons // lucide icons
import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; import { Minimize2, Maximize2, Circle, Plus } from "lucide-react";
import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types"; import { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types";
// ui // ui
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
@ -16,8 +16,8 @@ import { useEventTracker } from "@/hooks/store";
import { KanbanStoreType } from "../base-kanban-root"; import { KanbanStoreType } from "../base-kanban-root";
interface IHeaderGroupByCard { interface IHeaderGroupByCard {
sub_group_by: string | null; sub_group_by: TIssueGroupByOptions | undefined;
group_by: string | null; group_by: TIssueGroupByOptions | undefined;
column_id: string; column_id: string;
icon?: React.ReactNode; icon?: React.ReactNode;
title: string; title: string;

View File

@ -1,6 +1,8 @@
import { MutableRefObject } from "react"; import { MutableRefObject, useEffect, useRef, useState } from "react";
import { Droppable } from "@hello-pangea/dnd"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
// hooks import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
//types
import { import {
TGroupedIssues, TGroupedIssues,
TIssue, TIssue,
@ -8,10 +10,14 @@ import {
IIssueMap, IIssueMap,
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues, TUnGroupedIssues,
TIssueGroupByOptions,
} from "@plane/types"; } from "@plane/types";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
//components //components
//types import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
interface IKanbanGroup { interface IKanbanGroup {
@ -20,8 +26,8 @@ interface IKanbanGroup {
peekIssueId?: string; peekIssueId?: string;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: TIssueGroupByOptions | undefined;
group_by: string | null; group_by: TIssueGroupByOptions | undefined;
sub_group_id: string; sub_group_id: string;
isDragDisabled: boolean; isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
@ -38,7 +44,7 @@ interface IKanbanGroup {
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle?: boolean; groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
} }
export const KanbanGroup = (props: IKanbanGroup) => { export const KanbanGroup = (props: IKanbanGroup) => {
@ -60,14 +66,53 @@ export const KanbanGroup = (props: IKanbanGroup) => {
quickAddCallback, quickAddCallback,
viewId, viewId,
scrollableContainerRef, scrollableContainerRef,
isDragStarted, handleOnDrop,
} = props; } = props;
// hooks // hooks
const projectState = useProjectState(); const projectState = useProjectState();
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
const columnRef = useRef<HTMLDivElement | null>(null);
// Enable Kanban Columns as Drop Targets
useEffect(() => {
const element = columnRef.current;
if (!element) return;
return combine(
dropTargetForElements({
element,
getData: () => ({ groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN" }),
onDragEnter: () => {
setIsDraggingOverColumn(true);
},
onDragLeave: () => {
setIsDraggingOverColumn(false);
},
onDragStart: () => {
setIsDraggingOverColumn(true);
},
onDrop: (payload) => {
setIsDraggingOverColumn(false);
const source = getSourceFromDropPayload(payload);
const destination = getDestinationFromDropPayload(payload);
if (!source || !destination) return;
handleOnDrop(source, destination);
},
}),
autoScrollForElements({
element,
})
);
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]);
const prePopulateQuickAddData = ( const prePopulateQuickAddData = (
groupByKey: string | null, groupByKey: string | undefined,
subGroupByKey: string | null, subGroupByKey: string | undefined | null,
groupValue: string, groupValue: string,
subGroupValue: string subGroupValue: string
) => { ) => {
@ -118,48 +163,43 @@ export const KanbanGroup = (props: IKanbanGroup) => {
}; };
return ( return (
<div className={`relative w-full h-full transition-all`}> <div
<Droppable droppableId={`${groupId}__${sub_group_id}`}> id={`${groupId}__${sub_group_id}`}
{(provided: any, snapshot: any) => ( className={cn(
<div "relative h-full transition-all",
className={`relative h-full transition-all ${snapshot.isDraggingOver ? `bg-custom-background-80` : ``}`} { "bg-custom-background-80": isDraggingOverColumn },
{...provided.droppableProps} { "vertical-scrollbar scrollbar-md": !sub_group_by }
ref={provided.innerRef} )}
> ref={columnRef}
<KanbanIssueBlocksList >
sub_group_id={sub_group_id} <KanbanIssueBlocksList
columnId={groupId} sub_group_id={sub_group_id}
issuesMap={issuesMap} columnId={groupId}
peekIssueId={peekIssueId} issuesMap={issuesMap}
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []} peekIssueId={peekIssueId}
displayProperties={displayProperties} issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
isDragDisabled={isDragDisabled} displayProperties={displayProperties}
updateIssue={updateIssue} isDragDisabled={isDragDisabled}
quickActions={quickActions} updateIssue={updateIssue}
canEditProperties={canEditProperties} quickActions={quickActions}
scrollableContainerRef={scrollableContainerRef} canEditProperties={canEditProperties}
isDragStarted={isDragStarted} scrollableContainerRef={scrollableContainerRef}
/> />
{provided.placeholder} {enableQuickIssueCreate && !disableIssueCreation && (
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
{enableQuickIssueCreate && !disableIssueCreation && ( <KanBanQuickAddIssueForm
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0"> formKey="name"
<KanBanQuickAddIssueForm groupId={groupId}
formKey="name" subGroupId={sub_group_id}
groupId={groupId} prePopulatedData={{
subGroupId={sub_group_id} ...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
prePopulatedData={{ }}
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)), quickAddCallback={quickAddCallback}
}} viewId={viewId}
quickAddCallback={quickAddCallback} />
viewId={viewId} </div>
/> )}
</div>
)}
</div>
)}
</Droppable>
</div> </div>
); );
}; };

View File

@ -10,6 +10,7 @@ import {
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues, TUnGroupedIssues,
TIssueKanbanFilters, TIssueKanbanFilters,
TIssueGroupByOptions,
} from "@plane/types"; } from "@plane/types";
// components // components
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
@ -18,13 +19,14 @@ import { KanbanStoreType } from "./base-kanban-root";
import { KanBan } from "./default"; import { KanBan } from "./default";
import { HeaderGroupByCard } from "./headers/group-by-card"; import { HeaderGroupByCard } from "./headers/group-by-card";
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
import { KanbanDropLocation } from "./utils";
// types // types
// constants // constants
interface ISubGroupSwimlaneHeader { interface ISubGroupSwimlaneHeader {
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
sub_group_by: string | null; sub_group_by: TIssueGroupByOptions | undefined;
group_by: string | null; group_by: TIssueGroupByOptions | undefined;
list: IGroupByColumn[]; list: IGroupByColumn[];
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
@ -107,7 +109,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
isDragStarted?: boolean; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
storeType: KanbanStoreType; storeType: KanbanStoreType;
enableQuickIssueCreate: boolean; enableQuickIssueCreate: boolean;
@ -142,7 +144,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
quickAddCallback, quickAddCallback,
viewId, viewId,
scrollableContainerRef, scrollableContainerRef,
isDragStarted, handleOnDrop,
} = props; } = props;
const calculateIssueCount = (column_id: string) => { const calculateIssueCount = (column_id: string) => {
@ -213,7 +215,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId} viewId={viewId}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} handleOnDrop={handleOnDrop}
subGroupIssueHeaderCount={(groupByListId: string) => subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
} }
@ -231,14 +233,14 @@ export interface IKanBanSwimLanes {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: TIssueGroupByOptions | undefined;
group_by: string | null; group_by: TIssueGroupByOptions | undefined;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean; showEmptyGroup: boolean;
isDragStarted?: boolean; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
storeType: KanbanStoreType; storeType: KanbanStoreType;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
@ -267,7 +269,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
kanbanFilters, kanbanFilters,
handleKanbanFilters, handleKanbanFilters,
showEmptyGroup, showEmptyGroup,
isDragStarted, handleOnDrop,
disableIssueCreation, disableIssueCreation,
enableQuickIssueCreate, enableQuickIssueCreate,
canEditProperties, canEditProperties,
@ -337,7 +339,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
isDragStarted={isDragStarted} handleOnDrop={handleOnDrop}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}

View File

@ -1,29 +1,131 @@
import { DraggableLocation } from "@hello-pangea/dnd"; import pull from "lodash/pull";
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssue } from "@plane/types"; import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { export type KanbanDropLocation = {
columnId: string;
groupId: string;
subGroupId?: string;
id: string | undefined;
};
/**
* get Kanban Source data from Pragmatic Payload
* @param payload
* @returns
*/
export const getSourceFromDropPayload = (payload: IPragmaticDropPayload): KanbanDropLocation | undefined => {
const { location, source: sourceIssue } = payload;
const sourceIssueData = sourceIssue.data;
let sourceColumData;
const sourceDropTargets = location?.initial?.dropTargets ?? [];
for (const dropTarget of sourceDropTargets) {
const dropTargetData = dropTarget?.data;
if (!dropTargetData) continue;
if (dropTargetData.type === "COLUMN") {
sourceColumData = dropTargetData;
}
}
if (sourceIssueData?.id === undefined || !sourceColumData?.groupId) return;
return {
groupId: sourceColumData.groupId as string,
subGroupId: sourceColumData.subGroupId as string,
columnId: sourceColumData.columnId as string,
id: sourceIssueData.id as string,
};
};
/**
* get Destination Source data from Pragmatic Payload
* @param payload
* @returns
*/
export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): KanbanDropLocation | undefined => {
const { location } = payload;
let destinationIssueData, destinationColumnData;
const destDropTargets = location?.current?.dropTargets ?? [];
for (const dropTarget of destDropTargets) {
const dropTargetData = dropTarget?.data;
if (!dropTargetData) continue;
if (dropTargetData.type === "COLUMN" || dropTargetData.type === "DELETE") {
destinationColumnData = dropTargetData;
}
if (dropTargetData.type === "ISSUE") {
destinationIssueData = dropTargetData;
}
}
if (!destinationColumnData?.groupId) return;
return {
groupId: destinationColumnData.groupId as string,
subGroupId: destinationColumnData.subGroupId as string,
columnId: destinationColumnData.columnId as string,
id: destinationIssueData?.id as string | undefined,
};
};
/**
* Returns Sort order of the issue block at the position of drop
* @param destinationIssues
* @param destinationIssueId
* @param getIssueById
* @returns
*/
const handleSortOrder = (
destinationIssues: string[],
destinationIssueId: string | undefined,
getIssueById: (issueId: string) => TIssue | undefined
) => {
const sortOrderDefaultValue = 65535; const sortOrderDefaultValue = 65535;
let currentIssueState = {}; let currentIssueState = {};
const destinationIndex = destinationIssueId
? destinationIssues.indexOf(destinationIssueId)
: destinationIssues.length;
if (destinationIssues && destinationIssues.length > 0) { if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) { if (destinationIndex === 0) {
const destinationIssueId = destinationIssues[destinationIndex]; const destinationIssueId = destinationIssues[0];
const destinationIssue = getIssueById(destinationIssueId);
if (!destinationIssue) return currentIssueState;
currentIssueState = { currentIssueState = {
...currentIssueState, ...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue, sort_order: destinationIssue.sort_order - sortOrderDefaultValue,
}; };
} else if (destinationIndex === destinationIssues.length) { } else if (destinationIndex === destinationIssues.length) {
const destinationIssueId = destinationIssues[destinationIndex - 1]; const destinationIssueId = destinationIssues[destinationIssues.length - 1];
const destinationIssue = getIssueById(destinationIssueId);
if (!destinationIssue) return currentIssueState;
currentIssueState = { currentIssueState = {
...currentIssueState, ...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue, sort_order: destinationIssue.sort_order + sortOrderDefaultValue,
}; };
} else { } else {
const destinationTopIssueId = destinationIssues[destinationIndex - 1]; const destinationTopIssueId = destinationIssues[destinationIndex - 1];
const destinationBottomIssueId = destinationIssues[destinationIndex]; const destinationBottomIssueId = destinationIssues[destinationIndex];
const destinationTopIssue = getIssueById(destinationTopIssueId);
const destinationBottomIssue = getIssueById(destinationBottomIssueId);
if (!destinationTopIssue || !destinationBottomIssue) return currentIssueState;
currentIssueState = { currentIssueState = {
...currentIssueState, ...currentIssueState,
sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2, sort_order: (destinationTopIssue.sort_order + destinationBottomIssue.sort_order) / 2,
}; };
} }
} else { } else {
@ -37,116 +139,71 @@ const handleSortOrder = (destinationIssues: string[], destinationIndex: number,
}; };
export const handleDragDrop = async ( export const handleDragDrop = async (
source: DraggableLocation | null | undefined, source: KanbanDropLocation,
destination: DraggableLocation | null | undefined, destination: KanbanDropLocation,
workspaceSlug: string | undefined, getIssueById: (issueId: string) => TIssue | undefined,
projectId: string | undefined, // projectId for all views or user id in profile issues getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
subGroupBy: string | null,
groupBy: string | null,
issueMap: IIssueMap,
issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined,
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
removeIssue: (projectId: string, issueId: string) => Promise<void> | undefined groupBy: TIssueGroupByOptions | undefined,
subGroupBy: TIssueGroupByOptions | undefined
) => { ) => {
if (!issueMap || !issueWithIds || !source || !destination || !workspaceSlug || !projectId) return; if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
let updatedIssue: any = {}; let updatedIssue: Partial<TIssue> = {};
const sourceIssues = getIssueIds(source.groupId, source.subGroupId);
const destinationIssues = getIssueIds(destination.groupId, destination.subGroupId);
const sourceDroppableId = source?.droppableId; const sourceIssue = getIssueById(source.id);
const destinationDroppableId = destination?.droppableId;
const sourceColumnId = (sourceDroppableId && sourceDroppableId.split("__")) || null; if (!sourceIssues || !destinationIssues || !sourceIssue) return;
const destinationColumnId = (destinationDroppableId && destinationDroppableId.split("__")) || null;
if (!sourceColumnId || !destinationColumnId || !sourceDroppableId || !destinationDroppableId) return; updatedIssue = {
id: sourceIssue.id,
project_id: sourceIssue.project_id,
};
const sourceGroupByColumnId = sourceColumnId[0] || null; // for both horizontal and vertical dnd
const destinationGroupByColumnId = destinationColumnId[0] || null; updatedIssue = {
...updatedIssue,
...handleSortOrder(destinationIssues, destination.id, getIssueById),
};
const sourceSubGroupByColumnId = sourceColumnId[1] || null; if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
const destinationSubGroupByColumnId = destinationColumnId[1] || null; const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
let groupValue = sourceIssue[groupKey];
if ( if (Array.isArray(groupValue)) {
!workspaceSlug || pull(groupValue, source.groupId);
!projectId || groupValue.push(destination.groupId);
!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);
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
),
};
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,
};
}
} else { } else {
// for horizontal dnd groupValue = destination.groupId;
if (sourceColumnId != destinationColumnId) {
if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId };
if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId };
}
} }
if (updatedIssue && updatedIssue?.id) { updatedIssue = { ...updatedIssue, [groupKey]: groupValue };
return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); }
if (subGroupBy && source.subGroupId && destination.subGroupId && source.subGroupId !== destination.subGroupId) {
const subGroupKey = ISSUE_FILTER_DEFAULT_DATA[subGroupBy];
let subGroupValue = sourceIssue[subGroupKey];
if (Array.isArray(subGroupValue)) {
pull(subGroupValue, source.subGroupId);
subGroupValue.push(destination.subGroupId);
} else {
subGroupValue = destination.subGroupId;
} }
updatedIssue = { ...updatedIssue, [subGroupKey]: subGroupValue };
}
if (updatedIssue) {
return (
updateIssue &&
(await updateIssue(sourceIssue.project_id, sourceIssue.id, {
...updatedIssue,
id: sourceIssue.id,
project_id: sourceIssue.project_id,
}))
);
} }
}; };

View File

@ -12,6 +12,8 @@
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
}, },
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.0.3",
"@blueprintjs/popover2": "^1.13.3", "@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.3", "@headlessui/react": "^1.7.3",
"@hello-pangea/dnd": "^16.3.0", "@hello-pangea/dnd": "^16.3.0",
@ -61,6 +63,7 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.2.5",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/lodash": "^4.14.202", "@types/lodash": "^4.14.202",
@ -71,7 +74,6 @@
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"eslint-config-custom": "*", "eslint-config-custom": "*",
"prettier": "^2.8.7",
"tailwind-config-custom": "*", "tailwind-config-custom": "*",
"tsconfig": "*", "tsconfig": "*",
"typescript": "4.7.4" "typescript": "4.7.4"

View File

@ -23,6 +23,7 @@ export interface ICycleIssues {
// computed // computed
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
// actions // actions
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
fetchIssues: ( fetchIssues: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -142,6 +143,30 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
return issues; return issues;
} }
getIssueIds = (groupId?: string, subGroupId?: string) => {
const groupedIssueIds = this.groupedIssueIds;
const displayFilters = this.rootIssueStore?.cycleIssuesFilter?.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 ( fetchIssues = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,

View File

@ -20,6 +20,7 @@ export interface IDraftIssues {
// computed // computed
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
// actions // actions
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>; fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -97,6 +98,30 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
return issues; return issues;
} }
getIssueIds = (groupId?: string, subGroupId?: string) => {
const groupedIssueIds = this.groupedIssueIds;
const displayFilters = this.rootIssueStore?.draftIssuesFilter?.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") => { fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => {
try { try {
this.loader = loadType; this.loader = loadType;
@ -141,8 +166,6 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try { try {
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
this.rootStore.issues.updateIssue(issueId, data); this.rootStore.issues.updateIssue(issueId, data);
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) { if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
@ -153,6 +176,8 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
}); });
}); });
} }
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");
throw error; throw error;

View File

@ -35,7 +35,7 @@ export type TIssueHelperStore = {
getGroupArray(value: boolean | number | string | string[] | null, isDate?: boolean): string[]; getGroupArray(value: boolean | number | string | string[] | null, isDate?: boolean): string[];
}; };
const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = { export const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
project: "project_id", project: "project_id",
cycle: "cycle_id", cycle: "cycle_id",
module: "module_ids", module: "module_ids",

View File

@ -1,6 +1,7 @@
import { action, computed, makeObservable, observable } from "mobx"; import { action, computed, makeObservable, observable } from "mobx";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
import { IssueRootStore } from "./root.store"; import { IssueRootStore } from "./root.store";
import { TIssueGroupByOptions } from "@plane/types";
// types // types
export interface IIssueKanBanViewStore { export interface IIssueKanBanViewStore {
@ -8,12 +9,17 @@ export interface IIssueKanBanViewStore {
groupByHeaderMinMax: string[]; groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[]; subgroupByIssuesVisibility: string[];
}; };
isDragging: boolean;
// computed // 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; canUserDragDropVertically: boolean;
canUserDragDropHorizontally: boolean; canUserDragDropHorizontally: boolean;
// actions // actions
handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void; handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void;
setIsDragging: (isDragging: boolean) => void;
} }
export class IssueKanBanViewStore implements IIssueKanBanViewStore { export class IssueKanBanViewStore implements IIssueKanBanViewStore {
@ -21,30 +27,39 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
groupByHeaderMinMax: string[]; groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[]; subgroupByIssuesVisibility: string[];
} = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] }; } = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] };
isDragging = false;
// root store // root store
rootStore; rootStore;
constructor(_rootStore: IssueRootStore) { constructor(_rootStore: IssueRootStore) {
makeObservable(this, { makeObservable(this, {
kanBanToggle: observable, kanBanToggle: observable,
isDragging: observable.ref,
// computed // computed
canUserDragDropVertically: computed, canUserDragDropVertically: computed,
canUserDragDropHorizontally: computed, canUserDragDropHorizontally: computed,
// actions // actions
handleKanBanToggle: action, handleKanBanToggle: action,
setIsDragging: action.bound,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
} }
getCanUserDragDrop = computedFn((group_by: string | null, sub_group_by: string | null) => { setIsDragging = (isDragging: boolean) => {
if (group_by && ["state", "priority"].includes(group_by)) { this.isDragging = isDragging;
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() { get canUserDragDropVertically() {
return false; return false;

View File

@ -21,6 +21,7 @@ export interface IModuleIssues {
// computed // computed
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
// actions // actions
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
fetchIssues: ( fetchIssues: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -146,6 +147,30 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
return issues; return issues;
} }
getIssueIds = (groupId?: string, subGroupId?: string) => {
const groupedIssueIds = this.groupedIssueIds;
const displayFilters = this.rootIssueStore?.moduleIssuesFilter?.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 ( fetchIssues = async (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,

View File

@ -23,6 +23,7 @@ export interface IProfileIssues {
viewFlags: ViewFlags; viewFlags: ViewFlags;
// actions // actions
setViewId: (viewId: "assigned" | "created" | "subscribed") => void; setViewId: (viewId: "assigned" | "created" | "subscribed") => void;
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
fetchIssues: ( fetchIssues: (
workspaceSlug: string, workspaceSlug: string,
projectId: string | undefined, projectId: string | undefined,
@ -118,6 +119,30 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
return issues; 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() { get viewFlags() {
if (this.currentView === "subscribed") if (this.currentView === "subscribed")
return { return {

View File

@ -17,6 +17,7 @@ export interface IProjectViewIssues {
// computed // computed
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
// actions // actions
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
fetchIssues: ( fetchIssues: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -114,6 +115,30 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
return issues; return issues;
} }
getIssueIds = (groupId?: string, subGroupId?: string) => {
const groupedIssueIds = this.groupedIssueIds;
const displayFilters = this.rootIssueStore?.projectViewIssuesFilter?.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) => { fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader", viewId: string) => {
try { try {
this.loader = loadType; this.loader = loadType;

View File

@ -18,6 +18,7 @@ export interface IProjectIssues {
viewFlags: ViewFlags; viewFlags: ViewFlags;
// computed // computed
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined;
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined;
// action // action
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>; fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue[]>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
@ -100,6 +101,30 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
return issues; 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") => { fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => {
try { try {
this.loader = loadType; this.loader = loadType;

View File

@ -29,6 +29,23 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.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": "@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" version "7.23.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" 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" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== 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": "@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" version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" 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" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 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: bl@^4.0.3:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@ -6871,11 +6900,16 @@ prettier-plugin-tailwindcss@^0.5.4:
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.9.tgz#fdc2bd95a02b64702ebd2d6c7ddd300198de3cc6" resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.9.tgz#fdc2bd95a02b64702ebd2d6c7ddd300198de3cc6"
integrity sha512-9x3t1s2Cjbut2QiP+O0mDqV3gLXTe2CgRlQDgucopVkUdw26sQi53p/q4qvGxMLBDfk/dcTV57Aa/zYwz9l8Ew== integrity sha512-9x3t1s2Cjbut2QiP+O0mDqV3gLXTe2CgRlQDgucopVkUdw26sQi53p/q4qvGxMLBDfk/dcTV57Aa/zYwz9l8Ew==
prettier@^2.8.7, prettier@^2.8.8: prettier@^2.8.8:
version "2.8.8" version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== 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: prettier@latest:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848"