diff --git a/.gitignore b/.gitignore index f19497acc..f0093a0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,4 @@ build/ .react-router/ AGENTS.md temp/ +scripts/ diff --git a/apps/web/ce/components/navigations/top-navigation-root.tsx b/apps/web/ce/components/navigations/top-navigation-root.tsx index 2607d3832..df2300686 100644 --- a/apps/web/ce/components/navigations/top-navigation-root.tsx +++ b/apps/web/ce/components/navigations/top-navigation-root.tsx @@ -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 (
{
{/* Additional Actions */}
+ + + + {totalNotifications > 0 && ( + + )} +
+ ), + isActive: pathname?.includes("/notifications/"), + }} + /> +
diff --git a/apps/web/core/components/navigation/project-actions-menu.tsx b/apps/web/core/components/navigation/project-actions-menu.tsx index 12c960f6b..e947f9120 100644 --- a/apps/web/core/components/navigation/project-actions-menu.tsx +++ b/apps/web/core/components/navigation/project-actions-menu.tsx @@ -44,7 +44,7 @@ export const ProjectActionsMenu: FC = ({ customButton={ setIsMenuActive(!isMenuActive)} > diff --git a/apps/web/core/components/navigation/project-header-button.tsx b/apps/web/core/components/navigation/project-header-button.tsx new file mode 100644 index 000000000..eb0341540 --- /dev/null +++ b/apps/web/core/components/navigation/project-header-button.tsx @@ -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 ( + +
+
+ +
+
+

{project.name}

+
+
+
+ +
+
+
+
+ + ); +} diff --git a/apps/web/core/components/navigation/project-header.tsx b/apps/web/core/components/navigation/project-header.tsx index cf2f6f634..a88fd1cad 100644 --- a/apps/web/core/components/navigation/project-header.tsx +++ b/apps/web/core/components/navigation/project-header.tsx @@ -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 = ({ project }) => ( -
-
- -
-

{project.name}

-
-); +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( + () => + joinedProjectIds + .map((id): ICustomSearchSelectOption | null => { + const project = getPartialProjectById(id); + if (!project) return null; + + return { + value: id, + query: project.name, + content: ( + + ), + }; + }) + .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 ( + : 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" + /> + ); +}); diff --git a/apps/web/core/components/navigation/tab-navigation-root.tsx b/apps/web/core/components/navigation/tab-navigation-root.tsx index a3285fd82..103c4dba8 100644 --- a/apps/web/core/components/navigation/tab-navigation-root.tsx +++ b/apps/web/core/components/navigation/tab-navigation-root.tsx @@ -169,7 +169,7 @@ export const TabNavigationRoot: FC = observer((props) = {/* container for the tab navigation */}
- +
, + icon: , isActive: isNeedHelpOpen, }} />