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:
Prateek Shourya 2024-01-25 18:09:01 +05:30 committed by GitHub
parent f8208b1b5e
commit 60b5589c48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 72 additions and 133 deletions

View File

@ -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>
)} )}
</> </>
); );

View File

@ -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}
/> />
) )
)} )}

View File

@ -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);
}); });

View File

@ -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]);

View File

@ -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) =>