import { FC, useMemo } from "react"; import { useRouter } from "next/router"; // components import { IssuePeekOverview } from "components/issues"; import { IssueMainContent } from "./main-content"; import { IssueDetailsSidebar } from "./sidebar"; // ui import { EmptyState } from "components/common"; // images import emptyIssue from "public/empty-state/issue.svg"; // hooks import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "constants/event-tracker"; import { observer } from "mobx-react"; export type TIssueOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; update: ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, showToast?: boolean ) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; removeIssueFromModule?: ( workspaceSlug: string, projectId: string, moduleId: string, issueId: string ) => Promise; removeModulesFromIssue?: ( workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[] ) => Promise; }; export type TIssueDetailRoot = { workspaceSlug: string; projectId: string; issueId: string; is_archived?: boolean; }; export const IssueDetailRoot: FC = observer((props) => { const { workspaceSlug, projectId, issueId, is_archived = false } = props; // router const router = useRouter(); // hooks const { issue: { getIssueById }, fetchIssue, updateIssue, removeIssue, archiveIssue, addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, } = useIssueDetail(); const { issues: { removeIssue: removeArchivedIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); const { captureIssueEvent } = useEventTracker(); const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, } = useUser(); const { theme: themeStore } = useApplication(); const issueOperations: TIssueOperations = useMemo( () => ({ fetch: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await fetchIssue(workspaceSlug, projectId, issueId); } catch (error) { console.error("Error fetching the parent issue"); } }, update: async ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, showToast: boolean = true ) => { try { await updateIssue(workspaceSlug, projectId, issueId, data); if (showToast) { setToastAlert({ title: "Issue updated successfully", type: "success", message: "Issue updated successfully", }); } captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), change_details: Object.values(data).join(","), }, path: router.asPath, }); } catch (error) { captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), change_details: Object.values(data).join(","), }, path: router.asPath, }); setToastAlert({ title: "Issue update failed", type: "error", message: "Issue update failed", }); } }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { let response; if (is_archived) response = await removeArchivedIssue(workspaceSlug, projectId, issueId); else response = await removeIssue(workspaceSlug, projectId, issueId); setToastAlert({ title: "Issue deleted successfully", type: "success", message: "Issue deleted successfully", }); captureIssueEvent({ eventName: ISSUE_DELETED, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, path: router.asPath, }); } catch (error) { setToastAlert({ title: "Issue delete failed", type: "error", message: "Issue delete failed", }); captureIssueEvent({ eventName: ISSUE_DELETED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: router.asPath, }); } }, archive: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await archiveIssue(workspaceSlug, projectId, issueId); setToastAlert({ type: "success", title: "Success!", message: "Issue archived successfully.", }); captureIssueEvent({ eventName: ISSUE_ARCHIVED, payload: { id: issueId, state: "SUCCESS", element: "Issue details page" }, path: router.asPath, }); } catch (error) { setToastAlert({ type: "error", title: "Error!", message: "Issue could not be archived. Please try again.", }); captureIssueEvent({ eventName: ISSUE_ARCHIVED, payload: { id: issueId, state: "FAILED", element: "Issue details page" }, path: router.asPath, }); } }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); setToastAlert({ title: "Cycle added to issue successfully", type: "success", message: "Issue added to issue successfully", }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", change_details: cycleId, }, path: router.asPath, }); } catch (error) { captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", change_details: cycleId, }, path: router.asPath, }); setToastAlert({ title: "Cycle add to issue failed", type: "error", message: "Cycle add to issue failed", }); } }, removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { try { const response = await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); setToastAlert({ title: "Cycle removed from issue successfully", type: "success", message: "Cycle removed from issue successfully", }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", change_details: "", }, path: router.asPath, }); } catch (error) { captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", change_details: "", }, path: router.asPath, }); setToastAlert({ title: "Cycle remove from issue failed", type: "error", message: "Cycle remove from issue failed", }); } }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); setToastAlert({ title: "Module added to issue successfully", type: "success", message: "Module added to issue successfully", }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", change_details: moduleIds, }, path: router.asPath, }); } catch (error) { captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "module_id", change_details: moduleIds, }, path: router.asPath, }); setToastAlert({ title: "Module add to issue failed", type: "error", message: "Module add to issue failed", }); } }, removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); setToastAlert({ title: "Module removed from issue successfully", type: "success", message: "Module removed from issue successfully", }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", change_details: "", }, path: router.asPath, }); } catch (error) { captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "module_id", change_details: "", }, path: router.asPath, }); setToastAlert({ title: "Module remove from issue failed", type: "error", message: "Module remove from issue failed", }); } }, removeModulesFromIssue: async ( workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[] ) => { try { await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); setToastAlert({ type: "success", title: "Successful!", message: "Issue removed from module successfully.", }); } catch (error) { setToastAlert({ type: "error", title: "Error!", message: "Issue could not be removed from module. Please try again.", }); } }, }), [ is_archived, fetchIssue, updateIssue, removeIssue, archiveIssue, removeArchivedIssue, addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, setToastAlert, ] ); // issue details const issue = getIssueById(issueId); // checking if issue is editable, based on user role const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( <> {!issue ? ( router.push(`/${workspaceSlug}/projects/${projectId}/issues`), }} /> ) : (
)} {/* peek overview */} ); });