snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
257
apps/web/core/components/navigation/tab-navigation-root.tsx
Normal file
257
apps/web/core/components/navigation/tab-navigation-root.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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 (
|
||||
<>
|
||||
<PublishProjectModal isOpen={publishModalOpen} projectId={projectId} onClose={() => handlePublishModal(false)} />
|
||||
<LeaveProjectModal
|
||||
project={project}
|
||||
isOpen={leaveProjectModalOpen}
|
||||
onClose={() => handleLeaveProjectModal(false)}
|
||||
/>
|
||||
|
||||
{/* container for the tab navigation */}
|
||||
<div className="flex size-full items-center gap-3 overflow-hidden">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ProjectHeader workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<div className="shrink-0">
|
||||
<ProjectActionsMenu
|
||||
workspaceSlug={workspaceSlug}
|
||||
project={project}
|
||||
isAdmin={isAdmin}
|
||||
isAuthorized={isAuthorized}
|
||||
onCopyText={handleCopyText}
|
||||
onLeaveProject={handleLeaveProject}
|
||||
onPublishModal={() => handlePublishModal(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-1 shrink-0 border-l border-subtle" />
|
||||
|
||||
<div ref={containerRef} className="flex h-full min-w-0 flex-1 items-center overflow-hidden">
|
||||
<TabNavigationList className="h-full">
|
||||
{/* Render visible tab items */}
|
||||
{visibleItems.map((item) => {
|
||||
const itemIsActive = isActive(item);
|
||||
const originalIndex = allNavigationItems.indexOf(item);
|
||||
|
||||
return (
|
||||
<TabNavigationVisibleItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
isActive={itemIsActive}
|
||||
tabPreferences={tabPreferences}
|
||||
onToggleDefault={handleToggleDefaultTab}
|
||||
onHide={handleHideTab}
|
||||
itemRef={(el) => {
|
||||
itemRefs.current[originalIndex] = el;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Render overflow menu if needed */}
|
||||
{hasOverflow && (
|
||||
<TabNavigationOverflowMenu
|
||||
overflowItems={overflowItems}
|
||||
isActive={isActive}
|
||||
tabPreferences={tabPreferences}
|
||||
onToggleDefault={handleToggleDefaultTab}
|
||||
onShow={handleShowTab}
|
||||
/>
|
||||
)}
|
||||
</TabNavigationList>
|
||||
|
||||
{hasOverflow && (
|
||||
<div className="pointer-events-none absolute -z-10 opacity-0">
|
||||
{visibleNavigationItems.map((item: TNavigationItem) => {
|
||||
const itemIsActive = isActive(item);
|
||||
const originalIndex = allNavigationItems.indexOf(item);
|
||||
return (
|
||||
<div
|
||||
key={`measure-hidden-${item.key}`}
|
||||
ref={(el) => {
|
||||
itemRefs.current[originalIndex] = el;
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
<Link to={item.href}>
|
||||
<TabNavigationItem isActive={itemIsActive}>
|
||||
<span>{t(item.i18n_key)}</span>
|
||||
</TabNavigationItem>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user