mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 20:07:56 +01:00
[WEB-5556] chore: tab navigation project header enhancement (#8212)
This commit is contained in:
committed by
GitHub
parent
8b0a797906
commit
2a378b3bc1
1
.gitignore
vendored
1
.gitignore
vendored
@@ -111,3 +111,4 @@ build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
temp/
|
||||
scripts/
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
// components
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { cn } from "@plane/utils";
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { HelpMenuRoot } from "@/components/workspace/sidebar/help-section/root";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { useAppRailPreferences } from "@/hooks/use-navigation-preferences";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||
import { InboxIcon } from "@plane/propel/icons";
|
||||
import useSWR from "swr";
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
|
||||
export const TopNavigationRoot = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, workItem } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
// store hooks
|
||||
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||
const { preferences } = useAppRailPreferences();
|
||||
|
||||
const showLabel = preferences.displayMode === "icon_with_label";
|
||||
|
||||
// Fetch notification count
|
||||
useSWR(
|
||||
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
|
||||
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
// Calculate notification count
|
||||
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
|
||||
const totalNotifications = isMentionsEnabled
|
||||
? unreadNotificationsCount.mention_unread_notifications_count
|
||||
: unreadNotificationsCount.total_unread_notifications_count;
|
||||
|
||||
return (
|
||||
<div
|
||||
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(() => {
|
||||
</div>
|
||||
{/* Additional Actions */}
|
||||
<div className="shrink-0 flex-1 flex gap-1 items-center justify-end">
|
||||
<Tooltip tooltipContent="Inbox" position="bottom">
|
||||
<AppSidebarItem
|
||||
variant="link"
|
||||
item={{
|
||||
href: `/${workspaceSlug?.toString()}/notifications/`,
|
||||
icon: (
|
||||
<div className="relative">
|
||||
<InboxIcon className="size-5" />
|
||||
{totalNotifications > 0 && (
|
||||
<span className="absolute -top-0 -right-0 size-2 rounded-full bg-red-500" />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
isActive: pathname?.includes("/notifications/"),
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<HelpMenuRoot />
|
||||
<div className="flex items-center justify-center size-8 hover:bg-custom-background-80 rounded-md">
|
||||
<UserMenuRoot size="xs" />
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ProjectActionsMenu: FC<Props> = ({
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { TPartialProject } from "@/plane-web/types";
|
||||
// plane propel imports
|
||||
import { Logo } from "@plane/propel/emoji-icon-picker";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
|
||||
type TProjectHeaderButtonProps = {
|
||||
project: TPartialProject;
|
||||
};
|
||||
|
||||
export function ProjectHeaderButton({ project }: TProjectHeaderButtonProps) {
|
||||
return (
|
||||
<Tooltip tooltipContent={project.name} position="bottom">
|
||||
<div className="relative flex items-center text-left select-none w-full max-w-48 pr-1">
|
||||
<div className="size-7 rounded-md bg-custom-background-90 flex items-center justify-center flex-shrink-0">
|
||||
<Logo logo={project.logo_props} size={16} />
|
||||
</div>
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<p className="truncate text-base font-medium text-custom-sidebar-text-200 px-2">{project.name}</p>
|
||||
<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">
|
||||
<div className="relative h-full w-8 flex items-center justify-end">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-custom-background-90 to-custom-background-90 rounded-r" />
|
||||
<ChevronDownIcon className="relative z-10 size-4 text-custom-text-300" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,107 @@
|
||||
import type { FC } from "react";
|
||||
// plane imports
|
||||
import { Logo } from "@plane/propel/emoji-icon-picker";
|
||||
import type { TLogoProps } from "@plane/types";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane ui imports
|
||||
import type { ICustomSearchSelectOption } from "@plane/types";
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
// plane propel imports
|
||||
import { ProjectIcon } from "@plane/propel/icons";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useNavigationItems } from "@/plane-web/components/navigations";
|
||||
// local components
|
||||
import { SwitcherLabel } from "../common/switcher-label";
|
||||
import { ProjectHeaderButton } from "./project-header-button";
|
||||
// utils
|
||||
import { getTabUrl } from "./tab-navigation-utils";
|
||||
import { useTabPreferences } from "./use-tab-preferences";
|
||||
|
||||
type ProjectHeaderProps = {
|
||||
project: {
|
||||
name: string;
|
||||
logo_props: TLogoProps;
|
||||
};
|
||||
type TProjectHeaderProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ProjectHeader: FC<ProjectHeaderProps> = ({ project }) => (
|
||||
<div className="flex items-center gap-1.5 text-left select-none w-full">
|
||||
<div className="size-7 rounded-md bg-custom-background-90 flex items-center justify-center flex-shrink-0">
|
||||
<Logo logo={project.logo_props} size={16} />
|
||||
</div>
|
||||
<p className="truncate text-base font-medium text-custom-sidebar-text-200 flex-shrink-0">{project.name}</p>
|
||||
</div>
|
||||
);
|
||||
export const ProjectHeader = observer((props: TProjectHeaderProps) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { joinedProjectIds, getPartialProjectById } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// Get current project details
|
||||
const currentProjectDetails = getPartialProjectById(projectId);
|
||||
|
||||
// Get available navigation items for this project
|
||||
const navigationItems = useNavigationItems({
|
||||
workspaceSlug: workspaceSlug,
|
||||
projectId,
|
||||
project: currentProjectDetails,
|
||||
allowPermissions,
|
||||
});
|
||||
|
||||
// Get preferences from hook
|
||||
const { tabPreferences } = useTabPreferences(workspaceSlug, projectId);
|
||||
|
||||
// Memoize available tab keys
|
||||
const availableTabKeys = useMemo(() => navigationItems.map((item) => item.key), [navigationItems]);
|
||||
|
||||
// Memoize validated default tab key
|
||||
const validatedDefaultTabKey = useMemo(
|
||||
() =>
|
||||
availableTabKeys.includes(tabPreferences.defaultTab)
|
||||
? tabPreferences.defaultTab
|
||||
: availableTabKeys[0] || "work_items",
|
||||
[availableTabKeys, tabPreferences.defaultTab]
|
||||
);
|
||||
|
||||
// Memoize switcher options to prevent recalculation on every render
|
||||
const switcherOptions = useMemo<ICustomSearchSelectOption[]>(
|
||||
() =>
|
||||
joinedProjectIds
|
||||
.map((id): ICustomSearchSelectOption | null => {
|
||||
const project = getPartialProjectById(id);
|
||||
if (!project) return null;
|
||||
|
||||
return {
|
||||
value: id,
|
||||
query: project.name,
|
||||
content: (
|
||||
<SwitcherLabel
|
||||
name={project.name}
|
||||
logo_props={project.logo_props}
|
||||
LabelIcon={ProjectIcon}
|
||||
type="material"
|
||||
/>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((option): option is ICustomSearchSelectOption => option !== null),
|
||||
[joinedProjectIds, getPartialProjectById]
|
||||
);
|
||||
|
||||
// Memoize onChange handler
|
||||
const handleProjectChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value !== currentProjectDetails?.id) {
|
||||
router.push(getTabUrl(workspaceSlug, value, validatedDefaultTabKey));
|
||||
}
|
||||
},
|
||||
[currentProjectDetails?.id, router, workspaceSlug, validatedDefaultTabKey]
|
||||
);
|
||||
|
||||
// Early return if no project details
|
||||
if (!currentProjectDetails) return null;
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
options={switcherOptions}
|
||||
value={currentProjectDetails.id}
|
||||
onChange={handleProjectChange}
|
||||
customButton={currentProjectDetails ? <ProjectHeaderButton project={currentProjectDetails} /> : null}
|
||||
className="h-full rounded"
|
||||
customButtonClassName="group flex items-center gap-0.5 rounded hover:bg-custom-background-90 outline-none cursor-pointer h-full"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ export const TabNavigationRoot: FC<TTabNavigationRootProps> = observer((props) =
|
||||
{/* container for the tab navigation */}
|
||||
<div className="flex items-center gap-3 overflow-hidden size-full">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<ProjectHeader project={project} />
|
||||
<ProjectHeader workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<div className="shrink-0">
|
||||
<ProjectActionsMenu
|
||||
workspaceSlug={workspaceSlug}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
|
||||
<AppSidebarItem
|
||||
variant="button"
|
||||
item={{
|
||||
icon: <HelpCircle className="size-4" />,
|
||||
icon: <HelpCircle className="size-5" />,
|
||||
isActive: isNeedHelpOpen,
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user