-
Notifications
You must be signed in to change notification settings - Fork 3.5k
[WEB-5170] feat: navigation revamp #8162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
1af42a7
4b738e1
9b2c66a
ac353f3
e77baef
90afb26
d452baf
1c30b89
2a94eb8
2162a67
54a9109
32f3bd5
ed054a8
dca4a99
08f5bb3
36e145c
a2761a0
45d68b9
1ef6ebe
2733d2e
f56f9a9
e103462
56b327d
92867ba
c93dbc2
b454230
d775be7
c64d3fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,61 +1,43 @@ | ||
| import { observer } from "mobx-react"; | ||
| import { useParams } from "next/navigation"; | ||
| // plane imports | ||
| import { EProjectFeatureKey } from "@plane/constants"; | ||
| import { Breadcrumbs, Header } from "@plane/ui"; | ||
| // components | ||
| import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; | ||
| import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions"; | ||
| // hooks | ||
| import { Header, Row } from "@plane/ui"; | ||
| import { AppHeader } from "@/components/core/app-header"; | ||
| import { TabNavigationRoot } from "@/components/navigation"; | ||
| import { useIssueDetail } from "@/hooks/store/use-issue-detail"; | ||
| import { useProject } from "@/hooks/store/use-project"; | ||
| import { useAppRouter } from "@/hooks/use-app-router"; | ||
| import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common"; | ||
| // local components | ||
| import { WorkItemDetailsHeader } from "./work-item-header"; | ||
|
|
||
| export const ProjectIssueDetailsHeader = observer(function ProjectIssueDetailsHeader() { | ||
| export const ProjectWorkItemDetailsHeader = observer(function ProjectWorkItemDetailsHeader() { | ||
| // router | ||
| const router = useAppRouter(); | ||
| const { workspaceSlug, workItem } = useParams(); | ||
| // store hooks | ||
| const { getProjectById, loader } = useProject(); | ||
| const { | ||
| issue: { getIssueById, getIssueIdByIdentifier }, | ||
| } = useIssueDetail(); | ||
| // derived values | ||
| const issueId = getIssueIdByIdentifier(workItem?.toString()); | ||
| const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined; | ||
| const projectId = issueDetails ? issueDetails?.project_id : undefined; | ||
| const projectDetails = projectId ? getProjectById(projectId?.toString()) : undefined; | ||
|
|
||
| if (!workspaceSlug || !projectId || !issueId) return null; | ||
| const issueDetails = issueId ? getIssueById(issueId?.toString()) : undefined; | ||
|
|
||
| return ( | ||
| <Header> | ||
| <Header.LeftItem> | ||
| <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> | ||
| <CommonProjectBreadcrumbs | ||
| workspaceSlug={workspaceSlug?.toString()} | ||
| projectId={projectId?.toString()} | ||
| featureKey={EProjectFeatureKey.WORK_ITEMS} | ||
| /> | ||
| <Breadcrumbs.Item | ||
| component={ | ||
| <BreadcrumbLink | ||
| label={projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""} | ||
| /> | ||
| } | ||
| /> | ||
| </Breadcrumbs> | ||
| </Header.LeftItem> | ||
| <Header.RightItem> | ||
| {projectId && issueId && ( | ||
| <IssueDetailQuickActions | ||
| workspaceSlug={workspaceSlug?.toString()} | ||
| projectId={projectId?.toString()} | ||
| issueId={issueId?.toString()} | ||
| /> | ||
| )} | ||
| </Header.RightItem> | ||
| </Header> | ||
| <> | ||
| <div className="z-20"> | ||
| <Row className="h-header flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100"> | ||
| <div className="flex items-center gap-2 divide-x divide-custom-border-100 h-full w-full"> | ||
| <div className="flex items-center h-full w-full flex-1"> | ||
| <Header className="h-full"> | ||
| <Header.LeftItem className="h-full max-w-full"> | ||
| <TabNavigationRoot | ||
| workspaceSlug={workspaceSlug} | ||
| projectId={issueDetails?.project_id?.toString() ?? ""} | ||
| /> | ||
| </Header.LeftItem> | ||
| </Header> | ||
| </div> | ||
| </div> | ||
| </Row> | ||
| </div> | ||
| <AppHeader header={<WorkItemDetailsHeader />} /> | ||
| </> | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
| import { observer } from "mobx-react"; | ||
| import { useParams } from "next/navigation"; | ||
| // plane ui | ||
| import { WorkItemsIcon } from "@plane/propel/icons"; | ||
| import { Breadcrumbs, Header } from "@plane/ui"; | ||
| // components | ||
| import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; | ||
| import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions"; | ||
| // hooks | ||
| import { useIssueDetail } from "@/hooks/store/use-issue-detail"; | ||
| import { useProject } from "@/hooks/store/use-project"; | ||
| import { useAppRouter } from "@/hooks/use-app-router"; | ||
|
|
||
| export const WorkItemDetailsHeader = observer(() => { | ||
| // router | ||
| const router = useAppRouter(); | ||
| const { workspaceSlug, workItem } = useParams(); | ||
| // store hooks | ||
| const { getProjectById, loader } = useProject(); | ||
| const { | ||
| issue: { getIssueById, getIssueIdByIdentifier }, | ||
| } = useIssueDetail(); | ||
| // derived values | ||
| const issueId = getIssueIdByIdentifier(workItem?.toString()); | ||
| const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined; | ||
| const projectId = issueDetails ? issueDetails?.project_id : undefined; | ||
| const projectDetails = projectId ? getProjectById(projectId?.toString()) : undefined; | ||
|
|
||
| if (!workspaceSlug || !projectId || !issueId) return null; | ||
| return ( | ||
| <Header> | ||
| <Header.LeftItem> | ||
| <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> | ||
| <Breadcrumbs.Item | ||
| component={ | ||
| <BreadcrumbLink | ||
| label="Work Items" | ||
| href={`/${workspaceSlug}/projects/${projectId}/issues/`} | ||
| icon={<WorkItemsIcon className="h-4 w-4 text-custom-text-300" />} | ||
| /> | ||
| } | ||
| /> | ||
| <Breadcrumbs.Item | ||
| component={ | ||
| <BreadcrumbLink | ||
| label={projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""} | ||
| /> | ||
| } | ||
| /> | ||
| </Breadcrumbs> | ||
| </Header.LeftItem> | ||
| <Header.RightItem> | ||
| {projectId && issueId && ( | ||
| <IssueDetailQuickActions | ||
| workspaceSlug={workspaceSlug?.toString()} | ||
| projectId={projectId?.toString()} | ||
| issueId={issueId?.toString()} | ||
| /> | ||
| )} | ||
| </Header.RightItem> | ||
| </Header> | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; | |
| import { Plus, Search } from "lucide-react"; | ||
| import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; | ||
| import { useTranslation } from "@plane/i18n"; | ||
| import { EmptyStateCompact } from "@plane/propel/empty-state"; | ||
| import { TOAST_TYPE, setToast } from "@plane/propel/toast"; | ||
| import { Tooltip } from "@plane/propel/tooltip"; | ||
| import { copyUrlToClipboard, orderJoinedProjects } from "@plane/utils"; | ||
|
|
@@ -102,7 +103,7 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar() | |
| handleClose={handleClose} | ||
| excludedElementId="extended-project-sidebar-toggle" | ||
| > | ||
| <div className="flex flex-col gap-1 w-full sticky top-4 pt-0 px-4"> | ||
| <div className="flex flex-col gap-1 w-full sticky top-4 pt-0"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Padding inconsistency between empty state and list. The horizontal padding differs across states:
When users switch between empty and populated states (via search), the content will shift horizontally due to mismatched left padding (6 units vs 4 units). Additionally, the list's missing right padding may cause the scrollbar to overlap content. Consider applying consistent horizontal padding: -<div className="flex flex-col gap-1 w-full sticky top-4 pt-0">
+<div className="flex flex-col gap-1 w-full sticky top-4 pt-0 px-4">-<div className="flex flex-col items-center mt-4 px-6 pt-10">
+<div className="flex flex-col items-center mt-4 px-4 pt-10">-<div className="flex flex-col gap-0.5 overflow-x-hidden overflow-y-auto vertical-scrollbar scrollbar-sm flex-grow mt-4 pl-4">
+<div className="flex flex-col gap-0.5 overflow-x-hidden overflow-y-auto vertical-scrollbar scrollbar-sm flex-grow mt-4 px-4">Also applies to: 136-136, 146-146 🤖 Prompt for AI Agents |
||
| <div className="flex items-center justify-between"> | ||
| <span className="text-sm font-semibold text-custom-text-300 py-1.5">Projects</span> | ||
| {isAuthorizedUser && ( | ||
|
|
@@ -131,21 +132,33 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar() | |
| /> | ||
| </div> | ||
| </div> | ||
| <div className="flex flex-col gap-0.5 overflow-x-hidden overflow-y-auto vertical-scrollbar scrollbar-sm flex-grow mt-4 px-4"> | ||
| {filteredProjects.map((projectId, index) => ( | ||
| <SidebarProjectsListItem | ||
| key={projectId} | ||
| projectId={projectId} | ||
| handleCopyText={() => handleCopyText(projectId)} | ||
| projectListType={"JOINED"} | ||
| disableDrag={false} | ||
| disableDrop={false} | ||
| isLastChild={index === joinedProjects.length - 1} | ||
| handleOnProjectDrop={handleOnProjectDrop} | ||
| renderInExtendedSidebar | ||
| {filteredProjects.length === 0 ? ( | ||
| <div className="flex flex-col items-center mt-4 px-6 pt-10"> | ||
| <EmptyStateCompact | ||
| title={t("common_empty_state.search.title")} | ||
| description={t("common_empty_state.search.description")} | ||
| assetKey="search" | ||
| assetClassName="size-20" | ||
| align="center" | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <div className="flex flex-col gap-0.5 overflow-x-hidden overflow-y-auto vertical-scrollbar scrollbar-sm flex-grow mt-4 pl-4"> | ||
| {filteredProjects.map((projectId, index) => ( | ||
| <SidebarProjectsListItem | ||
| key={projectId} | ||
| projectId={projectId} | ||
| handleCopyText={() => handleCopyText(projectId)} | ||
| projectListType={"JOINED"} | ||
| disableDrag={false} | ||
| disableDrop={false} | ||
| isLastChild={index === filteredProjects.length - 1} | ||
| handleOnProjectDrop={handleOnProjectDrop} | ||
| renderInExtendedSidebar | ||
| /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </ExtendedSidebarWrapper> | ||
| </> | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Empty projectId passed to TabNavigationRoot component
When
issueDetails?.project_idis undefined, an empty string is passed toTabNavigationRootas theprojectIdprop. This causes the component to execute hooks likeuseTabPreferences,useNavigationItems, anduseProjectActionswith an invalid empty projectId before the null check at line 143 returns early. These hooks may attempt API calls or state operations with the empty string, potentially causing errors or unnecessary network requests. The component should return early or handle the undefined case before renderingTabNavigationRoot.