Skip to content

Commit 2a378b3

Browse files
[WEB-5556] chore: tab navigation project header enhancement (#8212)
1 parent 8b0a797 commit 2a378b3

File tree

7 files changed

+180
-21
lines changed

7 files changed

+180
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,4 @@ build/
111111
.react-router/
112112
AGENTS.md
113113
temp/
114+
scripts/

apps/web/ce/components/navigations/top-navigation-root.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
// components
22
import { observer } from "mobx-react";
3+
import { useParams, usePathname } from "next/navigation";
34
import { cn } from "@plane/utils";
45
import { TopNavPowerK } from "@/components/navigation";
56
import { HelpMenuRoot } from "@/components/workspace/sidebar/help-section/root";
67
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
78
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
89
import { useAppRailPreferences } from "@/hooks/use-navigation-preferences";
10+
import { Tooltip } from "@plane/propel/tooltip";
11+
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
12+
import { InboxIcon } from "@plane/propel/icons";
13+
import useSWR from "swr";
14+
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
915

1016
export const TopNavigationRoot = observer(() => {
17+
// router
18+
const { workspaceSlug, projectId, workItem } = useParams();
19+
const pathname = usePathname();
20+
21+
// store hooks
22+
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
1123
const { preferences } = useAppRailPreferences();
1224

1325
const showLabel = preferences.displayMode === "icon_with_label";
1426

27+
// Fetch notification count
28+
useSWR(
29+
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
30+
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug.toString()) : null
31+
);
32+
33+
// Calculate notification count
34+
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
35+
const totalNotifications = isMentionsEnabled
36+
? unreadNotificationsCount.mention_unread_notifications_count
37+
: unreadNotificationsCount.total_unread_notifications_count;
38+
1539
return (
1640
<div
1741
className={cn("flex items-center min-h-11 w-full px-3.5 z-[27] transition-all duration-300", {
@@ -28,6 +52,23 @@ export const TopNavigationRoot = observer(() => {
2852
</div>
2953
{/* Additional Actions */}
3054
<div className="shrink-0 flex-1 flex gap-1 items-center justify-end">
55+
<Tooltip tooltipContent="Inbox" position="bottom">
56+
<AppSidebarItem
57+
variant="link"
58+
item={{
59+
href: `/${workspaceSlug?.toString()}/notifications/`,
60+
icon: (
61+
<div className="relative">
62+
<InboxIcon className="size-5" />
63+
{totalNotifications > 0 && (
64+
<span className="absolute -top-0 -right-0 size-2 rounded-full bg-red-500" />
65+
)}
66+
</div>
67+
),
68+
isActive: pathname?.includes("/notifications/"),
69+
}}
70+
/>
71+
</Tooltip>
3172
<HelpMenuRoot />
3273
<div className="flex items-center justify-center size-8 hover:bg-custom-background-80 rounded-md">
3374
<UserMenuRoot size="xs" />

apps/web/core/components/navigation/project-actions-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const ProjectActionsMenu: FC<Props> = ({
4444
customButton={
4545
<span
4646
ref={actionSectionRef}
47-
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
47+
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded"
4848
onClick={() => setIsMenuActive(!isMenuActive)}
4949
>
5050
<MoreHorizontal className="size-4" />
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { TPartialProject } from "@/plane-web/types";
2+
// plane propel imports
3+
import { Logo } from "@plane/propel/emoji-icon-picker";
4+
import { ChevronDownIcon } from "@plane/propel/icons";
5+
import { Tooltip } from "@plane/propel/tooltip";
6+
7+
type TProjectHeaderButtonProps = {
8+
project: TPartialProject;
9+
};
10+
11+
export function ProjectHeaderButton({ project }: TProjectHeaderButtonProps) {
12+
return (
13+
<Tooltip tooltipContent={project.name} position="bottom">
14+
<div className="relative flex items-center text-left select-none w-full max-w-48 pr-1">
15+
<div className="size-7 rounded-md bg-custom-background-90 flex items-center justify-center flex-shrink-0">
16+
<Logo logo={project.logo_props} size={16} />
17+
</div>
18+
<div className="relative flex-1 min-w-0">
19+
<p className="truncate text-base font-medium text-custom-sidebar-text-200 px-2">{project.name}</p>
20+
<div className="absolute right-0 top-0 bottom-0 flex items-center justify-end pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-200">
21+
<div className="relative h-full w-8 flex items-center justify-end">
22+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-custom-background-90 to-custom-background-90 rounded-r" />
23+
<ChevronDownIcon className="relative z-10 size-4 text-custom-text-300" />
24+
</div>
25+
</div>
26+
</div>
27+
</div>
28+
</Tooltip>
29+
);
30+
}
Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,107 @@
1-
import type { FC } from "react";
2-
// plane imports
3-
import { Logo } from "@plane/propel/emoji-icon-picker";
4-
import type { TLogoProps } from "@plane/types";
5-
6-
type ProjectHeaderProps = {
7-
project: {
8-
name: string;
9-
logo_props: TLogoProps;
10-
};
1+
import { useCallback, useMemo } from "react";
2+
import { observer } from "mobx-react";
3+
// plane ui imports
4+
import type { ICustomSearchSelectOption } from "@plane/types";
5+
import { CustomSearchSelect } from "@plane/ui";
6+
// plane propel imports
7+
import { ProjectIcon } from "@plane/propel/icons";
8+
// hooks
9+
import { useAppRouter } from "@/hooks/use-app-router";
10+
import { useProject } from "@/hooks/store/use-project";
11+
import { useUserPermissions } from "@/hooks/store/user";
12+
import { useNavigationItems } from "@/plane-web/components/navigations";
13+
// local components
14+
import { SwitcherLabel } from "../common/switcher-label";
15+
import { ProjectHeaderButton } from "./project-header-button";
16+
// utils
17+
import { getTabUrl } from "./tab-navigation-utils";
18+
import { useTabPreferences } from "./use-tab-preferences";
19+
20+
type TProjectHeaderProps = {
21+
workspaceSlug: string;
22+
projectId: string;
1123
};
1224

13-
export const ProjectHeader: FC<ProjectHeaderProps> = ({ project }) => (
14-
<div className="flex items-center gap-1.5 text-left select-none w-full">
15-
<div className="size-7 rounded-md bg-custom-background-90 flex items-center justify-center flex-shrink-0">
16-
<Logo logo={project.logo_props} size={16} />
17-
</div>
18-
<p className="truncate text-base font-medium text-custom-sidebar-text-200 flex-shrink-0">{project.name}</p>
19-
</div>
20-
);
25+
export const ProjectHeader = observer((props: TProjectHeaderProps) => {
26+
const { workspaceSlug, projectId } = props;
27+
// router
28+
const router = useAppRouter();
29+
// store hooks
30+
const { joinedProjectIds, getPartialProjectById } = useProject();
31+
const { allowPermissions } = useUserPermissions();
32+
33+
// Get current project details
34+
const currentProjectDetails = getPartialProjectById(projectId);
35+
36+
// Get available navigation items for this project
37+
const navigationItems = useNavigationItems({
38+
workspaceSlug: workspaceSlug,
39+
projectId,
40+
project: currentProjectDetails,
41+
allowPermissions,
42+
});
43+
44+
// Get preferences from hook
45+
const { tabPreferences } = useTabPreferences(workspaceSlug, projectId);
46+
47+
// Memoize available tab keys
48+
const availableTabKeys = useMemo(() => navigationItems.map((item) => item.key), [navigationItems]);
49+
50+
// Memoize validated default tab key
51+
const validatedDefaultTabKey = useMemo(
52+
() =>
53+
availableTabKeys.includes(tabPreferences.defaultTab)
54+
? tabPreferences.defaultTab
55+
: availableTabKeys[0] || "work_items",
56+
[availableTabKeys, tabPreferences.defaultTab]
57+
);
58+
59+
// Memoize switcher options to prevent recalculation on every render
60+
const switcherOptions = useMemo<ICustomSearchSelectOption[]>(
61+
() =>
62+
joinedProjectIds
63+
.map((id): ICustomSearchSelectOption | null => {
64+
const project = getPartialProjectById(id);
65+
if (!project) return null;
66+
67+
return {
68+
value: id,
69+
query: project.name,
70+
content: (
71+
<SwitcherLabel
72+
name={project.name}
73+
logo_props={project.logo_props}
74+
LabelIcon={ProjectIcon}
75+
type="material"
76+
/>
77+
),
78+
};
79+
})
80+
.filter((option): option is ICustomSearchSelectOption => option !== null),
81+
[joinedProjectIds, getPartialProjectById]
82+
);
83+
84+
// Memoize onChange handler
85+
const handleProjectChange = useCallback(
86+
(value: string) => {
87+
if (value !== currentProjectDetails?.id) {
88+
router.push(getTabUrl(workspaceSlug, value, validatedDefaultTabKey));
89+
}
90+
},
91+
[currentProjectDetails?.id, router, workspaceSlug, validatedDefaultTabKey]
92+
);
93+
94+
// Early return if no project details
95+
if (!currentProjectDetails) return null;
96+
97+
return (
98+
<CustomSearchSelect
99+
options={switcherOptions}
100+
value={currentProjectDetails.id}
101+
onChange={handleProjectChange}
102+
customButton={currentProjectDetails ? <ProjectHeaderButton project={currentProjectDetails} /> : null}
103+
className="h-full rounded"
104+
customButtonClassName="group flex items-center gap-0.5 rounded hover:bg-custom-background-90 outline-none cursor-pointer h-full"
105+
/>
106+
);
107+
});

apps/web/core/components/navigation/tab-navigation-root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export const TabNavigationRoot: FC<TTabNavigationRootProps> = observer((props) =
169169
{/* container for the tab navigation */}
170170
<div className="flex items-center gap-3 overflow-hidden size-full">
171171
<div className="flex items-center gap-2 shrink-0">
172-
<ProjectHeader project={project} />
172+
<ProjectHeader workspaceSlug={workspaceSlug} projectId={projectId} />
173173
<div className="shrink-0">
174174
<ProjectActionsMenu
175175
workspaceSlug={workspaceSlug}

apps/web/core/components/workspace/sidebar/help-section/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
3838
<AppSidebarItem
3939
variant="button"
4040
item={{
41-
icon: <HelpCircle className="size-4" />,
41+
icon: <HelpCircle className="size-5" />,
4242
isActive: isNeedHelpOpen,
4343
}}
4444
/>

0 commit comments

Comments
 (0)