Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1af42a7
feat: update and get endpoint for project member preferences
sangeethailango Nov 21, 2025
4b738e1
chore: modify workspace-home-preference and workspace-user-preference…
sangeethailango Nov 21, 2025
9b2c66a
chore: add stickies
sangeethailango Nov 21, 2025
ac353f3
fix: revert back home prefernces endpoint changes
sangeethailango Nov 21, 2025
e77baef
feat: enhance menu and sortable components
anmolsinghbhatia Nov 23, 2025
90afb26
feat: add new navigation icons and constants
anmolsinghbhatia Nov 23, 2025
d452baf
feat: add navigation preference commands
anmolsinghbhatia Nov 23, 2025
1c30b89
refactor: update workspace path utilities
anmolsinghbhatia Nov 23, 2025
2a94eb8
feat: add translations for navigation preferences
anmolsinghbhatia Nov 23, 2025
2162a67
refactor: reorganize project route hierarchy
anmolsinghbhatia Nov 23, 2025
54a9109
refactor: remove deprecated breadcrumb and app-rail components
anmolsinghbhatia Nov 23, 2025
32f3bd5
feat: add project member navigation preferences
anmolsinghbhatia Nov 23, 2025
ed054a8
feat: add bulk sidebar preference updates
anmolsinghbhatia Nov 23, 2025
dca4a99
refactor: sidebar components refactor
anmolsinghbhatia Nov 23, 2025
08f5bb3
feat: implement flexible project navigation modes
anmolsinghbhatia Nov 23, 2025
36e145c
feat: enhance workspace menu items
anmolsinghbhatia Nov 23, 2025
a2761a0
refactor: standardize header components across features
anmolsinghbhatia Nov 23, 2025
45d68b9
refactor: update layout and sidebar structure
anmolsinghbhatia Nov 23, 2025
1ef6ebe
feat: enhance menu and sortable components and add new navigation ico…
anmolsinghbhatia Nov 23, 2025
2733d2e
feat: navigation enhancements and code refactor
anmolsinghbhatia Nov 23, 2025
f56f9a9
chore: code refactor
anmolsinghbhatia Nov 23, 2025
e103462
chore: code refactor
anmolsinghbhatia Nov 23, 2025
56b327d
chore: remove duplicate entry points
aaryan610 Nov 24, 2025
92867ba
refactor: object assignment
aaryan610 Nov 24, 2025
c93dbc2
Merge branch 'chore-revamp-navigation' of https://github.com/makeplan…
aaryan610 Nov 25, 2025
b454230
fix: merge conflicts resolved from preview
aaryan610 Nov 25, 2025
d775be7
Merge branch 'preview' of https://github.com/makeplane/plane into fea…
aaryan610 Nov 25, 2025
c64d3fa
fix: formatting issues
aaryan610 Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { useParams, usePathname } from "next/navigation";
import { SIDEBAR_WIDTH } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
// components
import { ResizableSidebar } from "@/components/sidebar/resizable-sidebar";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useAppRail } from "@/hooks/use-app-rail";
// local imports
import { ExtendedAppSidebar } from "./extended-sidebar";
import { AppSidebar } from "./sidebar";
Expand All @@ -26,14 +26,19 @@ export const ProjectAppSidebar = observer(function ProjectAppSidebar() {
const { storedValue, setValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH);
// states
const [sidebarWidth, setSidebarWidth] = useState<number>(storedValue ?? SIDEBAR_WIDTH);
// hooks
const { shouldRenderAppRail } = useAppRail();
// routes
const { workspaceSlug } = useParams();
const pathname = usePathname();
// derived values
const isAnyExtendedSidebarOpen = isExtendedSidebarOpened;

const isNotificationsPath = pathname.includes(`/${workspaceSlug}/notifications`);

// handlers
const handleWidthChange = (width: number) => setValue(width);

if (isNotificationsPath) return null;

return (
<>
<ResizableSidebar
Expand All @@ -55,7 +60,6 @@ export const ProjectAppSidebar = observer(function ProjectAppSidebar() {
}
isAnyExtendedSidebarExpanded={isAnyExtendedSidebarOpen}
isAnySidebarDropdownOpen={isAnySidebarDropdownOpen}
disablePeekTrigger={shouldRenderAppRail}
>
<AppSidebar />
</ResizableSidebar>
Expand Down
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() ?? ""}
/>
Copy link

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_id is undefined, an empty string is passed to TabNavigationRoot as the projectId prop. This causes the component to execute hooks like useTabPreferences, useNavigationItems, and useProjectActions with 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 rendering TabNavigationRoot.

Fix in Cursor Fix in Web

</Header.LeftItem>
</Header>
</div>
</div>
</Row>
</div>
<AppHeader header={<WorkItemDetailsHeader />} />
</>
);
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// components
import { Outlet } from "react-router";
import { AppHeader } from "@/components/core/app-header";
import { ContentWrapper } from "@/components/core/content-wrapper";
import { ProjectIssueDetailsHeader } from "./header";
import { ProjectWorkItemDetailsHeader } from "./header";

export default function ProjectIssueDetailsLayout() {
return (
<>
<AppHeader header={<ProjectIssueDetailsHeader />} />
<ProjectWorkItemDetailsHeader />
<ContentWrapper className="overflow-hidden">
<Outlet />
</ContentWrapper>
Expand Down
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
Expand Up @@ -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";
Expand Down Expand Up @@ -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">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Padding inconsistency between empty state and list.

The horizontal padding differs across states:

  • Header (Line 106): no horizontal padding (removed px-4)
  • Empty state (Line 136): px-6
  • List (Line 146): pl-4 (no right padding)

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
In apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx
around lines 106, 136 and 146, the header, empty state and list use inconsistent
horizontal padding causing content shift and potential scrollbar overlap;
standardize horizontal spacing (pick one padding value such as px-4 or px-6) on
the main container div (line 106) and remove or adjust conflicting px/pl classes
on the empty state (line 136) and list (line 146) so all three states share the
same left and right padding; also ensure the list retains right padding to
prevent scrollbar overlap (apply symmetric px-X on the wrapper or add pr-X to
the list if keeping pl-X).

<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-custom-text-300 py-1.5">Projects</span>
{isAuthorizedUser && (
Expand Down Expand Up @@ -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>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ExtendedSidebarWrapper = observer(function ExtendedSidebarWrapper(p
id={excludedElementId}
ref={extendedSidebarRef}
className={cn(
`absolute h-full z-[19] flex flex-col py-2 transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-sm`,
`absolute h-full z-[21] flex flex-col py-2 transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-sm`,
{
"translate-x-0 opacity-100": isExtendedSidebarOpened,
[`-translate-x-[${EXTENDED_SIDEBAR_WIDTH}px] opacity-0 hidden`]: !isExtendedSidebarOpened,
Expand Down
44 changes: 29 additions & 15 deletions apps/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React, { useMemo, useRef } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS } from "@plane/constants";
import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS, EUserPermissionsLevel } from "@plane/constants";
import type { EUserWorkspaceRoles } from "@plane/types";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkspaceNavigationPreferences } from "@/hooks/use-navigation-preferences";
// plane-web imports
import { ExtendedSidebarItem } from "@/plane-web/components/workspace/sidebar/extended-sidebar-item";
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
Expand All @@ -18,22 +19,38 @@ export const ExtendedAppSidebar = observer(function ExtendedAppSidebar() {
const { workspaceSlug } = useParams();
// store hooks
const { isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme();
const { updateSidebarPreference, getNavigationPreferences } = useWorkspace();
const { allowPermissions } = useUserPermissions();
const { preferences: workspacePreferences, updateWorkspaceItemSortOrder } = useWorkspaceNavigationPreferences();

// derived values
const currentWorkspaceNavigationPreferences = getNavigationPreferences(workspaceSlug.toString());
const currentWorkspaceNavigationPreferences = workspacePreferences.items;

const sortedNavigationItems = useMemo(
() =>
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.map((item) => {
const sortedNavigationItems = useMemo(() => {
const slug = workspaceSlug.toString();

return WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.filter((item) => {
// Permission check
const hasPermission = allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, slug);

return hasPermission;
})
.map((item) => {
const preference = currentWorkspaceNavigationPreferences?.[item.key];
return {
...item,
sort_order: preference ? preference.sort_order : 0,
sort_order: preference?.sort_order ?? 0,
is_pinned: preference?.is_pinned ?? false,
};
}).sort((a, b) => a.sort_order - b.sort_order),
[currentWorkspaceNavigationPreferences]
);
})
.sort((a, b) => {
// First sort by pinned status (pinned items first)
if (a.is_pinned !== b.is_pinned) {
return b.is_pinned ? 1 : -1;
}
// Then sort by sort_order within each group
return a.sort_order - b.sort_order;
});
}, [workspaceSlug, currentWorkspaceNavigationPreferences, allowPermissions]);

const sortedNavigationItemsKeys = sortedNavigationItems.map((item) => item.key);

Expand Down Expand Up @@ -87,10 +104,7 @@ export const ExtendedAppSidebar = observer(function ExtendedAppSidebar() {

const updatedSortOrder = orderNavigationItem(sourceIndex, destinationIndex, sortedNavigationItems);

if (updatedSortOrder != undefined)
updateSidebarPreference(workspaceSlug.toString(), sourceId, {
sort_order: updatedSortOrder,
});
if (updatedSortOrder != undefined) updateWorkspaceItemSortOrder(sourceId, updatedSortOrder);
};

const handleClose = () => toggleExtendedSidebar(false);
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Outlet } from "react-router";
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
// plane web components
import { ProjectAppSidebar } from "./_sidebar";
import { ExtendedProjectSidebar } from "./extended-project-sidebar";

function WorkspaceLayout() {
return (
Expand All @@ -12,6 +13,7 @@ function WorkspaceLayout() {
<div id="full-screen-portal" className="inset-0 absolute w-full" />
<div className="relative flex size-full overflow-hidden">
<ProjectAppSidebar />
<ExtendedProjectSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
<Outlet />
</main>
Expand Down
Loading
Loading