[WEB-3268] feat: url pattern (#6546)
* feat: meta endpoint for issue * chore: add detail endpoint * chore: getIssueMetaFromURL and retrieveWithIdentifier endpoint added * chore: issue store updated * chore: move issue detail to new route and add redirection for old route * fix: issue details permission * fix: work item detail header * chore: generateWorkItemLink helper function added * chore: copyTextToClipboard helper function updated * chore: workItemLink updated * chore: workItemLink updated * chore: workItemLink updated * fix: issues navigation tab active status * fix: invalid workitem error state * chore: peek view parent issue redirection improvement * fix: issue detail endpoint to not return epics and intake issue * fix: workitem empty state redirection and header * fix: workitem empty state redirection and header * chore: code refactor * chore: project auth wrapper improvement --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
82eea3e802
commit
4353cc0c4a
51 changed files with 1032 additions and 282 deletions
|
|
@ -1,107 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { IssueDetailRoot } from "@/components/issues";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
// hooks
|
||||
import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store";
|
||||
// assets
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import emptyIssueDark from "@/public/empty-state/search/issues-dark.webp";
|
||||
import emptyIssueLight from "@/public/empty-state/search/issues-light.webp";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue/issue.service";
|
||||
|
||||
const issueService = new IssueService();
|
||||
|
||||
const IssueDetailsPage = observer(() => {
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
fetchIssue,
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme();
|
||||
// fetching work item details
|
||||
const { isLoading, error } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null
|
||||
);
|
||||
// derived values
|
||||
const issue = getIssueById(issueId?.toString() || "") || undefined;
|
||||
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
|
||||
const issueLoader = !issue || isLoading;
|
||||
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const handleToggleIssueDetailSidebar = () => {
|
||||
if (window && window.innerWidth < 768) {
|
||||
toggleIssueDetailSidebar(true);
|
||||
}
|
||||
if (window && issueDetailSidebarCollapsed && window.innerWidth >= 768) {
|
||||
toggleIssueDetailSidebar(false);
|
||||
const redirectToBrowseUrl = async () => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
try {
|
||||
const meta = await issueService.getIssueMetaFromURL(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString()
|
||||
);
|
||||
router.push(`/${workspaceSlug}/browse/${meta.project_identifier}-${meta.sequence_id}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
window.addEventListener("resize", handleToggleIssueDetailSidebar);
|
||||
handleToggleIssueDetailSidebar();
|
||||
return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar);
|
||||
}, [issueDetailSidebarCollapsed, toggleIssueDetailSidebar]);
|
||||
|
||||
redirectToBrowseUrl();
|
||||
}, [workspaceSlug, projectId, issueId, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={resolvedTheme === "dark" ? emptyIssueDark : emptyIssueLight}
|
||||
title={t("issue.empty_state.issue_detail.title")}
|
||||
description={t("issue.empty_state.issue_detail.description")}
|
||||
primaryButton={{
|
||||
text: t("issue.empty_state.issue_detail.primary_button.text"),
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
|
||||
}}
|
||||
/>
|
||||
) : issueLoader ? (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="40%" />
|
||||
</div>
|
||||
<div className="basis-1/3 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
) : (
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
issueId && (
|
||||
<IssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={issueId.toString()}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
<div className="flex items-center justify-center size-full">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Breadcrumbs, LayersIcon, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { IssueDetailQuickActions } from "@/components/issues";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
export const ProjectIssueDetailsHeader = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
// derived values
|
||||
const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues`}
|
||||
label={t("issue.label", { count: 2 })} // count is for pluralization
|
||||
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label={
|
||||
currentProjectDetails && issueDetails
|
||||
? `${currentProjectDetails.identifier}-${issueDetails.sequence_id}`
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<IssueDetailQuickActions
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={issueId.toString()}
|
||||
/>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// components
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
import { ProjectIssueDetailsHeader } from "./header";
|
||||
|
||||
export default function ProjectIssueDetailsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<ProjectIssueDetailsHeader />} />
|
||||
<ContentWrapper className="overflow-hidden">{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,18 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane web layouts
|
||||
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
||||
|
||||
const ProjectDetailLayout = ({ children }: { children: ReactNode }) => (
|
||||
<ProjectAuthWrapper>{children}</ProjectAuthWrapper>
|
||||
);
|
||||
const ProjectDetailLayout = ({ children }: { children: ReactNode }) => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
return (
|
||||
<ProjectAuthWrapper workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()}>
|
||||
{children}
|
||||
</ProjectAuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetailLayout;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue