refactor: integrated global kanban view everywhere
This commit is contained in:
parent
58eda658c8
commit
85b7f39ed3
15 changed files with 374 additions and 658 deletions
306
apps/app/components/core/board-view/all-boards.tsx
Normal file
306
apps/app/components/core/board-view/all-boards.tsx
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// react-beautiful-dnd
|
||||||
|
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
import stateService from "services/state.service";
|
||||||
|
// hooks
|
||||||
|
import useIssueView from "hooks/use-issue-view";
|
||||||
|
// components
|
||||||
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
|
import { CommonSingleBoard } from "components/core/board-view/single-board";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
CycleIssueResponse,
|
||||||
|
IIssue,
|
||||||
|
IssueResponse,
|
||||||
|
IState,
|
||||||
|
ModuleIssueResponse,
|
||||||
|
UserAuth,
|
||||||
|
} from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type?: "issue" | "cycle" | "module";
|
||||||
|
issues: IIssue[];
|
||||||
|
openIssuesListModal?: () => void;
|
||||||
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
userAuth: UserAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllBoards: React.FC<Props> = ({
|
||||||
|
type = "issue",
|
||||||
|
issues,
|
||||||
|
openIssuesListModal,
|
||||||
|
handleDeleteIssue,
|
||||||
|
userAuth,
|
||||||
|
}) => {
|
||||||
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||||
|
const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false);
|
||||||
|
const [issueDeletionData, setIssueDeletionData] = useState<IIssue | undefined>();
|
||||||
|
const [preloadedData, setPreloadedData] = useState<
|
||||||
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
||||||
|
|
||||||
|
const { data: states, mutate: mutateState } = useSWR<IState[]>(
|
||||||
|
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||||
|
workspaceSlug
|
||||||
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnDragEnd = useCallback(
|
||||||
|
(result: DropResult) => {
|
||||||
|
if (!result.destination || !workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const { source, destination, type } = result;
|
||||||
|
|
||||||
|
if (type === "state") {
|
||||||
|
const newStates = Array.from(states ?? []);
|
||||||
|
const [reorderedState] = newStates.splice(source.index, 1);
|
||||||
|
newStates.splice(destination.index, 0, reorderedState);
|
||||||
|
const prevSequenceNumber = newStates[destination.index - 1]?.sequence;
|
||||||
|
const nextSequenceNumber = newStates[destination.index + 1]?.sequence;
|
||||||
|
|
||||||
|
const sequenceNumber =
|
||||||
|
prevSequenceNumber && nextSequenceNumber
|
||||||
|
? (prevSequenceNumber + nextSequenceNumber) / 2
|
||||||
|
: nextSequenceNumber
|
||||||
|
? nextSequenceNumber - 15000 / 2
|
||||||
|
: prevSequenceNumber
|
||||||
|
? prevSequenceNumber + 15000 / 2
|
||||||
|
: 15000;
|
||||||
|
|
||||||
|
newStates[destination.index].sequence = sequenceNumber;
|
||||||
|
|
||||||
|
mutateState(newStates, false);
|
||||||
|
|
||||||
|
stateService
|
||||||
|
.patchState(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
newStates[destination.index].id,
|
||||||
|
{
|
||||||
|
sequence: sequenceNumber,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const draggedItem = groupedByIssues[source.droppableId][source.index];
|
||||||
|
if (source.droppableId !== destination.droppableId) {
|
||||||
|
const sourceGroup = source.droppableId; // source group id
|
||||||
|
const destinationGroup = destination.droppableId; // destination group id
|
||||||
|
|
||||||
|
if (!sourceGroup || !destinationGroup) return;
|
||||||
|
|
||||||
|
if (selectedGroup === "priority") {
|
||||||
|
// update the removed item for mutation
|
||||||
|
draggedItem.priority = destinationGroup;
|
||||||
|
|
||||||
|
// patch request
|
||||||
|
issuesService.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||||
|
priority: destinationGroup,
|
||||||
|
});
|
||||||
|
} else if (selectedGroup === "state_detail.name") {
|
||||||
|
const destinationState = states?.find((s) => s.name === destinationGroup);
|
||||||
|
const destinationStateId = destinationState?.id;
|
||||||
|
|
||||||
|
// update the removed item for mutation
|
||||||
|
if (!destinationStateId || !destinationState) return;
|
||||||
|
draggedItem.state = destinationStateId;
|
||||||
|
draggedItem.state_detail = destinationState;
|
||||||
|
|
||||||
|
mutate<IssueResponse>(
|
||||||
|
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
const updatedIssues = prevData.results.map((issue) => {
|
||||||
|
if (issue.id === draggedItem.id)
|
||||||
|
return {
|
||||||
|
...draggedItem,
|
||||||
|
state_detail: destinationState,
|
||||||
|
state: destinationStateId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
results: updatedIssues,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cycleId)
|
||||||
|
mutate<CycleIssueResponse[]>(
|
||||||
|
CYCLE_ISSUES(cycleId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
const updatedIssues = prevData.map((issue) => {
|
||||||
|
if (issue.issue_detail.id === draggedItem.id) {
|
||||||
|
return {
|
||||||
|
...issue,
|
||||||
|
issue_detail: {
|
||||||
|
...draggedItem,
|
||||||
|
state_detail: destinationState,
|
||||||
|
state: destinationStateId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
return [...updatedIssues];
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (moduleId)
|
||||||
|
mutate<ModuleIssueResponse[]>(
|
||||||
|
MODULE_ISSUES(moduleId as string),
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
const updatedIssues = prevData.map((issue) => {
|
||||||
|
if (issue.issue_detail.id === draggedItem.id) {
|
||||||
|
return {
|
||||||
|
...issue,
|
||||||
|
issue_detail: {
|
||||||
|
...draggedItem,
|
||||||
|
state_detail: destinationState,
|
||||||
|
state: destinationStateId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
return [...updatedIssues];
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// patch request
|
||||||
|
issuesService
|
||||||
|
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
||||||
|
state: destinationStateId,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
|
mutate(MODULE_ISSUES(moduleId as string));
|
||||||
|
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
workspaceSlug,
|
||||||
|
cycleId,
|
||||||
|
moduleId,
|
||||||
|
mutateState,
|
||||||
|
groupedByIssues,
|
||||||
|
projectId,
|
||||||
|
selectedGroup,
|
||||||
|
states,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (issueView !== "kanban") return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteIssueModal
|
||||||
|
isOpen={isIssueDeletionOpen}
|
||||||
|
handleClose={() => setIsIssueDeletionOpen(false)}
|
||||||
|
data={issueDeletionData}
|
||||||
|
/>
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||||
|
handleClose={() => setCreateIssueModal(false)}
|
||||||
|
prePopulateData={{
|
||||||
|
...preloadedData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{groupedByIssues ? (
|
||||||
|
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
||||||
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
|
<div className="h-full w-full overflow-hidden">
|
||||||
|
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
className="h-full w-full"
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
>
|
||||||
|
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
||||||
|
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||||
|
const stateId =
|
||||||
|
selectedGroup === "state_detail.name"
|
||||||
|
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const bgColor =
|
||||||
|
selectedGroup === "state_detail.name"
|
||||||
|
? states?.find((s) => s.name === singleGroup)?.color
|
||||||
|
: "#000000";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable key={singleGroup} draggableId={singleGroup} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<CommonSingleBoard
|
||||||
|
type={type}
|
||||||
|
provided={provided}
|
||||||
|
snapshot={snapshot}
|
||||||
|
bgColor={bgColor}
|
||||||
|
groupTitle={singleGroup}
|
||||||
|
groupedByIssues={groupedByIssues}
|
||||||
|
selectedGroup={selectedGroup}
|
||||||
|
addIssueToState={() => {
|
||||||
|
setCreateIssueModal(true);
|
||||||
|
if (selectedGroup)
|
||||||
|
setPreloadedData({
|
||||||
|
state: stateId !== null ? stateId : undefined,
|
||||||
|
[selectedGroup]: singleGroup,
|
||||||
|
actionType: "createIssue",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
||||||
|
userAuth={userAuth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StrictModeDroppable>
|
||||||
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -14,6 +14,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, NestedKeyOf } from "types";
|
import { IIssue, NestedKeyOf } from "types";
|
||||||
type Props = {
|
type Props = {
|
||||||
|
provided: DraggableProvided;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
|
|
@ -24,7 +25,6 @@ type Props = {
|
||||||
createdBy: string | null;
|
createdBy: string | null;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
addIssueToState: () => void;
|
addIssueToState: () => void;
|
||||||
provided?: DraggableProvided;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardHeader: React.FC<Props> = ({
|
const BoardHeader: React.FC<Props> = ({
|
||||||
|
|
@ -44,18 +44,16 @@ const BoardHeader: React.FC<Props> = ({
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
||||||
{provided && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
{...provided.dragHandleProps}
|
||||||
{...provided.dragHandleProps}
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
!isCollapsed ? "" : "rotate-90"
|
||||||
!isCollapsed ? "" : "rotate-90"
|
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
|
||||||
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
|
>
|
||||||
>
|
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
|
||||||
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
|
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
|
||||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
// components
|
// components
|
||||||
import BoardHeader from "components/core/board-view/board-header";
|
import BoardHeader from "components/core/board-view/board-header";
|
||||||
import SingleIssue from "components/core/board-view/single-issue";
|
import SingleIssue from "components/core/board-view/single-issue";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
|
|
@ -22,8 +24,9 @@ import { IIssue, IWorkspaceMember, NestedKeyOf, UserAuth } from "types";
|
||||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
provided?: DraggableProvided;
|
type?: "issue" | "cycle" | "module";
|
||||||
snapshot?: DraggableStateSnapshot;
|
provided: DraggableProvided;
|
||||||
|
snapshot: DraggableStateSnapshot;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: {
|
groupedByIssues: {
|
||||||
|
|
@ -32,10 +35,12 @@ type Props = {
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
addIssueToState: () => void;
|
addIssueToState: () => void;
|
||||||
handleDeleteIssue?: Dispatch<SetStateAction<string | undefined>> | undefined;
|
handleDeleteIssue?: Dispatch<SetStateAction<string | undefined>> | undefined;
|
||||||
|
openIssuesListModal?: (() => void) | null;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CommonSingleBoard: React.FC<Props> = ({
|
export const CommonSingleBoard: React.FC<Props> = ({
|
||||||
|
type = "issue",
|
||||||
provided,
|
provided,
|
||||||
snapshot,
|
snapshot,
|
||||||
bgColor,
|
bgColor,
|
||||||
|
|
@ -44,6 +49,7 @@ export const CommonSingleBoard: React.FC<Props> = ({
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
addIssueToState,
|
addIssueToState,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
|
openIssuesListModal,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
// collapse/expand
|
// collapse/expand
|
||||||
|
|
@ -83,6 +89,7 @@ export const CommonSingleBoard: React.FC<Props> = ({
|
||||||
>
|
>
|
||||||
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
|
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
|
provided={provided}
|
||||||
addIssueToState={addIssueToState}
|
addIssueToState={addIssueToState}
|
||||||
bgColor={bgColor}
|
bgColor={bgColor}
|
||||||
createdBy={createdBy}
|
createdBy={createdBy}
|
||||||
|
|
@ -91,7 +98,6 @@ export const CommonSingleBoard: React.FC<Props> = ({
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
setIsCollapsed={setIsCollapsed}
|
setIsCollapsed={setIsCollapsed}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
provided={provided}
|
|
||||||
/>
|
/>
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
|
|
@ -99,8 +105,8 @@ export const CommonSingleBoard: React.FC<Props> = ({
|
||||||
className={`mt-3 h-full space-y-3 overflow-y-auto px-3 pb-3 ${
|
className={`mt-3 h-full space-y-3 overflow-y-auto px-3 pb-3 ${
|
||||||
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
||||||
} ${!isCollapsed ? "hidden" : "block"}`}
|
} ${!isCollapsed ? "hidden" : "block"}`}
|
||||||
{...provided.droppableProps}
|
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
|
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
|
||||||
const assignees = [
|
const assignees = [
|
||||||
|
|
@ -135,14 +141,35 @@ export const CommonSingleBoard: React.FC<Props> = ({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
{/* <button
|
{type === "issue" ? (
|
||||||
type="button"
|
<button
|
||||||
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
|
type="button"
|
||||||
onClick={addIssueToState}
|
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
|
||||||
>
|
onClick={addIssueToState}
|
||||||
<PlusIcon className="mr-1 h-3 w-3" />
|
>
|
||||||
Create
|
<PlusIcon className="mr-1 h-3 w-3" />
|
||||||
</button> */}
|
Create
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
Add issue
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
className="mt-1"
|
||||||
|
optionsPosition="left"
|
||||||
|
noBorder
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem>
|
||||||
|
{openIssuesListModal && (
|
||||||
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
|
Add an existing issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { DraggableStateSnapshot } from "react-beautiful-dnd";
|
import { DraggableStateSnapshot } from "react-beautiful-dnd";
|
||||||
// react-datepicker
|
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -51,7 +48,7 @@ type Props = {
|
||||||
typeId?: string;
|
typeId?: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
snapshot?: DraggableStateSnapshot;
|
snapshot: DraggableStateSnapshot;
|
||||||
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
|
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
|
||||||
people: IWorkspaceMember[] | undefined;
|
people: IWorkspaceMember[] | undefined;
|
||||||
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
|
@ -163,7 +160,7 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded border bg-white shadow-sm ${
|
className={`rounded border bg-white shadow-sm ${
|
||||||
snapshot && snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
|
snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="group/card relative select-none p-2">
|
<div className="group/card relative select-none p-2">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export * from "./not-authorized-view";
|
|
||||||
export * from "./bulk-delete-issues-modal";
|
export * from "./bulk-delete-issues-modal";
|
||||||
export * from "./existing-issues-list-modal";
|
export * from "./existing-issues-list-modal";
|
||||||
export * from "./image-upload-modal";
|
export * from "./image-upload-modal";
|
||||||
export * from "./view";
|
export * from "./issues-filter-view";
|
||||||
|
export * from "./not-authorized-view";
|
||||||
|
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
import React, { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
|
|
||||||
// react-beautiful-dnd
|
|
||||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
|
||||||
// hook
|
|
||||||
import useIssueView from "hooks/use-issue-view";
|
|
||||||
// services
|
|
||||||
import stateServices from "services/state.service";
|
|
||||||
import issuesServices from "services/issues.service";
|
|
||||||
// components
|
|
||||||
import { CommonSingleBoard } from "components/core/board-view/single-board";
|
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
// types
|
|
||||||
import type { IState, IIssue, IssueResponse, UserAuth } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { STATE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issues: IIssue[];
|
|
||||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
||||||
userAuth: UserAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssuesBoardView: React.FC<Props> = ({ issues, handleDeleteIssue, userAuth }) => {
|
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
|
||||||
const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false);
|
|
||||||
const [issueDeletionData, setIssueDeletionData] = useState<IIssue | undefined>();
|
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
|
||||||
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
|
||||||
|
|
||||||
const { data: states, mutate: mutateState } = useSWR<IState[]>(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug
|
|
||||||
? () => stateServices.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOnDragEnd = useCallback(
|
|
||||||
(result: DropResult) => {
|
|
||||||
if (!result.destination || !workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
const { source, destination, type } = result;
|
|
||||||
|
|
||||||
if (destination.droppableId === "trashBox") {
|
|
||||||
// setIssueDeletionData(draggedItem);
|
|
||||||
setIsIssueDeletionOpen(true);
|
|
||||||
} else {
|
|
||||||
if (type === "state") {
|
|
||||||
const newStates = Array.from(states ?? []);
|
|
||||||
const [reorderedState] = newStates.splice(source.index, 1);
|
|
||||||
newStates.splice(destination.index, 0, reorderedState);
|
|
||||||
const prevSequenceNumber = newStates[destination.index - 1]?.sequence;
|
|
||||||
const nextSequenceNumber = newStates[destination.index + 1]?.sequence;
|
|
||||||
|
|
||||||
const sequenceNumber =
|
|
||||||
prevSequenceNumber && nextSequenceNumber
|
|
||||||
? (prevSequenceNumber + nextSequenceNumber) / 2
|
|
||||||
: nextSequenceNumber
|
|
||||||
? nextSequenceNumber - 15000 / 2
|
|
||||||
: prevSequenceNumber
|
|
||||||
? prevSequenceNumber + 15000 / 2
|
|
||||||
: 15000;
|
|
||||||
|
|
||||||
newStates[destination.index].sequence = sequenceNumber;
|
|
||||||
|
|
||||||
mutateState(newStates, false);
|
|
||||||
|
|
||||||
stateServices
|
|
||||||
.patchState(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
newStates[destination.index].id,
|
|
||||||
{
|
|
||||||
sequence: sequenceNumber,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const draggedItem = groupedByIssues[source.droppableId][source.index];
|
|
||||||
if (source.droppableId !== destination.droppableId) {
|
|
||||||
const sourceGroup = source.droppableId; // source group id
|
|
||||||
const destinationGroup = destination.droppableId; // destination group id
|
|
||||||
|
|
||||||
if (!sourceGroup || !destinationGroup) return;
|
|
||||||
|
|
||||||
if (selectedGroup === "priority") {
|
|
||||||
// update the removed item for mutation
|
|
||||||
draggedItem.priority = destinationGroup;
|
|
||||||
|
|
||||||
// patch request
|
|
||||||
issuesServices.patchIssue(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
draggedItem.id,
|
|
||||||
{
|
|
||||||
priority: destinationGroup,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (selectedGroup === "state_detail.name") {
|
|
||||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
|
||||||
const destinationStateId = destinationState?.id;
|
|
||||||
|
|
||||||
// update the removed item for mutation
|
|
||||||
if (!destinationStateId || !destinationState) return;
|
|
||||||
draggedItem.state = destinationStateId;
|
|
||||||
draggedItem.state_detail = destinationState;
|
|
||||||
|
|
||||||
mutate<IssueResponse>(
|
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
|
|
||||||
const updatedIssues = prevData.results.map((issue) => {
|
|
||||||
if (issue.id === draggedItem.id)
|
|
||||||
return {
|
|
||||||
...draggedItem,
|
|
||||||
state_detail: destinationState,
|
|
||||||
state: destinationStateId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
results: updatedIssues,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// patch request
|
|
||||||
issuesServices
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
|
|
||||||
state: destinationStateId,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[workspaceSlug, mutateState, groupedByIssues, projectId, selectedGroup, states]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (issueView !== "kanban") return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DeleteIssueModal
|
|
||||||
isOpen={isIssueDeletionOpen}
|
|
||||||
handleClose={() => setIsIssueDeletionOpen(false)}
|
|
||||||
data={issueDeletionData}
|
|
||||||
/>
|
|
||||||
<CreateUpdateIssueModal
|
|
||||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
|
||||||
handleClose={() => setCreateIssueModal(false)}
|
|
||||||
prePopulateData={{
|
|
||||||
...preloadedData,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{groupedByIssues ? (
|
|
||||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
|
||||||
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
|
||||||
{(provided) => (
|
|
||||||
<div
|
|
||||||
className="h-full w-full"
|
|
||||||
{...provided.droppableProps}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
|
||||||
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
|
||||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
|
||||||
const stateId =
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Draggable key={singleGroup} draggableId={singleGroup} index={index}>
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<CommonSingleBoard
|
|
||||||
provided={provided}
|
|
||||||
snapshot={snapshot}
|
|
||||||
bgColor={
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.color
|
|
||||||
: "#000000"
|
|
||||||
}
|
|
||||||
groupTitle={singleGroup}
|
|
||||||
groupedByIssues={groupedByIssues}
|
|
||||||
selectedGroup={selectedGroup}
|
|
||||||
addIssueToState={() => {
|
|
||||||
setCreateIssueModal(true);
|
|
||||||
if (selectedGroup)
|
|
||||||
setPreloadedData({
|
|
||||||
state: stateId !== null ? stateId : undefined,
|
|
||||||
[selectedGroup]: singleGroup,
|
|
||||||
actionType: "createIssue",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
userAuth={userAuth}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</StrictModeDroppable>
|
|
||||||
</div>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export * from "./comment";
|
export * from "./comment";
|
||||||
export * from "./sidebar-select";
|
export * from "./sidebar-select";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
export * from "./board-view";
|
|
||||||
export * from "./delete-issue-modal";
|
export * from "./delete-issue-modal";
|
||||||
export * from "./description-form";
|
export * from "./description-form";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
import React, { useCallback } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
|
|
||||||
// react-beautiful-dnd
|
|
||||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
|
||||||
// services
|
|
||||||
import stateService from "services/state.service";
|
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
// hooks
|
|
||||||
import useIssueView from "hooks/use-issue-view";
|
|
||||||
// components
|
|
||||||
import { CommonSingleBoard } from "components/core/board-view/single-board";
|
|
||||||
// ui
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
// types
|
|
||||||
import { IIssue, ModuleIssueResponse, UserAuth } from "types";
|
|
||||||
// constants
|
|
||||||
import { STATE_LIST, MODULE_ISSUES } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issues: IIssue[];
|
|
||||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
|
||||||
openIssuesListModal: () => void;
|
|
||||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
||||||
setPreloadedData: React.Dispatch<
|
|
||||||
React.SetStateAction<
|
|
||||||
| (Partial<IIssue> & {
|
|
||||||
actionType: "createIssue" | "edit" | "delete";
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
userAuth: UserAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ModulesBoardView: React.FC<Props> = ({
|
|
||||||
issues,
|
|
||||||
openCreateIssueModal,
|
|
||||||
openIssuesListModal,
|
|
||||||
handleDeleteIssue,
|
|
||||||
setPreloadedData,
|
|
||||||
userAuth,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
|
||||||
|
|
||||||
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOnDragEnd = useCallback(
|
|
||||||
(result: DropResult) => {
|
|
||||||
if (!result.destination) return;
|
|
||||||
const { source, destination } = result;
|
|
||||||
|
|
||||||
if (source.droppableId !== destination.droppableId) {
|
|
||||||
const sourceGroup = source.droppableId; // source group id
|
|
||||||
const destinationGroup = destination.droppableId; // destination group id
|
|
||||||
if (!sourceGroup || !destinationGroup) return;
|
|
||||||
|
|
||||||
// removed/dragged item
|
|
||||||
const removedItem = groupedByIssues[source.droppableId][source.index];
|
|
||||||
|
|
||||||
if (selectedGroup === "priority") {
|
|
||||||
// update the removed item for mutation
|
|
||||||
removedItem.priority = destinationGroup;
|
|
||||||
|
|
||||||
// patch request
|
|
||||||
issuesService.patchIssue(workspaceSlug as string, projectId as string, removedItem.id, {
|
|
||||||
priority: destinationGroup,
|
|
||||||
});
|
|
||||||
} else if (selectedGroup === "state_detail.name") {
|
|
||||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
|
||||||
const destinationStateId = destinationState?.id;
|
|
||||||
|
|
||||||
// update the removed item for mutation
|
|
||||||
if (!destinationStateId || !destinationState) return;
|
|
||||||
removedItem.state = destinationStateId;
|
|
||||||
removedItem.state_detail = destinationState;
|
|
||||||
|
|
||||||
// patch request
|
|
||||||
issuesService.patchIssue(workspaceSlug as string, projectId as string, removedItem.id, {
|
|
||||||
state: destinationStateId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!moduleId) return;
|
|
||||||
mutate<ModuleIssueResponse[]>(
|
|
||||||
MODULE_ISSUES(moduleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === removedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: removedItem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove item from the source group
|
|
||||||
groupedByIssues[source.droppableId].splice(source.index, 1);
|
|
||||||
// add item to the destination group
|
|
||||||
groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[workspaceSlug, groupedByIssues, projectId, selectedGroup, states, moduleId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (issueView !== "kanban") return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{groupedByIssues ? (
|
|
||||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
|
||||||
const stateId =
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommonSingleBoard
|
|
||||||
key={singleGroup}
|
|
||||||
bgColor={
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.color
|
|
||||||
: "#000000"
|
|
||||||
}
|
|
||||||
groupTitle={singleGroup}
|
|
||||||
groupedByIssues={groupedByIssues}
|
|
||||||
selectedGroup={selectedGroup}
|
|
||||||
addIssueToState={() => {
|
|
||||||
openCreateIssueModal();
|
|
||||||
if (selectedGroup !== null) {
|
|
||||||
setPreloadedData({
|
|
||||||
state: stateId !== null ? stateId : undefined,
|
|
||||||
[selectedGroup]: singleGroup,
|
|
||||||
actionType: "createIssue",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// openIssuesListModal={openIssuesListModal}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
userAuth={userAuth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
export * from "./sidebar-select";
|
export * from "./sidebar-select";
|
||||||
export * from "./board-view";
|
|
||||||
export * from "./delete-module-modal";
|
export * from "./delete-module-modal";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
|
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
import React, { useCallback } from "react";
|
|
||||||
// swr
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
// services
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
|
||||||
import stateService from "services/state.service";
|
|
||||||
// hooks
|
|
||||||
import useIssueView from "hooks/use-issue-view";
|
|
||||||
// components
|
|
||||||
import { CommonSingleBoard } from "components/core/board-view/single-board";
|
|
||||||
// ui
|
|
||||||
import { Spinner } from "components/ui";
|
|
||||||
// types
|
|
||||||
import { CycleIssueResponse, IIssue, UserAuth } from "types";
|
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
// constants
|
|
||||||
import { STATE_LIST, CYCLE_ISSUES } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issues: IIssue[];
|
|
||||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
|
||||||
openIssuesListModal: () => void;
|
|
||||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
||||||
setPreloadedData: React.Dispatch<
|
|
||||||
React.SetStateAction<
|
|
||||||
| (Partial<IIssue> & {
|
|
||||||
actionType: "createIssue" | "edit" | "delete";
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
userAuth: UserAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CyclesBoardView: React.FC<Props> = ({
|
|
||||||
issues,
|
|
||||||
openCreateIssueModal,
|
|
||||||
openIssuesListModal,
|
|
||||||
handleDeleteIssue,
|
|
||||||
setPreloadedData,
|
|
||||||
userAuth,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
|
||||||
|
|
||||||
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOnDragEnd = useCallback(
|
|
||||||
(result: DropResult) => {
|
|
||||||
if (!result.destination) return;
|
|
||||||
const { source, destination } = result;
|
|
||||||
|
|
||||||
if (source.droppableId !== destination.droppableId) {
|
|
||||||
const sourceGroup = source.droppableId; // source group id
|
|
||||||
const destinationGroup = destination.droppableId; // destination group id
|
|
||||||
if (!sourceGroup || !destinationGroup) return;
|
|
||||||
|
|
||||||
// removed/dragged item
|
|
||||||
const removedItem = groupedByIssues[source.droppableId][source.index];
|
|
||||||
|
|
||||||
if (selectedGroup === "priority") {
|
|
||||||
// update the removed item for mutation
|
|
||||||
removedItem.priority = destinationGroup;
|
|
||||||
|
|
||||||
// patch request
|
|
||||||
issuesService.patchIssue(workspaceSlug as string, projectId as string, removedItem.id, {
|
|
||||||
priority: destinationGroup,
|
|
||||||
});
|
|
||||||
} else if (selectedGroup === "state_detail.name") {
|
|
||||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
|
||||||
const destinationStateId = destinationState?.id;
|
|
||||||
|
|
||||||
// update the removed item for mutation
|
|
||||||
if (!destinationStateId || !destinationState) return;
|
|
||||||
removedItem.state = destinationStateId;
|
|
||||||
removedItem.state_detail = destinationState;
|
|
||||||
|
|
||||||
// patch request
|
|
||||||
issuesService.patchIssue(workspaceSlug as string, projectId as string, removedItem.id, {
|
|
||||||
state: destinationStateId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!cycleId) return;
|
|
||||||
mutate<CycleIssueResponse[]>(
|
|
||||||
CYCLE_ISSUES(cycleId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
const updatedIssues = prevData.map((issue) => {
|
|
||||||
if (issue.issue_detail.id === removedItem.id) {
|
|
||||||
return {
|
|
||||||
...issue,
|
|
||||||
issue_detail: removedItem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return issue;
|
|
||||||
});
|
|
||||||
return [...updatedIssues];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove item from the source group
|
|
||||||
groupedByIssues[source.droppableId].splice(source.index, 1);
|
|
||||||
// add item to the destination group
|
|
||||||
groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[workspaceSlug, groupedByIssues, projectId, selectedGroup, states, cycleId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (issueView !== "kanban") return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{groupedByIssues ? (
|
|
||||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
|
||||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
|
||||||
const stateId =
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommonSingleBoard
|
|
||||||
key={singleGroup}
|
|
||||||
bgColor={
|
|
||||||
selectedGroup === "state_detail.name"
|
|
||||||
? states?.find((s) => s.name === singleGroup)?.color
|
|
||||||
: "#000000"
|
|
||||||
}
|
|
||||||
groupTitle={singleGroup}
|
|
||||||
groupedByIssues={groupedByIssues}
|
|
||||||
selectedGroup={selectedGroup}
|
|
||||||
addIssueToState={() => {
|
|
||||||
openCreateIssueModal();
|
|
||||||
if (selectedGroup !== null) {
|
|
||||||
setPreloadedData({
|
|
||||||
state: stateId !== null ? stateId : undefined,
|
|
||||||
[selectedGroup]: singleGroup,
|
|
||||||
actionType: "createIssue",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// openIssuesListModal={openIssuesListModal}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
userAuth={userAuth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CyclesBoardView;
|
|
||||||
|
|
@ -191,7 +191,7 @@ const WorkspacePage: NextPage = () => {
|
||||||
<a className="flex items-center justify-between">
|
<a className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
{project.icon ? (
|
{project.icon ? (
|
||||||
<span className="grid flex-shrink-0 place-items-center rounded uppercase text-white">
|
<span className="grid flex-shrink-0 place-items-center rounded uppercase">
|
||||||
{String.fromCodePoint(parseInt(project.icon))}
|
{String.fromCodePoint(parseInt(project.icon))}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import AppLayout from "layouts/app-layout";
|
||||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
// components
|
// components
|
||||||
import CyclesListView from "components/project/cycles/list-view";
|
import CyclesListView from "components/project/cycles/list-view";
|
||||||
import CyclesBoardView from "components/project/cycles/board-view";
|
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
|
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
|
||||||
import CycleDetailSidebar from "components/project/cycles/cycle-detail-sidebar";
|
import CycleDetailSidebar from "components/project/cycles/cycle-detail-sidebar";
|
||||||
|
import { AllBoards } from "components/core/board-view/all-boards";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "services/issues.service";
|
import issuesServices from "services/issues.service";
|
||||||
import cycleServices from "services/cycles.service";
|
import cycleServices from "services/cycles.service";
|
||||||
|
|
@ -242,12 +242,11 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
||||||
setPreloadedData={setPreloadedData}
|
setPreloadedData={setPreloadedData}
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
/>
|
/>
|
||||||
<CyclesBoardView
|
<AllBoards
|
||||||
|
type="cycle"
|
||||||
issues={cycleIssuesArray ?? []}
|
issues={cycleIssuesArray ?? []}
|
||||||
openCreateIssueModal={openCreateIssueModal}
|
|
||||||
openIssuesListModal={openIssuesListModal}
|
|
||||||
handleDeleteIssue={setDeleteIssue}
|
handleDeleteIssue={setDeleteIssue}
|
||||||
setPreloadedData={setPreloadedData}
|
openIssuesListModal={openIssuesListModal}
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
import { RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
import { PlusIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||||
// lib
|
// lib
|
||||||
|
|
@ -13,13 +13,9 @@ import AppLayout from "layouts/app-layout";
|
||||||
// contexts
|
// contexts
|
||||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
// components
|
// components
|
||||||
import {
|
|
||||||
CreateUpdateIssueModal,
|
|
||||||
DeleteIssueModal,
|
|
||||||
IssuesBoardView,
|
|
||||||
IssuesListView,
|
|
||||||
} from "components/issues";
|
|
||||||
import { IssuesFilterView } from "components/core";
|
import { IssuesFilterView } from "components/core";
|
||||||
|
import { CreateUpdateIssueModal, DeleteIssueModal, IssuesListView } from "components/issues";
|
||||||
|
import { AllBoards } from "components/core/board-view/all-boards";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton } from "components/ui";
|
import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
|
@ -119,7 +115,7 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
|
||||||
handleEditIssue={handleEditIssue}
|
handleEditIssue={handleEditIssue}
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
/>
|
/>
|
||||||
<IssuesBoardView
|
<AllBoards
|
||||||
issues={projectIssues?.results.filter((p) => p.parent === null) ?? []}
|
issues={projectIssues?.results.filter((p) => p.parent === null) ?? []}
|
||||||
handleDeleteIssue={setDeleteIssue}
|
handleDeleteIssue={setDeleteIssue}
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,8 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
|
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import {
|
import { AllBoards } from "components/core/board-view/all-boards";
|
||||||
DeleteModuleModal,
|
import { DeleteModuleModal, ModuleDetailsSidebar, ModulesListView } from "components/modules";
|
||||||
ModuleDetailsSidebar,
|
|
||||||
ModulesListView,
|
|
||||||
ModulesBoardView,
|
|
||||||
} from "components/modules";
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
|
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
|
@ -266,12 +262,11 @@ const SingleModule: React.FC<UserAuth> = (props) => {
|
||||||
setPreloadedData={setPreloadedData}
|
setPreloadedData={setPreloadedData}
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
/>
|
/>
|
||||||
<ModulesBoardView
|
<AllBoards
|
||||||
|
type="module"
|
||||||
issues={moduleIssuesArray ?? []}
|
issues={moduleIssuesArray ?? []}
|
||||||
openCreateIssueModal={openCreateIssueModal}
|
|
||||||
openIssuesListModal={openIssuesListModal}
|
|
||||||
handleDeleteIssue={setDeleteIssue}
|
handleDeleteIssue={setDeleteIssue}
|
||||||
setPreloadedData={setPreloadedData}
|
openIssuesListModal={openIssuesListModal}
|
||||||
userAuth={props}
|
userAuth={props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue