forked from github/plane
chore: archived issues restructure. (#3469)
* chore: archived issues restructure. * fix issue detail functionalities --------- Co-authored-by: rahulramesha <rahulramesham@gmail.com>
This commit is contained in:
parent
f8208b1b5e
commit
60b5589c48
@ -1,43 +1,22 @@
|
|||||||
import { useCallback, useEffect, useState, ReactElement } from "react";
|
import { useState, ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
// services
|
|
||||||
import { IssueService, IssueArchiveService } from "services/issue";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useIssueDetail, useIssues, useProject } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
// FIXME: have to replace this once the issue details page is ready --issue-detail--
|
import { IssueDetailRoot } from "components/issues";
|
||||||
// import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
|
||||||
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, Loader } from "@plane/ui";
|
import { ArchiveIcon, Loader } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { History } from "lucide-react";
|
import { History } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// fetch-keys
|
// constants
|
||||||
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
import { useProject } from "hooks/store";
|
|
||||||
|
|
||||||
const defaultValues: Partial<TIssue> = {
|
|
||||||
name: "",
|
|
||||||
// description: "",
|
|
||||||
description_html: "",
|
|
||||||
estimate_point: null,
|
|
||||||
state_id: "",
|
|
||||||
priority: "low",
|
|
||||||
target_date: new Date().toString(),
|
|
||||||
cycle_id: null,
|
|
||||||
module_id: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueService = new IssueService();
|
|
||||||
const issueArchiveService = new IssueArchiveService();
|
|
||||||
|
|
||||||
const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
||||||
// router
|
// router
|
||||||
@ -46,84 +25,43 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
// states
|
// states
|
||||||
const [isRestoring, setIsRestoring] = useState(false);
|
const [isRestoring, setIsRestoring] = useState(false);
|
||||||
// hooks
|
// hooks
|
||||||
|
const {
|
||||||
|
fetchIssue,
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
const {
|
||||||
|
issues: { removeIssueFromArchived },
|
||||||
|
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
|
||||||
const { data: issueDetails, mutate: mutateIssueDetails } = useSWR<TIssue | undefined>(
|
const { isLoading } = useSWR(
|
||||||
workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null,
|
|
||||||
workspaceSlug && projectId && archivedIssueId
|
workspaceSlug && projectId && archivedIssueId
|
||||||
? () =>
|
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
||||||
issueArchiveService.retrieveArchivedIssue(
|
: null,
|
||||||
workspaceSlug as string,
|
workspaceSlug && projectId && archivedIssueId
|
||||||
projectId as string,
|
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true)
|
||||||
archivedIssueId as string
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { reset, control, watch } = useForm<TIssue>({
|
const issue = getIssueById(archivedIssueId?.toString() || "") || undefined;
|
||||||
defaultValues,
|
if (!issue) return <></>;
|
||||||
});
|
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
|
||||||
async (formData: Partial<TIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !archivedIssueId) return;
|
|
||||||
|
|
||||||
mutate<TIssue>(
|
|
||||||
ISSUE_DETAILS(archivedIssueId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload: Partial<TIssue> = {
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
|
|
||||||
await issueService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, archivedIssueId as string, payload)
|
|
||||||
.then(() => {
|
|
||||||
mutateIssueDetails();
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, archivedIssueId, projectId, mutateIssueDetails]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!issueDetails) return;
|
|
||||||
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string));
|
|
||||||
reset({
|
|
||||||
...issueDetails,
|
|
||||||
});
|
|
||||||
}, [issueDetails, reset, archivedIssueId]);
|
|
||||||
|
|
||||||
const handleUnArchive = async () => {
|
const handleUnArchive = async () => {
|
||||||
if (!workspaceSlug || !projectId || !archivedIssueId) return;
|
if (!workspaceSlug || !projectId || !archivedIssueId) return;
|
||||||
|
|
||||||
setIsRestoring(true);
|
setIsRestoring(true);
|
||||||
|
|
||||||
await issueArchiveService
|
await removeIssueFromArchived(workspaceSlug as string, projectId as string, archivedIssueId as string)
|
||||||
.unarchiveIssue(workspaceSlug as string, projectId as string, archivedIssueId as string)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message:
|
message:
|
||||||
issueDetails &&
|
issue &&
|
||||||
`${getProjectById(issueDetails.project_id)?.identifier}-${
|
`${getProjectById(issue.project_id)?.identifier}-${
|
||||||
issueDetails?.sequence_id
|
issue?.sequence_id
|
||||||
} is restored successfully under the project ${getProjectById(issueDetails.project_id)?.name}`,
|
} is restored successfully under the project ${getProjectById(issue.project_id)?.name}`,
|
||||||
});
|
});
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`);
|
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`);
|
||||||
})
|
})
|
||||||
@ -137,45 +75,11 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
.finally(() => setIsRestoring(false));
|
.finally(() => setIsRestoring(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issueLoader = !issue || isLoading ? true : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueDetails && projectId ? (
|
{issueLoader ? (
|
||||||
<div className="flex h-full overflow-hidden">
|
|
||||||
<div className="h-full w-2/3 space-y-2 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
|
||||||
{issueDetails.archived_at && (
|
|
||||||
<div className="flex items-center justify-between gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 px-2.5 py-2 text-sm text-custom-text-200">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ArchiveIcon className="h-3.5 w-3.5" />
|
|
||||||
<p>This issue has been archived by Plane.</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 p-1.5 text-sm"
|
|
||||||
onClick={handleUnArchive}
|
|
||||||
disabled={isRestoring}
|
|
||||||
>
|
|
||||||
<History className="h-3.5 w-3.5" />
|
|
||||||
|
|
||||||
<span>{isRestoring ? "Restoring..." : "Restore Issue"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
|
||||||
{/* <div className="pointer-events-none space-y-5 divide-y-2 divide-custom-border-200 opacity-60">
|
|
||||||
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} uneditable />
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
|
||||||
{/* <div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 p-5">
|
|
||||||
<IssueDetailsSidebar
|
|
||||||
control={control}
|
|
||||||
issueDetail={issueDetails}
|
|
||||||
submitChanges={submitChanges}
|
|
||||||
watch={watch}
|
|
||||||
uneditable
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Loader className="flex h-full gap-5 p-5">
|
<Loader className="flex h-full gap-5 p-5">
|
||||||
<div className="basis-2/3 space-y-2">
|
<div className="basis-2/3 space-y-2">
|
||||||
<Loader.Item height="30px" width="40%" />
|
<Loader.Item height="30px" width="40%" />
|
||||||
@ -190,6 +94,35 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
</div>
|
</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full overflow-hidden">
|
||||||
|
<div className="h-full w-full space-y-2 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||||
|
{issue?.archived_at && (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 px-2.5 py-2 text-sm text-custom-text-200">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArchiveIcon className="h-3.5 w-3.5" />
|
||||||
|
<p>This issue has been archived by Plane.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 rounded-md border border-custom-border-200 p-1.5 text-sm"
|
||||||
|
onClick={handleUnArchive}
|
||||||
|
disabled={isRestoring}
|
||||||
|
>
|
||||||
|
<History className="h-3.5 w-3.5" />
|
||||||
|
<span>{isRestoring ? "Restoring..." : "Restore Issue"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{workspaceSlug && projectId && archivedIssueId && (
|
||||||
|
<IssueDetailRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
issueId={archivedIssueId.toString()}
|
||||||
|
is_archived
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -59,7 +59,6 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
issueId={issueId.toString()}
|
issueId={issueId.toString()}
|
||||||
is_archived={!!issue?.archived_at}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -128,8 +128,8 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
|
|||||||
try {
|
try {
|
||||||
const response = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId);
|
const response = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId);
|
const issueIndex = this.issues[projectId]?.findIndex((_issueId) => _issueId === issueId);
|
||||||
if (issueIndex >= 0)
|
if (issueIndex && issueIndex >= 0)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.issues[projectId].splice(issueIndex, 1);
|
this.issues[projectId].splice(issueIndex, 1);
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { makeObservable } from "mobx";
|
import { makeObservable } from "mobx";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueArchiveService, IssueService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { IIssueDetail } from "./root.store";
|
import { IIssueDetail } from "./root.store";
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
export interface IIssueStoreActions {
|
export interface IIssueStoreActions {
|
||||||
// actions
|
// actions
|
||||||
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
|
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
|
||||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
|
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<TIssue>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<TIssue>;
|
||||||
@ -31,6 +31,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
rootIssueDetailStore: IIssueDetail;
|
rootIssueDetailStore: IIssueDetail;
|
||||||
// services
|
// services
|
||||||
issueService;
|
issueService;
|
||||||
|
issueArchiveService;
|
||||||
|
|
||||||
constructor(rootStore: IIssueDetail) {
|
constructor(rootStore: IIssueDetail) {
|
||||||
makeObservable(this, {});
|
makeObservable(this, {});
|
||||||
@ -38,6 +39,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
this.rootIssueDetailStore = rootStore;
|
this.rootIssueDetailStore = rootStore;
|
||||||
// services
|
// services
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
|
this.issueArchiveService = new IssueArchiveService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper methods
|
// helper methods
|
||||||
@ -47,12 +49,17 @@ export class IssueStore implements IIssueStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
|
||||||
try {
|
try {
|
||||||
const query = {
|
const query = {
|
||||||
expand: "state,assignees,labels,parent",
|
expand: "state,assignees,labels,parent",
|
||||||
};
|
};
|
||||||
const issue = (await this.issueService.retrieve(workspaceSlug, projectId, issueId, query)) as any;
|
|
||||||
|
let issue: any;
|
||||||
|
|
||||||
|
if (isArchived) issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId);
|
||||||
|
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
|
||||||
|
|
||||||
if (!issue) throw new Error("Issue not found");
|
if (!issue) throw new Error("Issue not found");
|
||||||
|
|
||||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]);
|
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]);
|
||||||
|
@ -133,8 +133,8 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
||||||
|
|
||||||
// issue
|
// issue
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) =>
|
||||||
this.issue.fetchIssue(workspaceSlug, projectId, issueId);
|
this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived);
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
||||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
|
Loading…
Reference in New Issue
Block a user