[WEB-5511] regression: revamped navigation UI bugs (#8183)

This commit is contained in:
Aaryan Khandelwal 2025-11-26 18:51:03 +05:30 committed by GitHub
parent 05b1c147a9
commit eddf80aaed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 164 additions and 149 deletions

View file

@ -14,20 +14,20 @@ export const TopNavigationRoot = observer(() => {
return (
<div
className={cn("flex items-center justify-evenly min-h-11 w-full px-3.5 z-[27] transition-all duration-300", {
className={cn("flex items-center min-h-11 w-full px-3.5 z-[27] transition-all duration-300", {
"px-2": !showLabel,
})}
>
{/* Workspace Menu */}
<div className="flex items-center justify-start flex-shrink-0">
<div className="shrink-0 flex-1">
<WorkspaceMenuRoot />
</div>
{/* Power K Search */}
<div className="flex items-center justify-center flex-grow px-4">
<div className="shrink-0">
<TopNavPowerK />
</div>
{/* Additional Actions */}
<div className="flex gap-1 items-center justify-end flex-shrink-0 min-w-48">
<div className="shrink-0 flex-1 flex gap-1 items-center justify-end">
<HelpMenuRoot />
<div className="flex items-center justify-center size-8 hover:bg-custom-background-80 rounded-md">
<UserMenuRoot size="xs" />

View file

@ -5,6 +5,7 @@ import {
DraftIcon,
HomeIcon,
InboxIcon,
MultipleStickyIcon,
ProjectIcon,
ViewsIcon,
YourWorkIcon,
@ -31,5 +32,7 @@ export const getSidebarNavigationItemIcon = (key: string, className: string = ""
return <DraftIcon className={cn("size-4 flex-shrink-0", className)} />;
case "archives":
return <ArchiveIcon className={cn("size-4 flex-shrink-0", className)} />;
case "stickies":
return <MultipleStickyIcon className={cn("size-4 flex-shrink-0", className)} />;
}
};

View file

@ -71,7 +71,7 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
const filteredPersonalItems = PERSONAL_ITEMS;
// Filter workspace items by permissions and feature flags, then get pinned/unpinned items
const { pinnedItems, unpinnedItems } = useMemo(() => {
const workspaceItems = useMemo(() => {
const items = WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.filter((item) => {
// Permission check
const hasPermission = allowPermissions(
@ -94,11 +94,7 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
};
});
// Sort pinned items by sort_order
const pinned = items.filter((item) => item.isPinned).sort((a, b) => a.sortOrder - b.sortOrder);
const unpinned = items.filter((item) => !item.isPinned);
return { pinnedItems: pinned, unpinnedItems: unpinned };
return items.sort((a, b) => a.sortOrder - b.sortOrder);
}, [workspaceSlug, allowPermissions, workspacePreferences]);
// Handle checkbox toggle
@ -134,7 +130,7 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
);
// Separate personal items into enabled/disabled
const { enabledPersonalItems, disabledPersonalItems } = useMemo(() => {
const personalItems = useMemo(() => {
const items = filteredPersonalItems.map((item) => {
const itemState = personalPreferences.items[item.key];
const isEnabled = typeof itemState === "boolean" ? itemState : (itemState?.enabled ?? true);
@ -147,10 +143,7 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
};
});
const enabled = items.filter((item) => item.isEnabled).sort((a, b) => a.sortOrder - b.sortOrder);
const disabled = items.filter((item) => !item.isEnabled);
return { enabledPersonalItems: enabled, disabledPersonalItems: disabled };
return items.sort((a, b) => a.sortOrder - b.sortOrder);
}, [personalPreferences, filteredPersonalItems]);
// Prevent typing invalid characters in number input
@ -203,18 +196,19 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
{/* Personal Section */}
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-custom-text-400">{t("personal")}</h3>
{/* Enabled Items - Sortable */}
<div className="border border-custom-border-200 rounded-md py-2 bg-custom-background-90">
<Sortable
data={enabledPersonalItems}
data={personalItems}
onChange={handlePersonalReorder}
keyExtractor={(item) => item.key}
id="personal-enabled-items"
render={(item) => (
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-custom-background-90 transition-all duration-200">
<GripVertical className="size-4 text-custom-text-400 cursor-grab active:cursor-grabbing transition-colors" />
<Checkbox checked onChange={(e) => togglePersonalItem(item.key, e.target.checked)} />
<Checkbox
checked={!!personalPreferences.items[item.key]?.enabled}
onChange={(e) => togglePersonalItem(item.key, e.target.checked)}
/>
<div className="flex items-center gap-2 flex-1">
{getSidebarNavigationItemIcon(item.key)}
<label className="text-sm text-custom-text-200 flex-1 cursor-pointer">
@ -224,27 +218,6 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
</div>
)}
/>
{/* Disabled Items */}
{disabledPersonalItems.length > 0 && (
<div className={cn("space-y-1", enabledPersonalItems.length > 0 && "mt-1")}>
{disabledPersonalItems.map((item) => (
<div
key={item.key}
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-custom-background-90 transition-all duration-200"
>
<GripVertical className="size-4 text-custom-text-400 opacity-40" />
<Checkbox checked={false} onChange={(e) => togglePersonalItem(item.key, e.target.checked)} />
<div className="flex items-center gap-2 flex-1">
{getSidebarNavigationItemIcon(item.key)}
<label className="text-sm text-custom-text-200 flex-1 cursor-pointer">
{t(item.labelTranslationKey)}
</label>
</div>
</div>
))}
</div>
)}
</div>
</div>
@ -254,7 +227,7 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
<div className="border border-custom-border-200 rounded-md py-2 bg-custom-background-90">
{/* Pinned Items - Draggable */}
<Sortable
data={pinnedItems}
data={workspaceItems}
onChange={handleReorder}
keyExtractor={(item) => item.key}
id="workspace-pinned-items"
@ -263,7 +236,10 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
return (
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-custom-background-90 group transition-all duration-200">
<GripVertical className="size-4 text-custom-text-400 cursor-grab active:cursor-grabbing transition-colors" />
<Checkbox checked onChange={(e) => handleWorkspaceItemToggle(item.key, e.target.checked)} />
<Checkbox
checked={!!workspacePreferences.items[item.key]?.is_pinned}
onChange={(e) => handleWorkspaceItemToggle(item.key, e.target.checked)}
/>
<div className="flex items-center gap-2 flex-1">
{icon}
<span className="text-sm text-custom-text-200">{t(item.labelTranslationKey)}</span>
@ -272,31 +248,6 @@ export const CustomizeNavigationDialog: FC<TCustomizeNavigationDialogProps> = ob
);
}}
/>
{/* Unpinned Items */}
{unpinnedItems.length > 0 && (
<div className="space-y-1 mt-1">
{unpinnedItems.map((item) => {
const icon = getSidebarNavigationItemIcon(item.key);
return (
<div
key={item.key}
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-custom-background-90 transition-all duration-200"
>
<GripVertical className="size-4 text-custom-text-400 opacity-40" />
<Checkbox
checked={false}
onChange={(e) => handleWorkspaceItemToggle(item.key, e.target.checked)}
/>
<div className="flex items-center gap-2 flex-1">
{icon}
<span className="text-sm text-custom-text-200">{t(item.labelTranslationKey)}</span>
</div>
</div>
);
})}
</div>
)}
</div>
</div>

View file

@ -1,14 +1,15 @@
"use client";
import type { FC } from "react";
import React, { useState, useRef } from "react";
import { useState, useRef } from "react";
import { useNavigate } from "react-router";
import { LinkIcon, LogOut, MoreHorizontal, Settings, Share2, ArchiveIcon } from "lucide-react";
// plane imports
import { MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { CustomMenu } from "@plane/ui";
type ProjectActionsMenuProps = {
type Props = {
workspaceSlug: string;
project: {
id: string;
@ -20,7 +21,7 @@ type ProjectActionsMenuProps = {
onPublishModal: () => void;
};
export const ProjectActionsMenu: FC<ProjectActionsMenuProps> = ({
export const ProjectActionsMenu: FC<Props> = ({
workspaceSlug,
project,
isAdmin,
@ -29,10 +30,14 @@ export const ProjectActionsMenu: FC<ProjectActionsMenuProps> = ({
onLeaveProject,
onPublishModal,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// states
const [isMenuActive, setIsMenuActive] = useState(false);
// translation
const { t } = useTranslation();
// refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// router
const navigate = useNavigate();
return (
<CustomMenu

View file

@ -1,7 +1,7 @@
import type { FC } from "react";
// plane imports
import { Logo } from "@plane/propel/emoji-icon-picker";
import type { TLogoProps } from "@plane/types";
import { cn } from "@plane/utils";
type ProjectHeaderProps = {
project: {
@ -11,7 +11,7 @@ type ProjectHeaderProps = {
};
export const ProjectHeader: FC<ProjectHeaderProps> = ({ project }) => (
<div className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full flex-shrink-0")}>
<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>

View file

@ -1,14 +1,17 @@
import React from "react";
import { Link } from "react-router";
import { MoreHorizontal, Star, Pin } from "lucide-react";
import { MoreHorizontal, Pin } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { SetAsDefaultIcon } from "@plane/propel/icons";
import { Menu } from "@plane/propel/menu";
import { TabNavigationItem } from "@plane/propel/tab-navigation";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// local imports
import type { TNavigationItem } from "./tab-navigation-root";
import type { TTabPreferences } from "./tab-navigation-utils";
export type TTabNavigationOverflowMenuProps = {
type Props = {
overflowItems: TNavigationItem[];
isActive: (item: TNavigationItem) => boolean;
tabPreferences: TTabPreferences;
@ -19,9 +22,9 @@ export type TTabNavigationOverflowMenuProps = {
/**
* Overflow menu for tab navigation items
* Displays items that don't fit in the visible area, with action icons
* Shows "Eye" icon for user-hidden items, "Star" icon for all items
* Shows "Eye" icon for user-hidden items, "Set as default" icon for all items
*/
export const TabNavigationOverflowMenu: React.FC<TTabNavigationOverflowMenuProps> = ({
export const TabNavigationOverflowMenu: React.FC<Props> = ({
overflowItems,
isActive,
tabPreferences,
@ -48,23 +51,12 @@ export const TabNavigationOverflowMenu: React.FC<TTabNavigationOverflowMenuProps
const isDefault = item.key === tabPreferences.defaultTab;
return (
<Menu.MenuItem
key={`${item.key}-overflow-${itemIsActive ? "active" : "inactive"}`}
className={cn("p-0 w-full", {
"bg-custom-background-80": itemIsActive,
})}
>
<div className="flex items-center justify-between w-full group">
<Link to={item.href} className="flex-1 min-w-0 w-full">
<TabNavigationItem isActive={itemIsActive}>
<span className="text-sm">{t(item.i18n_key)}</span>
</TabNavigationItem>
<Menu.MenuItem key={`${item.key}-overflow-${itemIsActive ? "active" : "inactive"}`} className="p-0 w-full">
<div className="flex items-center justify-between w-full group/menu-item">
<Link to={item.href} className="flex-1 min-w-0 w-full p-1">
<span className="text-xs">{t(item.i18n_key)}</span>
</Link>
<div
className={cn("flex items-center gap-1 px-2 opacity-0 group-hover:opacity-100 transition-opacity", {
"opacity-100": itemIsActive,
})}
>
<div className="flex items-center">
{/* Show Eye icon ONLY for user-hidden items */}
{isHidden && (
<button
@ -74,23 +66,30 @@ export const TabNavigationOverflowMenu: React.FC<TTabNavigationOverflowMenuProps
e.preventDefault();
onShow(item.key);
}}
className="p-1 rounded hover:bg-custom-background-90"
className="invisible group-hover/menu-item:visible p-1 rounded text-custom-text-300 hover:text-custom-text-100 transition-colors"
title="Show"
>
<Pin className="h-3.5 w-3.5 text-custom-text-300 rotate-45" />
<Pin className="size-3" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onToggleDefault(item.key);
}}
className="p-1 rounded hover:bg-custom-background-90"
title={isDefault ? "Clear default" : "Set as default"}
>
<Star className={`h-3.5 w-3.5 text-custom-text-300 ${isDefault ? "fill-current" : ""}`} />
</button>
<Tooltip tooltipContent={isDefault ? "Clear default" : "Set as default"}>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onToggleDefault(item.key);
}}
className={cn(
"invisible group-hover/menu-item:visible p-1 rounded text-custom-text-300 hover:text-custom-text-100 transition-colors",
{
visible: isDefault,
}
)}
title={isDefault ? "Clear default" : "Set as default"}
>
<SetAsDefaultIcon className="size-3" />
</button>
</Tooltip>
</div>
</div>
</Menu.MenuItem>

View file

@ -167,21 +167,23 @@ export const TabNavigationRoot: FC<TTabNavigationRootProps> = observer((props) =
/>
{/* container for the tab navigation */}
<div className="flex items-center gap-3 overflow-hidden pl-1.5 w-full h-full">
<div className="flex items-center gap-2 flex-shrink-0 max-w-48 truncate">
<div className="flex items-center gap-3 overflow-hidden pl-1.5 size-full">
<div className="flex items-center gap-2 shrink-0">
<ProjectHeader project={project} />
<ProjectActionsMenu
workspaceSlug={workspaceSlug}
project={project}
isAdmin={isAdmin}
isAuthorized={isAuthorized}
onCopyText={handleCopyText}
onLeaveProject={handleLeaveProject}
onPublishModal={() => handlePublishModal(true)}
/>
<div className="shrink-0">
<ProjectActionsMenu
workspaceSlug={workspaceSlug}
project={project}
isAdmin={isAdmin}
isAuthorized={isAuthorized}
onCopyText={handleCopyText}
onLeaveProject={handleLeaveProject}
onPublishModal={() => handlePublishModal(true)}
/>
</div>
</div>
<div className="flex-shrink-0 h-5 w-1 border-l border-custom-border-200" />
<div className="shrink-0 h-5 w-1 border-l border-custom-border-200" />
<div ref={containerRef} className="flex items-center h-full flex-1 min-w-0 overflow-hidden">
<TabNavigationList className="h-full">

View file

@ -1,8 +1,12 @@
import React from "react";
import { Link } from "react-router";
import { PinOff } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { ContextMenu } from "@plane/propel/context-menu";
import { SetAsDefaultIcon } from "@plane/propel/icons";
import { TabNavigationItem } from "@plane/propel/tab-navigation";
// local imports
import type { TNavigationItem } from "./tab-navigation-root";
import type { TTabPreferences } from "./tab-navigation-utils";
@ -51,7 +55,9 @@ export const TabNavigationVisibleItem: React.FC<TTabNavigationVisibleItemProps>
e.stopPropagation();
onToggleDefault(item.key);
}}
className="flex items-center gap-2 text-custom-text-200 transition-colors cursor-pointer"
>
<SetAsDefaultIcon className="shrink-0 size-3" />
<span className="text-xs">{isDefault ? "Clear default" : "Set as default"}</span>
</ContextMenu.Item>
<ContextMenu.Item
@ -59,7 +65,9 @@ export const TabNavigationVisibleItem: React.FC<TTabNavigationVisibleItemProps>
e.stopPropagation();
onHide(item.key);
}}
className="flex items-center gap-2 text-custom-text-200 transition-colors cursor-pointer"
>
<PinOff className="shrink-0 size-3" />
<span className="text-xs">Hide in more menu</span>
</ContextMenu.Item>
</ContextMenu.Content>

View file

@ -207,19 +207,21 @@ export const TopNavPowerK = observer(() => {
);
return (
<div ref={containerRef} className="relative flex justify-center">
<div ref={containerRef} className="relative">
<div
className={cn(
"relative flex items-center transition-all duration-300 ease-in-out z-30",
isOpen ? "w-[554px]" : "w-[364px]"
)}
className={cn("relative w-[364px] flex items-center transition-all duration-300 ease-in-out z-30", {
"w-[554px]": isOpen,
})}
>
<div
className={cn(
"flex items-center w-full h-7 px-2 py-2 rounded-md bg-custom-sidebar-background-80 hover:bg-custom-background-80 transition-colors duration-200",
isOpen && "border border-custom-border-200"
"flex items-center w-full h-7 p-2 rounded-md bg-custom-sidebar-background-80 hover:bg-custom-background-80 border border-transparent transition-colors duration-200",
{
"border-custom-border-200": isOpen,
}
)}
onClick={() => inputRef.current?.focus()}
role="button"
>
<SearchIcon className="shrink-0 size-3.5 text-custom-text-350 mr-2" />
<input
@ -239,7 +241,6 @@ export const TopNavPowerK = observer(() => {
)}
</div>
</div>
<div
className={cn(
"absolute -top-[6px] left-1/2 -translate-x-1/2 bg-custom-background-100 border border-custom-border-200 rounded-md shadow-lg overflow-hidden z-20 transition-all duration-300 ease-in-out flex flex-col px-0 pt-10",

View file

@ -57,19 +57,10 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
const updatePreferences = async (newPreferences: TTabPreferences) => {
if (!memberId) return;
try {
await updateProjectMemberPreferences(workspaceSlug, projectId, memberId, {
default_tab: newPreferences.defaultTab,
hide_in_more_menu: newPreferences.hiddenTabs,
});
} catch (error) {
console.error("Error updating tab preferences:", error);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong. Please try again later.",
});
}
await updateProjectMemberPreferences(workspaceSlug, projectId, memberId, {
default_tab: newPreferences.defaultTab,
hide_in_more_menu: newPreferences.hiddenTabs,
});
};
/**
@ -79,7 +70,21 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
const handleToggleDefaultTab = (tabKey: string) => {
const newDefaultTab = tabKey === tabPreferences.defaultTab ? DEFAULT_TAB_KEY : tabKey;
const newPreferences = { ...tabPreferences, defaultTab: newDefaultTab };
updatePreferences(newPreferences);
updatePreferences(newPreferences)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Default tab updated successfully.",
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to update default tab. Please try again later.",
});
});
};
/**
@ -90,7 +95,16 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
...tabPreferences,
hiddenTabs: [...tabPreferences.hiddenTabs, tabKey],
};
updatePreferences(newPreferences);
try {
updatePreferences(newPreferences);
} catch (error) {
console.error("Error hiding tab:", error);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to hide tab. Please try again later.",
});
}
};
/**
@ -101,7 +115,16 @@ export const useTabPreferences = (workspaceSlug: string, projectId: string): TTa
...tabPreferences,
hiddenTabs: tabPreferences.hiddenTabs.filter((key) => key !== tabKey),
};
updatePreferences(newPreferences);
try {
updatePreferences(newPreferences);
} catch (error) {
console.error("Error showing tab:", error);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong. Please try again later.",
});
}
};
return {

View file

@ -65,14 +65,13 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
// Toggle sidebar dropdown state when either menu is open
useEffect(() => {
if (isWorkspaceMenuOpen) toggleAnySidebarDropdown(true);
else toggleAnySidebarDropdown(false);
}, [isWorkspaceMenuOpen]);
toggleAnySidebarDropdown(isWorkspaceMenuOpen);
}, [isWorkspaceMenuOpen, toggleAnySidebarDropdown]);
return (
<Menu
as="div"
className={cn("relative h-full flex max-w-48 truncate", {
className={cn("relative h-full flex max-w-48 w-fit whitespace-nowrap truncate", {
"justify-center text-center": renderLogoOnly,
"flex-grow justify-stretch text-left truncate": !renderLogoOnly,
})}

View file

@ -54,6 +54,7 @@ export * from "./properties";
export * from "./related-icon";
export * from "./sans-serif-icon";
export * from "./serif-icon";
export * from "./set-as-default-icon";
export * from "./side-panel-icon";
export * from "./state";
export * from "./sticky-note-icon";

View file

@ -0,0 +1,23 @@
import type { ISvgIcons } from "./type";
export function SetAsDefaultIcon({ className = "text-current", ...rest }: ISvgIcons) {
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...rest}
>
<path
d="M7.29167 0.625V13.9583M12.0057 2.57762L2.57762 12.0057M13.9583 7.29167H0.625M12.0057 12.0057L2.57762 2.57762"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}