mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 11:57:56 +01:00
[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:
committed by
GitHub
parent
fe867135c4
commit
1090b3e938
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
1
apps/web/ce/hooks/app-rail/index.ts
Normal file
1
apps/web/ce/hooks/app-rail/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./provider";
|
||||
17
apps/web/ce/hooks/app-rail/provider.tsx
Normal file
17
apps/web/ce/hooks/app-rail/provider.tsx
Normal 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>
|
||||
));
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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" : ""}`}>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
25
apps/web/core/lib/app-rail/context.tsx
Normal file
25
apps/web/core/lib/app-rail/context.tsx
Normal 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;
|
||||
};
|
||||
3
apps/web/core/lib/app-rail/index.ts
Normal file
3
apps/web/core/lib/app-rail/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./context";
|
||||
export * from "./provider";
|
||||
export * from "./types";
|
||||
46
apps/web/core/lib/app-rail/provider.tsx
Normal file
46
apps/web/core/lib/app-rail/provider.tsx
Normal 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>;
|
||||
});
|
||||
26
apps/web/core/lib/app-rail/types.ts
Normal file
26
apps/web/core/lib/app-rail/types.ts
Normal 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;
|
||||
}
|
||||
1
apps/web/ee/hooks/app-rail/index.ts
Normal file
1
apps/web/ee/hooks/app-rail/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "ce/hooks/app-rail";
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user