refactor: integrated global kanban view everywhere

This commit is contained in:
Aaryan Khandelwal 2023-02-04 20:08:13 +05:30
parent 58eda658c8
commit 85b7f39ed3
15 changed files with 374 additions and 658 deletions

View 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>
)}
</>
);
};

View File

@ -14,6 +14,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, NestedKeyOf } from "types";
type Props = {
provided: DraggableProvided;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
groupedByIssues: {
@ -24,7 +25,6 @@ type Props = {
createdBy: string | null;
bgColor?: string;
addIssueToState: () => void;
provided?: DraggableProvided;
};
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"}`}>
{provided && (
<button
type="button"
{...provided.dragHandleProps}
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
!isCollapsed ? "" : "rotate-90"
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
>
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
</button>
)}
<button
type="button"
{...provided.dragHandleProps}
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
!isCollapsed ? "" : "rotate-90"
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
>
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
</button>
<div
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" : ""

View File

@ -14,6 +14,8 @@ import useIssuesProperties from "hooks/use-issue-properties";
// components
import BoardHeader from "components/core/board-view/board-header";
import SingleIssue from "components/core/board-view/single-issue";
// ui
import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
@ -22,8 +24,9 @@ import { IIssue, IWorkspaceMember, NestedKeyOf, UserAuth } from "types";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type Props = {
provided?: DraggableProvided;
snapshot?: DraggableStateSnapshot;
type?: "issue" | "cycle" | "module";
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
bgColor?: string;
groupTitle: string;
groupedByIssues: {
@ -32,10 +35,12 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
addIssueToState: () => void;
handleDeleteIssue?: Dispatch<SetStateAction<string | undefined>> | undefined;
openIssuesListModal?: (() => void) | null;
userAuth: UserAuth;
};
export const CommonSingleBoard: React.FC<Props> = ({
type = "issue",
provided,
snapshot,
bgColor,
@ -44,6 +49,7 @@ export const CommonSingleBoard: React.FC<Props> = ({
selectedGroup,
addIssueToState,
handleDeleteIssue,
openIssuesListModal,
userAuth,
}) => {
// 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"}`}>
<BoardHeader
provided={provided}
addIssueToState={addIssueToState}
bgColor={bgColor}
createdBy={createdBy}
@ -91,7 +98,6 @@ export const CommonSingleBoard: React.FC<Props> = ({
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
selectedGroup={selectedGroup}
provided={provided}
/>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(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 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!isCollapsed ? "hidden" : "block"}`}
{...provided.droppableProps}
ref={provided.innerRef}
{...provided.droppableProps}
>
{groupedByIssues[groupTitle].map((childIssue, index: number) => {
const assignees = [
@ -135,14 +141,35 @@ export const CommonSingleBoard: React.FC<Props> = ({
);
})}
{provided.placeholder}
{/* <button
type="button"
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
</button> */}
{type === "issue" ? (
<button
type="button"
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
</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>
)}
</StrictModeDroppable>

View File

@ -8,9 +8,6 @@ import useSWR, { mutate } from "swr";
// 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
import { Listbox, Transition } from "@headlessui/react";
// constants
@ -51,7 +48,7 @@ type Props = {
typeId?: string;
issue: IIssue;
properties: Properties;
snapshot?: DraggableStateSnapshot;
snapshot: DraggableStateSnapshot;
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
people: IWorkspaceMember[] | undefined;
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>;
@ -163,7 +160,7 @@ const SingleBoardIssue: React.FC<Props> = ({
return (
<div
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">

View File

@ -1,5 +1,5 @@
export * from "./not-authorized-view";
export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal";
export * from "./image-upload-modal";
export * from "./view";
export * from "./issues-filter-view";
export * from "./not-authorized-view";

View File

@ -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>
)}
</>
);
};

View File

@ -1,7 +1,6 @@
export * from "./comment";
export * from "./sidebar-select";
export * from "./activity";
export * from "./board-view";
export * from "./delete-issue-modal";
export * from "./description-form";
export * from "./form";

View File

@ -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>
)}
</>
);
};

View File

@ -1,6 +1,5 @@
export * from "./select";
export * from "./sidebar-select";
export * from "./board-view";
export * from "./delete-module-modal";
export * from "./form";
export * from "./list-view";

View File

@ -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;

View File

@ -191,7 +191,7 @@ const WorkspacePage: NextPage = () => {
<a className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm">
{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))}
</span>
) : (

View File

@ -12,10 +12,10 @@ import AppLayout from "layouts/app-layout";
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import CyclesListView from "components/project/cycles/list-view";
import CyclesBoardView from "components/project/cycles/board-view";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
import CycleDetailSidebar from "components/project/cycles/cycle-detail-sidebar";
import { AllBoards } from "components/core/board-view/all-boards";
// services
import issuesServices from "services/issues.service";
import cycleServices from "services/cycles.service";
@ -242,12 +242,11 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
setPreloadedData={setPreloadedData}
userAuth={props}
/>
<CyclesBoardView
<AllBoards
type="cycle"
issues={cycleIssuesArray ?? []}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={openIssuesListModal}
handleDeleteIssue={setDeleteIssue}
setPreloadedData={setPreloadedData}
openIssuesListModal={openIssuesListModal}
userAuth={props}
/>
</div>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import useSWR from "swr";
import { RectangleStackIcon } from "@heroicons/react/24/outline";
import { PlusIcon } from "@heroicons/react/20/solid";
// lib
@ -13,13 +13,9 @@ import AppLayout from "layouts/app-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import {
CreateUpdateIssueModal,
DeleteIssueModal,
IssuesBoardView,
IssuesListView,
} from "components/issues";
import { IssuesFilterView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal, IssuesListView } from "components/issues";
import { AllBoards } from "components/core/board-view/all-boards";
// ui
import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -119,7 +115,7 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
handleEditIssue={handleEditIssue}
userAuth={props}
/>
<IssuesBoardView
<AllBoards
issues={projectIssues?.results.filter((p) => p.parent === null) ?? []}
handleDeleteIssue={setDeleteIssue}
userAuth={props}

View File

@ -16,12 +16,8 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import {
DeleteModuleModal,
ModuleDetailsSidebar,
ModulesListView,
ModulesBoardView,
} from "components/modules";
import { AllBoards } from "components/core/board-view/all-boards";
import { DeleteModuleModal, ModuleDetailsSidebar, ModulesListView } from "components/modules";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -266,12 +262,11 @@ const SingleModule: React.FC<UserAuth> = (props) => {
setPreloadedData={setPreloadedData}
userAuth={props}
/>
<ModulesBoardView
<AllBoards
type="module"
issues={moduleIssuesArray ?? []}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={openIssuesListModal}
handleDeleteIssue={setDeleteIssue}
setPreloadedData={setPreloadedData}
openIssuesListModal={openIssuesListModal}
userAuth={props}
/>
</div>