[WEB-1716] chore: sidebar improvements for guests/viewers (#4941)
* chore: sidebar improvements for guests/viewers * chore: store workspace menu open state in local storage
This commit is contained in:
parent
eda1599c0d
commit
67784b45fd
4 changed files with 93 additions and 101 deletions
|
|
@ -351,11 +351,11 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="size-4" />
|
<MoreHorizontal className="size-3.5" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"opacity-0 pointer-events-none flex-shrink-0 mr-1 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
|
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
|
||||||
{
|
{
|
||||||
"opacity-100 pointer-events-auto": isMenuActive,
|
"opacity-100 pointer-events-auto": isMenuActive,
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +404,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
<span>Copy link</span>
|
<span>Copy link</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
|
|
||||||
{!isViewerOrGuest && (
|
{!isViewerOrGuest && (
|
||||||
<CustomMenu.MenuItem>
|
<CustomMenu.MenuItem>
|
||||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/archives/issues`}>
|
<Link href={`/${workspaceSlug}/projects/${project?.id}/archives/issues`}>
|
||||||
|
|
|
||||||
|
|
@ -220,21 +220,23 @@ export const SidebarProjectsList: FC = observer(() => {
|
||||||
<span>{isCollapsed ? <section.icon className="flex-shrink-0 size-3" /> : section.title}</span>
|
<span>{isCollapsed ? <section.icon className="flex-shrink-0 size-3" /> : section.title}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
{!isCollapsed && isAuthorizedUser && (
|
{!isCollapsed && (
|
||||||
<div className="flex items-center opacity-0 group-hover:opacity-100">
|
<div className="flex items-center opacity-0 group-hover:opacity-100">
|
||||||
<Tooltip tooltipHeading="Create project" tooltipContent="">
|
{isAuthorizedUser && (
|
||||||
<button
|
<Tooltip tooltipHeading="Create project" tooltipContent="">
|
||||||
type="button"
|
<button
|
||||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
type="button"
|
||||||
onClick={() => {
|
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||||
setTrackElement(`APP_SIDEBAR_${section.type}_BLOCK`);
|
onClick={() => {
|
||||||
setIsFavoriteProjectCreate(section.key === "favorite");
|
setTrackElement(`APP_SIDEBAR_${section.type}_BLOCK`);
|
||||||
setIsProjectModalOpen(true);
|
setIsFavoriteProjectCreate(section.key === "favorite");
|
||||||
}}
|
setIsProjectModalOpen(true);
|
||||||
>
|
}}
|
||||||
<Plus className="size-3" />
|
>
|
||||||
</button>
|
<Plus className="size-3" />
|
||||||
</Tooltip>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -296,14 +298,14 @@ export const SidebarProjectsList: FC = observer(() => {
|
||||||
{isAuthorizedUser && joinedProjects?.length === 0 && (
|
{isAuthorizedUser && joinedProjects?.length === 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
|
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={() => {
|
||||||
setTrackElement("Sidebar");
|
setTrackElement("Sidebar");
|
||||||
toggleCreateProjectModal(true);
|
toggleCreateProjectModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-5" />
|
<Plus className="flex-shrink-0 size-4" />
|
||||||
{!isCollapsed && "Add Project"}
|
{!isCollapsed && "Add project"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,10 @@ import { TIssue } from "@plane/types";
|
||||||
import { CreateUpdateIssueModal } from "@/components/issues";
|
import { CreateUpdateIssueModal } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
import { useAppTheme, useCommandPalette, useEventTracker, useProject } from "@/hooks/store";
|
||||||
import useLocalStorage from "@/hooks/use-local-storage";
|
import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
|
|
||||||
export const SidebarQuickActions = observer(() => {
|
export const SidebarQuickActions = observer(() => {
|
||||||
|
|
@ -30,16 +29,11 @@ export const SidebarQuickActions = observer(() => {
|
||||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { joinedProjectIds } = useProject();
|
const { joinedProjectIds } = useProject();
|
||||||
const {
|
|
||||||
membership: { currentWorkspaceRole },
|
|
||||||
} = useUser();
|
|
||||||
// local storage
|
// local storage
|
||||||
const { storedValue, setValue } = useLocalStorage<Record<string, Partial<TIssue>>>("draftedIssue", {});
|
const { storedValue, setValue } = useLocalStorage<Record<string, Partial<TIssue>>>("draftedIssue", {});
|
||||||
// derived values
|
// derived values
|
||||||
const disabled = joinedProjectIds.length === 0;
|
const disabled = joinedProjectIds.length === 0;
|
||||||
const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined;
|
const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined;
|
||||||
// auth
|
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
// if enter before time out clear the timeout
|
// if enter before time out clear the timeout
|
||||||
|
|
@ -74,66 +68,64 @@ export const SidebarQuickActions = observer(() => {
|
||||||
"flex-col gap-0": isSidebarCollapsed,
|
"flex-col gap-0": isSidebarCollapsed,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isAuthorizedUser && (
|
<div
|
||||||
<div
|
className={cn(
|
||||||
|
"relative flex-grow flex items-center justify-between gap-1 rounded h-8 hover:bg-custom-sidebar-background-90",
|
||||||
|
{
|
||||||
|
"size-8 aspect-square": isSidebarCollapsed,
|
||||||
|
"px-3 border-[0.5px] border-custom-sidebar-border-300": !isSidebarCollapsed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex-grow flex items-center justify-between gap-1 rounded h-8 hover:bg-custom-sidebar-background-90",
|
"relative flex flex-shrink-0 flex-grow items-center gap-2 text-custom-sidebar-text-300 rounded outline-none",
|
||||||
{
|
{
|
||||||
"size-8 aspect-square": isSidebarCollapsed,
|
|
||||||
"px-3 border-[0.5px] border-custom-sidebar-border-300": !isSidebarCollapsed,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn("relative flex flex-shrink-0 flex-grow items-center gap-2 rounded outline-none", {
|
|
||||||
"justify-center": isSidebarCollapsed,
|
"justify-center": isSidebarCollapsed,
|
||||||
"cursor-not-allowed opacity-50": disabled,
|
"cursor-not-allowed opacity-50": disabled,
|
||||||
})}
|
}
|
||||||
onClick={() => {
|
|
||||||
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
|
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<PenSquare className="size-4 text-custom-sidebar-text-300" />
|
|
||||||
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
|
|
||||||
</button>
|
|
||||||
{!disabled && workspaceDraftIssue && (
|
|
||||||
<>
|
|
||||||
{!isSidebarCollapsed && (
|
|
||||||
<button type="button" className="grid place-items-center">
|
|
||||||
<ChevronUp
|
|
||||||
className={cn(
|
|
||||||
"size-4 transform !text-custom-sidebar-text-300 transition-transform duration-300",
|
|
||||||
{
|
|
||||||
"rotate-180": isDraftButtonOpen,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{isDraftButtonOpen && (
|
|
||||||
<div
|
|
||||||
className={`fixed left-4 mt-0 h-10 w-[203px] pt-2 ${isSidebarCollapsed ? "top-[5.5rem]" : "top-24"}`}
|
|
||||||
>
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsDraftIssueModalOpen(true)}
|
|
||||||
className="flex w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-3 py-[10px] text-sm text-custom-text-300 shadow"
|
|
||||||
>
|
|
||||||
<PenSquare size={16} className="mr-2 !text-lg !leading-4 text-custom-sidebar-text-300" />
|
|
||||||
Last Drafted Issue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
onClick={() => {
|
||||||
)}
|
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
|
||||||
|
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<PenSquare className="size-4" />
|
||||||
|
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
|
||||||
|
</button>
|
||||||
|
{!disabled && workspaceDraftIssue && (
|
||||||
|
<>
|
||||||
|
{!isSidebarCollapsed && (
|
||||||
|
<button type="button" className="grid place-items-center">
|
||||||
|
<ChevronUp
|
||||||
|
className={cn("size-4 transform !text-custom-sidebar-text-300 transition-transform duration-300", {
|
||||||
|
"rotate-180": isDraftButtonOpen,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isDraftButtonOpen && (
|
||||||
|
<div
|
||||||
|
className={`fixed left-4 mt-0 h-10 w-[203px] pt-2 ${isSidebarCollapsed ? "top-[5.5rem]" : "top-24"}`}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||||
|
className="flex w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-3 py-[10px] text-sm text-custom-text-300 shadow"
|
||||||
|
>
|
||||||
|
<PenSquare size={16} className="mr-2 !text-lg !leading-4 text-custom-sidebar-text-300" />
|
||||||
|
Last Drafted Issue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 size-8 aspect-square grid place-items-center rounded hover:bg-custom-sidebar-background-90 outline-none",
|
"flex-shrink-0 size-8 aspect-square grid place-items-center rounded hover:bg-custom-sidebar-background-90 outline-none",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
|
|
@ -16,11 +16,10 @@ import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAppTheme, useEventTracker, useUser } from "@/hooks/store";
|
import { useAppTheme, useEventTracker, useUser } from "@/hooks/store";
|
||||||
|
import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
export const SidebarWorkspaceMenu = observer(() => {
|
export const SidebarWorkspaceMenu = observer(() => {
|
||||||
// states
|
|
||||||
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(true);
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
@ -32,7 +31,11 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
// pathname
|
// pathname
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// computed
|
// local storage
|
||||||
|
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage<boolean>("is_workspace_menu_open", true);
|
||||||
|
// derived values
|
||||||
|
const isWorkspaceMenuOpen = !!storedValue;
|
||||||
|
// auth
|
||||||
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
||||||
|
|
||||||
const handleLinkClick = (itemKey: string) => {
|
const handleLinkClick = (itemKey: string) => {
|
||||||
|
|
@ -45,8 +48,8 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sidebarCollapsed) setIsWorkspaceMenuOpen(true);
|
if (sidebarCollapsed) toggleWorkspaceMenu(true);
|
||||||
}, [sidebarCollapsed]);
|
}, [sidebarCollapsed, toggleWorkspaceMenu]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure as="div" defaultOpen>
|
<Disclosure as="div" defaultOpen>
|
||||||
|
|
@ -54,20 +57,16 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="button"
|
as="button"
|
||||||
className="group/workspace-button w-full px-2 py-0.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-sm font-semibold"
|
className="group/workspace-button w-full px-2 py-0.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-sm font-semibold"
|
||||||
onClick={() => setIsWorkspaceMenuOpen((prev) => !prev)}
|
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
<span>Workspace</span>
|
||||||
<>
|
<span className="flex-shrink-0 hidden group-hover/workspace-button:inline-block rounded p-0.5 hover:bg-custom-sidebar-background-80">
|
||||||
<span>Workspace</span>
|
<ChevronRight
|
||||||
<span className="flex-shrink-0 hidden group-hover/workspace-button:inline-block rounded p-0.5 hover:bg-custom-sidebar-background-80">
|
className={cn("size-3.5 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
||||||
<ChevronRight
|
"rotate-90": isWorkspaceMenuOpen,
|
||||||
className={cn("size-3.5 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
})}
|
||||||
"rotate-90": open,
|
/>
|
||||||
})}
|
</span>
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
)}
|
)}
|
||||||
<Transition
|
<Transition
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue