forked from github/plane
9075f9441c
* style: added cta at the bottom of sidebar, added missing icons as well, showing dynamic workspace member count on workspace dropdown * refractor: running parallel request, made create/edit label function to async function * fix: sidebar dropdown content going below kanban items outside click detection in need help dropdown * refractor: making parallel api calls fix: create state input comes at bottom, create state input gets on focus automatically, form is getting submitted on enter click * refactoring file structure and signin page * style: changed text and added spinner for signing in loading * refractor: removed unused type * fix: my issue cta in profile page sending to 404 page * fix: added new s3 bucket url in next.config.js file increased image modal height * packaging UI components * eslint config * eslint fixes * refactoring changes * build fixes * minor fixes * adding todo comments for reference * refactor: cleared unused imports and re ordered imports * refactor: removed unused imports * fix: added workspace argument to useissues hook * refactor: removed api-routes file, unnecessary constants * refactor: created helpers folder, removed unnecessary constants * refactor: new context for issue view * refactoring issues page * build fixes * refactoring * refactor: create issue modal * refactor: module ui * fix: sub-issues mutation * fix: create more option in create issue modal * description form debounce issue * refactor: global component for assignees list * fix: link module interface * fix: priority icons and sub-issues count added * fix: cycle mutation in issue details page * fix: remove issue from cycle mutation * fix: create issue modal in home page * fix: removed unnecessary props * fix: updated create issue form status * fix: settings auth breaking * refactor: issue details page Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: venkatesh-soulpage <venkatesh.marreboyina@soulpageit.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
import React, { useState } from "react";
|
|
|
|
import { useRouter } from "next/router";
|
|
|
|
import useSWR, { mutate } from "swr";
|
|
|
|
// layouts
|
|
import AppLayout from "layouts/app-layout";
|
|
// contexts
|
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
|
// icons
|
|
import {
|
|
ArrowLeftIcon,
|
|
ArrowPathIcon,
|
|
ListBulletIcon,
|
|
PlusIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
// components
|
|
import CyclesListView from "components/project/cycles/list-view";
|
|
import CyclesBoardView from "components/project/cycles/board-view";
|
|
import { CreateUpdateIssueModal } from "components/issues";
|
|
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
|
import ExistingIssuesListModal from "components/common/existing-issues-list-modal";
|
|
import CycleDetailSidebar from "components/project/cycles/cycle-detail-sidebar";
|
|
import View from "components/core/view";
|
|
// services
|
|
import issuesServices from "services/issues.service";
|
|
import cycleServices from "services/cycles.service";
|
|
import projectService from "services/project.service";
|
|
// ui
|
|
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
|
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|
// icons
|
|
// types
|
|
import { CycleIssueResponse, IIssue, SelectIssue } from "types";
|
|
// fetch-keys
|
|
import {
|
|
CYCLE_ISSUES,
|
|
CYCLE_LIST,
|
|
PROJECT_ISSUES_LIST,
|
|
PROJECT_MEMBERS,
|
|
PROJECT_DETAILS,
|
|
} from "constants/fetch-keys";
|
|
|
|
const SingleCycle: React.FC = () => {
|
|
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
|
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
|
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
|
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
|
|
const [cycleSidebar, setCycleSidebar] = useState(false);
|
|
|
|
const [preloadedData, setPreloadedData] = useState<
|
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | null
|
|
>(null);
|
|
|
|
const router = useRouter();
|
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
|
|
|
const { data: activeProject } = useSWR(
|
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
|
workspaceSlug && projectId
|
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
|
: null
|
|
);
|
|
|
|
const { data: issues } = useSWR(
|
|
workspaceSlug && projectId
|
|
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
|
: null,
|
|
workspaceSlug && projectId
|
|
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
|
: null
|
|
);
|
|
|
|
const { data: cycles } = useSWR(
|
|
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
|
|
workspaceSlug && projectId
|
|
? () => cycleServices.getCycles(workspaceSlug as string, projectId as string)
|
|
: null
|
|
);
|
|
|
|
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
|
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
|
workspaceSlug && projectId && cycleId
|
|
? () =>
|
|
cycleServices.getCycleIssues(
|
|
workspaceSlug as string,
|
|
projectId as string,
|
|
cycleId as string
|
|
)
|
|
: null
|
|
);
|
|
const cycleIssuesArray = cycleIssues?.map((issue) => ({
|
|
...issue.issue_detail,
|
|
sub_issues_count: issue.sub_issues_count,
|
|
bridge: issue.id,
|
|
}));
|
|
|
|
const { data: members } = useSWR(
|
|
workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug as string) : null,
|
|
workspaceSlug && projectId
|
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
|
: null,
|
|
{
|
|
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
|
|
if (err?.status === 403) return;
|
|
setTimeout(() => revalidate(revalidateOpts), 5000);
|
|
},
|
|
}
|
|
);
|
|
|
|
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
|
if (!workspaceSlug || !projectId) return;
|
|
issuesServices
|
|
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData)
|
|
.then(() => {
|
|
mutate(CYCLE_ISSUES(cycleId as string));
|
|
})
|
|
.catch((error) => {
|
|
console.log(error);
|
|
});
|
|
};
|
|
|
|
const openCreateIssueModal = (
|
|
issue?: IIssue,
|
|
actionType: "create" | "edit" | "delete" = "create"
|
|
) => {
|
|
if (issue) {
|
|
setPreloadedData(null);
|
|
setSelectedIssues({ ...issue, actionType });
|
|
} else setSelectedIssues(null);
|
|
|
|
setIsIssueModalOpen(true);
|
|
};
|
|
|
|
const openIssuesListModal = () => {
|
|
setCycleIssuesListModal(true);
|
|
};
|
|
|
|
const handleAddIssuesToCycle = async (data: { issues: string[] }) => {
|
|
if (workspaceSlug && projectId) {
|
|
await issuesServices
|
|
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
|
|
.then((res) => {
|
|
console.log(res);
|
|
mutate(CYCLE_ISSUES(cycleId as string));
|
|
})
|
|
.catch((e) => {
|
|
console.log(e);
|
|
});
|
|
}
|
|
};
|
|
|
|
const removeIssueFromCycle = (bridgeId: string) => {
|
|
if (!workspaceSlug || !projectId) return;
|
|
|
|
mutate<CycleIssueResponse[]>(
|
|
CYCLE_ISSUES(cycleId as string),
|
|
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
|
|
false
|
|
);
|
|
|
|
issuesServices
|
|
.removeIssueFromCycle(
|
|
workspaceSlug as string,
|
|
projectId as string,
|
|
cycleId as string,
|
|
bridgeId
|
|
)
|
|
.then((res) => {
|
|
console.log(res);
|
|
})
|
|
.catch((e) => {
|
|
console.log(e);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<IssueViewContextProvider>
|
|
<CreateUpdateIssueModal
|
|
isOpen={isIssueModalOpen && selectedIssues?.actionType !== "delete"}
|
|
data={selectedIssues}
|
|
prePopulateData={
|
|
preloadedData
|
|
? { cycle: cycleId as string, ...preloadedData }
|
|
: { cycle: cycleId as string, ...selectedIssues }
|
|
}
|
|
handleClose={() => setIsIssueModalOpen(false)}
|
|
/>
|
|
<ExistingIssuesListModal
|
|
isOpen={cycleIssuesListModal}
|
|
handleClose={() => setCycleIssuesListModal(false)}
|
|
type="cycle"
|
|
issues={issues?.results.filter((i) => !i.issue_cycle) ?? []}
|
|
handleOnSubmit={handleAddIssuesToCycle}
|
|
/>
|
|
<ConfirmIssueDeletion
|
|
handleClose={() => setDeleteIssue(undefined)}
|
|
isOpen={!!deleteIssue}
|
|
data={issues?.results.find((issue) => issue.id === deleteIssue)}
|
|
/>
|
|
<AppLayout
|
|
breadcrumbs={
|
|
<Breadcrumbs>
|
|
<BreadcrumbItem
|
|
title={`${activeProject?.name ?? "Project"} Cycles`}
|
|
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
|
|
/>
|
|
</Breadcrumbs>
|
|
}
|
|
left={
|
|
<CustomMenu
|
|
label={
|
|
<>
|
|
<ArrowPathIcon className="h-3 w-3" />
|
|
{cycles?.find((c) => c.id === cycleId)?.name}
|
|
</>
|
|
}
|
|
className="ml-1.5"
|
|
width="auto"
|
|
>
|
|
{cycles?.map((cycle) => (
|
|
<CustomMenu.MenuItem
|
|
key={cycle.id}
|
|
renderAs="a"
|
|
href={`/${workspaceSlug}/projects/${activeProject?.id}/cycles/${cycle.id}`}
|
|
>
|
|
{cycle.name}
|
|
</CustomMenu.MenuItem>
|
|
))}
|
|
</CustomMenu>
|
|
}
|
|
right={
|
|
<div
|
|
className={`flex items-center gap-2 ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}
|
|
>
|
|
<View issues={cycleIssuesArray ?? []} />
|
|
<button
|
|
type="button"
|
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100 ${
|
|
cycleSidebar ? "rotate-180" : ""
|
|
}`}
|
|
onClick={() => setCycleSidebar((prevData) => !prevData)}
|
|
>
|
|
<ArrowLeftIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
}
|
|
>
|
|
{cycleIssuesArray ? (
|
|
cycleIssuesArray.length > 0 ? (
|
|
<div className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} duration-300`}>
|
|
<CyclesListView
|
|
issues={cycleIssuesArray ?? []}
|
|
openCreateIssueModal={openCreateIssueModal}
|
|
openIssuesListModal={openIssuesListModal}
|
|
removeIssueFromCycle={removeIssueFromCycle}
|
|
setPreloadedData={setPreloadedData}
|
|
/>
|
|
<CyclesBoardView
|
|
issues={cycleIssuesArray ?? []}
|
|
removeIssueFromCycle={removeIssueFromCycle}
|
|
members={members}
|
|
openCreateIssueModal={openCreateIssueModal}
|
|
openIssuesListModal={openIssuesListModal}
|
|
handleDeleteIssue={setDeleteIssue}
|
|
partialUpdateIssue={partialUpdateIssue}
|
|
setPreloadedData={setPreloadedData}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={`flex h-full flex-col items-center justify-center px-4 ${
|
|
cycleSidebar ? "mr-[24rem]" : ""
|
|
} duration-300`}
|
|
>
|
|
<EmptySpace
|
|
title="You don't have any issue yet."
|
|
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
|
|
Icon={ArrowPathIcon}
|
|
>
|
|
<EmptySpaceItem
|
|
title="Create a new issue"
|
|
description="Click to create a new issue inside the cycle."
|
|
Icon={PlusIcon}
|
|
action={() => openCreateIssueModal()}
|
|
/>
|
|
<EmptySpaceItem
|
|
title="Add an existing issue"
|
|
description="Open list"
|
|
Icon={ListBulletIcon}
|
|
action={() => openIssuesListModal()}
|
|
/>
|
|
</EmptySpace>
|
|
</div>
|
|
)
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center">
|
|
<Spinner />
|
|
</div>
|
|
)}
|
|
<CycleDetailSidebar
|
|
cycle={cycles?.find((c) => c.id === (cycleId as string))}
|
|
isOpen={cycleSidebar}
|
|
cycleIssues={cycleIssues ?? []}
|
|
/>
|
|
</AppLayout>
|
|
</IssueViewContextProvider>
|
|
);
|
|
};
|
|
|
|
export default SingleCycle;
|