/** * Copyright (c) 2023-present Plane Software, Inc. and contributors * SPDX-License-Identifier: AGPL-3.0-only * See the LICENSE file for details. */ import React, { useEffect } from "react"; import { observer } from "mobx-react"; import { useParams, useLocation, Link, useNavigate } from "react-router"; import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TabNavigationList, TabNavigationItem } from "@plane/propel/tab-navigation"; import type { EUserProjectRoles } from "@plane/types"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; // local imports import { LeaveProjectModal } from "../project/leave-project-modal"; import { PublishProjectModal } from "../project/publish-project/modal"; import { ProjectActionsMenu } from "./project-actions-menu"; import { ProjectHeader } from "./project-header"; import { TabNavigationOverflowMenu } from "./tab-navigation-overflow-menu"; import { DEFAULT_TAB_KEY } from "./tab-navigation-utils"; import { TabNavigationVisibleItem } from "./tab-navigation-visible-item"; import { useActiveTab } from "./use-active-tab"; import { useProjectActions } from "./use-project-actions"; import { useResponsiveTabLayout } from "./use-responsive-tab-layout"; import { useTabPreferences } from "./use-tab-preferences"; import { useNavigationItems } from "./use-navigation-items"; // Local type definition for navigation items with app-specific fields export type TNavigationItem = { name: string; href: string; icon: React.ElementType; access: EUserPermissions[] | EUserProjectRoles[]; shouldRender: boolean; sortOrder: number; i18n_key: string; key: string; }; type TTabNavigationRootProps = { workspaceSlug: string; projectId: string; }; export const TabNavigationRoot = observer(function TabNavigationRoot(props: TTabNavigationRootProps) { const { workspaceSlug, projectId } = props; const { workItem: workItemIdentifierFromRoute } = useParams(); const location = useLocation(); const pathname = location.pathname; const navigate = useNavigate(); const { t } = useTranslation(); // Store hooks const { getPartialProjectById } = useProject(); const { allowPermissions } = useUserPermissions(); const { issue: { getIssueIdByIdentifier, getIssueById }, } = useIssueDetail(); // Tab preferences hook const { tabPreferences, handleToggleDefaultTab, handleHideTab, handleShowTab } = useTabPreferences( workspaceSlug, projectId ); // Derived values const workItemId = workItemIdentifierFromRoute ? getIssueIdByIdentifier(workItemIdentifierFromRoute?.toString()) : undefined; const workItem = workItemId ? getIssueById(workItemId) : undefined; const project = getPartialProjectById(projectId); // Navigation items hook const navigationItems = useNavigationItems({ workspaceSlug, projectId, project, allowPermissions, }); // Active tab hook const { isActive, activeItem } = useActiveTab({ navigationItems, pathname, workItemId, workItem, projectId, }); // Project actions hook const { publishModalOpen, leaveProjectModalOpen, handleLeaveProject, handleCopyText, handlePublishModal, handleLeaveProjectModal, } = useProjectActions({ workspaceSlug, projectId, activeItem, }); // Filter and sort navigation items const allNavigationItems = navigationItems .filter((item) => item.shouldRender) // oxlint-disable-next-line unicorn/no-array-sort .sort((a: TNavigationItem, b: TNavigationItem) => a.sortOrder - b.sortOrder); // Split items into two categories: // 1. visibleNavigationItems: Items NOT user-hidden (may still overflow due to space) // 2. hiddenNavigationItems: Items user explicitly hid (always in overflow with "Show" icon) const visibleNavigationItems = allNavigationItems.filter( (item: TNavigationItem) => !tabPreferences.hiddenTabs.includes(item.key) ); const hiddenNavigationItems = allNavigationItems.filter((item: TNavigationItem) => tabPreferences.hiddenTabs.includes(item.key) ); // Responsive tab layout hook const { visibleItems, overflowItems, hasOverflow, itemRefs, containerRef } = useResponsiveTabLayout({ visibleNavigationItems, hiddenNavigationItems, isActive, }); // Redirect to default tab when navigating to project root useEffect(() => { const projectRootPath = `/${workspaceSlug}/projects/${projectId}`; const isProjectRoot = pathname === projectRootPath || pathname === `${projectRootPath}/`; if (isProjectRoot && allNavigationItems.length > 0) { // Find the default tab in available items const defaultTabItem = allNavigationItems.find((item: TNavigationItem) => item.key === tabPreferences.defaultTab); // If default tab exists and is enabled, use it; otherwise fall back to work_items const targetItem = defaultTabItem || allNavigationItems.find((item: TNavigationItem) => item.key === DEFAULT_TAB_KEY); if (targetItem) { navigate(targetItem.href, { replace: true }); } } }, [pathname, workspaceSlug, projectId, tabPreferences.defaultTab, allNavigationItems, navigate]); if (allNavigationItems.length === 0) return null; if (!project) return null; // Permission checks const isAdmin = allowPermissions( [EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug.toString(), project?.id ); const isAuthorized = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT, workspaceSlug.toString(), project?.id ); return ( <> handlePublishModal(false)} /> handleLeaveProjectModal(false)} /> {/* container for the tab navigation */}
handlePublishModal(true)} />
{/* Render visible tab items */} {visibleItems.map((item) => { const itemIsActive = isActive(item); const originalIndex = allNavigationItems.indexOf(item); return ( { itemRefs.current[originalIndex] = el; }} /> ); })} {/* Render overflow menu if needed */} {hasOverflow && ( )} {hasOverflow && (
{visibleNavigationItems.map((item: TNavigationItem) => { const itemIsActive = isActive(item); const originalIndex = allNavigationItems.indexOf(item); return (
{ itemRefs.current[originalIndex] = el; }} className="inline-block" > {t(item.i18n_key)}
); })}
)}
); });