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