mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
refactor: integrated global list view everywhere
This commit is contained in:
parent
85b7f39ed3
commit
d673aedf48
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
4
apps/app/components/core/board-view/index.ts
Normal file
4
apps/app/components/core/board-view/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./all-boards";
|
||||
export * from "./board-header";
|
||||
export * from "./single-board";
|
||||
export * from "./single-issue";
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
408
apps/app/components/core/issues-view.tsx
Normal file
408
apps/app/components/core/issues-view.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
60
apps/app/components/core/list-view/all-lists.tsx
Normal file
60
apps/app/components/core/list-view/all-lists.tsx
Normal 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>
|
||||
);
|
||||
};
|
3
apps/app/components/core/list-view/index.ts
Normal file
3
apps/app/components/core/list-view/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./all-lists";
|
||||
export * from "./single-issue";
|
||||
export * from "./single-list";
|
@ -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;
|
||||
|
142
apps/app/components/core/list-view/single-list.tsx
Normal file
142
apps/app/components/core/list-view/single-list.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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 }) => {
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user