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 // react-beautiful-dnd
import { DragDropContext, Draggable, DropResult } from "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 // hooks
import useIssueView from "hooks/use-issue-view"; import useIssueView from "hooks/use-issue-view";
// components // components
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CommonSingleBoard } from "components/core/board-view/single-board"; import { SingleBoard } from "components/core/board-view/single-board";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// types // types
import { import { IIssue, IProjectMember, IState, UserAuth } from "types";
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 Props = {
type?: "issue" | "cycle" | "module"; type: "issue" | "cycle" | "module";
issues: IIssue[]; issues: IIssue[];
openIssuesListModal?: () => void; states: IState[] | undefined;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>; members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleOnDragEnd: (result: DropResult) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const AllBoards: React.FC<Props> = ({ export const AllBoards: React.FC<Props> = ({
type = "issue", type,
issues, issues,
states,
members,
addIssueToState,
openIssuesListModal, openIssuesListModal,
handleDeleteIssue, handleDeleteIssue,
handleOnDragEnd,
userAuth, userAuth,
}) => { }) => {
const [createIssueModal, setCreateIssueModal] = useState(false); const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
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 ( return (
<> <>
<DeleteIssueModal
isOpen={isIssueDeletionOpen}
handleClose={() => setIsIssueDeletionOpen(false)}
data={issueDeletionData}
/>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
/>
{groupedByIssues ? ( {groupedByIssues ? (
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full"> <div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
@ -265,7 +61,7 @@ export const AllBoards: React.FC<Props> = ({
return ( return (
<Draggable key={singleGroup} draggableId={singleGroup} index={index}> <Draggable key={singleGroup} draggableId={singleGroup} index={index}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<CommonSingleBoard <SingleBoard
type={type} type={type}
provided={provided} provided={provided}
snapshot={snapshot} snapshot={snapshot}
@ -273,17 +69,10 @@ export const AllBoards: React.FC<Props> = ({
groupTitle={singleGroup} groupTitle={singleGroup}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup} selectedGroup={selectedGroup}
addIssueToState={() => { members={members}
setCreateIssueModal(true); addIssueToState={() => addIssueToState(singleGroup, stateId)}
if (selectedGroup)
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: singleGroup,
actionType: "createIssue",
});
}}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null} openIssuesListModal={openIssuesListModal ?? null}
userAuth={userAuth} userAuth={userAuth}
/> />
)} )}

View File

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

View File

@ -19,15 +19,15 @@ import projectService from "services/project.service";
// components // components
import { AssigneesList, CustomDatePicker } from "components/ui"; import { AssigneesList, CustomDatePicker } from "components/ui";
// helpers // helpers
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { findHowManyDaysLeft } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import { import {
CycleIssueResponse, CycleIssueResponse,
IIssue, IIssue,
IProjectMember,
IssueResponse, IssueResponse,
IUserLite, IUserLite,
IWorkspaceMember,
ModuleIssueResponse, ModuleIssueResponse,
Properties, Properties,
UserAuth, UserAuth,
@ -50,12 +50,12 @@ type Props = {
properties: Properties; properties: Properties;
snapshot: DraggableStateSnapshot; snapshot: DraggableStateSnapshot;
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[]; assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
people: IWorkspaceMember[] | undefined; people: IProjectMember[] | undefined;
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>; handleDeleteIssue: (issue: IIssue) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
const SingleBoardIssue: React.FC<Props> = ({ export const SingleBoardIssue: React.FC<Props> = ({
type, type,
typeId, typeId,
issue, issue,
@ -164,12 +164,12 @@ const SingleBoardIssue: React.FC<Props> = ({
}`} }`}
> >
<div className="group/card relative select-none p-2"> <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"> <div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
<button <button
type="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" 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" /> <TrashIcon className="h-4 w-4" />
</button> </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"> <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) => ( {people?.map((person) => (
<Listbox.Option <Listbox.Option
key={person.id} key={person.member.id}
className={({ active }) => className={({ active }) =>
`cursor-pointer select-none p-2 ${active ? "bg-indigo-50" : "bg-white"}` `cursor-pointer select-none p-2 ${active ? "bg-indigo-50" : "bg-white"}`
} }
@ -457,5 +457,3 @@ const SingleBoardIssue: React.FC<Props> = ({
</div> </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 "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal"; export * from "./existing-issues-list-modal";
export * from "./image-upload-modal"; export * from "./image-upload-modal";
export * from "./issues-filter-view"; export * from "./issues-view-filter";
export * from "./issues-view";
export * from "./not-authorized-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; properties: Properties;
editIssue: () => void; editIssue: () => void;
removeIssue?: () => void; removeIssue?: () => void;
handleDeleteIssue: (issue: IIssue) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
const SingleListIssue: React.FC<Props> = ({ export const SingleListIssue: React.FC<Props> = ({
type, type,
typeId, typeId,
issue, issue,
properties, properties,
editIssue, editIssue,
removeIssue, removeIssue,
handleDeleteIssue,
userAuth, userAuth,
}) => { }) => {
const [deleteIssue, setDeleteIssue] = useState<IIssue | undefined>();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -151,284 +151,275 @@ const SingleListIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( return (
<> <div className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<DeleteIssueModal <div className="flex items-center gap-2">
handleClose={() => setDeleteIssue(undefined)} <span
isOpen={!!deleteIssue} className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
data={deleteIssue} 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 <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`} <a className="group relative flex items-center gap-2">
style={{ {properties.key && (
backgroundColor: issue.state_detail.color, <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"
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> disabled={isNotAllowed}
<a className="group relative flex items-center gap-2"> >
{properties.key && ( {({ open }) => (
<span className="flex-shrink-0 text-xs text-gray-500"> <>
{issue.project_detail?.identifier}-{issue.sequence_id} <div>
</span> <Listbox.Button
)} className={`flex ${
<span>{issue.name}</span> isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
</a> } 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 ${
</Link> issue.priority === "urgent"
</div> ? "bg-red-100 text-red-600"
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs"> : issue.priority === "high"
{properties.priority && ( ? "bg-orange-100 text-orange-500"
<Listbox : issue.priority === "medium"
as="div" ? "bg-yellow-100 text-yellow-500"
value={issue.priority} : issue.priority === "low"
onChange={(data: string) => { ? "bg-green-100 text-green-500"
partialUpdateIssue({ priority: data }); : "bg-gray-100"
}} }`}
className="group relative flex-shrink-0" >
disabled={isNotAllowed} {getPriorityIcon(
> issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
{({ open }) => ( "text-sm"
<> )}
<div> </Listbox.Button>
<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 <Transition
show={open} show={open}
as={React.Fragment} as={React.Fragment}
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" 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"> <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) => ( {PRIORITIES?.map((priority) => (
<Listbox.Option <Listbox.Option
key={priority} key={priority}
className={({ active }) => className={({ active }) =>
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${ `flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
active ? "bg-indigo-50" : "bg-white" active ? "bg-indigo-50" : "bg-white"
}` }`
} }
value={priority} value={priority}
> >
{getPriorityIcon(priority, "text-sm")} {getPriorityIcon(priority, "text-sm")}
{priority ?? "None"} {priority ?? "None"}
</Listbox.Option> </Listbox.Option>
))} ))}
</Listbox.Options> </Listbox.Options>
</Transition> </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>
<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"> </div>
<h5 className="mb-1 font-medium text-gray-900">Priority</h5> </>
<div )}
className={`capitalize ${ </Listbox>
issue.priority === "urgent" )}
? "text-red-600" {properties.state && (
: issue.priority === "high" <CustomSelect
? "text-orange-500" label={
: issue.priority === "medium" <>
? "text-yellow-500" <span
: issue.priority === "low" className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
? "text-green-500" style={{
: "" backgroundColor: issue.state_detail.color,
}`} }}
> />
{issue.priority ?? "None"} {addSpaceIfCamelCase(issue.state_detail.name)}
</div> </>
</div> }
</> value={issue.state}
)} onChange={(data: string) => {
</Listbox> partialUpdateIssue({ state: data });
)} }}
{properties.state && ( maxHeight="md"
<CustomSelect noChevron
label={ disabled={isNotAllowed}
>
{states?.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<> <>
<span <span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: issue.state_detail.color, backgroundColor: state.color,
}} }}
/> />
{addSpaceIfCamelCase(issue.state_detail.name)} {addSpaceIfCamelCase(state.name)}
</> </>
} </CustomSelect.Option>
value={issue.state} ))}
onChange={(data: string) => { </CustomSelect>
partialUpdateIssue({ state: data }); )}
}} {/* {properties.cycle && !typeId && (
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 && (
<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"> <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"} {issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"}
</div> </div>
)} */} )} */}
{properties.due_date && ( {properties.due_date && (
<div <div
className={`group relative ${ className={`group relative ${
issue.target_date === null issue.target_date === null
? "" ? ""
: issue.target_date < new Date().toISOString() : issue.target_date < new Date().toISOString()
? "text-red-600" ? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400" : findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
}`} }`}
> >
<CustomDatePicker <CustomDatePicker
placeholder="N/A" placeholder="N/A"
value={issue?.target_date} value={issue?.target_date}
onChange={(val) => onChange={(val) =>
partialUpdateIssue({ partialUpdateIssue({
target_date: val, target_date: val,
}) })
} }
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} 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"> <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> <h5 className="mb-1 font-medium text-gray-900">Due date</h5>
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div> <div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
<div> <div>
{issue.target_date {issue.target_date
? issue.target_date < new Date().toISOString() ? issue.target_date < new Date().toISOString()
? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days` ? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days`
: findHowManyDaysLeft(issue.target_date) <= 3 : findHowManyDaysLeft(issue.target_date) <= 3
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days` ? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
: "Due date" : "Due date"
: "N/A"} : "N/A"}
</div>
</div> </div>
</div> </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"> {properties.sub_issue_count && (
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} <div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm">
</div> {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
)} </div>
{properties.assignee && ( )}
<Listbox {properties.assignee && (
as="div" <Listbox
value={issue.assignees} as="div"
onChange={(data: any) => { value={issue.assignees}
const newData = issue.assignees ?? []; onChange={(data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data); else newData.push(data);
partialUpdateIssue({ assignees_list: newData }); partialUpdateIssue({ assignees_list: newData });
}} }}
className="group relative flex-shrink-0" className="group relative flex-shrink-0"
disabled={isNotAllowed} disabled={isNotAllowed}
> >
{({ open }) => ( {({ open }) => (
<> <>
<div> <div>
<Listbox.Button> <Listbox.Button>
<div <div
className={`flex ${ className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-1 text-xs`} } 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"
> >
<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"> <AssigneesList userIds={issue.assignees ?? []} />
{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> </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> </div>
</> </div>
)} </>
</Listbox> )}
)} </Listbox>
{type && !isNotAllowed && ( )}
<CustomMenu width="auto" ellipsis> {type && !isNotAllowed && (
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem> <CustomMenu width="auto" ellipsis>
{type !== "issue" && ( <CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={removeIssue}> {type !== "issue" && (
<>Remove from {type}</> <CustomMenu.MenuItem onClick={removeIssue}>
</CustomMenu.MenuItem> <>Remove from {type}</>
)}
<CustomMenu.MenuItem onClick={() => setDeleteIssue(issue)}>
Delete permanently
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> )}
)} <CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
</div> Delete permanently
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div> </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 = { type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
data?: IIssue; data: IIssue | null;
}; };
export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data }) => { 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 "./delete-issue-modal";
export * from "./description-form"; export * from "./description-form";
export * from "./form"; export * from "./form";
export * from "./list-view";
export * from "./modal"; export * from "./modal";
export * from "./my-issues-list-item"; export * from "./my-issues-list-item";
export * from "./parent-issues-list-modal"; 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) => { .then((res) => {
if (isUpdatingSingleIssue) { if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else } else {
mutate<IssueResponse>( mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""), PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""),
(prevData) => ({ (prevData) => ({
@ -186,8 +186,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}), }),
}) })
); );
}
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) handleClose(); if (!createMore) handleClose();

View File

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

View File

@ -2,7 +2,6 @@ export * from "./select";
export * from "./sidebar-select"; export * from "./sidebar-select";
export * from "./delete-module-modal"; export * from "./delete-module-modal";
export * from "./form"; export * from "./form";
export * from "./list-view";
export * from "./modal"; export * from "./modal";
export * from "./module-link-modal"; export * from "./module-link-modal";
export * from "./sidebar"; 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 ?? ""), ...groupBy(projectIssues ?? [], groupByProperty ?? ""),
}; };
if (groupByProperty === "priority") delete groupedByIssues.None;
if (orderBy) { if (orderBy) {
groupedByIssues = Object.fromEntries( groupedByIssues = Object.fromEntries(
Object.entries(groupedByIssues).map(([key, value]) => [ Object.entries(groupedByIssues).map(([key, value]) => [
@ -74,7 +76,7 @@ const useIssueView = (projectIssues: IIssue[]) => {
); );
} }
if (filterIssue !== null) { if (filterIssue) {
if (filterIssue === "activeIssue") { if (filterIssue === "activeIssue") {
const filteredStates = states?.filter( const filteredStates = states?.filter(
(state) => state.group === "started" || state.group === "unstarted" (state) => state.group === "started" || state.group === "unstarted"

View File

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

View File

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

View File

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