[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,21 +2,22 @@
|
|||
|
||||
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 />
|
||||
<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,47 +52,23 @@ 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
|
||||
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,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
<SidebarDropdown />
|
||||
<div className="flex-shrink-0 h-4" />
|
||||
{/* App switcher */}
|
||||
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
|
||||
{!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>
|
||||
)}
|
||||
{/* 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,
|
||||
})}
|
||||
>
|
||||
<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 />
|
||||
{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 */}
|
||||
|
|
@ -101,11 +77,13 @@ export const AppSidebar: FC = observer(() => {
|
|||
<SidebarProjectsList />
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<SidebarHelpSection />
|
||||
<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">
|
||||
<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="px-4 md:pl-12 md:flex w-full">
|
||||
<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}
|
||||
>
|
||||
<SidebarNavItem 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>}
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
</div>
|
||||
{item.key === "inbox" && (
|
||||
<NotificationAppSidebarOption
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
isSidebarCollapsed={sidebarCollapsed ?? false}
|
||||
/>
|
||||
)}
|
||||
{item.key === "inbox" && <NotificationAppSidebarOption workspaceSlug={workspaceSlug?.toString()} />}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
{
|
||||
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 "./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,33 +166,13 @@ 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}>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
className="size-8 aspect-square flex-shrink-0 grid place-items-center"
|
||||
>
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
<FavoriteFolderIcon />
|
||||
</div>
|
||||
</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,
|
||||
})}
|
||||
className="flex-grow flex items-center gap-1.5 text-left select-none w-full"
|
||||
>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
|
|
@ -212,7 +189,6 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
|||
{
|
||||
"cursor-not-allowed opacity-60": favorite.sort_order === null,
|
||||
"cursor-grabbing": isDragging,
|
||||
"!hidden": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
@ -279,7 +255,6 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
|||
/>
|
||||
</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,16 +7,12 @@ 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(
|
||||
|
|
@ -28,7 +24,6 @@ export const FavoriteItemWrapper: FC<Props> = (props) => {
|
|||
>
|
||||
{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,10 +131,9 @@ 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 && (
|
||||
<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}
|
||||
|
|
@ -149,7 +141,6 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
|
|||
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,15 +174,10 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
return (
|
||||
<>
|
||||
<Disclosure as="div" defaultOpen ref={containerRef}>
|
||||
{!sidebarCollapsed && (
|
||||
<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",
|
||||
{
|
||||
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
|
||||
sidebarCollapsed,
|
||||
}
|
||||
"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
|
||||
|
|
@ -206,7 +186,6 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
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,
|
||||
}
|
||||
)}
|
||||
|
|
@ -252,7 +231,6 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
show={isFavoriteMenuOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
|
|
@ -263,36 +241,17 @@ 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}
|
||||
|
|
@ -311,8 +270,6 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
handleDrop={handleDrop}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
)}
|
||||
|
|
@ -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)}
|
||||
>
|
||||
<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]"}`}
|
||||
/>
|
||||
{!isSidebarCollapsed && <span className="text-xs font-medium">{t(item.i18n_key)}</span>}
|
||||
<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>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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,28 +251,7 @@ 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,
|
||||
})}
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
<Disclosure.Button as="button" className="size-8 aspect-square flex-shrink-0 grid place-items-center">
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
<Logo logo={project.logo_props} size={16} />
|
||||
</div>
|
||||
</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"
|
||||
|
|
@ -279,9 +260,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<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,
|
||||
})}
|
||||
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")
|
||||
|
|
@ -294,7 +273,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{project.name}</p>
|
||||
</Disclosure.Button>
|
||||
</ControlLink>
|
||||
</Tooltip>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
|
|
@ -316,6 +294,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
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 && (
|
||||
|
|
@ -408,7 +387,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
/>
|
||||
</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,17 +160,8 @@ 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>
|
||||
</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="">
|
||||
|
|
@ -221,7 +196,6 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
</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,
|
||||
})}
|
||||
>
|
||||
<>
|
||||
<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"
|
||||
>
|
||||
{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 className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}>
|
||||
<SidebarNavItem>
|
||||
<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,
|
||||
})}
|
||||
className="flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350"
|
||||
id="extended-sidebar-toggle"
|
||||
aria-label={t(
|
||||
extendedSidebarCollapsed
|
||||
? "aria_labels.projects_sidebar.open_extended_sidebar"
|
||||
: "aria_labels.projects_sidebar.close_extended_sidebar"
|
||||
isExtendedSidebarOpened
|
||||
? "aria_labels.app_sidebar.close_extended_sidebar"
|
||||
: "aria_labels.app_sidebar.open_extended_sidebar"
|
||||
)}
|
||||
>
|
||||
<Ellipsis className="flex-shrink-0 size-4" />
|
||||
{!sidebarCollapsed && <span>More</span>}
|
||||
<span>{isExtendedSidebarOpened ? "Hide" : "More"}</span>
|
||||
</button>
|
||||
</SidebarNavItem>
|
||||
</div>
|
||||
</>
|
||||
</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}
|
||||
>
|
||||
<SidebarNavItem 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>}
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
</div>
|
||||
{item.key === "notifications" && (
|
||||
<NotificationAppSidebarOption
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
isSidebarCollapsed={sidebarCollapsed ?? false}
|
||||
/>
|
||||
)}
|
||||
{item.key === "notifications" && <NotificationAppSidebarOption workspaceSlug={workspaceSlug.toString()} />}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
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}
|
||||
>
|
||||
<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",
|
||||
})}
|
||||
/>
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -35,3 +35,4 @@ export * from "./settings";
|
|||
export * from "./icon";
|
||||
export * from "./estimates";
|
||||
export * from "./analytics";
|
||||
export * from "./sidebar";
|
||||
|
|
|
|||
2
packages/constants/src/sidebar.ts
Normal file
2
packages/constants/src/sidebar.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const SIDEBAR_WIDTH = 250;
|
||||
export const EXTENDED_SIDEBAR_WIDTH = 300;
|
||||
|
|
@ -317,6 +317,9 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS: Record<string, IWorkspac
|
|||
export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarNavigationItem[] = [
|
||||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["home"],
|
||||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["inbox"],
|
||||
];
|
||||
|
||||
export const WORKSPACE_SIDEBAR_STATIC_PINNED_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarNavigationItem[] = [
|
||||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
|
||||
];
|
||||
|
||||
|
|
|
|||
30
packages/ui/src/icons/ai-icon.tsx
Normal file
30
packages/ui/src/icons/ai-icon.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const AiIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16", className, color = "currentColor" }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clip-path="url(#clip0_888_35571)">
|
||||
<path
|
||||
d="M14.2082 0H1.79185C0.801553 0 0 0.801553 0 1.79185V14.2093C0 15.1984 0.801553 16.0012 1.79185 16.0012H14.2093C15.1984 16.0012 16.0012 15.1996 16.0012 14.2093V1.79185C16.0012 0.802748 15.1996 0 14.2093 0H14.2082ZM13.1032 11.5276C13.1032 12.3984 12.3972 13.1032 11.5276 13.1032H4.47245C3.60161 13.1032 2.89682 12.3972 2.89682 11.5276V4.47245C2.89682 3.60161 3.60281 2.89682 4.47245 2.89682H11.5276C12.3984 2.89682 13.1032 3.60281 13.1032 4.47245V11.5276Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M9.61291 4.94336H6.38759C5.58996 4.94336 4.94336 5.58996 4.94336 6.38759V9.61291C4.94336 10.4105 5.58996 11.0571 6.38759 11.0571H9.61291C10.4105 11.0571 11.0571 10.4105 11.0571 9.61291V6.38759C11.0571 5.58996 10.4105 4.94336 9.61291 4.94336Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_888_35571">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -52,3 +52,6 @@ export * from "./sticky-note-icon";
|
|||
export * from "./bar-icon";
|
||||
export * from "./tree-map-icon";
|
||||
export * from "./display-properties";
|
||||
export * from "./ai-icon";
|
||||
export * from "./plane-icon";
|
||||
export * from "./wiki-icon";
|
||||
|
|
|
|||
35
packages/ui/src/icons/plane-icon.tsx
Normal file
35
packages/ui/src/icons/plane-icon.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const PlaneNewIcon: React.FC<ISvgIcons> = ({
|
||||
width = "16",
|
||||
height = "16",
|
||||
className,
|
||||
color = "currentColor",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clip-path="url(#clip0_888_35560)">
|
||||
<path
|
||||
d="M5.15383 9.50566V5.15381H1.34152C0.601228 5.15381 0 5.75399 0 6.49533V14.6595C0 15.3998 0.600183 16.001 1.34152 16.001H9.50568C10.246 16.001 10.8472 15.4008 10.8472 14.6595V10.8461H6.49536C5.75506 10.8461 5.15383 10.246 5.15383 9.50461V9.50566Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M14.66 0H6.49582C5.75553 0 5.1543 0.600183 5.1543 1.34152V5.15488H9.50615C10.2464 5.15488 10.8477 5.75506 10.8477 6.49641V10.8483H14.661C15.4013 10.8483 16.0026 10.2481 16.0026 9.50673V1.34152C16.0026 0.601229 15.4024 0 14.661 0H14.66Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_888_35560">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
26
packages/ui/src/icons/wiki-icon.tsx
Normal file
26
packages/ui/src/icons/wiki-icon.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const WikiIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16", className, color = "currentColor" }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clip-path="url(#clip0_888_35566)">
|
||||
<path
|
||||
d="M15.558 6.93332L9.06623 0.441504C8.47755 -0.147168 7.5229 -0.147168 6.93332 0.441504L0.441504 6.93332C-0.147168 7.52199 -0.147168 8.47664 0.441504 9.06623L6.93332 15.558C7.52199 16.1467 8.47664 16.1467 9.06623 15.558L15.558 9.06623C16.1467 8.47755 16.1467 7.5229 15.558 6.93332ZM10.7629 9.65855C10.7629 10.2682 10.2691 10.762 9.65946 10.762H6.341C5.73133 10.762 5.23758 10.2682 5.23758 9.65855V6.34008C5.23758 5.73042 5.73133 5.23667 6.341 5.23667H9.65946C10.2691 5.23667 10.7629 5.73042 10.7629 6.34008V9.65855Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_888_35566">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue