mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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 "./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
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 "./loader";
|
||||||
export * from "./control-link";
|
export * from "./control-link";
|
||||||
export * from "./toast";
|
export * from "./toast";
|
||||||
|
export * from "./drop-indicator";
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
36
yarn.lock
36
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user