forked from github/plane
[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:
parent
384624a21b
commit
7a21855ab6
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
@ -27,3 +27,4 @@ export * from "./api_token";
|
||||
export * from "./instance";
|
||||
export * from "./app";
|
||||
export * from "./common";
|
||||
export * from "./pragmatic";
|
||||
|
25
packages/types/src/pragmatic.d.ts
vendored
Normal file
25
packages/types/src/pragmatic.d.ts
vendored
Normal 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;
|
||||
}
|
21
packages/ui/src/drop-indicator.tsx
Normal file
21
packages/ui/src/drop-indicator.tsx
Normal 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,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -12,3 +12,4 @@ export * from "./tooltip";
|
||||
export * from "./loader";
|
||||
export * from "./control-link";
|
||||
export * from "./toast";
|
||||
export * from "./drop-indicator";
|
@ -1,5 +1,7 @@
|
||||
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,7 +11,7 @@ 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, useIssueDetail, useIssues, useKanbanView, useUser } from "@/hooks/store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
// ui
|
||||
// types
|
||||
@ -17,7 +19,7 @@ import { IQuickActionProps } from "../list/list-view-types";
|
||||
//components
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { handleDragDrop } from "./utils";
|
||||
import { KanbanDropLocation, handleDragDrop, getSourceFromDropPayload } from "./utils";
|
||||
|
||||
export type KanbanStoreType =
|
||||
| EIssuesStoreType.PROJECT
|
||||
@ -36,12 +38,6 @@ export interface IBaseKanBanLayout {
|
||||
isCompletedCycle?: boolean;
|
||||
}
|
||||
|
||||
type KanbanDragState = {
|
||||
draggedIssueId?: string | null;
|
||||
source?: DraggableLocation | null;
|
||||
destination?: DraggableLocation | null;
|
||||
};
|
||||
|
||||
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
|
||||
const {
|
||||
QuickActions,
|
||||
@ -61,16 +57,24 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
} = useUser();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { issueMap, issuesFilter, issues } = useIssues(storeType);
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
|
||||
useIssuesActions(storeType);
|
||||
|
||||
const deleteAreaRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isDragOverDelete, setIsDragOverDelete] = useState(false);
|
||||
|
||||
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;
|
||||
|
||||
@ -81,8 +85,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// states
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
const [dragState, setDragState] = useState<KanbanDragState>({});
|
||||
const [draggedIssueId, setDraggedIssueId] = useState<string | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
@ -97,57 +100,72 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
|
||||
);
|
||||
|
||||
const onDragStart = (dragStart: DragStart) => {
|
||||
setDragState({
|
||||
draggedIssueId: dragStart.draggableId.split("__")[0],
|
||||
});
|
||||
setIsDragStarted(true);
|
||||
};
|
||||
// Enable Auto Scroll for Main Kanban
|
||||
useEffect(() => {
|
||||
const element = scrollableContainerRef.current;
|
||||
|
||||
const onDragEnd = async (result: DropResult) => {
|
||||
setIsDragStarted(false);
|
||||
if (!element) return;
|
||||
|
||||
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 (
|
||||
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") {
|
||||
setDragState({
|
||||
...dragState,
|
||||
source: result.source,
|
||||
destination: result.destination,
|
||||
});
|
||||
setDeleteIssueModal(true);
|
||||
} else {
|
||||
await handleDragDrop(
|
||||
result.source,
|
||||
result.destination,
|
||||
workspaceSlug?.toString(),
|
||||
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",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
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(
|
||||
@ -168,26 +186,16 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
);
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
if (!handleDragDrop || !dragState.draggedIssueId) return;
|
||||
await handleDragDrop(
|
||||
dragState.source,
|
||||
dragState.destination,
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString(),
|
||||
sub_group_by,
|
||||
group_by,
|
||||
issueMap,
|
||||
issueIds,
|
||||
updateIssue,
|
||||
removeIssue
|
||||
).finally(() => {
|
||||
const draggedIssue = issueMap[dragState.draggedIssueId!];
|
||||
removeIssue(draggedIssue.project_id, draggedIssue.id);
|
||||
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,
|
||||
});
|
||||
});
|
||||
@ -209,7 +217,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
return (
|
||||
<>
|
||||
<DeleteIssueModal
|
||||
dataId={dragState.draggedIssueId}
|
||||
dataId={draggedIssueId}
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDeleteIssue}
|
||||
@ -222,58 +230,51 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
)}
|
||||
|
||||
<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}
|
||||
>
|
||||
<div className="relative h-max w-max min-w-full bg-custom-background-90 px-2">
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||
{/* drag and delete component */}
|
||||
<div className="relative h-full w-max min-w-full bg-custom-background-90 px-2">
|
||||
{/* 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
|
||||
className={`fixed left-1/2 -translate-x-1/2 ${
|
||||
isDragStarted ? "z-40" : ""
|
||||
} top-3 mx-3 flex w-72 items-center justify-center`}
|
||||
className={`${
|
||||
isDragging ? `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 ${
|
||||
isDragOverDelete ? "bg-red-500 opacity-70 blur-2xl" : ""
|
||||
} transition duration-300`}
|
||||
>
|
||||
<Droppable droppableId="issue-trash-box" isDropDisabled={!isDragStarted}>
|
||||
{(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>
|
||||
Drop here to delete the issue.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-max w-max">
|
||||
<KanBanView
|
||||
issuesMap={issueMap}
|
||||
issueIds={issueIds}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
kanbanFilters={kanbanFilters}
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
|
||||
quickAddCallback={issues?.quickAddIssue}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||
canEditProperties={canEditProperties}
|
||||
storeType={storeType}
|
||||
addIssuesToView={addIssuesToView}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
<div className="h-full w-max">
|
||||
<KanBanView
|
||||
issuesMap={issueMap}
|
||||
issueIds={issueIds}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
kanbanFilters={kanbanFilters}
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
|
||||
quickAddCallback={issues?.quickAddIssue}
|
||||
viewId={viewId}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||
canEditProperties={canEditProperties}
|
||||
storeType={storeType}
|
||||
addIssuesToView={addIssuesToView}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
handleOnDrop={handleOnDrop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,12 +1,13 @@
|
||||
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 { 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, 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 +23,10 @@ interface IssueBlockProps {
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
isDragDisabled: boolean;
|
||||
draggableId: string;
|
||||
index: number;
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
quickActions: (issue: TIssue) => React.ReactNode;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
|
||||
}
|
||||
|
||||
@ -97,16 +96,14 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
||||
issuesMap,
|
||||
displayProperties,
|
||||
isDragDisabled,
|
||||
draggableId,
|
||||
index,
|
||||
updateIssue,
|
||||
quickActions,
|
||||
canEditProperties,
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
issueIds,
|
||||
} = props;
|
||||
|
||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
router: { workspaceSlug },
|
||||
} = useApplication();
|
||||
@ -122,63 +119,98 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
||||
|
||||
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;
|
||||
|
||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={draggableId}
|
||||
draggableId={draggableId}
|
||||
index={index}
|
||||
isDragDisabled={!canEditIssueProperties || isDragDisabled}
|
||||
>
|
||||
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
||||
<div
|
||||
className="group/kanban-block relative p-1.5 hover:cursor-default"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
<>
|
||||
<DropIndicator isVisible={!isCurrentBlockDragging && isDraggingOverBlock} />
|
||||
<div
|
||||
// make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
|
||||
className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
|
||||
onDragStart={() => !isDragDisabled && setIsCurrentBlockDragging(true)}
|
||||
>
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${
|
||||
issue.id
|
||||
}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
disabled={!!issue?.tempId}
|
||||
>
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${
|
||||
issue.id
|
||||
}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
disabled={!!issue?.tempId}
|
||||
<div
|
||||
className={cn(
|
||||
"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",
|
||||
{ "hover:cursor-grab": !isDragDisabled },
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id },
|
||||
{ "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
|
||||
)}
|
||||
ref={cardRef}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"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",
|
||||
{ "hover:cursor-pointer": !isDragDisabled },
|
||||
{ "border-custom-primary-100": snapshot.isDragging },
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
||||
)}
|
||||
<RenderIfVisible
|
||||
classNames="space-y-2 px-3 py-2"
|
||||
root={scrollableContainerRef}
|
||||
defaultHeight="100px"
|
||||
horizontalOffset={50}
|
||||
changingReference={issueIds}
|
||||
>
|
||||
<RenderIfVisible
|
||||
classNames="space-y-2 px-3 py-2"
|
||||
root={scrollableContainerRef}
|
||||
defaultHeight="100px"
|
||||
horizontalOffset={50}
|
||||
alwaysRender={snapshot.isDragging}
|
||||
pauseHeightUpdateWhileRendering={isDragStarted}
|
||||
changingReference={issueIds}
|
||||
>
|
||||
<KanbanIssueDetailsBlock
|
||||
issue={issue}
|
||||
displayProperties={displayProperties}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
isReadOnly={!canEditIssueProperties}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
</div>
|
||||
</ControlLink>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<KanbanIssueDetailsBlock
|
||||
issue={issue}
|
||||
displayProperties={displayProperties}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
isReadOnly={!canEditIssueProperties}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
</div>
|
||||
</ControlLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -16,7 +16,6 @@ interface IssueBlocksListProps {
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
}
|
||||
|
||||
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
||||
@ -32,14 +31,13 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (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<IssueBlocksListProps> = (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
|
||||
/>
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
TSubGroupedIssues,
|
||||
TUnGroupedIssues,
|
||||
TIssueKanbanFilters,
|
||||
TIssueGroupByOptions,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
// hooks
|
||||
@ -30,13 +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 { 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<TIssue>) => Promise<void>) | undefined;
|
||||
@ -56,7 +58,7 @@ export interface IGroupByKanBan {
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
showEmptyGroup?: boolean;
|
||||
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||
}
|
||||
@ -82,7 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
addIssuesToView,
|
||||
canEditProperties,
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
handleOnDrop,
|
||||
showEmptyGroup = true,
|
||||
subGroupIssueHeaderCount,
|
||||
} = props;
|
||||
@ -188,7 +190,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
canEditProperties={canEditProperties}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
handleOnDrop={handleOnDrop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -202,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<TIssue>) => Promise<void>) | undefined;
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
@ -223,7 +225,7 @@ export interface IKanBan {
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||
}
|
||||
|
||||
@ -247,7 +249,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
addIssuesToView,
|
||||
canEditProperties,
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
handleOnDrop,
|
||||
showEmptyGroup,
|
||||
subGroupIssueHeaderCount,
|
||||
} = props;
|
||||
@ -275,7 +277,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
addIssuesToView={addIssuesToView}
|
||||
canEditProperties={canEditProperties}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
handleOnDrop={handleOnDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
@ -1,6 +1,8 @@
|
||||
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,
|
||||
@ -8,10 +10,14 @@ import {
|
||||
IIssueMap,
|
||||
TSubGroupedIssues,
|
||||
TUnGroupedIssues,
|
||||
TIssueGroupByOptions,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
//components
|
||||
//types
|
||||
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
|
||||
interface IKanbanGroup {
|
||||
@ -20,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<TIssue>) => Promise<void>) | undefined;
|
||||
@ -38,7 +44,7 @@ interface IKanbanGroup {
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
groupByVisibilityToggle?: boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
}
|
||||
|
||||
export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
@ -60,14 +66,53 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
handleOnDrop,
|
||||
} = props;
|
||||
// hooks
|
||||
const projectState = useProjectState();
|
||||
|
||||
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
|
||||
|
||||
const columnRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Enable Kanban Columns as Drop Targets
|
||||
useEffect(() => {
|
||||
const element = columnRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element,
|
||||
getData: () => ({ groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN" }),
|
||||
onDragEnter: () => {
|
||||
setIsDraggingOverColumn(true);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setIsDraggingOverColumn(false);
|
||||
},
|
||||
onDragStart: () => {
|
||||
setIsDraggingOverColumn(true);
|
||||
},
|
||||
onDrop: (payload) => {
|
||||
setIsDraggingOverColumn(false);
|
||||
const source = getSourceFromDropPayload(payload);
|
||||
const destination = getDestinationFromDropPayload(payload);
|
||||
|
||||
if (!source || !destination) return;
|
||||
|
||||
handleOnDrop(source, destination);
|
||||
},
|
||||
}),
|
||||
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
|
||||
) => {
|
||||
@ -118,48 +163,43 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-full transition-all`}>
|
||||
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
|
||||
{(provided: any, snapshot: any) => (
|
||||
<div
|
||||
className={`relative h-full transition-all ${snapshot.isDraggingOver ? `bg-custom-background-80` : ``}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<KanbanIssueBlocksList
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={groupId}
|
||||
issuesMap={issuesMap}
|
||||
peekIssueId={peekIssueId}
|
||||
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
|
||||
displayProperties={displayProperties}
|
||||
isDragDisabled={isDragDisabled}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
<div
|
||||
id={`${groupId}__${sub_group_id}`}
|
||||
className={cn(
|
||||
"relative h-full transition-all",
|
||||
{ "bg-custom-background-80": isDraggingOverColumn },
|
||||
{ "vertical-scrollbar scrollbar-md": !sub_group_by }
|
||||
)}
|
||||
ref={columnRef}
|
||||
>
|
||||
<KanbanIssueBlocksList
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={groupId}
|
||||
issuesMap={issuesMap}
|
||||
peekIssueId={peekIssueId}
|
||||
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
|
||||
displayProperties={displayProperties}
|
||||
isDragDisabled={isDragDisabled}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
/>
|
||||
|
||||
{provided.placeholder}
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||
<KanBanQuickAddIssueForm
|
||||
formKey="name"
|
||||
groupId={groupId}
|
||||
subGroupId={sub_group_id}
|
||||
prePopulatedData={{
|
||||
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||
<KanBanQuickAddIssueForm
|
||||
formKey="name"
|
||||
groupId={groupId}
|
||||
subGroupId={sub_group_id}
|
||||
prePopulatedData={{
|
||||
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
TSubGroupedIssues,
|
||||
TUnGroupedIssues,
|
||||
TIssueKanbanFilters,
|
||||
TIssueGroupByOptions,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
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 { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
|
||||
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;
|
||||
@ -107,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;
|
||||
isDragStarted?: boolean;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
disableIssueCreation?: boolean;
|
||||
storeType: KanbanStoreType;
|
||||
enableQuickIssueCreate: boolean;
|
||||
@ -142,7 +144,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
handleOnDrop,
|
||||
} = props;
|
||||
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
@ -213,7 +215,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
handleOnDrop={handleOnDrop}
|
||||
subGroupIssueHeaderCount={(groupByListId: string) =>
|
||||
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
||||
}
|
||||
@ -231,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<TIssue>) => Promise<void>) | undefined;
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||
showEmptyGroup: boolean;
|
||||
isDragStarted?: boolean;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
disableIssueCreation?: boolean;
|
||||
storeType: KanbanStoreType;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
@ -267,7 +269,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
kanbanFilters,
|
||||
handleKanbanFilters,
|
||||
showEmptyGroup,
|
||||
isDragStarted,
|
||||
handleOnDrop,
|
||||
disableIssueCreation,
|
||||
enableQuickIssueCreate,
|
||||
canEditProperties,
|
||||
@ -337,7 +339,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
isDragStarted={isDragStarted}
|
||||
handleOnDrop={handleOnDrop}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
addIssuesToView={addIssuesToView}
|
||||
|
@ -1,29 +1,131 @@
|
||||
import { DraggableLocation } from "@hello-pangea/dnd";
|
||||
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, 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";
|
||||
|
||||
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;
|
||||
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];
|
||||
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[destinationIndex - 1];
|
||||
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 {
|
||||
@ -37,116 +139,71 @@ const handleSortOrder = (destinationIssues: string[], destinationIndex: number,
|
||||
};
|
||||
|
||||
export const handleDragDrop = async (
|
||||
source: DraggableLocation | null | undefined,
|
||||
destination: DraggableLocation | 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,
|
||||
source: KanbanDropLocation,
|
||||
destination: KanbanDropLocation,
|
||||
getIssueById: (issueId: string) => TIssue | undefined,
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
|
||||
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 destinationDroppableId = destination?.droppableId;
|
||||
const sourceIssue = getIssueById(source.id);
|
||||
|
||||
const sourceColumnId = (sourceDroppableId && sourceDroppableId.split("__")) || null;
|
||||
const destinationColumnId = (destinationDroppableId && destinationDroppableId.split("__")) || null;
|
||||
if (!sourceIssues || !destinationIssues || !sourceIssue) return;
|
||||
|
||||
if (!sourceColumnId || !destinationColumnId || !sourceDroppableId || !destinationDroppableId) return;
|
||||
updatedIssue = {
|
||||
id: sourceIssue.id,
|
||||
project_id: sourceIssue.project_id,
|
||||
};
|
||||
|
||||
const sourceGroupByColumnId = sourceColumnId[0] || null;
|
||||
const destinationGroupByColumnId = destinationColumnId[0] || null;
|
||||
// for both horizontal and vertical dnd
|
||||
updatedIssue = {
|
||||
...updatedIssue,
|
||||
...handleSortOrder(destinationIssues, destination.id, getIssueById),
|
||||
};
|
||||
|
||||
const sourceSubGroupByColumnId = sourceColumnId[1] || null;
|
||||
const destinationSubGroupByColumnId = destinationColumnId[1] || null;
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
||||
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
|
||||
let groupValue = sourceIssue[groupKey];
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(groupValue)) {
|
||||
pull(groupValue, source.groupId);
|
||||
groupValue.push(destination.groupId);
|
||||
} else {
|
||||
// for horizontal dnd
|
||||
if (sourceColumnId != destinationColumnId) {
|
||||
if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId };
|
||||
if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId };
|
||||
}
|
||||
groupValue = destination.groupId;
|
||||
}
|
||||
|
||||
if (updatedIssue && updatedIssue?.id) {
|
||||
return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue));
|
||||
updatedIssue = { ...updatedIssue, [groupKey]: groupValue };
|
||||
}
|
||||
|
||||
if (subGroupBy && source.subGroupId && destination.subGroupId && source.subGroupId !== destination.subGroupId) {
|
||||
const subGroupKey = ISSUE_FILTER_DEFAULT_DATA[subGroupBy];
|
||||
let subGroupValue = sourceIssue[subGroupKey];
|
||||
|
||||
if (Array.isArray(subGroupValue)) {
|
||||
pull(subGroupValue, source.subGroupId);
|
||||
subGroupValue.push(destination.subGroupId);
|
||||
} else {
|
||||
subGroupValue = destination.subGroupId;
|
||||
}
|
||||
|
||||
updatedIssue = { ...updatedIssue, [subGroupKey]: subGroupValue };
|
||||
}
|
||||
|
||||
if (updatedIssue) {
|
||||
return (
|
||||
updateIssue &&
|
||||
(await updateIssue(sourceIssue.project_id, sourceIssue.id, {
|
||||
...updatedIssue,
|
||||
id: sourceIssue.id,
|
||||
project_id: sourceIssue.project_id,
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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"
|
||||
|
@ -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.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 (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -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<TIssue[]>;
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
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;
|
||||
}
|
||||
|
||||
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") => {
|
||||
try {
|
||||
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>) => {
|
||||
try {
|
||||
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
|
||||
|
||||
this.rootStore.issues.updateIssue(issueId, data);
|
||||
|
||||
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) {
|
||||
this.fetchIssues(workspaceSlug, projectId, "mutation");
|
||||
throw error;
|
||||
|
@ -35,7 +35,7 @@ export type TIssueHelperStore = {
|
||||
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",
|
||||
cycle: "cycle_id",
|
||||
module: "module_ids",
|
||||
|
@ -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,12 +9,17 @@ export interface IIssueKanBanViewStore {
|
||||
groupByHeaderMinMax: string[];
|
||||
subgroupByIssuesVisibility: string[];
|
||||
};
|
||||
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
|
||||
handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void;
|
||||
setIsDragging: (isDragging: boolean) => void;
|
||||
}
|
||||
|
||||
export class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
||||
@ -21,30 +27,39 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
setIsDragging = (isDragging: boolean) => {
|
||||
this.isDragging = isDragging;
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -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.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 (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -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 {
|
||||
|
@ -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.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) => {
|
||||
try {
|
||||
this.loader = loadType;
|
||||
|
@ -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<TIssue[]>;
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
@ -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 || !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;
|
||||
|
36
yarn.lock
36
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"
|
||||
@ -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"
|
||||
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"
|
||||
|
Loading…
Reference in New Issue
Block a user