forked from github/plane
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
|
// 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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;
|
|
||||||
|
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 { 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}
|
||||||
|
@ -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;
|
|
||||||
|
@ -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";
|
||||||
|
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;
|
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;
|
|
||||||
|
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 = {
|
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 }) => {
|
||||||
|
@ -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";
|
||||||
|
@ -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) => {
|
.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();
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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";
|
||||||
|
@ -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 ?? ""),
|
...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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user