[WEB-5573] refactor: app rail enhancements (#8239)

* chore: app rail context added

* chore: dock/undock app rail implementation

* chore: refactor

* chore: code refactor

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia
2025-12-04 18:14:59 +05:30
committed by GitHub
parent fe867135c4
commit 1090b3e938
17 changed files with 162 additions and 22 deletions

View File

@@ -1,15 +1,18 @@
import { Outlet } from "react-router";
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/content-wrapper";
import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
export default function WorkspaceLayout() {
return (
<AuthenticationWrapper>
<WorkspaceAuthWrapper>
<WorkspaceContentWrapper>
<Outlet />
</WorkspaceContentWrapper>
<AppRailVisibilityProvider>
<WorkspaceContentWrapper>
<Outlet />
</WorkspaceContentWrapper>
</AppRailVisibilityProvider>
</WorkspaceAuthWrapper>
</AuthenticationWrapper>
);

View File

@@ -20,7 +20,7 @@ export function withDockItems<P extends WithDockItemsProps>(WrappedComponent: Re
const dockItems: (AppSidebarItemData & { shouldRender: boolean })[] = [
{
label: "Projects",
icon: <PlaneNewIcon className="size-4" />,
icon: <PlaneNewIcon className="size-5" />,
href: `/${workspaceSlug}/`,
isActive: isProjectsPath && !isNotificationsPath,
shouldRender: true,

View File

@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
// plane imports
import { cn } from "@plane/utils";
import { AppRailRoot } from "@/components/navigation";
// plane web imports
import { useAppRailVisibility } from "@/lib/app-rail";
// local imports
import { TopNavigationRoot } from "../navigations";
export const WorkspaceContentWrapper = observer(function WorkspaceContentWrapper({
@@ -11,14 +12,21 @@ export const WorkspaceContentWrapper = observer(function WorkspaceContentWrapper
}: {
children: React.ReactNode;
}) {
// Use the context to determine if app rail should render
const { shouldRenderAppRail } = useAppRailVisibility();
return (
<div className="flex flex-col relative size-full overflow-hidden bg-custom-background-90 transition-all ease-in-out duration-300">
<TopNavigationRoot />
<div className="relative flex size-full overflow-hidden">
<AppRailRoot />
{/* Conditionally render AppRailRoot based on context */}
{shouldRenderAppRail && <AppRailRoot />}
<div
className={cn(
"relative size-full pb-2 pr-2 flex-grow transition-all ease-in-out duration-300 overflow-hidden"
"relative size-full pl-0 pb-2 pr-2 flex-grow transition-all ease-in-out duration-300 overflow-hidden",
{
"pl-2": shouldRenderAppRail,
}
)}
>
{children}

View File

@@ -0,0 +1 @@
export * from "./provider";

View File

@@ -0,0 +1,17 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { AppRailVisibilityProvider as CoreProvider } from "@/lib/app-rail";
interface AppRailVisibilityProviderProps {
children: React.ReactNode;
}
/**
* CE AppRailVisibilityProvider
* Wraps core provider with isEnabled hardcoded to false
*/
export const AppRailVisibilityProvider = observer(({ children }: AppRailVisibilityProviderProps) => (
<CoreProvider isEnabled={false}>{children}</CoreProvider>
));

View File

@@ -8,6 +8,7 @@ import { cn } from "@plane/utils";
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
// hooks
import { useAppRailPreferences } from "@/hooks/use-navigation-preferences";
import { useAppRailVisibility } from "@/lib/app-rail/context";
// plane web imports
import { DesktopSidebarWorkspaceMenu } from "@/plane-web/components/desktop";
// local imports
@@ -19,6 +20,7 @@ export const AppRailRoot = observer(() => {
const pathname = usePathname();
// preferences
const { preferences, updateDisplayMode } = useAppRailPreferences();
const { isCollapsed, toggleAppRail } = useAppRailVisibility();
const isSettingsPath = pathname.includes(`/${workspaceSlug}/settings`);
const showLabel = preferences.displayMode === "icon_with_label";
@@ -70,6 +72,10 @@ export const AppRailRoot = observer(() => {
{preferences.displayMode === "icon_with_label" && <Check className="size-3.5" />}
</div>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={toggleAppRail}>
<span className="text-xs">{isCollapsed ? "Dock App Rail" : "Undock App Rail"}</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>

View File

@@ -32,7 +32,7 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
<AppSidebarItem
variant="button"
item={{
icon: <HelpCircle className="size-4" />,
icon: <HelpCircle className="size-5" />,
isActive: isNeedHelpOpen,
}}
/>

View File

@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// icons
import { LogOut, Settings } from "lucide-react";
import { LogOut, Settings, Settings2 } from "lucide-react";
// plane imports
import { GOD_MODE_URL } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@@ -74,7 +74,7 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) {
maxHeight="lg"
closeOnSelect
>
<div className="flex flex-col gap-2.5 pb-2">
<div className="flex flex-col gap-2">
<span className="px-2 text-custom-sidebar-text-200 truncate">{currentUser?.email}</span>
<Link href={`/${workspaceSlug}/settings/account`}>
<CustomMenu.MenuItem>
@@ -84,6 +84,14 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) {
</div>
</CustomMenu.MenuItem>
</Link>
<Link href={`/${workspaceSlug}/settings/account/preferences`}>
<CustomMenu.MenuItem>
<div className="flex w-full items-center gap-2 rounded text-xs">
<Settings2 className="h-4 w-4 stroke-[1.5]" />
<span>Preferences</span>
</div>
</CustomMenu.MenuItem>
</Link>
</div>
<div className="my-1 border-t border-custom-border-200" />
<div className={`${isUserInstanceAdmin ? "pb-2" : ""}`}>

View File

@@ -118,7 +118,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
<WorkspaceLogo
logo={activeWorkspace?.logo_url}
name={activeWorkspace?.name}
classNames="border border-custom-border-200 size-7"
classNames="border border-custom-border-200 rounded-md size-7"
/>
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ?? t("loading")}

View File

@@ -0,0 +1,25 @@
"use client";
import { createContext, useContext } from "react";
import type { IAppRailVisibilityContext } from "./types";
/**
* Context for app-rail visibility control
* Provides access to app rail enabled state, collapse state, and toggle function
*/
export const AppRailVisibilityContext = createContext<IAppRailVisibilityContext | undefined>(undefined);
/**
* Hook to consume the AppRailVisibilityContext
* Must be used within an AppRailVisibilityProvider
*
* @returns The app rail visibility context
* @throws Error if used outside of AppRailVisibilityProvider
*/
export const useAppRailVisibility = (): IAppRailVisibilityContext => {
const context = useContext(AppRailVisibilityContext);
if (context === undefined) {
throw new Error("useAppRailVisibility must be used within AppRailVisibilityProvider");
}
return context;
};

View File

@@ -0,0 +1,3 @@
export * from "./context";
export * from "./provider";
export * from "./types";

View File

@@ -0,0 +1,46 @@
"use client";
import React, { useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useLocalStorage from "@/hooks/use-local-storage";
import { AppRailVisibilityContext } from "./context";
import type { IAppRailVisibilityContext } from "./types";
interface AppRailVisibilityProviderProps {
children: React.ReactNode;
isEnabled?: boolean; // Allow override, default false
}
/**
* AppRailVisibilityProvider - manages app rail visibility state
* Base provider that accepts isEnabled as a prop
*/
export const AppRailVisibilityProvider = observer(({ children, isEnabled = false }: AppRailVisibilityProviderProps) => {
const { workspaceSlug } = useParams();
// User preference from localStorage
const { storedValue: isCollapsed, setValue: setIsCollapsed } = useLocalStorage<boolean>(
`APP_RAIL_${workspaceSlug}`,
false // Default: not collapsed (app rail visible)
);
const toggleAppRail = useCallback(() => {
setIsCollapsed(!isCollapsed);
}, [isCollapsed, setIsCollapsed]);
// Compute final visibility: enabled and not collapsed
const shouldRenderAppRail = isEnabled && !isCollapsed;
const value: IAppRailVisibilityContext = useMemo(
() => ({
isEnabled,
isCollapsed: isCollapsed ?? false,
shouldRenderAppRail,
toggleAppRail,
}),
[isEnabled, isCollapsed, shouldRenderAppRail, toggleAppRail]
);
return <AppRailVisibilityContext.Provider value={value}>{children}</AppRailVisibilityContext.Provider>;
});

View File

@@ -0,0 +1,26 @@
/**
* Type definitions for app-rail visibility context
*/
export interface IAppRailVisibilityContext {
/**
* Whether the app rail is enabled
*/
isEnabled: boolean;
/**
* Whether the app rail is collapsed (user preference from localStorage)
*/
isCollapsed: boolean;
/**
* Computed property: whether the app rail should actually render
* True only if isEnabled && !isCollapsed
*/
shouldRenderAppRail: boolean;
/**
* Toggle the collapse state of the app rail
*/
toggleAppRail: () => void;
}

View File

@@ -0,0 +1 @@
export * from "ce/hooks/app-rail";

View File

@@ -2699,8 +2699,8 @@ export default {
// Navigation customization
customize_navigation: "Customize navigation",
personal: "Personal",
accordion_navigation_control: "Accordion navigation control",
horizontal_navigation_bar: "Horizontal navigation bar",
accordion_navigation_control: "Accordion sidebar navigation",
horizontal_navigation_bar: "Tabbed Navigation",
show_limited_projects_on_sidebar: "Show limited projects on sidebar",
enter_number_of_projects: "Enter number of projects",
pin: "Pin",

View File

@@ -4,16 +4,14 @@ import { IconWrapper } from "../icon-wrapper";
import type { ISvgIcons } from "../type";
export function PlaneNewIcon({ color = "currentColor", ...rest }: ISvgIcons) {
const clipPathId = React.useId();
return (
<IconWrapper color={color} clipPathId={clipPathId} {...rest}>
<IconWrapper color={color} {...rest}>
<path
d="M5.15383 9.50566V5.15381H1.34152C0.601228 5.15381 0 5.75399 0 6.49533V14.6595C0 15.3998 0.600183 16.001 1.34152 16.001H9.50568C10.246 16.001 10.8472 15.4008 10.8472 14.6595V10.8461H6.49536C5.75506 10.8461 5.15383 10.246 5.15383 9.50461V9.50566Z"
d="M10.3617 10.3629V12.8365C10.3617 13.8272 9.55787 14.6303 8.56797 14.6303H3.17365C2.18298 14.6303 1.37988 13.8272 1.37988 12.8365V7.44221C1.37988 6.45077 2.18298 5.64844 3.17365 5.64844H5.64726V8.56915C5.64726 9.55982 6.45036 10.3629 7.44103 10.3629H10.3617Z"
fill={color}
/>
<path
d="M14.66 0H6.49582C5.75553 0 5.1543 0.600183 5.1543 1.34152V5.15488H9.50615C10.2464 5.15488 10.8477 5.75506 10.8477 6.49641V10.8483H14.661C15.4013 10.8483 16.0026 10.2481 16.0026 9.50673V1.34152C16.0026 0.601229 15.4024 0 14.661 0H14.66Z"
d="M14.6291 3.17365V8.56797C14.6291 9.55864 13.826 10.3617 12.8353 10.3617H10.3625V7.44103C10.3625 6.44959 9.55864 5.64726 8.56874 5.64726H5.64803V3.17365C5.64803 2.18298 6.45113 1.37988 7.44179 1.37988H12.8361C13.8275 1.37988 14.6291 2.18375 14.6291 3.17365Z"
fill={color}
/>
</IconWrapper>

View File

@@ -4,12 +4,10 @@ import { IconWrapper } from "../icon-wrapper";
import type { ISvgIcons } from "../type";
export function WikiIcon({ color = "currentColor", ...rest }: ISvgIcons) {
const clipPathId = React.useId();
return (
<IconWrapper color={color} clipPathId={clipPathId} {...rest}>
<IconWrapper color={color} {...rest}>
<path
d="M15.558 6.93332L9.06623 0.441504C8.47755 -0.147168 7.5229 -0.147168 6.93332 0.441504L0.441504 6.93332C-0.147168 7.52199 -0.147168 8.47664 0.441504 9.06623L6.93332 15.558C7.52199 16.1467 8.47664 16.1467 9.06623 15.558L15.558 9.06623C16.1467 8.47755 16.1467 7.5229 15.558 6.93332ZM10.7629 9.65855C10.7629 10.2682 10.2691 10.762 9.65946 10.762H6.341C5.73133 10.762 5.23758 10.2682 5.23758 9.65855V6.34008C5.23758 5.73042 5.73133 5.23667 6.341 5.23667H9.65946C10.2691 5.23667 10.7629 5.73042 10.7629 6.34008V9.65855Z"
d="M14.1062 6.74052L9.26925 1.90354C8.57104 1.20533 7.43873 1.20533 6.74052 1.90354L1.90354 6.74052C1.20533 7.43873 1.20533 8.57103 1.90354 9.26925L6.74052 14.1062C7.43873 14.8044 8.57104 14.8044 9.26925 14.1062L14.1062 9.26925C14.8044 8.57103 14.8044 7.43873 14.1062 6.74052ZM10.3648 9.74697C10.3648 10.0877 10.0884 10.364 9.74697 10.364H6.26279C5.92211 10.364 5.64496 10.0877 5.64496 9.74697V6.26203C5.64496 5.92134 5.92134 5.6442 6.26279 5.6442H9.74697C10.0884 5.6442 10.3648 5.92057 10.3648 6.26203V9.74697Z"
fill={color}
/>
</IconWrapper>