[WEB-4328] feat: sidebar revamp (#7217)
* chore: sidebar peek state added to theme store * chore: extended sidebar wrapper added * chore: resizeable sidebar component added * chore: appsidebar root component * chore: updated sidebar and applied necessary changes across codebase * chore: code refactor * chore: code refactor * chore: code refactor * chore: breadcrumb changes * chore: sidebar improvements and fixes * chore: enhancements and fixes * fix: peek sidebar leave * chore: code refactor * chore: code refactor * chore: code refactor * chore: icons added * chore: add dock variable and toggle function to theme store * chore: code refactor * chore: code refactor * chore: code refactor * chore: theme and workspace store updated * chore: workspace content wrapper and apprail context * chore: workspace and project wrapper updated * chore: app rail component * chore: content wrapper * chore: sidebar component updated * chore: layout changes and code refactoring * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: appsidebar toggle button * chore: code refactor * chore: workspace menu improvements * chore: sidebar spacing and padding improvements * chore: settings layout improvement * chore: enhancements * chore: extended sidebar code refactor * chore: code refactor * fix: merge conflict * fix: merge conflict * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor
This commit is contained in:
parent
fd9da3164e
commit
0225d806cc
69 changed files with 2126 additions and 1143 deletions
63
apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx
Normal file
63
apps/web/app/(all)/[workspaceSlug]/(projects)/_sidebar.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"use client";
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { SIDEBAR_WIDTH } from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
// hooks
|
||||
import { ResizableSidebar } from "@/components/sidebar";
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
// local imports
|
||||
import { ExtendedAppSidebar } from "./extended-sidebar";
|
||||
import { AppSidebar } from "./sidebar";
|
||||
|
||||
export const ProjectAppSidebar: FC = observer(() => {
|
||||
// store hooks
|
||||
const {
|
||||
sidebarCollapsed,
|
||||
toggleSidebar,
|
||||
sidebarPeek,
|
||||
toggleSidebarPeek,
|
||||
isExtendedSidebarOpened,
|
||||
isAnySidebarDropdownOpen,
|
||||
} = useAppTheme();
|
||||
const { storedValue, setValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH);
|
||||
// states
|
||||
const [sidebarWidth, setSidebarWidth] = useState<number>(storedValue ?? SIDEBAR_WIDTH);
|
||||
// hooks
|
||||
const { shouldRenderAppRail } = useAppRail();
|
||||
// derived values
|
||||
const isAnyExtendedSidebarOpen = isExtendedSidebarOpened;
|
||||
|
||||
// handlers
|
||||
const handleWidthChange = (width: number) => setValue(width);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableSidebar
|
||||
showPeek={sidebarPeek}
|
||||
defaultWidth={storedValue ?? 250}
|
||||
width={sidebarWidth}
|
||||
setWidth={setSidebarWidth}
|
||||
defaultCollapsed={sidebarCollapsed}
|
||||
peekDuration={1500}
|
||||
onWidthChange={handleWidthChange}
|
||||
onCollapsedChange={toggleSidebar}
|
||||
isCollapsed={sidebarCollapsed}
|
||||
toggleCollapsed={toggleSidebar}
|
||||
togglePeek={toggleSidebarPeek}
|
||||
extendedSidebar={
|
||||
<>
|
||||
<ExtendedAppSidebar />
|
||||
</>
|
||||
}
|
||||
isAnyExtendedSidebarExpanded={isAnyExtendedSidebarOpen}
|
||||
isAnySidebarDropdownOpen={isAnySidebarDropdownOpen}
|
||||
disablePeekTrigger={shouldRenderAppRail}
|
||||
>
|
||||
<AppSidebar />
|
||||
</ResizableSidebar>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -8,14 +8,14 @@ import { Plus, Search } from "lucide-react";
|
|||
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
import { cn, copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
|
||||
import { copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
|
||||
// components
|
||||
import { CreateProjectModal } from "@/components/project";
|
||||
import { SidebarProjectsListItem } from "@/components/workspace";
|
||||
// hooks
|
||||
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
|
||||
import { TProject } from "@/plane-web/types";
|
||||
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
|
||||
|
||||
export const ExtendedProjectSidebar = observer(() => {
|
||||
// refs
|
||||
|
|
@ -27,7 +27,7 @@ export const ExtendedProjectSidebar = observer(() => {
|
|||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed, extendedProjectSidebarCollapsed, toggleExtendedProjectSidebar } = useAppTheme();
|
||||
const { isExtendedProjectSidebarOpened, toggleExtendedProjectSidebar } = useAppTheme();
|
||||
const { getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
|
|
@ -74,15 +74,7 @@ export const ExtendedProjectSidebar = observer(() => {
|
|||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
useExtendedSidebarOutsideClickDetector(
|
||||
extendedProjectSidebarRef,
|
||||
() => {
|
||||
if (!isProjectModalOpen) {
|
||||
toggleExtendedProjectSidebar(false);
|
||||
}
|
||||
},
|
||||
"extended-project-sidebar-toggle"
|
||||
);
|
||||
const handleClose = () => toggleExtendedProjectSidebar(false);
|
||||
|
||||
const handleCopyText = (projectId: string) => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
|
||||
|
|
@ -103,17 +95,11 @@ export const ExtendedProjectSidebar = observer(() => {
|
|||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={extendedProjectSidebarRef}
|
||||
className={cn(
|
||||
"absolute top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 shadow-md",
|
||||
{
|
||||
"translate-x-0 opacity-100 pointer-events-auto": extendedProjectSidebarCollapsed,
|
||||
"-translate-x-full opacity-0 pointer-events-none": !extendedProjectSidebarCollapsed,
|
||||
"left-[70px]": sidebarCollapsed,
|
||||
"left-[250px]": !sidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
<ExtendedSidebarWrapper
|
||||
isExtendedSidebarOpened={!!isExtendedProjectSidebarOpened}
|
||||
extendedSidebarRef={extendedProjectSidebarRef}
|
||||
handleClose={handleClose}
|
||||
excludedElementId="extended-project-sidebar-toggle"
|
||||
>
|
||||
<div className="flex flex-col gap-1 w-full sticky top-4 pt-0 px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -159,7 +145,7 @@ export const ExtendedProjectSidebar = observer(() => {
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ExtendedSidebarWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EXTENDED_SIDEBAR_WIDTH, SIDEBAR_WIDTH } from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
extendedSidebarRef: React.RefObject<HTMLDivElement>;
|
||||
isExtendedSidebarOpened: boolean;
|
||||
handleClose: () => void;
|
||||
excludedElementId: string;
|
||||
};
|
||||
|
||||
export const ExtendedSidebarWrapper: FC<Props> = observer((props) => {
|
||||
const { children, extendedSidebarRef, isExtendedSidebarOpened, handleClose, excludedElementId } = props;
|
||||
// store hooks
|
||||
const { storedValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH);
|
||||
|
||||
useExtendedSidebarOutsideClickDetector(extendedSidebarRef, handleClose, excludedElementId);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={excludedElementId}
|
||||
ref={extendedSidebarRef}
|
||||
className={cn(
|
||||
`absolute h-full z-[19] flex flex-col py-2 transform transition-all duration-300 ease-in-out bg-custom-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-sm`,
|
||||
{
|
||||
"translate-x-0 opacity-100": isExtendedSidebarOpened,
|
||||
[`-translate-x-[${EXTENDED_SIDEBAR_WIDTH}px] opacity-0 hidden`]: !isExtendedSidebarOpened,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
left: `${storedValue ?? SIDEBAR_WIDTH}px`,
|
||||
width: `${isExtendedSidebarOpened ? EXTENDED_SIDEBAR_WIDTH : 0}px`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -6,12 +6,11 @@ import { useParams } from "next/navigation";
|
|||
// plane imports
|
||||
import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS } from "@plane/constants";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useAppTheme, useWorkspace } from "@/hooks/store";
|
||||
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
|
||||
// plane-web imports
|
||||
import { ExtendedSidebarItem } from "@/plane-web/components/workspace/sidebar";
|
||||
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
|
||||
|
||||
export const ExtendedAppSidebar = observer(() => {
|
||||
// refs
|
||||
|
|
@ -19,7 +18,7 @@ export const ExtendedAppSidebar = observer(() => {
|
|||
// routers
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
|
||||
const { isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme();
|
||||
const { updateSidebarPreference, getNavigationPreferences } = useWorkspace();
|
||||
|
||||
// derived values
|
||||
|
|
@ -95,24 +94,14 @@ export const ExtendedAppSidebar = observer(() => {
|
|||
});
|
||||
};
|
||||
|
||||
useExtendedSidebarOutsideClickDetector(
|
||||
extendedSidebarRef,
|
||||
() => toggleExtendedSidebar(true),
|
||||
"extended-sidebar-toggle"
|
||||
);
|
||||
const handleClose = () => toggleExtendedSidebar(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={extendedSidebarRef}
|
||||
className={cn(
|
||||
"absolute top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6",
|
||||
{
|
||||
"-translate-x-full opacity-0 pointer-events-none": extendedSidebarCollapsed,
|
||||
"translate-x-0 opacity-100 pointer-events-auto": !extendedSidebarCollapsed,
|
||||
"left-[70px]": sidebarCollapsed,
|
||||
"left-[250px]": !sidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
<ExtendedSidebarWrapper
|
||||
isExtendedSidebarOpened={!!isExtendedSidebarOpened}
|
||||
extendedSidebarRef={extendedSidebarRef}
|
||||
handleClose={handleClose}
|
||||
excludedElementId="extended-sidebar-toggle"
|
||||
>
|
||||
{sortedNavigationItems.map((item, index) => (
|
||||
<ExtendedSidebarItem
|
||||
|
|
@ -122,6 +111,6 @@ export const ExtendedAppSidebar = observer(() => {
|
|||
handleOnNavigationItemDrop={handleOnNavigationItemDrop}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ExtendedSidebarWrapper>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Home } from "lucide-react";
|
||||
|
|
@ -16,16 +17,17 @@ import { BreadcrumbLink } from "@/components/common";
|
|||
// hooks
|
||||
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
|
||||
|
||||
export const WorkspaceDashboardHeader = () => {
|
||||
export const WorkspaceDashboardHeader = observer(() => {
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
|
|
@ -65,4 +67,4 @@ export const WorkspaceDashboardHeader = () => {
|
|||
</Header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
// plane web components
|
||||
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
|
||||
import { AppSidebar } from "./sidebar";
|
||||
import { ProjectAppSidebar } from "./_sidebar";
|
||||
|
||||
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<CommandPalette />
|
||||
<WorkspaceAuthWrapper>
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<AppSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{children}
|
||||
</main>
|
||||
<div className="relative flex flex-col h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<div className="relative flex size-full overflow-hidden">
|
||||
<ProjectAppSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceAuthWrapper>
|
||||
</AuthenticationWrapper>
|
||||
|
|
|
|||
|
|
@ -5,25 +5,25 @@ import { observer } from "mobx-react";
|
|||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { SidebarDropdown, SidebarHelpSection, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace";
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar";
|
||||
import { SidebarDropdown, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace";
|
||||
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
|
||||
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
|
||||
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useAppTheme, useUserPermissions } from "@/hooks/store";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
|
||||
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
|
||||
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
|
||||
import { ExtendedProjectSidebar } from "./extended-project-sidebar";
|
||||
import { ExtendedAppSidebar } from "./extended-sidebar";
|
||||
|
||||
export const AppSidebar: FC = observer(() => {
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
|
||||
const { groupedFavorites } = useFavorite();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
|
|
@ -52,60 +52,38 @@ export const AppSidebar: FC = observer(() => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 w-[250px] md:relative md:ml-0",
|
||||
{
|
||||
"w-[70px] -ml-[250px]": sidebarCollapsed,
|
||||
}
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
{!shouldRenderAppRail && <SidebarDropdown />}
|
||||
|
||||
{isAppRailEnabled && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-md text-custom-text-200 font-medium pt-1">Projects</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppSidebarToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("size-full flex flex-col flex-1 pt-4 pb-0", {
|
||||
"p-2 pt-4": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn("px-2", {
|
||||
"px-4": !sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
{/* Workspace switcher and settings */}
|
||||
<SidebarDropdown />
|
||||
<div className="flex-shrink-0 h-4" />
|
||||
{/* App switcher */}
|
||||
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
|
||||
{/* Quick actions */}
|
||||
<SidebarQuickActions />
|
||||
</div>
|
||||
<hr
|
||||
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
|
||||
"opacity-0": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2 py-0.5", {
|
||||
"vertical-scrollbar px-4": !sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<SidebarMenuItems />
|
||||
{sidebarCollapsed && (
|
||||
<hr className="flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1" />
|
||||
)}
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<SidebarHelpSection />
|
||||
{/* Quick actions */}
|
||||
<SidebarQuickActions />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
<div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div>
|
||||
</div>
|
||||
<ExtendedAppSidebar />
|
||||
<ExtendedProjectSidebar />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
|
|||
<AuthenticationWrapper>
|
||||
<WorkspaceAuthWrapper>
|
||||
<CommandPalette />
|
||||
<main className="relative flex h-screen w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{/* Header */}
|
||||
<SettingsHeader />
|
||||
{/* Content */}
|
||||
<ContentWrapper className="px-4 md:pl-12 md:flex w-full">
|
||||
<div className="w-full h-full overflow-hidden">{children}</div>
|
||||
</ContentWrapper>
|
||||
</main>
|
||||
<div className="relative flex h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{/* Header */}
|
||||
<SettingsHeader />
|
||||
{/* Content */}
|
||||
<ContentWrapper className="p-page-x md:flex w-full">
|
||||
<div className="w-full h-full overflow-hidden">{children}</div>
|
||||
</ContentWrapper>
|
||||
</main>
|
||||
</div>
|
||||
</WorkspaceAuthWrapper>
|
||||
</AuthenticationWrapper>
|
||||
);
|
||||
|
|
|
|||
12
apps/web/app/(all)/[workspaceSlug]/layout.tsx
Normal file
12
apps/web/app/(all)/[workspaceSlug]/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { AppRailProvider } from "@/hooks/context/app-rail-context";
|
||||
import { WorkspaceContentWrapper } from "@/plane-web/components/workspace";
|
||||
|
||||
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AppRailProvider>
|
||||
<WorkspaceContentWrapper>{children}</WorkspaceContentWrapper>
|
||||
</AppRailProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export default function ProfileSettingsLayout(props: Props) {
|
|||
<>
|
||||
<CommandPalette />
|
||||
<AuthenticationWrapper>
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
<div className="relative flex h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
|
||||
<ProfileLayoutSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
<div className="h-full w-full overflow-hidden">{children}</div>
|
||||
|
|
|
|||
1
apps/web/ce/components/app-rail/index.ts
Normal file
1
apps/web/ce/components/app-rail/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
4
apps/web/ce/components/app-rail/root.tsx
Normal file
4
apps/web/ce/components/app-rail/root.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
|
||||
export const AppRailRoot = () => <></>;
|
||||
|
|
@ -7,12 +7,9 @@ import { ProjectNavigation } from "@/components/workspace";
|
|||
type TProjectItemsRootProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
};
|
||||
|
||||
export const ProjectNavigationRoot: FC<TProjectItemsRootProps> = (props) => {
|
||||
const { workspaceSlug, projectId, isSidebarCollapsed } = props;
|
||||
return (
|
||||
<ProjectNavigation workspaceSlug={workspaceSlug} projectId={projectId} isSidebarCollapsed={isSidebarCollapsed} />
|
||||
);
|
||||
const { workspaceSlug, projectId } = props;
|
||||
return <ProjectNavigation workspaceSlug={workspaceSlug} projectId={projectId} />;
|
||||
};
|
||||
|
|
|
|||
5
apps/web/ce/components/workspace/app-switcher.tsx
Normal file
5
apps/web/ce/components/workspace/app-switcher.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const WorkspaceAppSwitcher = () => <></>;
|
||||
9
apps/web/ce/components/workspace/content-wrapper.tsx
Normal file
9
apps/web/ce/components/workspace/content-wrapper.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const WorkspaceContentWrapper = observer(({ children }: { children: React.ReactNode }) => (
|
||||
<div className="flex relative size-full overflow-hidden bg-custom-background-90 rounded-lg transition-all ease-in-out duration-300">
|
||||
<div className="size-full p-2 flex-grow transition-all ease-in-out duration-300">{children}</div>
|
||||
</div>
|
||||
));
|
||||
|
|
@ -4,3 +4,5 @@ export * from "./billing";
|
|||
export * from "./delete-workspace-section";
|
||||
export * from "./sidebar";
|
||||
export * from "./members";
|
||||
export * from "./content-wrapper";
|
||||
export * from "./app-switcher";
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import { observer } from "mobx-react";
|
|||
import { Search } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useAppTheme, useCommandPalette } from "@/hooks/store";
|
||||
import { useCommandPalette } from "@/hooks/store";
|
||||
|
||||
export const AppSearch = observer(() => {
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { toggleCommandPaletteModal } = useCommandPalette();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -17,12 +14,7 @@ export const AppSearch = observer(() => {
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex-shrink-0 size-8 aspect-square grid place-items-center rounded hover:bg-custom-sidebar-background-90 outline-none",
|
||||
{
|
||||
"border-[0.5px] border-custom-sidebar-border-300": !sidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
className="flex-shrink-0 size-8 aspect-square grid place-items-center rounded hover:bg-custom-sidebar-background-90 outline-none border-[0.5px] border-custom-sidebar-border-300"
|
||||
onClick={() => toggleCommandPaletteModal(true)}
|
||||
aria-label={t("aria_labels.projects_sidebar.open_command_palette")}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -171,10 +171,7 @@ export const ExtendedSidebarItem: FC<TExtendedSidebarItemProps> = observer((prop
|
|||
className={cn(
|
||||
"flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
|
||||
{
|
||||
// "cursor-not-allowed opacity-60": project.sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
|
||||
// "!hidden": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
ref={dragHandleRef}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import Link from "next/link";
|
|||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
import { NotificationAppSidebarOption } from "@/components/workspace-notifications";
|
||||
|
|
@ -31,14 +29,13 @@ export const SidebarItem: FC<TSidebarItemProps> = observer((props) => {
|
|||
const { data } = useUser();
|
||||
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { toggleSidebar, isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme();
|
||||
|
||||
const handleLinkClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
if (!extendedSidebarCollapsed) toggleExtendedSidebar();
|
||||
if (isExtendedSidebarOpened) toggleExtendedSidebar(false);
|
||||
};
|
||||
|
||||
const staticItems = ["home", "inbox", "pi-chat", "projects"];
|
||||
|
|
@ -61,30 +58,14 @@ export const SidebarItem: FC<TSidebarItemProps> = observer((props) => {
|
|||
const icon = getSidebarNavigationItemIcon(item.key);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipContent={t(item.labelTranslationKey)}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link href={itemHref} onClick={() => handleLinkClick()}>
|
||||
<SidebarNavItem
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={isActive}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
{icon}
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
|
||||
</div>
|
||||
{item.key === "inbox" && (
|
||||
<NotificationAppSidebarOption
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
isSidebarCollapsed={sidebarCollapsed ?? false}
|
||||
/>
|
||||
)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Link href={itemHref} onClick={() => handleLinkClick()}>
|
||||
<SidebarNavItem isActive={isActive}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
{icon}
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
</div>
|
||||
{item.key === "inbox" && <NotificationAppSidebarOption workspaceSlug={workspaceSlug?.toString()} />}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
// components
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { Row } from "@plane/ui";
|
||||
import { SidebarHamburgerToggle } from "@/components/core";
|
||||
// components
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
|
||||
export interface AppHeaderProps {
|
||||
header: ReactNode;
|
||||
mobileHeader?: ReactNode;
|
||||
}
|
||||
|
||||
export const AppHeader = (props: AppHeaderProps) => {
|
||||
export const AppHeader = observer((props: AppHeaderProps) => {
|
||||
const { header, mobileHeader } = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
|
||||
return (
|
||||
<div className="z-[18]">
|
||||
<Row className="h-[3.75rem] flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100">
|
||||
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
{sidebarCollapsed && <AppSidebarToggleButton />}
|
||||
<div className="w-full">{header}</div>
|
||||
</Row>
|
||||
{mobileHeader && mobileHeader}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Menu } from "lucide-react";
|
||||
import { PanelRight } from "lucide-react";
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
|
||||
export const SidebarHamburgerToggle = observer(() => {
|
||||
// store hooks
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="group flex-shrink-0 size-7 grid place-items-center rounded bg-custom-background-80 transition-all hover:bg-custom-background-90 md:hidden"
|
||||
onClick={() => toggleSidebar()}
|
||||
className="group flex-shrink-0 size-7 grid place-items-center rounded hover:bg-custom-background-80 transition-all bg-custom-background-90"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Menu className="size-3.5 text-custom-text-200 transition-all group-hover:text-custom-text-100" />
|
||||
<PanelRight className="size-3.5 text-custom-text-200 transition-all group-hover:text-custom-text-100" />
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { getButtonStyling } from "@plane/ui/src/button";
|
||||
|
|
@ -15,16 +16,16 @@ export const SettingsHeader = observer(() => {
|
|||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { isScrolled } = useUserSettings();
|
||||
// resolved theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// redirect url for normal mode
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-custom-background-90 px-4 py-4 gap-2 md:px-12 md:py-8 transition-all duration-300 ease-in-out relative",
|
||||
{
|
||||
"!pt-4 flex md:flex-col": isScrolled,
|
||||
}
|
||||
)}
|
||||
className={cn("bg-custom-background-90 p-page-x transition-all duration-300 ease-in-out relative", {
|
||||
"!pt-4 flex md:flex-col": isScrolled,
|
||||
"bg-custom-background-90/50": resolvedTheme === "dark",
|
||||
})}
|
||||
>
|
||||
<Link
|
||||
href={`/${currentWorkspace?.slug}`}
|
||||
|
|
@ -41,7 +42,7 @@ export const SettingsHeader = observer(() => {
|
|||
<Link
|
||||
href={`/${currentWorkspace?.slug}`}
|
||||
className={cn(
|
||||
"group flex gap-2 text-custom-text-300 mb-4 border border-transparent w-fit rounded-lg",
|
||||
"group flex gap-2 text-custom-text-300 mb-3 border border-transparent w-fit rounded-lg",
|
||||
!isScrolled ? "hover:bg-custom-background-100 hover:border-custom-border-200 items-center pr-2 " : " h-0 m-0"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ScrollArea } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { SettingsSidebarHeader } from "./header";
|
||||
import SettingsSidebarNavItem, { TSettingItem } from "./nav-item";
|
||||
|
|
@ -45,12 +46,15 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
|
|||
{/* Header */}
|
||||
<SettingsSidebarHeader customHeader={customHeader} />
|
||||
{/* Navigation */}
|
||||
<div className="divide-y divide-custom-border-100 overflow-x-hidden w-full h-full overflow-y-scroll">
|
||||
<ScrollArea
|
||||
className="divide-y divide-custom-border-100 overflow-x-hidden w-full h-full overflow-y-scroll"
|
||||
type="hover"
|
||||
>
|
||||
{categories.map((category) => {
|
||||
if (groupedSettings[category].length === 0) return null;
|
||||
return (
|
||||
<div key={category} className="py-3">
|
||||
<span className="text-sm font-semibold text-custom-text-350 capitalize mb-2">{t(category)}</span>
|
||||
<span className="text-sm font-semibold text-custom-text-350 capitalize mb-2 px-2">{t(category)}</span>
|
||||
<div className="relative flex flex-col gap-0.5 h-full mt-2">
|
||||
{groupedSettings[category].map(
|
||||
(setting) =>
|
||||
|
|
@ -70,7 +74,7 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
export * from "./sidebar-navigation";
|
||||
export * from "./sidebar-navigation";
|
||||
export * from "./resizable-sidebar";
|
||||
export * from "./sidebar-item";
|
||||
export * from "./sidebar-toggle-button";
|
||||
|
|
|
|||
287
apps/web/core/components/sidebar/resizable-sidebar.tsx
Normal file
287
apps/web/core/components/sidebar/resizable-sidebar.tsx
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
"use client";
|
||||
|
||||
import React, { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useState, useRef } from "react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
interface ResizableSidebarProps {
|
||||
showPeek?: boolean;
|
||||
togglePeek: (value?: boolean) => void;
|
||||
isCollapsed?: boolean;
|
||||
width: number;
|
||||
setWidth: Dispatch<SetStateAction<number>>;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
defaultCollapsed?: boolean;
|
||||
peekDuration?: number;
|
||||
toggleCollapsed: (value?: boolean) => void;
|
||||
onWidthChange?: (width: number) => void;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
className?: string;
|
||||
children?: ReactElement;
|
||||
extendedSidebar?: ReactElement;
|
||||
isAnyExtendedSidebarExpanded?: boolean;
|
||||
isAnySidebarDropdownOpen?: boolean;
|
||||
disablePeekTrigger?: boolean;
|
||||
}
|
||||
|
||||
export function ResizableSidebar({
|
||||
showPeek = false,
|
||||
togglePeek,
|
||||
peekDuration = 500,
|
||||
isCollapsed = false,
|
||||
toggleCollapsed: toggleCollapsedProp,
|
||||
onCollapsedChange,
|
||||
width,
|
||||
setWidth,
|
||||
onWidthChange,
|
||||
minWidth = 236,
|
||||
maxWidth = 350,
|
||||
className = "",
|
||||
children,
|
||||
extendedSidebar,
|
||||
isAnyExtendedSidebarExpanded = false,
|
||||
isAnySidebarDropdownOpen = false,
|
||||
disablePeekTrigger = false,
|
||||
}: ResizableSidebarProps) {
|
||||
// states
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [isHoveringTrigger, setIsHoveringTrigger] = useState(false);
|
||||
// refs
|
||||
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// handlers
|
||||
const setShowPeek = useCallback(
|
||||
(value: boolean) => {
|
||||
togglePeek(value);
|
||||
},
|
||||
[togglePeek]
|
||||
);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
const newWidth = Math.min(Math.max(e.clientX, minWidth), maxWidth);
|
||||
setWidth(newWidth);
|
||||
},
|
||||
[isResizing, minWidth, maxWidth, setWidth]
|
||||
);
|
||||
|
||||
const startResizing = useCallback(() => {
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
toggleCollapsedProp();
|
||||
setShowPeek(false);
|
||||
setIsHoveringTrigger(false);
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}, [toggleCollapsedProp, setShowPeek]);
|
||||
|
||||
const handleTriggerEnter = useCallback(() => {
|
||||
if (isCollapsed) {
|
||||
setIsHoveringTrigger(true);
|
||||
setShowPeek(true);
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}
|
||||
}, [isCollapsed, setShowPeek]);
|
||||
|
||||
const handleTriggerLeave = useCallback(() => {
|
||||
if (isCollapsed && !isAnyExtendedSidebarExpanded) {
|
||||
setIsHoveringTrigger(false);
|
||||
peekTimeoutRef.current = setTimeout(() => {
|
||||
setShowPeek(false);
|
||||
}, peekDuration);
|
||||
}
|
||||
}, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded]);
|
||||
|
||||
const handlePeekEnter = useCallback(() => {
|
||||
if (isCollapsed && showPeek) {
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}
|
||||
}, [isCollapsed, showPeek]);
|
||||
|
||||
const handlePeekLeave = useCallback(() => {
|
||||
if (isCollapsed && !isAnyExtendedSidebarExpanded && !isAnySidebarDropdownOpen) {
|
||||
peekTimeoutRef.current = setTimeout(() => {
|
||||
setShowPeek(false);
|
||||
}, peekDuration);
|
||||
}
|
||||
}, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded, isAnySidebarDropdownOpen]);
|
||||
|
||||
// Set up event listeners for resizing
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener("mousemove", handleResize);
|
||||
document.addEventListener("mouseup", stopResizing);
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleResize);
|
||||
document.removeEventListener("mouseup", stopResizing);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [isResizing, handleResize, stopResizing]);
|
||||
|
||||
// Clean up timeout on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnySidebarDropdownOpen && isCollapsed && isHoveringTrigger) {
|
||||
handlePeekLeave();
|
||||
}
|
||||
}, [isAnySidebarDropdownOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnyExtendedSidebarExpanded && isCollapsed && isHoveringTrigger) {
|
||||
handlePeekLeave();
|
||||
}
|
||||
}, [isAnyExtendedSidebarExpanded]);
|
||||
|
||||
// Reset peek when sidebar is expanded
|
||||
useEffect(() => {
|
||||
if (!isCollapsed) {
|
||||
setShowPeek(false);
|
||||
setIsHoveringTrigger(false);
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}
|
||||
}, [isCollapsed, setShowPeek]);
|
||||
|
||||
// Call external handlers when state changes
|
||||
useEffect(() => {
|
||||
onWidthChange?.(width);
|
||||
}, [width, onWidthChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onCollapsedChange?.(isCollapsed);
|
||||
}, [isCollapsed, onCollapsedChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main Sidebar */}
|
||||
<div
|
||||
className={cn(
|
||||
"h-full z-20 bg-custom-background-100 border-r border-custom-sidebar-border-200",
|
||||
!isResizing && "transition-all duration-300 ease-in-out",
|
||||
isCollapsed ? "translate-x-[-100%] opacity-0 w-0" : "translate-x-0 opacity-100",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: `${isCollapsed ? 0 : width}px`,
|
||||
minWidth: `${isCollapsed ? 0 : width}px`,
|
||||
maxWidth: `${isCollapsed ? 0 : width}px`,
|
||||
}}
|
||||
role="complementary"
|
||||
aria-label="Main sidebar"
|
||||
>
|
||||
<aside
|
||||
className={cn(
|
||||
"group/sidebar h-full w-full bg-custom-background-100 overflow-hidden relative flex flex-col pt-3",
|
||||
isAnyExtendedSidebarExpanded && "rounded-none"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-200 cursor-ew-resize absolute h-full w-1 z-[20]",
|
||||
!isResizing && "hover:bg-custom-background-90",
|
||||
isResizing && "w-1.5 bg-custom-background-80",
|
||||
"top-0 right-0"
|
||||
)}
|
||||
// onDoubleClick toggle sidebar
|
||||
onDoubleClick={() => toggleCollapsed()}
|
||||
onMouseDown={startResizing}
|
||||
role="separator"
|
||||
aria-label="Resize sidebar"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Peek Trigger Area */}
|
||||
{isCollapsed && !disablePeekTrigger && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0 left-0 w-1 h-full z-50 bg-transparent",
|
||||
"transition-opacity duration-200",
|
||||
isHoveringTrigger ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
onMouseEnter={handleTriggerEnter}
|
||||
onMouseLeave={handleTriggerLeave}
|
||||
role="button"
|
||||
aria-label="Show sidebar peek"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Peek View */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 z-20 bg-custom-background-100 shadow-sm h-full",
|
||||
!isResizing && "transition-all duration-300 ease-in-out",
|
||||
isCollapsed && showPeek ? "translate-x-0 opacity-100" : "translate-x-[-100%] opacity-0",
|
||||
"pointer-events-none",
|
||||
isCollapsed && showPeek && "pointer-events-auto",
|
||||
!showPeek ? "w-0" : "w-full"
|
||||
)}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
}}
|
||||
onMouseEnter={handlePeekEnter}
|
||||
onMouseLeave={handlePeekLeave}
|
||||
role="complementary"
|
||||
aria-label="Sidebar peek view"
|
||||
>
|
||||
<aside
|
||||
className={cn(
|
||||
"group/sidebar h-full w-full bg-custom-background-100 overflow-hidden relative flex flex-col z-20 pt-4",
|
||||
"self-center border-r border-custom-sidebar-border-200 rounded-md rounded-tl-none rounded-bl-none",
|
||||
isAnyExtendedSidebarExpanded && "rounded-none"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-200 cursor-ew-resize absolute h-full w-1 z-[20]",
|
||||
!isResizing && "hover:bg-custom-background-90",
|
||||
isResizing && "bg-custom-background-80",
|
||||
"top-0 right-0"
|
||||
)}
|
||||
// onDoubleClick toggle sidebar
|
||||
onDoubleClick={() => toggleCollapsed()}
|
||||
onMouseDown={startResizing}
|
||||
role="separator"
|
||||
aria-label="Resize sidebar"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Extended Sidebar */}
|
||||
{extendedSidebar && extendedSidebar}
|
||||
</>
|
||||
);
|
||||
}
|
||||
158
apps/web/core/components/sidebar/sidebar-item.tsx
Normal file
158
apps/web/core/components/sidebar/sidebar-item.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import Link from "next/link";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
interface AppSidebarItemData {
|
||||
href?: string;
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface AppSidebarItemProps {
|
||||
variant?: "link" | "button";
|
||||
item?: AppSidebarItemData;
|
||||
}
|
||||
|
||||
interface AppSidebarItemLabelProps {
|
||||
highlight?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface AppSidebarItemIconProps {
|
||||
icon?: React.ReactNode;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
interface AppSidebarLinkItemProps {
|
||||
href?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface AppSidebarButtonItemProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = {
|
||||
base: "group flex flex-col gap-0.5 items-center justify-center text-custom-text-300",
|
||||
icon: "flex items-center justify-center gap-2 size-8 rounded-md text-custom-text-300",
|
||||
iconActive: "bg-custom-background-80 text-custom-text-200",
|
||||
iconInactive: "group-hover:text-custom-text-200 group-hover:bg-custom-background-80",
|
||||
label: "text-xs font-semibold",
|
||||
labelActive: "text-custom-text-200",
|
||||
labelInactive: "group-hover:text-custom-text-200 text-custom-text-300",
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// SUB-COMPONENTS
|
||||
// ============================================================================
|
||||
|
||||
const AppSidebarItemLabel: React.FC<AppSidebarItemLabelProps> = ({ highlight = false, label }) => {
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(styles.label, {
|
||||
[styles.labelActive]: highlight,
|
||||
[styles.labelInactive]: !highlight,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const AppSidebarItemIcon: React.FC<AppSidebarItemIconProps> = ({ icon, highlight }) => {
|
||||
if (!icon) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.icon, {
|
||||
[styles.iconActive]: highlight,
|
||||
[styles.iconInactive]: !highlight,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppSidebarLinkItem: React.FC<AppSidebarLinkItemProps> = ({ href, children, className }) => {
|
||||
if (!href) return null;
|
||||
|
||||
return (
|
||||
<Link href={href} className={cn(styles.base, className)}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const AppSidebarButtonItem: React.FC<AppSidebarButtonItemProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
}) => (
|
||||
<button className={cn(styles.base, className)} onClick={onClick} disabled={disabled} type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// MAIN COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
type AppSidebarItemComponent = React.FC<AppSidebarItemProps> & {
|
||||
Label: React.FC<AppSidebarItemLabelProps>;
|
||||
Icon: React.FC<AppSidebarItemIconProps>;
|
||||
Link: React.FC<AppSidebarLinkItemProps>;
|
||||
Button: React.FC<AppSidebarButtonItemProps>;
|
||||
};
|
||||
|
||||
const AppSidebarItem: AppSidebarItemComponent = ({ variant = "link", item }) => {
|
||||
if (!item) return null;
|
||||
|
||||
const { icon, isActive, label, href, onClick, disabled } = item;
|
||||
|
||||
const commonItems = (
|
||||
<>
|
||||
<AppSidebarItemIcon icon={icon} highlight={isActive} />
|
||||
<AppSidebarItemLabel highlight={isActive} label={label} />
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === "link") {
|
||||
return <AppSidebarLinkItem href={href}>{commonItems}</AppSidebarLinkItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppSidebarButtonItem onClick={onClick} disabled={disabled}>
|
||||
{commonItems}
|
||||
</AppSidebarButtonItem>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// COMPOUND COMPONENT ASSIGNMENT
|
||||
// ============================================================================
|
||||
|
||||
AppSidebarItem.Label = AppSidebarItemLabel;
|
||||
AppSidebarItem.Icon = AppSidebarItemIcon;
|
||||
AppSidebarItem.Link = AppSidebarLinkItem;
|
||||
AppSidebarItem.Button = AppSidebarButtonItem;
|
||||
|
||||
export { AppSidebarItem };
|
||||
export type { AppSidebarItemData, AppSidebarItemProps };
|
||||
23
apps/web/core/components/sidebar/sidebar-toggle-button.tsx
Normal file
23
apps/web/core/components/sidebar/sidebar-toggle-button.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
|
||||
export const AppSidebarToggleButton = observer(() => {
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center justify-center size-6 rounded-md text-custom-text-400 hover:text-custom-primary-100 hover:bg-custom-background-90"
|
||||
onClick={() => {
|
||||
if (sidebarPeek) toggleSidebarPeek(false);
|
||||
toggleSidebar();
|
||||
}}
|
||||
>
|
||||
<PanelLeft className="size-4" />
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
@ -12,11 +12,10 @@ import { useWorkspaceNotifications } from "@/hooks/store";
|
|||
|
||||
type TNotificationAppSidebarOption = {
|
||||
workspaceSlug: string;
|
||||
isSidebarCollapsed: boolean | undefined;
|
||||
};
|
||||
|
||||
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
|
||||
const { workspaceSlug, isSidebarCollapsed } = props;
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||
|
||||
|
|
@ -33,9 +32,6 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = o
|
|||
|
||||
if (totalNotifications <= 0) return <></>;
|
||||
|
||||
if (isSidebarCollapsed)
|
||||
return <div className="absolute right-2 top-1.5 h-2 w-2 rounded-full bg-custom-primary-300" />;
|
||||
|
||||
return (
|
||||
<div className="ml-auto">
|
||||
<CountChip count={`${isMentionsEnabled ? `@ ` : ``}${getNumberCount(totalNotifications)}`} />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { Breadcrumbs, Header } from "@plane/ui";
|
|||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { SidebarHamburgerToggle } from "@/components/core";
|
||||
import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
|
||||
type TNotificationSidebarHeader = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -17,14 +19,14 @@ type TNotificationSidebarHeader = {
|
|||
export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
|
||||
if (!workspaceSlug) return <></>;
|
||||
return (
|
||||
<Header className="my-auto bg-custom-background-100">
|
||||
<Header.LeftItem>
|
||||
<div className="block bg-custom-sidebar-background-100 md:hidden">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
{sidebarCollapsed && <SidebarHamburgerToggle />}
|
||||
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const SidebarDropdownItem = observer((props: TProps) => {
|
|||
</span>
|
||||
<div className="w-[inherit]">
|
||||
<div
|
||||
className={`truncate text-ellipsis text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"}`}
|
||||
className={`truncate text-left text-ellipsis text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"}`}
|
||||
>
|
||||
{workspace.name}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,258 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment, Ref, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { usePopper } from "react-popper";
|
||||
// icons
|
||||
import { ChevronDown, CirclePlus, LogOut, Mails, Settings } from "lucide-react";
|
||||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { GOD_MODE_URL } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { orderWorkspacesList, cn, getFileURL } from "@plane/utils";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
|
||||
// plane web helpers
|
||||
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
// components
|
||||
import { WorkspaceLogo } from "../logo";
|
||||
import SidebarDropdownItem from "./dropdown-item";
|
||||
import { WorkspaceAppSwitcher } from "@/plane-web/components/workspace/app-switcher";
|
||||
import { UserMenuRoot } from "./user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "./workspace-menu-root";
|
||||
|
||||
export const SidebarDropdown = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { data: currentUser } = useUser();
|
||||
const { signOut } = useUser();
|
||||
const { updateUserProfile } = useUserProfile();
|
||||
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
|
||||
// derived values
|
||||
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
|
||||
const isUserInstanceAdmin = false;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "right",
|
||||
modifiers: [{ name: "preventOverflow", options: { padding: 12 } }],
|
||||
});
|
||||
// hooks
|
||||
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
|
||||
|
||||
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut().catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("sign_out.toast.error.title"),
|
||||
message: t("sign_out.toast.error.message"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
||||
// TODO: fix workspaces list scroll
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center justify-center gap-x-3 gap-y-2", {
|
||||
"flex-col gap-y-3": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Menu
|
||||
as="div"
|
||||
className={cn("relative h-full truncate text-left flex-grow flex justify-stretch", {
|
||||
"flex-grow-0 justify-center": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={cn(
|
||||
"group/menu-button flex items-center justify-between gap-1 p-1 truncate rounded text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none",
|
||||
{ "flex-grow": !sidebarCollapsed }
|
||||
)}
|
||||
aria-label={t("aria_labels.projects_sidebar.open_workspace_switcher")}
|
||||
>
|
||||
<div className="flex-grow flex items-center gap-2 truncate">
|
||||
<WorkspaceLogo logo={activeWorkspace?.logo_url} name={activeWorkspace?.name} />
|
||||
{!sidebarCollapsed && (
|
||||
<h4 className="truncate text-base font-medium text-custom-text-100">
|
||||
{activeWorkspace?.name ?? t("loading")}
|
||||
</h4>
|
||||
)}
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"flex-shrink-0 mx-1 hidden size-4 group-hover/menu-button:block text-custom-sidebar-text-400 duration-300",
|
||||
{ "rotate-180": open }
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items as={Fragment}>
|
||||
<div className="fixed top-12 left-4 z-[21] mt-1 flex w-full max-w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
|
||||
<div className="overflow-x-hidden vertical-scrollbar scrollbar-sm flex max-h-96 flex-col items-start justify-start overflow-y-scroll">
|
||||
<span className="rounded-md px-4 sticky top-0 z-[21] h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-text-400 truncate flex-shrink-0">
|
||||
{currentUser?.email}
|
||||
</span>
|
||||
{workspacesList ? (
|
||||
<div className="size-full flex flex-col items-start justify-start">
|
||||
{(activeWorkspace
|
||||
? [
|
||||
activeWorkspace,
|
||||
...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id),
|
||||
]
|
||||
: workspacesList
|
||||
).map((workspace) => (
|
||||
<SidebarDropdownItem
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
activeWorkspace={activeWorkspace}
|
||||
handleItemClick={handleItemClick}
|
||||
handleWorkspaceNavigation={handleWorkspaceNavigation}
|
||||
handleClose={close}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
|
||||
{isWorkspaceCreationEnabled && (
|
||||
<Link href="/create-workspace" className="w-full">
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<CirclePlus className="size-4 flex-shrink-0" />
|
||||
{t("create_workspace")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="/invitations" className="w-full" onClick={handleItemClick}>
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<Mails className="h-4 w-4 flex-shrink-0" />
|
||||
{t("workspace_invites")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
|
||||
<div className="w-full">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 flex-shrink-0" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button
|
||||
className="grid place-items-center outline-none"
|
||||
ref={setReferenceElement}
|
||||
aria-label={t("aria_labels.projects_sidebar.open_user_menu")}
|
||||
>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={24}
|
||||
shape="circle"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute left-0 z-[21] mt-1 flex w-52 origin-top-left flex-col divide-y
|
||||
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
<Link href={`/${workspaceSlug}/settings/account`}>
|
||||
<Menu.Item as="div">
|
||||
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<Settings className="h-4 w-4 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 stroke-[1.5]" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
{isUserInstanceAdmin && (
|
||||
<div className="p-2 pb-0">
|
||||
<Link href={GOD_MODE_URL}>
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
|
||||
{t("enter_god_mode")}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<div className="flex items-center justify-center gap-1.5 w-full">
|
||||
<WorkspaceMenuRoot />
|
||||
{isAppRailEnabled && !shouldRenderAppRail && <WorkspaceAppSwitcher />}
|
||||
<UserMenuRoot />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { CustomMenu, Tooltip, DropIndicator, FavoriteFolderIcon, DragHandle } fr
|
|||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local imports
|
||||
|
|
@ -44,7 +43,6 @@ type Props = {
|
|||
export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
const { favorite, handleRemoveFromFavorites, isLastChild, handleDrop } = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
const { getGroupedFavorites } = useFavorite();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { workspaceSlug } = useParams();
|
||||
|
|
@ -159,7 +157,6 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
|||
"group/project-item relative w-full px-2 py-1.5 flex items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"bg-custom-sidebar-background-90": isMenuActive,
|
||||
"p-0 size-8 aspect-square justify-center mx-auto": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
@ -169,117 +166,95 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
|||
<GripVertical className="w-3 h-3" />
|
||||
</div>
|
||||
|
||||
{isSidebarCollapsed ? (
|
||||
<div
|
||||
className={cn("flex-grow flex items-center gap-1.5 truncate text-left select-none", {
|
||||
"justify-center": isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Tooltip tooltipContent={favorite.name} position="right" isMobile={isMobile}>
|
||||
<>
|
||||
<Tooltip tooltipContent={`${favorite.name}`} position="right" className="ml-8" isMobile={isMobile}>
|
||||
<div className="flex-grow flex truncate">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
className="size-8 aspect-square flex-shrink-0 grid place-items-center"
|
||||
type="button"
|
||||
className="flex-grow flex items-center gap-1.5 text-left select-none w-full"
|
||||
>
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
favorite.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"
|
||||
}
|
||||
position="top-right"
|
||||
disabled={isDragging}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
|
||||
{
|
||||
"cursor-not-allowed opacity-60": favorite.sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<DragHandle className="bg-transparent" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="size-5 grid place-items-center flex-shrink-0">
|
||||
<FavoriteFolderIcon />
|
||||
</div>
|
||||
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{favorite.name}</p>
|
||||
</Disclosure.Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip tooltipContent={`${favorite.name}`} position="right" className="ml-8" isMobile={isMobile}>
|
||||
<div className="flex-grow flex truncate">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", {
|
||||
"justify-center": isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
favorite.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"
|
||||
}
|
||||
position="top-right"
|
||||
disabled={isDragging}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
|
||||
{
|
||||
"cursor-not-allowed opacity-60": favorite.sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
"!hidden": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<DragHandle className="bg-transparent" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="size-5 grid place-items-center flex-shrink-0">
|
||||
<FavoriteFolderIcon />
|
||||
</div>
|
||||
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{favorite.name}</p>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
>
|
||||
<MoreHorizontal className="size-3" />
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
>
|
||||
<MoreHorizontal className="size-3" />
|
||||
</span>
|
||||
}
|
||||
menuButtonOnClick={() => setIsMenuActive(!isMenuActive)}
|
||||
className={cn(
|
||||
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": isMenuActive,
|
||||
}
|
||||
menuButtonOnClick={() => setIsMenuActive(!isMenuActive)}
|
||||
className={cn(
|
||||
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": isMenuActive,
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setFolderToRename(favorite.id)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
|
||||
<span>Rename Folder</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
|
||||
{
|
||||
"inline-block": isMenuActive,
|
||||
}
|
||||
)}
|
||||
aria-label={t(
|
||||
open ? "aria_labels.projects_sidebar.close_folder" : "aria_labels.projects_sidebar.open_folder"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-3 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
||||
"rotate-90": open,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setFolderToRename(favorite.id)}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
|
||||
<span>Rename Folder</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
|
||||
{
|
||||
"inline-block": isMenuActive,
|
||||
}
|
||||
)}
|
||||
aria-label={t(
|
||||
open ? "aria_labels.projects_sidebar.close_folder" : "aria_labels.projects_sidebar.open_folder"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-3 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
||||
"rotate-90": open,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</>
|
||||
</div>
|
||||
{favorite.children && favorite.children.length > 0 && (
|
||||
<Transition
|
||||
|
|
@ -290,12 +265,7 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
|||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("flex flex-col gap-0.5 mt-1", {
|
||||
"px-2": !isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Disclosure.Panel as="div" className="flex flex-col gap-0.5 mt-1 px-2">
|
||||
{orderBy(favorite.children, "sequence", "desc").map((child, index) => (
|
||||
<FavoriteRoot
|
||||
key={child.id}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React, { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
|
|
@ -11,28 +10,23 @@ type Props = {
|
|||
href: string;
|
||||
title: string;
|
||||
icon: JSX.Element;
|
||||
isSidebarCollapsed: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteItemTitle: FC<Props> = observer((props) => {
|
||||
const { href, title, icon, isSidebarCollapsed } = props;
|
||||
const { href, title, icon } = props;
|
||||
// store hooks
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const linkClass = "flex items-center gap-1.5 truncate w-full";
|
||||
const collapsedClass =
|
||||
"group/project-item cursor-pointer relative group w-full flex items-center justify-center gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90 truncate p-0 size-8 aspect-square mx-auto";
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (isMobile) toggleSidebar();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip tooltipContent={title} isMobile={isMobile} position="right" className={cn(!isSidebarCollapsed && "ml-8")}>
|
||||
<Link href={href} className={isSidebarCollapsed ? collapsedClass : linkClass} draggable onClick={handleOnClick}>
|
||||
<Tooltip tooltipContent={title} isMobile={isMobile} position="right" className="ml-8">
|
||||
<Link href={href} className="flex items-center gap-1.5 truncate w-full" draggable onClick={handleOnClick}>
|
||||
<span className="flex items-center justify-center size-5">{icon}</span>
|
||||
{!isSidebarCollapsed && <span className="text-sm leading-5 font-medium flex-1 truncate">{title}</span>}
|
||||
<span className="text-sm leading-5 font-medium flex-1 truncate">{title}</span>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,28 +7,23 @@ type Props = {
|
|||
children: React.ReactNode;
|
||||
elementRef: React.RefObject<HTMLDivElement>;
|
||||
isMenuActive?: boolean;
|
||||
sidebarCollapsed?: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteItemWrapper: FC<Props> = (props) => {
|
||||
const { children, elementRef, isMenuActive = false, sidebarCollapsed = false } = props;
|
||||
const { children, elementRef, isMenuActive = false } = props;
|
||||
return (
|
||||
<>
|
||||
{sidebarCollapsed ? (
|
||||
<div ref={elementRef}>{children}</div>
|
||||
) : (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={cn(
|
||||
"group/project-item cursor-pointer relative group flex items-center justify-between w-full gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"bg-custom-sidebar-background-90": isMenuActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={cn(
|
||||
"group/project-item cursor-pointer relative group flex items-center justify-between w-full gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"bg-custom-sidebar-background-90": isMenuActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import {
|
|||
FavoriteItemTitle,
|
||||
} from "@/components/workspace/sidebar/favorites";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useFavoriteItemDetails } from "@/hooks/use-favorite-item-details";
|
||||
//helpers
|
||||
import { getCanDrop, getInstructionFromPayload } from "../favorites.helpers";
|
||||
|
|
@ -45,7 +44,6 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
|
|||
// props
|
||||
const { isLastChild, parentId, workspaceSlug, favorite, handleRemoveFromFavorites, handleDrop } = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { itemLink, itemIcon, itemTitle } = useFavoriteItemDetails(workspaceSlug, favorite);
|
||||
//state
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
|
@ -82,12 +80,7 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
|
|||
const root = createRoot(container);
|
||||
root.render(
|
||||
<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">
|
||||
<FavoriteItemTitle
|
||||
href={itemLink}
|
||||
icon={itemIcon}
|
||||
title={itemTitle}
|
||||
isSidebarCollapsed={!!sidebarCollapsed}
|
||||
/>
|
||||
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} />
|
||||
</div>
|
||||
);
|
||||
return () => root.unmount();
|
||||
|
|
@ -138,18 +131,16 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
|
|||
return (
|
||||
<>
|
||||
<DropIndicator isVisible={instruction === "reorder-above"} />
|
||||
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive} sidebarCollapsed={sidebarCollapsed}>
|
||||
{!sidebarCollapsed && <FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />}
|
||||
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} isSidebarCollapsed={!!sidebarCollapsed} />
|
||||
{!sidebarCollapsed && (
|
||||
<FavoriteItemQuickAction
|
||||
favorite={favorite}
|
||||
ref={actionSectionRef}
|
||||
isMenuActive={isMenuActive}
|
||||
onChange={handleQuickAction}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
/>
|
||||
)}
|
||||
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive}>
|
||||
<FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />
|
||||
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} />
|
||||
<FavoriteItemQuickAction
|
||||
favorite={favorite}
|
||||
ref={actionSectionRef}
|
||||
isMenuActive={isMenuActive}
|
||||
onChange={handleQuickAction}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
/>
|
||||
</FavoriteItemWrapper>
|
||||
{isLastChild && <DropIndicator isVisible={instruction === "reorder-below"} />}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,8 @@ import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
|||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { FavoriteFolder } from "./favorite-folder";
|
||||
import { FavoriteRoot } from "./favorite-items";
|
||||
|
|
@ -40,19 +38,10 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
// navigation
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const {
|
||||
favoriteIds,
|
||||
groupedFavorites,
|
||||
deleteFavorite,
|
||||
removeFromFavoriteFolder,
|
||||
reOrderFavorite,
|
||||
moveFavoriteToFolder,
|
||||
} = useFavorite();
|
||||
const { groupedFavorites, deleteFavorite, removeFromFavoriteFolder, reOrderFavorite, moveFavoriteToFolder } =
|
||||
useFavorite();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// platform hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
|
||||
// derived values
|
||||
|
|
@ -154,10 +143,6 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
[workspaceSlug, reOrderFavorite, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarCollapsed) toggleFavoriteMenu(true);
|
||||
}, [sidebarCollapsed, toggleFavoriteMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
|
||||
|
|
@ -189,27 +174,48 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
return (
|
||||
<>
|
||||
<Disclosure as="div" defaultOpen ref={containerRef}>
|
||||
{!sidebarCollapsed && (
|
||||
<div
|
||||
ref={elementRef}
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={cn(
|
||||
"group/favorites-button w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90"
|
||||
)}
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"group/favorites-button w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90",
|
||||
"w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
|
||||
{
|
||||
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
|
||||
sidebarCollapsed,
|
||||
"bg-custom-sidebar-background-80 opacity-60": isDragging,
|
||||
}
|
||||
)}
|
||||
onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)}
|
||||
aria-label={t(
|
||||
isFavoriteMenuOpen
|
||||
? "aria_labels.projects_sidebar.close_favorites_menu"
|
||||
: "aria_labels.projects_sidebar.open_favorites_menu"
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-semibold">{t("favorites")}</span>
|
||||
</Disclosure.Button>
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover/favorites-button:opacity-100 group-hover/favorites-button:pointer-events-auto">
|
||||
<Tooltip tooltipHeading={t("create_folder")} tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
|
||||
onClick={() => {
|
||||
setCreateNewFolder(true);
|
||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(!isFavoriteMenuOpen);
|
||||
}}
|
||||
aria-label={t("aria_labels.projects_sidebar.create_favorites_folder")}
|
||||
>
|
||||
<FolderPlus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
|
||||
{
|
||||
"!text-center w-8 px-2 py-1.5 justify-center": sidebarCollapsed,
|
||||
"bg-custom-sidebar-background-80 opacity-60": isDragging,
|
||||
}
|
||||
)}
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
|
||||
onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)}
|
||||
aria-label={t(
|
||||
isFavoriteMenuOpen
|
||||
|
|
@ -217,42 +223,14 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
: "aria_labels.projects_sidebar.open_favorites_menu"
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-semibold">{t("favorites")}</span>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-3 transition-all", {
|
||||
"rotate-90": isFavoriteMenuOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover/favorites-button:opacity-100 group-hover/favorites-button:pointer-events-auto">
|
||||
<Tooltip tooltipHeading={t("create_folder")} tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
|
||||
onClick={() => {
|
||||
setCreateNewFolder(true);
|
||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(!isFavoriteMenuOpen);
|
||||
}}
|
||||
aria-label={t("aria_labels.projects_sidebar.create_favorites_folder")}
|
||||
>
|
||||
<FolderPlus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
|
||||
onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)}
|
||||
aria-label={t(
|
||||
isFavoriteMenuOpen
|
||||
? "aria_labels.projects_sidebar.close_favorites_menu"
|
||||
: "aria_labels.projects_sidebar.open_favorites_menu"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-3 transition-all", {
|
||||
"rotate-90": isFavoriteMenuOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Transition
|
||||
show={isFavoriteMenuOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
|
|
@ -263,55 +241,34 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
{isFavoriteMenuOpen && (
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("flex flex-col mt-0.5 gap-0.5", {
|
||||
"space-y-0 mt-0 ml-0": sidebarCollapsed,
|
||||
})}
|
||||
static
|
||||
>
|
||||
<Disclosure.Panel as="div" className="flex flex-col mt-0.5 gap-0.5" static>
|
||||
{createNewFolder && <NewFavoriteFolder setCreateNewFolder={setCreateNewFolder} actionType="create" />}
|
||||
{Object.keys(groupedFavorites).length === 0 ? (
|
||||
<>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="text-custom-text-400 text-xs font-medium px-8 py-1.5">
|
||||
{t("no_favorites_yet")}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-400 text-xs font-medium px-8 py-1.5">{t("no_favorites_yet")}</span>
|
||||
</>
|
||||
) : (
|
||||
orderBy(Object.values(groupedFavorites), "sequence", "desc")
|
||||
.filter((fav) => !fav.parent)
|
||||
.map((fav, index, { length }) => (
|
||||
<>
|
||||
{fav?.id && (
|
||||
<Tooltip
|
||||
key={fav?.id}
|
||||
tooltipContent={fav?.entity_data ? fav?.entity_data?.name : fav?.name}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
{fav?.is_folder ? (
|
||||
<FavoriteFolder
|
||||
favorite={fav}
|
||||
isLastChild={index === length - 1}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
|
||||
handleDrop={handleDrop}
|
||||
/>
|
||||
) : (
|
||||
<FavoriteRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
favorite={fav}
|
||||
isLastChild={index === length - 1}
|
||||
parentId={undefined}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
handleDrop={handleDrop}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
{fav?.is_folder ? (
|
||||
<FavoriteFolder
|
||||
favorite={fav}
|
||||
isLastChild={index === length - 1}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
|
||||
handleDrop={handleDrop}
|
||||
/>
|
||||
) : (
|
||||
<FavoriteRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
favorite={fav}
|
||||
isLastChild={index === length - 1}
|
||||
parentId={undefined}
|
||||
handleRemoveFromFavorites={handleRemoveFromFavorites}
|
||||
handleDrop={handleDrop}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
|
|
@ -320,10 +277,6 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
)}
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
|
||||
{sidebarCollapsed && favoriteIds.length > 0 && (
|
||||
<hr className="flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
149
apps/web/core/components/workspace/sidebar/help-menu.tsx
Normal file
149
apps/web/core/components/workspace/sidebar/help-menu.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText, HelpCircle, MessagesSquare, User } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { ProductUpdatesModal } from "@/components/global";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useCommandPalette, useInstance, useTransient, useUserSettings } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
||||
|
||||
export interface WorkspaceHelpSectionProps {
|
||||
setSidebarActive?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const HelpMenu: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { config } = useInstance();
|
||||
const { isIntercomToggle, toggleIntercom } = useTransient();
|
||||
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
||||
|
||||
const handleCrispWindowShow = () => {
|
||||
toggleIntercom(!isIntercomToggle);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 justify-evenly">
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div
|
||||
className={cn(
|
||||
"grid place-items-center rounded-md p-1 outline-none text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90",
|
||||
{
|
||||
"bg-custom-background-90": isNeedHelpOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Tooltip tooltipContent="Help" isMobile={isMobile} disabled={isNeedHelpOpen}>
|
||||
<HelpCircle className="h-[18px] w-[18px] outline-none" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
|
||||
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
|
||||
onMenuClose={() => setIsNeedHelpOpen(false)}
|
||||
placement="top-end"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
<CustomMenu.MenuItem>
|
||||
<a
|
||||
href="https://go.plane.so/p-docs"
|
||||
target="_blank"
|
||||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<span className="text-xs">{t("documentation")}</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
{config?.intercom_app_id && config?.is_intercom_enabled && (
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCrispWindowShow}
|
||||
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className="text-xs">{t("message_support")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem>
|
||||
<a
|
||||
href="mailto:sales@plane.so"
|
||||
target="_blank"
|
||||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<span className="text-xs">{t("contact_sales")}</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="my-1 border-t border-custom-border-200" />
|
||||
<CustomMenu.MenuItem>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="racking-tight">{t("hyper_mode")}</span>
|
||||
<ToggleSwitch
|
||||
value={canUseLocalDB}
|
||||
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShortcutModal(true)}
|
||||
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">{t("keyboard_shortcuts")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProductUpdatesModalOpen(true)}
|
||||
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">{t("whats_new")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<a
|
||||
href="https://go.plane.so/p-discord"
|
||||
target="_blank"
|
||||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">Discord</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="px-1 pt-2 mt-1 text-xs text-custom-text-200 border-t border-custom-border-200">
|
||||
<PlaneVersionNumber />
|
||||
</div>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -26,7 +26,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { sidebarCollapsed: isCollapsed, toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { config } = useInstance();
|
||||
|
|
@ -40,22 +40,11 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
toggleIntercom(!isIntercomToggle);
|
||||
};
|
||||
|
||||
const isCollapsed = sidebarCollapsed || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-2 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12 flex-shrink-0",
|
||||
{
|
||||
"flex-col h-auto py-1.5": isCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`relative flex flex-shrink-0 items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "justify-evenly"}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between px-2 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12 flex-shrink-0">
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 justify-evenly">
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div
|
||||
|
|
@ -71,10 +60,10 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
customButtonClassName={`relative grid place-items-center rounded-md p-1.5 outline-none ${isCollapsed ? "w-full" : ""}`}
|
||||
customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
|
||||
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
|
||||
onMenuClose={() => setIsNeedHelpOpen(false)}
|
||||
placement={isCollapsed ? "left-end" : "top-end"}
|
||||
placement="top-end"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
|
|
@ -158,23 +147,18 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
</div>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div
|
||||
className={cn("w-full flex-grow px-0.5", {
|
||||
hidden: isCollapsed,
|
||||
})}
|
||||
>
|
||||
<div className="w-full flex-grow px-0.5">
|
||||
<WorkspaceEditionBadge />
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-shrink-0 items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "justify-evenly"}`}
|
||||
>
|
||||
<div className="flex flex-shrink-0 items-center gap-1 justify-evenly">
|
||||
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`} isMobile={isMobile}>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar()}
|
||||
className="grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100"
|
||||
onClick={() => {
|
||||
if (sidebarPeek) toggleSidebarPeek(false);
|
||||
toggleSidebar();
|
||||
}}
|
||||
aria-label={t(
|
||||
isCollapsed
|
||||
? "aria_labels.projects_sidebar.expand_sidebar"
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
136
apps/web/core/components/workspace/sidebar/help-section/root.tsx
Normal file
136
apps/web/core/components/workspace/sidebar/help-section/root.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText, HelpCircle, MessagesSquare, User } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { ProductUpdatesModal } from "@/components/global";
|
||||
import { AppSidebarItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useCommandPalette, useInstance, useTransient, useUserSettings } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
||||
|
||||
export const HelpMenuRoot = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { config } = useInstance();
|
||||
const { isIntercomToggle, toggleIntercom } = useTransient();
|
||||
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
||||
|
||||
const handleCrispWindowShow = () => {
|
||||
toggleIntercom(!isIntercomToggle);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<AppSidebarItem
|
||||
variant="button"
|
||||
item={{
|
||||
icon: <HelpCircle className="size-5" />,
|
||||
isActive: isNeedHelpOpen,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
// customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
|
||||
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
|
||||
onMenuClose={() => setIsNeedHelpOpen(false)}
|
||||
placement="top-end"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
<CustomMenu.MenuItem>
|
||||
<a
|
||||
href="https://go.plane.so/p-docs"
|
||||
target="_blank"
|
||||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<span className="text-xs">{t("documentation")}</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
{config?.intercom_app_id && config?.is_intercom_enabled && (
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCrispWindowShow}
|
||||
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className="text-xs">{t("message_support")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem>
|
||||
<a
|
||||
href="mailto:sales@plane.so"
|
||||
target="_blank"
|
||||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<span className="text-xs">{t("contact_sales")}</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="my-1 border-t border-custom-border-200" />
|
||||
<CustomMenu.MenuItem>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="racking-tight">{t("hyper_mode")}</span>
|
||||
<ToggleSwitch
|
||||
value={canUseLocalDB}
|
||||
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShortcutModal(true)}
|
||||
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">{t("keyboard_shortcuts")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProductUpdatesModalOpen(true)}
|
||||
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">{t("whats_new")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<a
|
||||
href="https://go.plane.so/p-discord"
|
||||
target="_blank"
|
||||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">Discord</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="px-1 pt-2 mt-1 text-xs text-custom-text-200 border-t border-custom-border-200">
|
||||
<PlaneVersionNumber />
|
||||
</div>
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -10,3 +10,4 @@ export * from "./user-menu-item";
|
|||
export * from "./workspace-menu";
|
||||
export * from "./workspace-menu-item";
|
||||
export * from "./workspace-menu-header";
|
||||
export * from "./help-section";
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserProjectRoles } from "@plane/types";
|
||||
// plane ui
|
||||
import { Tooltip, DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui";
|
||||
import { DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useAppTheme, useIssueDetail, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane-web constants
|
||||
|
||||
export type TNavigationItem = {
|
||||
name: string;
|
||||
|
|
@ -32,17 +30,15 @@ type TProjectItemsProps = {
|
|||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
additionalNavigationItems?: (workspaceSlug: string, projectId: string) => TNavigationItem[];
|
||||
isSidebarCollapsed: boolean;
|
||||
};
|
||||
|
||||
export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, additionalNavigationItems, isSidebarCollapsed } = props;
|
||||
const { workspaceSlug, projectId, additionalNavigationItems } = props;
|
||||
const { workItem: workItemIdentifierFromRoute } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
const { getPartialProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const {
|
||||
issue: { getIssueIdByIdentifier, getIssueById },
|
||||
|
|
@ -176,28 +172,14 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
if (!hasAccess) return null;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={item.name}
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${project?.name}: ${t(item.i18n_key)}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!isSidebarCollapsed}
|
||||
>
|
||||
<Link href={item.href} onClick={handleProjectClick}>
|
||||
<SidebarNavItem
|
||||
className={`pl-[18px] ${isSidebarCollapsed ? "p-0 size-7 justify-center mx-auto" : ""}`}
|
||||
isActive={!!isActive(item)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.icon
|
||||
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
|
||||
/>
|
||||
{!isSidebarCollapsed && <span className="text-xs font-medium">{t(item.i18n_key)}</span>}
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Link key={item.key} href={item.href} onClick={handleProjectClick}>
|
||||
<SidebarNavItem className="pl-[18px]" isActive={!!isActive(item)}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.icon className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`} />
|
||||
<span className="text-xs font-medium">{t(item.i18n_key)}</span>
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -57,12 +57,13 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
renderInExtendedSidebar = false,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { t } = useTranslation();
|
||||
const { getPartialProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { getIsProjectListOpen, toggleProjectListOpen } = useCommandPalette();
|
||||
const { toggleAnySidebarDropdown } = useAppTheme();
|
||||
|
||||
// states
|
||||
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
||||
const [publishModalOpen, setPublishModal] = useState(false);
|
||||
|
|
@ -99,8 +100,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
setLeaveProjectModal(true);
|
||||
};
|
||||
|
||||
const isSidebarCollapsed = sidebarCollapsed && !renderInExtendedSidebar;
|
||||
|
||||
useEffect(() => {
|
||||
const element = projectRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
|
|
@ -110,7 +109,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
canDrag: () => !disableDrag && !isSidebarCollapsed,
|
||||
canDrag: () => !disableDrag,
|
||||
dragHandle: dragHandleElement ?? undefined,
|
||||
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
|
||||
onDragStart: () => {
|
||||
|
|
@ -190,6 +189,11 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
);
|
||||
}, [projectId, isLastChild, projectListType, handleOnProjectDrop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuActive) toggleAnySidebarDropdown(true);
|
||||
else toggleAnySidebarDropdown(false);
|
||||
}, [isMenuActive]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS));
|
||||
|
||||
|
|
@ -218,7 +222,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
"group/project-item relative w-full px-2 py-1.5 flex items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"bg-custom-sidebar-background-90": isMenuActive,
|
||||
"p-0 size-8 aspect-square justify-center mx-auto": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
id={`${project?.id}`}
|
||||
|
|
@ -240,7 +243,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
"cursor-not-allowed opacity-60": project.sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
flex: isMenuActive || renderInExtendedSidebar,
|
||||
"!hidden": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
ref={dragHandleRef}
|
||||
|
|
@ -249,76 +251,53 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isSidebarCollapsed ? (
|
||||
<>
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${project.id}/issues`}
|
||||
className={cn("flex-grow flex items-center gap-1.5 truncate text-left select-none", {
|
||||
"justify-center": isSidebarCollapsed,
|
||||
})}
|
||||
className="flex-grow flex truncate"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
<Disclosure.Button as="button" className="size-8 aspect-square flex-shrink-0 grid place-items-center">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", {})}
|
||||
aria-label={
|
||||
isProjectListOpen
|
||||
? t("aria_labels.projects_sidebar.close_project_menu")
|
||||
: t("aria_labels.projects_sidebar.open_project_menu")
|
||||
}
|
||||
>
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
<Logo logo={project.logo_props} size={16} />
|
||||
</div>
|
||||
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{project.name}</p>
|
||||
</Disclosure.Button>
|
||||
</ControlLink>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip
|
||||
tooltipContent={`${project.name}`}
|
||||
position="right"
|
||||
disabled={!isSidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${project.id}/issues`}
|
||||
className="flex-grow flex truncate"
|
||||
onClick={handleItemClick}
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", {
|
||||
"justify-center": isSidebarCollapsed,
|
||||
})}
|
||||
aria-label={
|
||||
isProjectListOpen
|
||||
? t("aria_labels.projects_sidebar.close_project_menu")
|
||||
: t("aria_labels.projects_sidebar.open_project_menu")
|
||||
}
|
||||
>
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
<Logo logo={project.logo_props} size={16} />
|
||||
</div>
|
||||
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{project.name}</p>
|
||||
</Disclosure.Button>
|
||||
</ControlLink>
|
||||
</Tooltip>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
ref={actionSectionRef}
|
||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
}
|
||||
className={cn(
|
||||
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": isMenuActive,
|
||||
}
|
||||
className={cn(
|
||||
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": isMenuActive,
|
||||
}
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
useCaptureForOutsideClick
|
||||
closeOnSelect
|
||||
>
|
||||
{/* TODO: Removed is_favorite logic due to the optimization in projects API */}
|
||||
{/* {isAuthorized && (
|
||||
)}
|
||||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
|
||||
useCaptureForOutsideClick
|
||||
closeOnSelect
|
||||
onMenuClose={() => setIsMenuActive(false)}
|
||||
>
|
||||
{/* TODO: Removed is_favorite logic due to the optimization in projects API */}
|
||||
{/* {isAuthorized && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
|
||||
>
|
||||
|
|
@ -333,82 +312,81 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</CustomMenu.MenuItem>
|
||||
)} */}
|
||||
|
||||
{/* publish project settings */}
|
||||
{isAdmin && (
|
||||
<CustomMenu.MenuItem onClick={() => setPublishModal(true)}>
|
||||
<div className="relative flex flex-shrink-0 items-center justify-start gap-2">
|
||||
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
|
||||
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
</div>
|
||||
<div>{t("publish_project")}</div>
|
||||
{/* publish project settings */}
|
||||
{isAdmin && (
|
||||
<CustomMenu.MenuItem onClick={() => setPublishModal(true)}>
|
||||
<div className="relative flex flex-shrink-0 items-center justify-start gap-2">
|
||||
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
|
||||
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("copy_link")}</span>
|
||||
</span>
|
||||
<div>{t("publish_project")}</div>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isAuthorized && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
router.push(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 cursor-pointer">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("copy_link")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
{isAuthorized && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`);
|
||||
router.push(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 cursor-pointer">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{/* leave project */}
|
||||
{!isAuthorized && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={handleLeaveProject}
|
||||
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("leave_project")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
|
||||
{
|
||||
"inline-block": isMenuActive,
|
||||
}
|
||||
)}
|
||||
onClick={() => setIsProjectListOpen(!isProjectListOpen)}
|
||||
aria-label={t(
|
||||
isProjectListOpen
|
||||
? "aria_labels.projects_sidebar.close_project_menu"
|
||||
: "aria_labels.projects_sidebar.open_project_menu"
|
||||
)}
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`);
|
||||
}}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
||||
"rotate-90": isProjectListOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-start gap-2 cursor-pointer">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{/* leave project */}
|
||||
{!isAuthorized && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={handleLeaveProject}
|
||||
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>{t("leave_project")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
|
||||
{
|
||||
"inline-block": isMenuActive,
|
||||
}
|
||||
)}
|
||||
onClick={() => setIsProjectListOpen(!isProjectListOpen)}
|
||||
aria-label={t(
|
||||
isProjectListOpen
|
||||
? "aria_labels.projects_sidebar.close_project_menu"
|
||||
: "aria_labels.projects_sidebar.open_project_menu"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
||||
"rotate-90": isProjectListOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</>
|
||||
</div>
|
||||
<Transition
|
||||
show={isProjectListOpen}
|
||||
|
|
@ -421,11 +399,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
>
|
||||
{isProjectListOpen && (
|
||||
<Disclosure.Panel as="div" className="flex flex-col gap-0.5 mt-1">
|
||||
<ProjectNavigationRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isSidebarCollapsed={!!isSidebarCollapsed}
|
||||
/>
|
||||
<ProjectNavigationRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Transition>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { Briefcase, ChevronRight, Plus } from "lucide-react";
|
||||
import { ChevronRight, Plus } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -17,7 +17,7 @@ import { CreateProjectModal } from "@/components/project";
|
|||
import { SidebarProjectsListItem } from "@/components/workspace";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useAppTheme, useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
|
||||
// plane web types
|
||||
import { TProject } from "@/plane-web/types";
|
||||
|
||||
|
|
@ -32,7 +32,6 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { loader, getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
|
||||
|
|
@ -86,8 +85,6 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
});
|
||||
};
|
||||
|
||||
const isCollapsed = sidebarCollapsed || false;
|
||||
|
||||
/**
|
||||
* Implementing scroll animation styles based on the scroll length of the container
|
||||
*/
|
||||
|
|
@ -151,24 +148,11 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
>
|
||||
<>
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
|
||||
<div
|
||||
className={cn(
|
||||
"group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90",
|
||||
{
|
||||
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
|
||||
isCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
|
||||
{
|
||||
"!text-center w-8 px-2 py-1.5 justify-center": isCollapsed,
|
||||
}
|
||||
)}
|
||||
className="w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400"
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
aria-label={t(
|
||||
isAllProjectsListOpen
|
||||
|
|
@ -176,52 +160,42 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
: "aria_labels.projects_sidebar.open_projects_menu"
|
||||
)}
|
||||
>
|
||||
<Tooltip tooltipHeading={t("projects")} tooltipContent="" position="right" disabled={!isCollapsed}>
|
||||
<>
|
||||
{isCollapsed ? (
|
||||
<Briefcase className="flex-shrink-0 size-3" />
|
||||
) : (
|
||||
<span className="text-sm font-semibold">{t("projects")}</span>
|
||||
)}
|
||||
</>
|
||||
</Tooltip>
|
||||
<span className="text-sm font-semibold">{t("projects")}</span>
|
||||
</Disclosure.Button>
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||
{isAuthorizedUser && (
|
||||
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_TOOLTIP}
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => {
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
aria-label={t("aria_labels.projects_sidebar.create_new_project")}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||
{isAuthorizedUser && (
|
||||
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_TOOLTIP}
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => {
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
aria-label={t("aria_labels.projects_sidebar.create_new_project")}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
aria-label={t(
|
||||
isAllProjectsListOpen
|
||||
? "aria_labels.projects_sidebar.close_projects_menu"
|
||||
: "aria_labels.projects_sidebar.open_projects_menu"
|
||||
)}
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
aria-label={t(
|
||||
isAllProjectsListOpen
|
||||
? "aria_labels.projects_sidebar.close_projects_menu"
|
||||
: "aria_labels.projects_sidebar.open_projects_menu"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-3 transition-all", {
|
||||
"rotate-90": isAllProjectsListOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-3 transition-all", {
|
||||
"rotate-90": isAllProjectsListOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
show={isAllProjectsListOpen}
|
||||
|
|
@ -240,13 +214,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
</Loader>
|
||||
)}
|
||||
{isAllProjectsListOpen && (
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("flex flex-col gap-0.5", {
|
||||
"space-y-0 ml-0": isCollapsed,
|
||||
})}
|
||||
static
|
||||
>
|
||||
<Disclosure.Panel as="div" className="flex flex-col gap-0.5" static>
|
||||
<>
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
<SidebarProjectsListItem
|
||||
|
|
@ -270,18 +238,13 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
{isAuthorizedUser && joinedProjects?.length === 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
`w-full flex items-center gap-1.5 px-2 py-1.5 text-sm leading-5 font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 rounded-md`,
|
||||
{
|
||||
"p-0 size-8 aspect-square justify-center mx-auto": sidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_BUTTON}
|
||||
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-sm leading-5 font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 rounded-md"
|
||||
onClick={() => {
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
{!isCollapsed && t("add_project")}
|
||||
{t("add_project")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { CreateUpdateIssueModal } from "@/components/issues";
|
|||
// constants
|
||||
// helpers
|
||||
// hooks
|
||||
import { useAppTheme, useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// plane web components
|
||||
import { AppSearch } from "@/plane-web/components/workspace";
|
||||
|
|
@ -30,7 +30,6 @@ export const SidebarQuickActions = observer(() => {
|
|||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
// store hooks
|
||||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
const { joinedProjectIds } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// local storage
|
||||
|
|
@ -73,19 +72,13 @@ export const SidebarQuickActions = observer(() => {
|
|||
onSubmit={() => removeWorkspaceDraftIssue()}
|
||||
isDraft
|
||||
/>
|
||||
<div
|
||||
className={cn("flex items-center justify-between gap-1 cursor-pointer", {
|
||||
"flex-col gap-0": isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<div className={cn("flex items-center justify-between gap-1 cursor-pointer", {})}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"relative flex flex-shrink-0 flex-grow items-center gap-2 h-8 text-custom-sidebar-text-300 rounded outline-none hover:bg-custom-sidebar-background-90",
|
||||
"relative flex flex-shrink-0 flex-grow items-center gap-2 h-8 text-custom-sidebar-text-300 rounded outline-none hover:bg-custom-sidebar-background-90 px-3 border-[0.5px] border-custom-sidebar-border-300",
|
||||
{
|
||||
"justify-center size-8 aspect-square": isSidebarCollapsed,
|
||||
"cursor-not-allowed opacity-50 ": disabled,
|
||||
"px-3 border-[0.5px] border-custom-sidebar-border-300": !isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
data-ph-element={SIDEBAR_TRACKER_ELEMENTS.CREATE_WORK_ITEM_BUTTON}
|
||||
|
|
@ -97,9 +90,7 @@ export const SidebarQuickActions = observer(() => {
|
|||
disabled={disabled}
|
||||
>
|
||||
<PenSquare className="size-4" />
|
||||
{!isSidebarCollapsed && (
|
||||
<span className="text-sm font-medium truncate max-w-[145px]">{t("sidebar.new_work_item")}</span>
|
||||
)}
|
||||
<span className="text-sm font-medium truncate max-w-[145px]">{t("sidebar.new_work_item")}</span>
|
||||
</button>
|
||||
<AppSearch />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Ellipsis } from "lucide-react";
|
||||
import { ChevronRight, Ellipsis } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import {
|
||||
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
|
||||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS,
|
||||
WORKSPACE_SIDEBAR_STATIC_PINNED_NAVIGATION_ITEMS_LINKS,
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@plane/utils";
|
||||
|
|
@ -14,20 +16,30 @@ import { cn } from "@plane/utils";
|
|||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// store hooks
|
||||
import { useAppTheme, useWorkspace } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// plane-web imports
|
||||
import { SidebarItem } from "@/plane-web/components/workspace/sidebar";
|
||||
|
||||
export const SidebarMenuItems = observer(() => {
|
||||
// routers
|
||||
const { workspaceSlug } = useParams();
|
||||
const { setValue: toggleWorkspaceMenu, storedValue: isWorkspaceMenuOpen } = useLocalStorage<boolean>(
|
||||
"is_workspace_menu_open",
|
||||
true
|
||||
);
|
||||
|
||||
// store hooks
|
||||
const { sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
|
||||
const { isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme();
|
||||
const { getNavigationPreferences } = useWorkspace();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const currentWorkspaceNavigationPreferences = getNavigationPreferences(workspaceSlug.toString());
|
||||
|
||||
const toggleListDisclosure = (isOpen: boolean) => {
|
||||
toggleWorkspaceMenu(isOpen);
|
||||
};
|
||||
|
||||
const sortedNavigationItems = useMemo(
|
||||
() =>
|
||||
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.map((item) => {
|
||||
|
|
@ -41,35 +53,86 @@ export const SidebarMenuItems = observer(() => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-0.5", {
|
||||
"space-y-0": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
{WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
|
||||
<SidebarItem key={`static_${_index}`} item={item} />
|
||||
))}
|
||||
{sortedNavigationItems.map((item, _index) => (
|
||||
<SidebarItem key={`dynamic_${_index}`} item={item} />
|
||||
))}
|
||||
<SidebarNavItem className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleExtendedSidebar()}
|
||||
className={cn("flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350", {
|
||||
"justify-center": sidebarCollapsed,
|
||||
})}
|
||||
id="extended-sidebar-toggle"
|
||||
aria-label={t(
|
||||
extendedSidebarCollapsed
|
||||
? "aria_labels.projects_sidebar.open_extended_sidebar"
|
||||
: "aria_labels.projects_sidebar.close_extended_sidebar"
|
||||
)}
|
||||
<>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
|
||||
<SidebarItem key={`static_${_index}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen={!!isWorkspaceMenuOpen}>
|
||||
<div className="group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400"
|
||||
onClick={() => toggleListDisclosure(!isWorkspaceMenuOpen)}
|
||||
aria-label={t(
|
||||
isWorkspaceMenuOpen
|
||||
? "aria_labels.app_sidebar.close_workspace_menu"
|
||||
: "aria_labels.app_sidebar.open_workspace_menu"
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-semibold">{t("workspace")}</span>
|
||||
</Disclosure.Button>
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
onClick={() => toggleListDisclosure(!isWorkspaceMenuOpen)}
|
||||
aria-label={t(
|
||||
isWorkspaceMenuOpen
|
||||
? "aria_labels.app_sidebar.close_workspace_menu"
|
||||
: "aria_labels.app_sidebar.open_workspace_menu"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("flex-shrink-0 size-3 transition-all", {
|
||||
"rotate-90": isWorkspaceMenuOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
show={!!isWorkspaceMenuOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Ellipsis className="flex-shrink-0 size-4" />
|
||||
{!sidebarCollapsed && <span>More</span>}
|
||||
</button>
|
||||
</SidebarNavItem>
|
||||
</div>
|
||||
{isWorkspaceMenuOpen && (
|
||||
<Disclosure.Panel as="div" className="flex flex-col gap-0.5" static>
|
||||
<>
|
||||
{WORKSPACE_SIDEBAR_STATIC_PINNED_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
|
||||
<SidebarItem key={`static_${_index}`} item={item} />
|
||||
))}
|
||||
{sortedNavigationItems.map((item, _index) => (
|
||||
<SidebarItem key={`dynamic_${_index}`} item={item} />
|
||||
))}
|
||||
<SidebarNavItem>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleExtendedSidebar()}
|
||||
className="flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350"
|
||||
id="extended-sidebar-toggle"
|
||||
aria-label={t(
|
||||
isExtendedSidebarOpened
|
||||
? "aria_labels.app_sidebar.close_extended_sidebar"
|
||||
: "aria_labels.app_sidebar.open_extended_sidebar"
|
||||
)}
|
||||
>
|
||||
<Ellipsis className="flex-shrink-0 size-4" />
|
||||
<span>{isExtendedSidebarOpened ? "Hide" : "More"}</span>
|
||||
</button>
|
||||
</SidebarNavItem>
|
||||
</>
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ import Link from "next/link";
|
|||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, SIDEBAR_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
import { NotificationAppSidebarOption } from "@/components/workspace-notifications";
|
||||
|
|
@ -36,8 +35,7 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
|
|||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
|
||||
const isActive = pathname === item.href;
|
||||
|
||||
|
|
@ -59,30 +57,14 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
|
|||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipContent={t(item.labelTranslationKey)}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link href={item.href} onClick={() => handleLinkClick(item.key)}>
|
||||
<SidebarNavItem
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={isActive}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.Icon className="size-4 flex-shrink-0" />
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
|
||||
</div>
|
||||
{item.key === "notifications" && (
|
||||
<NotificationAppSidebarOption
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
isSidebarCollapsed={sidebarCollapsed ?? false}
|
||||
/>
|
||||
)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Link href={item.href} onClick={() => handleLinkClick(item.key)}>
|
||||
<SidebarNavItem isActive={isActive}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.Icon className="size-4 flex-shrink-0" />
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
</div>
|
||||
{item.key === "notifications" && <NotificationAppSidebarOption workspaceSlug={workspaceSlug.toString()} />}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
159
apps/web/core/components/workspace/sidebar/user-menu-root.tsx
Normal file
159
apps/web/core/components/workspace/sidebar/user-menu-root.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment, Ref, useState, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { usePopper } from "react-popper";
|
||||
// icons
|
||||
import { LogOut, PanelLeftDashed, Settings } from "lucide-react";
|
||||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { GOD_MODE_URL } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useAppTheme, useUser } from "@/hooks/store";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
|
||||
type Props = {
|
||||
size?: "sm" | "md";
|
||||
};
|
||||
|
||||
export const UserMenuRoot = observer((props: Props) => {
|
||||
const { size = "sm" } = props;
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { toggleAnySidebarDropdown, sidebarPeek, toggleSidebarPeek } = useAppTheme();
|
||||
|
||||
const { isEnabled, shouldRenderAppRail, toggleAppRail } = useAppRail();
|
||||
const { data: currentUser } = useUser();
|
||||
const { signOut } = useUser();
|
||||
// derived values
|
||||
|
||||
const isUserInstanceAdmin = false;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// local state
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "right",
|
||||
modifiers: [{ name: "preventOverflow", options: { padding: 12 } }],
|
||||
});
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut().catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("sign_out.toast.error.title"),
|
||||
message: t("sign_out.toast.error.message"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Toggle sidebar dropdown state when either menu is open
|
||||
useEffect(() => {
|
||||
if (isUserMenuOpen) toggleAnySidebarDropdown(true);
|
||||
else toggleAnySidebarDropdown(false);
|
||||
}, [isUserMenuOpen]);
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||
// Update local state directly
|
||||
if (isUserMenuOpen !== open) {
|
||||
setIsUserMenuOpen(open);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Button
|
||||
className="grid place-items-center outline-none"
|
||||
ref={setReferenceElement}
|
||||
aria-label={t("aria_labels.projects_sidebar.open_user_menu")}
|
||||
>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={size === "sm" ? 24 : 28}
|
||||
shape="circle"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute left-0 z-[21] mt-1 flex w-44 origin-top-left flex-col divide-y
|
||||
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
<Link href={`/${workspaceSlug}/settings/account`}>
|
||||
<Menu.Item as="div">
|
||||
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<Settings className="h-4 w-4 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
{isEnabled && (
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={() => {
|
||||
if (sidebarPeek) toggleSidebarPeek(false);
|
||||
toggleAppRail();
|
||||
}}
|
||||
>
|
||||
<PanelLeftDashed className="h-4 w-4 stroke-[1.5]" />
|
||||
<span>{shouldRenderAppRail ? "Undock AppRail" : "Dock AppRail"}</span>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</div>
|
||||
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 stroke-[1.5]" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
{isUserInstanceAdmin && (
|
||||
<div className="p-2 pb-0">
|
||||
<Link href={GOD_MODE_URL}>
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
|
||||
{t("enter_god_mode")}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
|
@ -8,15 +8,12 @@ import { EUserWorkspaceRoles } from "@plane/types";
|
|||
// plane imports
|
||||
import { UserActivityIcon } from "@plane/ui";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { SidebarUserMenuItem } from "@/components/workspace/sidebar";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useAppTheme, useUserPermissions, useUser } from "@/hooks/store";
|
||||
import { useUserPermissions, useUser } from "@/hooks/store";
|
||||
|
||||
export const SidebarUserMenu = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { workspaceUserInfo } = useUserPermissions();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
|
|
@ -54,11 +51,7 @@ export const SidebarUserMenu = observer(() => {
|
|||
const draftIssueCount = workspaceUserInfo[workspaceSlug.toString()]?.draft_issue_count;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-0.5", {
|
||||
"space-y-0": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{SIDEBAR_USER_MENU_ITEMS.map((item) => (
|
||||
<SidebarUserMenuItem key={item.key} item={item} draftIssueCount={draftIssueCount} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { EUserWorkspaceRoles } from "@plane/types";
|
|||
import { CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// store hooks
|
||||
import { useAppTheme, useUserPermissions } from "@/hooks/store";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
|
||||
export type SidebarWorkspaceMenuHeaderProps = {
|
||||
isWorkspaceMenuOpen: boolean;
|
||||
|
|
@ -27,7 +27,6 @@ export const SidebarWorkspaceMenuHeader: FC<SidebarWorkspaceMenuHeaderProps> = o
|
|||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { workspaceSlug } = useParams();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -37,19 +36,8 @@ export const SidebarWorkspaceMenuHeader: FC<SidebarWorkspaceMenuHeaderProps> = o
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isAdmin = allowPermissions([EUserWorkspaceRoles.ADMIN] as any, EUserPermissionsLevel.WORKSPACE);
|
||||
|
||||
if (sidebarCollapsed) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded",
|
||||
{
|
||||
"mt-2.5": !sidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded mt-2.5">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
className="flex-1 sticky top-0 z-10 w-full py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 text-sm font-semibold"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import Link from "next/link";
|
|||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
|
|
@ -35,8 +33,7 @@ export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = obser
|
|||
const { workspaceSlug } = useParams();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
|
||||
const handleLinkClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
|
|
@ -51,33 +48,20 @@ export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = obser
|
|||
const isActive = item.href === pathname;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipContent={t(item.labelTranslationKey)}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link href={item.href} onClick={() => handleLinkClick()}>
|
||||
<SidebarNavItem
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={isActive}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.Icon
|
||||
className={cn("size-4", {
|
||||
"rotate-180": item.key === "active_cycles",
|
||||
})}
|
||||
/>
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
|
||||
</div>
|
||||
{!sidebarCollapsed && item.key === "active_cycles" && (
|
||||
<div className="flex-shrink-0">
|
||||
<UpgradeBadge />
|
||||
</div>
|
||||
)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Link href={item.href} onClick={() => handleLinkClick()}>
|
||||
<SidebarNavItem isActive={isActive}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.Icon
|
||||
className={cn("size-4", {
|
||||
"rotate-180": item.key === "active_cycles",
|
||||
})}
|
||||
/>
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<UpgradeBadge />
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
"use client";
|
||||
|
||||
import React, { Fragment, useState, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// icons
|
||||
import { ChevronDown, CirclePlus, LogOut, Mails } from "lucide-react";
|
||||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { orderWorkspacesList, cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { AppSidebarItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
|
||||
// plane web helpers
|
||||
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
|
||||
// components
|
||||
import { WorkspaceLogo } from "../logo";
|
||||
import SidebarDropdownItem from "./dropdown-item";
|
||||
|
||||
type WorkspaceMenuRootProps = {
|
||||
renderLogoOnly?: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceMenuRoot = observer((props: WorkspaceMenuRootProps) => {
|
||||
const { renderLogoOnly } = props;
|
||||
// store hooks
|
||||
const { toggleSidebar, toggleAnySidebarDropdown } = useAppTheme();
|
||||
const { data: currentUser } = useUser();
|
||||
const { signOut } = useUser();
|
||||
const { updateUserProfile } = useUserProfile();
|
||||
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
|
||||
// derived values
|
||||
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// local state
|
||||
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
|
||||
|
||||
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut().catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("sign_out.toast.error.title"),
|
||||
message: t("sign_out.toast.error.message"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
||||
// TODO: fix workspaces list scroll
|
||||
|
||||
// Toggle sidebar dropdown state when either menu is open
|
||||
useEffect(() => {
|
||||
if (isWorkspaceMenuOpen) toggleAnySidebarDropdown(true);
|
||||
else toggleAnySidebarDropdown(false);
|
||||
}, [isWorkspaceMenuOpen]);
|
||||
|
||||
const logo = activeWorkspace?.logo_url;
|
||||
const name = activeWorkspace?.name;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
as="div"
|
||||
className={cn("relative h-full flex ", {
|
||||
"justify-center text-center": renderLogoOnly,
|
||||
"flex-grow justify-stretch text-left truncate": !renderLogoOnly,
|
||||
})}
|
||||
>
|
||||
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||
// Update local state directly
|
||||
if (isWorkspaceMenuOpen !== open) {
|
||||
setIsWorkspaceMenuOpen(open);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderLogoOnly ? (
|
||||
<Menu.Button className="flex items-center justify-center size-8">
|
||||
<AppSidebarItem
|
||||
variant="button"
|
||||
item={{
|
||||
icon: (
|
||||
<WorkspaceLogo
|
||||
logo={activeWorkspace?.logo_url}
|
||||
name={activeWorkspace?.name}
|
||||
classNames="size-8 rounded-md"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Menu.Button>
|
||||
) : (
|
||||
<Menu.Button
|
||||
className={cn(
|
||||
"group/menu-button flex items-center gap-1 p-1 truncate rounded text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none ",
|
||||
{
|
||||
"justify-center text-center": renderLogoOnly,
|
||||
"justify-between flex-grow": !renderLogoOnly,
|
||||
}
|
||||
)}
|
||||
aria-label={t("aria_labels.projects_sidebar.open_workspace_switcher")}
|
||||
>
|
||||
<div className="flex-grow flex items-center gap-2 truncate">
|
||||
<WorkspaceLogo logo={activeWorkspace?.logo_url} name={activeWorkspace?.name} />
|
||||
<h4 className="truncate text-base font-medium text-custom-text-100">
|
||||
{activeWorkspace?.name ?? t("loading")}
|
||||
</h4>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"flex-shrink-0 mx-1 hidden size-4 group-hover/menu-button:block text-custom-sidebar-text-400 duration-300",
|
||||
{ "rotate-180": open }
|
||||
)}
|
||||
/>
|
||||
</Menu.Button>
|
||||
)}
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="trnsform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items as={Fragment}>
|
||||
<div className="fixed top-12 left-4 z-[21] mt-1 flex w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
|
||||
<div className="overflow-x-hidden vertical-scrollbar scrollbar-sm flex max-h-96 flex-col items-start justify-start overflow-y-scroll">
|
||||
<span className="rounded-md text-left px-4 sticky top-0 z-[21] h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-text-400 truncate flex-shrink-0">
|
||||
{currentUser?.email}
|
||||
</span>
|
||||
{workspacesList ? (
|
||||
<div className="size-full flex flex-col items-start justify-start">
|
||||
{(activeWorkspace
|
||||
? [
|
||||
activeWorkspace,
|
||||
...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id),
|
||||
]
|
||||
: workspacesList
|
||||
).map((workspace) => (
|
||||
<SidebarDropdownItem
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
activeWorkspace={activeWorkspace}
|
||||
handleItemClick={handleItemClick}
|
||||
handleWorkspaceNavigation={handleWorkspaceNavigation}
|
||||
handleClose={close}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<Loader className="space-y-2">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
|
||||
{isWorkspaceCreationEnabled && (
|
||||
<Link href="/create-workspace" className="w-full">
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<CirclePlus className="size-4 flex-shrink-0" />
|
||||
{t("create_workspace")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="/invitations" className="w-full" onClick={handleItemClick}>
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<Mails className="h-4 w-4 flex-shrink-0" />
|
||||
{t("workspace_invites")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
|
||||
<div className="w-full">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 flex-shrink-0" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { BarChart2, Briefcase, Layers } from "lucide-react";
|
||||
|
|
@ -11,25 +11,17 @@ import { ContrastIcon } from "@plane/ui";
|
|||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { SidebarWorkspaceMenuHeader, SidebarWorkspaceMenuItem } from "@/components/workspace/sidebar";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
||||
export const SidebarWorkspaceMenu = observer(() => {
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
// local storage
|
||||
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage<boolean>("is_workspace_menu_open", true);
|
||||
// derived values
|
||||
const isWorkspaceMenuOpen = !!storedValue;
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarCollapsed) toggleWorkspaceMenu(true);
|
||||
}, [sidebarCollapsed, toggleWorkspaceMenu]);
|
||||
|
||||
const SIDEBAR_WORKSPACE_MENU_ITEMS = [
|
||||
{
|
||||
key: "projects",
|
||||
|
|
@ -74,13 +66,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
|||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
{isWorkspaceMenuOpen && (
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("flex flex-col mt-0.5 gap-0.5", {
|
||||
"space-y-0 mt-0 ml-0": sidebarCollapsed,
|
||||
})}
|
||||
static
|
||||
>
|
||||
<Disclosure.Panel as="div" className="flex flex-col mt-0.5 gap-0.5" static>
|
||||
{SIDEBAR_WORKSPACE_MENU_ITEMS.map((item) => (
|
||||
<SidebarWorkspaceMenuItem key={item.key} item={item} />
|
||||
))}
|
||||
|
|
|
|||
47
apps/web/core/hooks/context/app-rail-context.tsx
Normal file
47
apps/web/core/hooks/context/app-rail-context.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
||||
export interface AppRailContextType {
|
||||
isEnabled: boolean;
|
||||
shouldRenderAppRail: boolean;
|
||||
toggleAppRail: (value?: boolean) => void;
|
||||
}
|
||||
|
||||
const AppRailContext = createContext<AppRailContextType | undefined>(undefined);
|
||||
|
||||
export { AppRailContext };
|
||||
|
||||
interface AppRailProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AppRailProvider = observer(({ children }: AppRailProviderProps) => {
|
||||
const { workspaceSlug } = useParams();
|
||||
const { storedValue: isAppRailVisible, setValue: setIsAppRailVisible } = useLocalStorage<boolean>(
|
||||
`APP_RAIL_${workspaceSlug}`,
|
||||
false
|
||||
);
|
||||
|
||||
const isEnabled = false;
|
||||
|
||||
const toggleAppRail = (value?: boolean) => {
|
||||
if (value === undefined) {
|
||||
setIsAppRailVisible(!isAppRailVisible);
|
||||
} else {
|
||||
setIsAppRailVisible(value);
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue: AppRailContextType = {
|
||||
isEnabled,
|
||||
shouldRenderAppRail: !!isAppRailVisible && isEnabled,
|
||||
toggleAppRail,
|
||||
};
|
||||
|
||||
return <AppRailContext.Provider value={contextValue}>{children}</AppRailContext.Provider>;
|
||||
});
|
||||
10
apps/web/core/hooks/use-app-rail.tsx
Normal file
10
apps/web/core/hooks/use-app-rail.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useContext } from "react";
|
||||
import { AppRailContext } from "./context/app-rail-context";
|
||||
|
||||
export const useAppRail = () => {
|
||||
const context = useContext(AppRailContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAppRail must be used within AppRailProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
24
apps/web/core/hooks/use-workspace-paths.ts
Normal file
24
apps/web/core/hooks/use-workspace-paths.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Custom hook to detect different workspace paths
|
||||
* @returns Object containing boolean flags for different workspace paths
|
||||
*/
|
||||
export const useWorkspacePaths = () => {
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const isSettingsPath = pathname.includes(`/${workspaceSlug}/settings`);
|
||||
const isWikiPath = pathname.includes(`/${workspaceSlug}/pages`);
|
||||
const isAiPath = pathname.includes(`/${workspaceSlug}/pi-chat`);
|
||||
const isProjectsPath = pathname.includes(`/${workspaceSlug}/`) && !isWikiPath && !isAiPath && !isSettingsPath;
|
||||
|
||||
return {
|
||||
isSettingsPath,
|
||||
isWikiPath,
|
||||
isAiPath,
|
||||
isProjectsPath,
|
||||
};
|
||||
};
|
||||
|
|
@ -165,7 +165,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||
// check if the project member apis is loading
|
||||
if (isParentLoading || (!projectMemberInfo && projectId && hasPermissionToCurrentProject === null))
|
||||
return (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
||||
<div className="grid h-full place-items-center bg-custom-background-100 p-4 rounded-lg border border-custom-border-200">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
|
|
@ -183,7 +183,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||
// check if the project info is not found.
|
||||
if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false)
|
||||
return (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100">
|
||||
<div className="grid h-full place-items-center bg-custom-background-100">
|
||||
<DetailedEmptyState
|
||||
title={t("workspace_projects.empty_state.general.title")}
|
||||
description={t("workspace_projects.empty_state.general.description")}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { useFavorite } from "@/hooks/store/use-favorite";
|
|||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local
|
||||
import { persistence } from "@/local-db/storage.sqlite";
|
||||
// constants
|
||||
// images
|
||||
import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
|
||||
import PlaneWhiteLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
|
||||
|
|
@ -136,7 +135,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||
// if list of workspaces are not there then we have to render the spinner
|
||||
if (isParentLoading || allWorkspaces === undefined || loader || isDBInitializing) {
|
||||
return (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
||||
<div className="grid h-full place-items-center bg-custom-background-100 p-4 rounded-lg border border-custom-border-200">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
|
|
@ -147,7 +146,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||
// if workspaces are there and we are trying to access the workspace that we are not part of then show the existing workspaces
|
||||
if (currentWorkspace === undefined && !currentWorkspaceInfo) {
|
||||
return (
|
||||
<div className="relative flex h-screen w-full flex-col items-center justify-center bg-custom-background-90 ">
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-center bg-custom-background-90 ">
|
||||
<div className="container relative mx-auto flex h-full w-full flex-col overflow-hidden overflow-y-auto px-5 py-14 md:px-0">
|
||||
<div className="relative flex flex-shrink-0 items-center justify-between gap-4">
|
||||
<div className="z-10 flex-shrink-0 bg-custom-background-90 py-4">
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import { action, observable, makeObservable, runInAction } from "mobx";
|
|||
|
||||
export interface IThemeStore {
|
||||
// observables
|
||||
isAnySidebarDropdownOpen: boolean | undefined;
|
||||
sidebarCollapsed: boolean | undefined;
|
||||
extendedSidebarCollapsed: boolean | undefined;
|
||||
extendedProjectSidebarCollapsed: boolean | undefined;
|
||||
sidebarPeek: boolean | undefined;
|
||||
isExtendedSidebarOpened: boolean | undefined;
|
||||
isExtendedProjectSidebarOpened: boolean | undefined;
|
||||
profileSidebarCollapsed: boolean | undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined;
|
||||
|
|
@ -12,7 +14,9 @@ export interface IThemeStore {
|
|||
initiativesSidebarCollapsed: boolean | undefined;
|
||||
projectOverviewSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
toggleAnySidebarDropdown: (open?: boolean) => void;
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
toggleSidebarPeek: (peek?: boolean) => void;
|
||||
toggleExtendedSidebar: (collapsed?: boolean) => void;
|
||||
toggleExtendedProjectSidebar: (collapsed?: boolean) => void;
|
||||
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||
|
|
@ -25,9 +29,11 @@ export interface IThemeStore {
|
|||
|
||||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
isAnySidebarDropdownOpen: boolean | undefined = undefined;
|
||||
sidebarCollapsed: boolean | undefined = undefined;
|
||||
extendedSidebarCollapsed: boolean | undefined = true;
|
||||
extendedProjectSidebarCollapsed: boolean | undefined = undefined;
|
||||
sidebarPeek: boolean | undefined = undefined;
|
||||
isExtendedSidebarOpened: boolean | undefined = undefined;
|
||||
isExtendedProjectSidebarOpened: boolean | undefined = undefined;
|
||||
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined = undefined;
|
||||
|
|
@ -38,9 +44,11 @@ export class ThemeStore implements IThemeStore {
|
|||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isAnySidebarDropdownOpen: observable.ref,
|
||||
sidebarCollapsed: observable.ref,
|
||||
extendedSidebarCollapsed: observable.ref,
|
||||
extendedProjectSidebarCollapsed: observable.ref,
|
||||
sidebarPeek: observable.ref,
|
||||
isExtendedSidebarOpened: observable.ref,
|
||||
isExtendedProjectSidebarOpened: observable.ref,
|
||||
profileSidebarCollapsed: observable.ref,
|
||||
workspaceAnalyticsSidebarCollapsed: observable.ref,
|
||||
issueDetailSidebarCollapsed: observable.ref,
|
||||
|
|
@ -48,7 +56,9 @@ export class ThemeStore implements IThemeStore {
|
|||
initiativesSidebarCollapsed: observable.ref,
|
||||
projectOverviewSidebarCollapsed: observable.ref,
|
||||
// action
|
||||
toggleAnySidebarDropdown: action,
|
||||
toggleSidebar: action,
|
||||
toggleSidebarPeek: action,
|
||||
toggleExtendedSidebar: action,
|
||||
toggleExtendedProjectSidebar: action,
|
||||
toggleProfileSidebar: action,
|
||||
|
|
@ -60,6 +70,14 @@ export class ThemeStore implements IThemeStore {
|
|||
});
|
||||
}
|
||||
|
||||
toggleAnySidebarDropdown = (open?: boolean) => {
|
||||
if (open === undefined) {
|
||||
this.isAnySidebarDropdownOpen = !this.isAnySidebarDropdownOpen;
|
||||
} else {
|
||||
this.isAnySidebarDropdownOpen = open;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the sidebar collapsed state
|
||||
* @param collapsed
|
||||
|
|
@ -73,14 +91,26 @@ export class ThemeStore implements IThemeStore {
|
|||
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the sidebar peek state
|
||||
* @param peek
|
||||
*/
|
||||
toggleSidebarPeek = (peek?: boolean) => {
|
||||
if (peek === undefined) {
|
||||
this.sidebarPeek = !this.sidebarPeek;
|
||||
} else {
|
||||
this.sidebarPeek = peek;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the extended sidebar collapsed state
|
||||
* @param collapsed
|
||||
*/
|
||||
toggleExtendedSidebar = (collapsed?: boolean) => {
|
||||
const updatedState = collapsed ?? !this.extendedSidebarCollapsed;
|
||||
const updatedState = collapsed ?? !this.isExtendedSidebarOpened;
|
||||
runInAction(() => {
|
||||
this.extendedSidebarCollapsed = updatedState;
|
||||
this.isExtendedSidebarOpened = updatedState;
|
||||
});
|
||||
localStorage.setItem("extended_sidebar_collapsed", updatedState.toString());
|
||||
};
|
||||
|
|
@ -91,11 +121,11 @@ export class ThemeStore implements IThemeStore {
|
|||
*/
|
||||
toggleExtendedProjectSidebar = (collapsed?: boolean) => {
|
||||
if (collapsed === undefined) {
|
||||
this.extendedProjectSidebarCollapsed = !this.extendedProjectSidebarCollapsed;
|
||||
this.isExtendedProjectSidebarOpened = !this.isExtendedProjectSidebarOpened;
|
||||
} else {
|
||||
this.extendedProjectSidebarCollapsed = collapsed;
|
||||
this.isExtendedProjectSidebarOpened = collapsed;
|
||||
}
|
||||
localStorage.setItem("extended_project_sidebar_collapsed", this.extendedProjectSidebarCollapsed.toString());
|
||||
localStorage.setItem("extended_project_sidebar_collapsed", this.isExtendedProjectSidebarOpened.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
1
apps/web/ee/components/app-rail/index.ts
Normal file
1
apps/web/ee/components/app-rail/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/app-rail";
|
||||
|
|
@ -3,3 +3,4 @@ export * from "./upgrade-badge";
|
|||
export * from "./billing";
|
||||
export * from "./delete-workspace-section";
|
||||
export * from "./sidebar";
|
||||
export * from "ce/components/workspace/app-switcher";
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "ce/components/workspace/upgrade-badge";
|
||||
export * from "ce/components/workspace/content-wrapper";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue