refactor: integrated global list view everywhere

This commit is contained in:
Aaryan Khandelwal 2023-02-05 16:57:37 +05:30
parent 85b7f39ed3
commit d673aedf48
24 changed files with 938 additions and 1172 deletions

View File

@ -1,244 +1,40 @@
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";
import { SingleBoard } from "components/core/board-view/single-board";
// 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";
import { IIssue, IProjectMember, IState, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
type: "issue" | "cycle" | "module";
issues: IIssue[];
openIssuesListModal?: () => void;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
states: IState[] | undefined;
members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleOnDragEnd: (result: DropResult) => void;
userAuth: UserAuth;
};
export const AllBoards: React.FC<Props> = ({
type = "issue",
type,
issues,
states,
members,
addIssueToState,
openIssuesListModal,
handleDeleteIssue,
handleOnDragEnd,
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 <></>;
const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
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}>
@ -265,7 +61,7 @@ export const AllBoards: React.FC<Props> = ({
return (
<Draggable key={singleGroup} draggableId={singleGroup} index={index}>
{(provided, snapshot) => (
<CommonSingleBoard
<SingleBoard
type={type}
provided={provided}
snapshot={snapshot}
@ -273,17 +69,10 @@ export const AllBoards: React.FC<Props> = ({
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
addIssueToState={() => {
setCreateIssueModal(true);
if (selectedGroup)
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: singleGroup,
actionType: "createIssue",
});
}}
members={members}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
openIssuesListModal={openIssuesListModal ?? null}
userAuth={userAuth}
/>
)}

View File

@ -27,7 +27,7 @@ type Props = {
addIssueToState: () => void;
};
const BoardHeader: React.FC<Props> = ({
export const BoardHeader: React.FC<Props> = ({
isCollapsed,
setIsCollapsed,
provided,
@ -103,5 +103,3 @@ const BoardHeader: React.FC<Props> = ({
</div>
</div>
);
export default BoardHeader;

View File

@ -0,0 +1,4 @@
export * from "./all-boards";
export * from "./board-header";
export * from "./single-board";
export * from "./single-issue";

View File

@ -1,27 +1,20 @@
import { Dispatch, SetStateAction, useState } from "react";
import { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-beautiful-dnd
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
// services
import workspaceService from "services/workspace.service";
// hooks
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";
import { BoardHeader, SingleBoardIssue } from "components/core";
// ui
import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IWorkspaceMember, NestedKeyOf, UserAuth } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
@ -33,13 +26,14 @@ type Props = {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
addIssueToState: () => void;
handleDeleteIssue?: Dispatch<SetStateAction<string | undefined>> | undefined;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
userAuth: UserAuth;
};
export const CommonSingleBoard: React.FC<Props> = ({
export const SingleBoard: React.FC<Props> = ({
type = "issue",
provided,
snapshot,
@ -47,6 +41,7 @@ export const CommonSingleBoard: React.FC<Props> = ({
groupTitle,
groupedByIssues,
selectedGroup,
members,
addIssueToState,
handleDeleteIssue,
openIssuesListModal,
@ -60,11 +55,6 @@ export const CommonSingleBoard: React.FC<Props> = ({
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { data: members } = useSWR<IWorkspaceMember[]>(
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const createdBy =
selectedGroup === "created_by"
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..."
@ -126,7 +116,7 @@ export const CommonSingleBoard: React.FC<Props> = ({
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<SingleIssue
<SingleBoardIssue
issue={childIssue}
properties={properties}
snapshot={snapshot}

View File

@ -19,15 +19,15 @@ import projectService from "services/project.service";
// components
import { AssigneesList, CustomDatePicker } from "components/ui";
// helpers
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { findHowManyDaysLeft } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import {
CycleIssueResponse,
IIssue,
IProjectMember,
IssueResponse,
IUserLite,
IWorkspaceMember,
ModuleIssueResponse,
Properties,
UserAuth,
@ -50,12 +50,12 @@ type Props = {
properties: Properties;
snapshot: DraggableStateSnapshot;
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
people: IWorkspaceMember[] | undefined;
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>;
people: IProjectMember[] | undefined;
handleDeleteIssue: (issue: IIssue) => void;
userAuth: UserAuth;
};
const SingleBoardIssue: React.FC<Props> = ({
export const SingleBoardIssue: React.FC<Props> = ({
type,
typeId,
issue,
@ -164,12 +164,12 @@ const SingleBoardIssue: React.FC<Props> = ({
}`}
>
<div className="group/card relative select-none p-2">
{handleDeleteIssue && !isNotAllowed && (
{!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
<button
type="button"
className="grid h-7 w-7 place-items-center rounded bg-white p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
onClick={() => handleDeleteIssue(issue.id)}
onClick={() => handleDeleteIssue(issue)}
>
<TrashIcon className="h-4 w-4" />
</button>
@ -400,7 +400,7 @@ const SingleBoardIssue: React.FC<Props> = ({
<Listbox.Options className="absolute left-0 z-20 mt-1 max-h-28 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{people?.map((person) => (
<Listbox.Option
key={person.id}
key={person.member.id}
className={({ active }) =>
`cursor-pointer select-none p-2 ${active ? "bg-indigo-50" : "bg-white"}`
}
@ -457,5 +457,3 @@ const SingleBoardIssue: React.FC<Props> = ({
</div>
);
};
export default SingleBoardIssue;

View File

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

View File

@ -0,0 +1,408 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// react-beautiful-dnd
import { DropResult } from "react-beautiful-dnd";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
import projectService from "services/project.service";
// hooks
import useIssueView from "hooks/use-issue-view";
// components
import { AllLists, AllBoards } from "components/core";
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,
PROJECT_MEMBERS,
STATE_LIST,
} from "constants/fetch-keys";
type Props = {
type?: "issue" | "cycle" | "module";
issues: IIssue[];
openIssuesListModal?: () => void;
userAuth: UserAuth;
};
export const IssuesView: React.FC<Props> = ({
type = "issue",
issues,
openIssuesListModal,
userAuth,
}) => {
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
// updates issue modal
const [editIssueModal, setEditIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<
(IIssue & { actionType: "edit" | "delete" }) | undefined
>(undefined);
// delete issue modal
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [issueToDelete, setIssueToDelete] = useState<IIssue | null>(null);
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 { data: members } = useSWR(
projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(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;
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,
priority: destinationGroup,
},
};
}
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,
priority: destinationGroup,
},
};
}
return issue;
});
return [...updatedIssues];
},
false
);
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,
priority: destinationGroup,
};
return issue;
});
return {
...prevData,
results: updatedIssues,
};
},
false
);
// patch request
issuesService
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
priority: destinationGroup,
})
.then((res) => {
mutate(
cycleId
? CYCLE_ISSUES(cycleId as string)
: CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "")
);
mutate(
moduleId
? MODULE_ISSUES(moduleId as string)
: MODULE_ISSUES(draggedItem.issue_module?.module ?? "")
);
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
});
} 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;
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
);
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
issuesService
.patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, {
state: destinationStateId,
})
.then((res) => {
mutate(
cycleId
? CYCLE_ISSUES(cycleId as string)
: CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "")
);
mutate(
moduleId
? MODULE_ISSUES(moduleId as string)
: MODULE_ISSUES(draggedItem.issue_module?.module ?? "")
);
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
});
}
}
}
},
[
workspaceSlug,
cycleId,
moduleId,
mutateState,
groupedByIssues,
projectId,
selectedGroup,
states,
]
);
const addIssueToState = (groupTitle: string, stateId: string | null) => {
setCreateIssueModal(true);
if (selectedGroup)
setPreloadedData({
state: stateId ?? undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
};
const handleEditIssue = (issue: IIssue) => {
setEditIssueModal(true);
setIssueToEdit({
...issue,
actionType: "edit",
cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
module: issue.issue_module ? issue.issue_module.module : null,
});
};
const handleDeleteIssue = (issue: IIssue) => {
setDeleteIssueModal(true);
setIssueToDelete(issue);
};
return (
<>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
prePopulateData={{ ...issueToEdit }}
handleClose={() => setEditIssueModal(false)}
data={issueToEdit}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
/>
{issueView === "list" ? (
<AllLists
type={type}
issues={issues}
states={states}
members={members}
addIssueToState={addIssueToState}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
userAuth={userAuth}
/>
) : (
<AllBoards
type={type}
issues={issues}
states={states}
members={members}
addIssueToState={addIssueToState}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
handleDeleteIssue={handleDeleteIssue}
handleOnDragEnd={handleOnDragEnd}
userAuth={userAuth}
/>
)}
</>
);
};

View File

@ -0,0 +1,60 @@
// hooks
import useIssueView from "hooks/use-issue-view";
// components
import { SingleList } from "components/core/list-view/single-list";
// types
import { IIssue, IProjectMember, IState, UserAuth } from "types";
// types
type Props = {
type: "issue" | "cycle" | "module";
issues: IIssue[];
states: IState[] | undefined;
members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
openIssuesListModal?: (() => void) | null;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
userAuth: UserAuth;
};
export const AllLists: React.FC<Props> = ({
type,
issues,
states,
members,
addIssueToState,
openIssuesListModal,
handleEditIssue,
handleDeleteIssue,
userAuth,
}) => {
const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
return (
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => {
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
return (
<SingleList
key={singleGroup}
type={type}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
members={members}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
userAuth={userAuth}
/>
);
})}
</div>
);
};

View File

@ -0,0 +1,3 @@
export * from "./all-lists";
export * from "./single-issue";
export * from "./single-list";

View File

@ -47,20 +47,20 @@ type Props = {
properties: Properties;
editIssue: () => void;
removeIssue?: () => void;
handleDeleteIssue: (issue: IIssue) => void;
userAuth: UserAuth;
};
const SingleListIssue: React.FC<Props> = ({
export const SingleListIssue: React.FC<Props> = ({
type,
typeId,
issue,
properties,
editIssue,
removeIssue,
handleDeleteIssue,
userAuth,
}) => {
const [deleteIssue, setDeleteIssue] = useState<IIssue | undefined>();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -151,284 +151,275 @@ const SingleListIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<>
<DeleteIssueModal
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={deleteIssue}
/>
<div className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<span
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
<div className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<span
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
)}
<span>{issue.name}</span>
</a>
</Link>
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<Listbox
as="div"
value={issue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data });
}}
/>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
)}
<span>{issue.name}</span>
</a>
</Link>
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<Listbox
as="div"
value={issue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data });
}}
className="group relative flex-shrink-0"
disabled={isNotAllowed}
>
{({ open }) => (
<>
<div>
<Listbox.Button
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600"
: issue.priority === "high"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</Listbox.Button>
className="group relative flex-shrink-0"
disabled={isNotAllowed}
>
{({ open }) => (
<>
<div>
<Listbox.Button
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600"
: issue.priority === "high"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{PRIORITIES?.map((priority) => (
<Listbox.Option
key={priority}
className={({ active }) =>
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
active ? "bg-indigo-50" : "bg-white"
}`
}
value={priority}
>
{getPriorityIcon(priority, "text-sm")}
{priority ?? "None"}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{PRIORITIES?.map((priority) => (
<Listbox.Option
key={priority}
className={({ active }) =>
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
active ? "bg-indigo-50" : "bg-white"
}`
}
value={priority}
>
{getPriorityIcon(priority, "text-sm")}
{priority ?? "None"}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">Priority</h5>
<div
className={`capitalize ${
issue.priority === "urgent"
? "text-red-600"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"
? "text-yellow-500"
: issue.priority === "low"
? "text-green-500"
: ""
}`}
>
{issue.priority ?? "None"}
</div>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">Priority</h5>
<div
className={`capitalize ${
issue.priority === "urgent"
? "text-red-600"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"
? "text-yellow-500"
: issue.priority === "low"
? "text-green-500"
: ""
}`}
>
{issue.priority ?? "None"}
</div>
</div>
</>
)}
</Listbox>
)}
{properties.state && (
<CustomSelect
label={
</div>
</>
)}
</Listbox>
)}
{properties.state && (
<CustomSelect
label={
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
{addSpaceIfCamelCase(issue.state_detail.name)}
</>
}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data });
}}
maxHeight="md"
noChevron
disabled={isNotAllowed}
>
{states?.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
backgroundColor: state.color,
}}
/>
{addSpaceIfCamelCase(issue.state_detail.name)}
{addSpaceIfCamelCase(state.name)}
</>
}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data });
}}
maxHeight="md"
noChevron
disabled={isNotAllowed}
>
{states?.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: state.color,
}}
/>
{addSpaceIfCamelCase(state.name)}
</>
</CustomSelect.Option>
))}
</CustomSelect>
)}
{/* {properties.cycle && !typeId && (
</CustomSelect.Option>
))}
</CustomSelect>
)}
{/* {properties.cycle && !typeId && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
{issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"}
</div>
)} */}
{properties.due_date && (
<div
className={`group relative ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
}`}
>
<CustomDatePicker
placeholder="N/A"
value={issue?.target_date}
onChange={(val) =>
partialUpdateIssue({
target_date: val,
})
}
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
/>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">Due date</h5>
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
<div>
{issue.target_date
? issue.target_date < new Date().toISOString()
? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days`
: findHowManyDaysLeft(issue.target_date) <= 3
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
: "Due date"
: "N/A"}
</div>
{properties.due_date && (
<div
className={`group relative ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
}`}
>
<CustomDatePicker
placeholder="N/A"
value={issue?.target_date}
onChange={(val) =>
partialUpdateIssue({
target_date: val,
})
}
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
/>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">Due date</h5>
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
<div>
{issue.target_date
? issue.target_date < new Date().toISOString()
? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days`
: findHowManyDaysLeft(issue.target_date) <= 3
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
: "Due date"
: "N/A"}
</div>
</div>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.assignee && (
<Listbox
as="div"
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
</div>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.assignee && (
<Listbox
as="div"
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: newData });
}}
className="group relative flex-shrink-0"
disabled={isNotAllowed}
>
{({ open }) => (
<>
<div>
<Listbox.Button>
<div
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-1 text-xs`}
>
<AssigneesList userIds={issue.assignees ?? []} />
</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
partialUpdateIssue({ assignees_list: newData });
}}
className="group relative flex-shrink-0"
disabled={isNotAllowed}
>
{({ open }) => (
<>
<div>
<Listbox.Button>
<div
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-1 text-xs`}
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{people?.map((person) => (
<Listbox.Option
key={person.id}
className={({ active, selected }) =>
`flex items-center gap-x-1 cursor-pointer select-none p-2 ${
active ? "bg-indigo-50" : ""
} ${
selected || issue.assignees?.includes(person.member.id)
? "bg-indigo-50 font-medium"
: "font-normal"
}`
}
value={person.member.id}
>
<Avatar user={person.member} />
<p>
{person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email}
</p>
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium">Assigned to</h5>
<div>
{issue.assignee_details?.length > 0
? issue.assignee_details.map((assignee) => assignee.first_name).join(", ")
: "No one"}
<AssigneesList userIds={issue.assignees ?? []} />
</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{people?.map((person) => (
<Listbox.Option
key={person.id}
className={({ active, selected }) =>
`flex items-center gap-x-1 cursor-pointer select-none p-2 ${
active ? "bg-indigo-50" : ""
} ${
selected || issue.assignees?.includes(person.member.id)
? "bg-indigo-50 font-medium"
: "font-normal"
}`
}
value={person.member.id}
>
<Avatar user={person.member} />
<p>
{person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email}
</p>
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium">Assigned to</h5>
<div>
{issue.assignee_details?.length > 0
? issue.assignee_details.map((assignee) => assignee.first_name).join(", ")
: "No one"}
</div>
</>
)}
</Listbox>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
{type !== "issue" && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => setDeleteIssue(issue)}>
Delete permanently
</div>
</>
)}
</Listbox>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
{type !== "issue" && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
Delete permanently
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
</>
</div>
);
};
export default SingleListIssue;

View File

@ -0,0 +1,142 @@
import { useRouter } from "next/router";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
// components
import { SingleListIssue } from "components/core";
// icons
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
groupTitle: string;
groupedByIssues: {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
addIssueToState: () => void;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
userAuth: UserAuth;
};
export const SingleList: React.FC<Props> = ({
type = "issue",
groupTitle,
groupedByIssues,
selectedGroup,
members,
addIssueToState,
handleEditIssue,
handleDeleteIssue,
openIssuesListModal,
userAuth,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const createdBy =
selectedGroup === "created_by"
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..."
: null;
return (
<Disclosure key={groupTitle} as="div" defaultOpen>
{({ open }) => (
<div className="rounded-lg bg-white">
<div className="rounded-t-lg bg-gray-100 px-4 py-3">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium capitalize leading-5">
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-sm text-gray-500">
{groupedByIssues[groupTitle as keyof IIssue].length}
</p>
</div>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="divide-y-2">
{groupedByIssues[groupTitle] ? (
groupedByIssues[groupTitle].length > 0 ? (
groupedByIssues[groupTitle].map((issue: IIssue) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = members?.find((p) => p.member.id === assignee)?.member;
return tempPerson;
});
return (
<SingleListIssue
key={issue.id}
type="issue"
issue={issue}
properties={properties}
editIssue={() => handleEditIssue(issue)}
handleDeleteIssue={handleDeleteIssue}
userAuth={userAuth}
/>
);
})
) : (
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
)
) : (
<div className="flex h-full w-full items-center justify-center">Loading...</div>
)}
</div>
</Disclosure.Panel>
</Transition>
<div className="p-3">
{type === "issue" ? (
<button
type="button"
className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-gray-100"
onClick={addIssueToState}
>
<PlusIcon className="h-3 w-3" />
Add issue
</button>
) : null}
</div>
</div>
)}
</Disclosure>
);
};

View File

@ -22,7 +22,7 @@ import { CYCLE_ISSUES, PROJECT_ISSUES_LIST, MODULE_ISSUES } from "constants/fetc
type Props = {
isOpen: boolean;
handleClose: () => void;
data?: IIssue;
data: IIssue | null;
};
export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data }) => {

View File

@ -4,7 +4,6 @@ export * from "./activity";
export * from "./delete-issue-modal";
export * from "./description-form";
export * from "./form";
export * from "./list-view";
export * from "./modal";
export * from "./my-issues-list-item";
export * from "./parent-issues-list-modal";

View File

@ -1,182 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// services
import stateService from "services/state.service";
import workspaceService from "services/workspace.service";
// ui
import { Spinner } from "components/ui";
// components
import { CreateUpdateIssueModal } from "components/issues/modal";
import SingleListIssue from "components/core/list-view/single-issue";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IWorkspaceMember, UserAuth } from "types";
// fetch-keys
import { STATE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// types
type Props = {
issues: IIssue[];
handleEditIssue: (issue: IIssue) => void;
userAuth: UserAuth;
};
export const IssuesListView: React.FC<Props> = ({ issues, handleEditIssue, userAuth }) => {
const [isCreateIssuesModalOpen, setIsCreateIssuesModalOpen] = useState(false);
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 } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const { data: people } = useSWR<IWorkspaceMember[]>(
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
if (issueView !== "list") return <></>;
return (
<>
<CreateUpdateIssueModal
isOpen={isCreateIssuesModalOpen && preloadedData?.actionType === "createIssue"}
handleClose={() => setIsCreateIssuesModalOpen(false)}
prePopulateData={{
...preloadedData,
}}
/>
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => (
<Disclosure key={singleGroup} as="div" defaultOpen>
{({ open }) => (
<div className="rounded-lg bg-white">
<div className="rounded-t-lg bg-gray-100 px-4 py-3">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium capitalize leading-5">
{singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority"
: selectedGroup === "created_by"
? people?.find((p) => p.member.id === singleGroup)?.member
?.first_name ?? "Loading..."
: addSpaceIfCamelCase(singleGroup)}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-sm text-gray-500">
{groupedByIssues[singleGroup as keyof IIssue].length}
</p>
</div>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="divide-y-2">
{groupedByIssues[singleGroup] ? (
groupedByIssues[singleGroup].length > 0 ? (
groupedByIssues[singleGroup].map((issue: IIssue) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find(
(p) => p.member.id === assignee
)?.member;
return tempPerson;
});
return (
<SingleListIssue
key={issue.id}
type="issue"
issue={issue}
properties={properties}
editIssue={() => handleEditIssue(issue)}
userAuth={userAuth}
/>
);
})
) : (
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</div>
</Disclosure.Panel>
</Transition>
<div className="p-3">
<button
type="button"
className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-gray-100"
onClick={() => {
setIsCreateIssuesModalOpen(true);
if (selectedGroup !== null) {
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: singleGroup,
actionType: "createIssue",
});
} else {
setPreloadedData({
actionType: "createIssue",
});
}
}}
>
<PlusIcon className="h-3 w-3" />
Add issue
</button>
</div>
</div>
)}
</Disclosure>
))}
</div>
</>
);
};

View File

@ -175,7 +175,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else
} else {
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""),
(prevData) => ({
@ -186,8 +186,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}),
})
);
}
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) handleClose();

View File

@ -130,7 +130,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetail}
data={issueDetail ?? null}
/>
<div className="w-full divide-y-2 divide-gray-100 sticky top-5">
<div className="flex items-center justify-between pb-3">

View File

@ -2,7 +2,6 @@ export * from "./select";
export * from "./sidebar-select";
export * from "./delete-module-modal";
export * from "./form";
export * from "./list-view";
export * from "./modal";
export * from "./module-link-modal";
export * from "./sidebar";

View File

@ -1,194 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// icons
import { Disclosure, Transition } from "@headlessui/react";
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
// services
import workspaceService from "services/workspace.service";
import stateService from "services/state.service";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// components
import SingleListIssue from "components/core/list-view/single-issue";
// ui
import { CustomMenu, Spinner } from "components/ui";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IWorkspaceMember, UserAuth } from "types";
// fetch-keys
import { STATE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
type Props = {
issues: IIssue[];
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: () => void;
removeIssueFromModule: (issueId: string) => void;
setPreloadedData: React.Dispatch<
React.SetStateAction<
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| null
>
>;
userAuth: UserAuth;
};
export const ModulesListView: React.FC<Props> = ({
issues,
openCreateIssueModal,
openIssuesListModal,
removeIssueFromModule,
setPreloadedData,
userAuth,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
const { data: people } = useSWR<IWorkspaceMember[]>(
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
if (issueView !== "list") return <></>;
return (
<div className="flex h-full flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => {
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
return (
<Disclosure key={singleGroup} as="div" defaultOpen>
{({ open }) => (
<div className="rounded-lg bg-white">
<div className="rounded-t-lg bg-gray-100 px-4 py-3">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium capitalize leading-5">
{singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority"
: addSpaceIfCamelCase(singleGroup)}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-sm text-gray-500">
{groupedByIssues[singleGroup as keyof IIssue].length}
</p>
</div>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="divide-y-2">
{groupedByIssues[singleGroup] ? (
groupedByIssues[singleGroup].length > 0 ? (
groupedByIssues[singleGroup].map((issue) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find(
(p) => p.member.id === assignee
)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
});
return (
<SingleListIssue
key={issue.id}
type="module"
typeId={moduleId as string}
issue={issue}
properties={properties}
editIssue={() => openCreateIssueModal(issue, "edit")}
removeIssue={() => removeIssueFromModule(issue.bridge ?? "")}
userAuth={userAuth}
/>
);
})
) : (
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</div>
</Disclosure.Panel>
</Transition>
<div className="p-3">
<CustomMenu
label={
<span className="flex items-center gap-1">
<PlusIcon className="h-3 w-3" />
Add issue
</span>
}
optionsPosition="left"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
openCreateIssueModal();
if (selectedGroup !== null) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: singleGroup,
actionType: "createIssue",
});
}
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => openIssuesListModal()}>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
)}
</Disclosure>
);
})}
</div>
);
};

View File

@ -1,199 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// icons
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
// services
import workspaceService from "services/workspace.service";
import stateService from "services/state.service";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// components
import SingleListIssue from "components/core/list-view/single-issue";
// ui
import { CustomMenu, Spinner } from "components/ui";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IWorkspaceMember, UserAuth } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS, STATE_LIST } from "constants/fetch-keys";
type Props = {
issues: IIssue[];
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: () => void;
removeIssueFromCycle: (bridgeId: string) => void;
setPreloadedData: React.Dispatch<
React.SetStateAction<
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| null
>
>;
userAuth: UserAuth;
};
const CyclesListView: React.FC<Props> = ({
issues,
openCreateIssueModal,
openIssuesListModal,
removeIssueFromCycle,
setPreloadedData,
userAuth,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
const { data: people } = useSWR<IWorkspaceMember[]>(
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
if (issueView !== "list") return <></>;
return (
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => {
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
return (
<Disclosure key={singleGroup} as="div" defaultOpen>
{({ open }) => (
<div className="rounded-lg bg-white">
<div className="rounded-t-lg bg-gray-100 px-4 py-3">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium capitalize leading-5">
{singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority"
: addSpaceIfCamelCase(singleGroup)}
</h2>
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-sm text-gray-500">
{groupedByIssues[singleGroup as keyof IIssue].length}
</p>
</div>
</Disclosure.Button>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="divide-y-2">
{groupedByIssues[singleGroup] ? (
groupedByIssues[singleGroup].length > 0 ? (
groupedByIssues[singleGroup].map((issue) => {
const assignees = [
...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []),
]?.map((assignee) => {
const tempPerson = people?.find(
(p) => p.member.id === assignee
)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
});
return (
<SingleListIssue
key={issue.id}
type="cycle"
typeId={cycleId as string}
issue={issue}
properties={properties}
editIssue={() => openCreateIssueModal(issue, "edit")}
removeIssue={() => removeIssueFromCycle(issue.bridge ?? "")}
userAuth={userAuth}
/>
);
})
) : (
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</div>
</Disclosure.Panel>
</Transition>
<div className="p-3">
<CustomMenu
label={
<span className="flex items-center gap-1">
<PlusIcon className="h-3 w-3" />
Add issue
</span>
}
optionsPosition="left"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
openCreateIssueModal();
if (selectedGroup !== null) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: singleGroup,
actionType: "createIssue",
});
}
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => openIssuesListModal()}>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
)}
</Disclosure>
);
})}
</div>
);
};
export default CyclesListView;

View File

@ -65,6 +65,8 @@ const useIssueView = (projectIssues: IIssue[]) => {
...groupBy(projectIssues ?? [], groupByProperty ?? ""),
};
if (groupByProperty === "priority") delete groupedByIssues.None;
if (orderBy) {
groupedByIssues = Object.fromEntries(
Object.entries(groupedByIssues).map(([key, value]) => [
@ -74,7 +76,7 @@ const useIssueView = (projectIssues: IIssue[]) => {
);
}
if (filterIssue !== null) {
if (filterIssue) {
if (filterIssue === "activeIssue") {
const filteredStates = states?.filter(
(state) => state.group === "started" || state.group === "unstarted"

View File

@ -11,11 +11,9 @@ import AppLayout from "layouts/app-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import CyclesListView from "components/project/cycles/list-view";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
import { CreateUpdateIssueModal } from "components/issues";
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } 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";
@ -42,7 +40,6 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
const [cycleSidebar, setCycleSidebar] = useState(true);
const [preloadedData, setPreloadedData] = useState<
@ -178,11 +175,6 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
issues={issues?.results.filter((i) => !i.issue_cycle) ?? []}
handleOnSubmit={handleAddIssuesToCycle}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={issues?.results.find((issue) => issue.id === deleteIssue)}
/>
<AppLayout
breadcrumbs={
<Breadcrumbs>
@ -234,21 +226,15 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
{cycleIssuesArray ? (
cycleIssuesArray.length > 0 ? (
<div className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}>
<CyclesListView
{/* <CyclesListView
issues={cycleIssuesArray ?? []}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={openIssuesListModal}
removeIssueFromCycle={removeIssueFromCycle}
setPreloadedData={setPreloadedData}
userAuth={props}
/>
<AllBoards
type="cycle"
issues={cycleIssuesArray ?? []}
handleDeleteIssue={setDeleteIssue}
openIssuesListModal={openIssuesListModal}
userAuth={props}
/>
/> */}
<IssuesView issues={cycleIssuesArray ?? []} userAuth={props} />
</div>
) : (
<div

View File

@ -13,9 +13,8 @@ import AppLayout from "layouts/app-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { IssuesFilterView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal, IssuesListView } from "components/issues";
import { AllBoards } from "components/core/board-view/all-boards";
import { IssuesFilterView, IssuesView } from "components/core";
import { CreateUpdateIssueModal } from "components/issues";
// ui
import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -30,7 +29,6 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
const [selectedIssue, setSelectedIssue] = useState<
(IIssue & { actionType: "edit" | "delete" }) | undefined
>(undefined);
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
const {
query: { workspaceSlug, projectId },
@ -61,13 +59,14 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
}
}, [isOpen]);
const handleEditIssue = (issue: IIssue) => {
setIsOpen(true);
setSelectedIssue({ ...issue, actionType: "edit" });
};
return (
<IssueViewContextProvider>
<CreateUpdateIssueModal
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
prePopulateData={{ ...selectedIssue }}
handleClose={() => setIsOpen(false)}
data={selectedIssue}
/>
<AppLayout
breadcrumbs={
<Breadcrumbs>
@ -93,34 +92,15 @@ const ProjectIssues: NextPage<UserAuth> = (props) => {
</div>
}
>
<CreateUpdateIssueModal
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
prePopulateData={{ ...selectedIssue }}
handleClose={() => setIsOpen(false)}
data={selectedIssue}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={projectIssues?.results.find((issue) => issue.id === deleteIssue)}
/>
{!projectIssues ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : projectIssues.count > 0 ? (
<>
<IssuesListView
issues={projectIssues?.results.filter((p) => p.parent === null) ?? []}
handleEditIssue={handleEditIssue}
userAuth={props}
/>
<AllBoards
issues={projectIssues?.results.filter((p) => p.parent === null) ?? []}
handleDeleteIssue={setDeleteIssue}
userAuth={props}
/>
</>
<IssuesView
issues={projectIssues?.results.filter((p) => p.parent === null) ?? []}
userAuth={props}
/>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<EmptySpace

View File

@ -14,10 +14,9 @@ import AppLayout from "layouts/app-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { AllBoards } from "components/core/board-view/all-boards";
import { DeleteModuleModal, ModuleDetailsSidebar, ModulesListView } from "components/modules";
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
import { CreateUpdateIssueModal } from "components/issues";
import { DeleteModuleModal, ModuleDetailsSidebar } from "components/modules";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -53,7 +52,6 @@ const SingleModule: React.FC<UserAuth> = (props) => {
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>(null);
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
const [selectedModuleForDelete, setSelectedModuleForDelete] = useState<SelectModuleType>();
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | null
@ -189,11 +187,6 @@ const SingleModule: React.FC<UserAuth> = (props) => {
issues={issues?.results.filter((i) => !i.issue_module) ?? []}
handleOnSubmit={handleAddIssuesToModule}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={moduleIssuesArray?.find((issue) => issue.id === deleteIssue)}
/>
<DeleteModuleModal
isOpen={
moduleDeleteModal &&
@ -254,21 +247,15 @@ const SingleModule: React.FC<UserAuth> = (props) => {
{moduleIssuesArray ? (
moduleIssuesArray.length > 0 ? (
<div className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}>
<ModulesListView
{/* <ModulesListView
issues={moduleIssuesArray ?? []}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={openIssuesListModal}
removeIssueFromModule={removeIssueFromModule}
setPreloadedData={setPreloadedData}
userAuth={props}
/>
<AllBoards
type="module"
issues={moduleIssuesArray ?? []}
handleDeleteIssue={setDeleteIssue}
openIssuesListModal={openIssuesListModal}
userAuth={props}
/>
/> */}
<IssuesView issues={moduleIssuesArray ?? []} userAuth={props} />
</div>
) : (
<div