diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx index 090eefb..a4fe414 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx @@ -7,9 +7,7 @@ import { observer } from "mobx-react"; import { Outlet } from "react-router"; import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; -// plane web components -import { ProjectAppSidebar } from "./_sidebar"; -import { ExtendedProjectSidebar } from "./extended-project-sidebar"; +import { ProjectShellTopToolbar } from "./project-shell-top-toolbar"; function WorkspaceLayout() { return ( @@ -18,10 +16,11 @@ function WorkspaceLayout() {
- - -
- +
+ +
+ +
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx new file mode 100644 index 0000000..c48cc14 --- /dev/null +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx @@ -0,0 +1,284 @@ +"use client"; + +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useMemo } from "react"; +import Link from "next/link"; +import { Menu } from "@headlessui/react"; +import { observer } from "mobx-react"; +import { useParams, usePathname } from "next/navigation"; +import useSWR from "swr"; +import { + EUserPermissions, + EUserPermissionsLevel, + WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS, + WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS, + WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS, +} from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { InboxIcon, PlusIcon, ProjectIcon } from "@plane/propel/icons"; +import { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import { Tooltip } from "@plane/propel/tooltip"; +import { copyUrlToClipboard, joinUrlPath } from "@plane/utils"; +import { TopNavPowerK } from "@/components/navigation"; +import { useCommandPalette } from "@/hooks/store/use-command-palette"; +import { useProject } from "@/hooks/store/use-project"; +import { useWorkspaceNotifications } from "@/hooks/store/notifications"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { + usePersonalNavigationPreferences, + useWorkspaceNavigationPreferences, +} from "@/hooks/use-navigation-preferences"; +import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item"; +import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root"; +import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root"; +import { getSidebarNavigationItemIcon } from "@/plane-web/components/workspace/sidebar/helper"; + +type TToolbarItem = { + key: string; + href?: string; + labelTranslationKey: string; + active: boolean; + icon: React.ReactNode; + onClick?: () => void; +}; + +const ToolbarIconLink = ({ item }: { item: TToolbarItem }) => { + const { t } = useTranslation(); + + return ( + + + {item.icon} + + + ); +}; + +const ToolbarIconButton = ({ + label, + active = false, + children, + onClick, + disabled = false, +}: { + label: string; + active?: boolean; + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; +}) => ( + + + +); + +const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu() { + const { t } = useTranslation(); + const pathname = usePathname(); + const { workspaceSlug } = useParams(); + const { joinedProjectIds } = useProject(); + + const handleCopyText = (projectId: string) => + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("link_copied"), + message: t("project_link_copied_to_clipboard"), + }); + }); + + return ( + + + + + + + + +
+
+ {joinedProjectIds.map((projectId, index) => ( + handleCopyText(projectId)} + projectListType="JOINED" + disableDrag + disableDrop + isLastChild={index === joinedProjectIds.length - 1} + /> + ))} +
+
+
+
+ ); +}); + +export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar() { + const { t } = useTranslation(); + const pathname = usePathname(); + const { workspaceSlug } = useParams(); + const { toggleCreateIssueModal } = useCommandPalette(); + const { joinedProjectIds } = useProject(); + const { data: currentUser } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications(); + const { preferences: personalPreferences } = usePersonalNavigationPreferences(); + const { preferences: workspacePreferences } = useWorkspaceNavigationPreferences(); + + const canCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + useSWR( + workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null, + workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug.toString()) : null + ); + + const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0; + const totalNotifications = isMentionsEnabled + ? unreadNotificationsCount.mention_unread_notifications_count + : unreadNotificationsCount.total_unread_notifications_count; + + const primaryItems = useMemo(() => { + const items = [...WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS]; + const personalItems: Array<(typeof items)[0] & { sort_order: number }> = []; + + const stickiesItem = WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["stickies"]; + if (personalPreferences.items.stickies?.enabled && stickiesItem) { + personalItems.push({ + ...stickiesItem, + sort_order: personalPreferences.items.stickies.sort_order, + }); + } + if (personalPreferences.items.your_work?.enabled && WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["your-work"]) { + personalItems.push({ + ...WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["your-work"], + sort_order: personalPreferences.items.your_work.sort_order, + }); + } + if (personalPreferences.items.drafts?.enabled && WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["drafts"]) { + personalItems.push({ + ...WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["drafts"], + sort_order: personalPreferences.items.drafts.sort_order, + }); + } + + personalItems.sort((a, b) => a.sort_order - b.sort_order); + + return [...items, ...personalItems].map((item) => { + const href = + item.key === "your_work" && currentUser?.id + ? joinUrlPath(workspaceSlug?.toString() ?? "", item.href, currentUser.id) + : joinUrlPath(workspaceSlug?.toString() ?? "", item.href); + + return { + key: item.key, + href, + labelTranslationKey: item.labelTranslationKey, + active: item.highlight(pathname, href), + icon: getSidebarNavigationItemIcon(item.key), + } satisfies TToolbarItem; + }); + }, [currentUser?.id, pathname, personalPreferences, workspaceSlug]); + + const secondaryItems = useMemo( + () => + WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.map((item) => { + const href = joinUrlPath(workspaceSlug?.toString() ?? "", item.href); + const preference = workspacePreferences.items[item.key]; + + return { + key: item.key, + href, + labelTranslationKey: item.labelTranslationKey, + active: item.highlight(pathname, href), + icon: getSidebarNavigationItemIcon(item.key), + sort_order: preference ? preference.sort_order : 0, + }; + }).sort((a, b) => a.sort_order - b.sort_order), + [pathname, workspacePreferences, workspaceSlug] + ); + + return ( +
+
+
+
+ toggleCreateIssueModal(true)} + disabled={!canCreateIssue || joinedProjectIds.length === 0} + > + + + + + + + + + {totalNotifications > 0 && ( + + )} + + + + +
+
+ +
+
+ {primaryItems.map((item) => ( + + ))} +
+ +
+ + {secondaryItems.map((item) => ( + + ))} +
+
+
+
+ ); +}); diff --git a/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx b/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx index b117763..da7d016 100644 --- a/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx +++ b/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx @@ -132,7 +132,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { const width = 320; const viewportPadding = 16; const left = Math.min(rect.left, window.innerWidth - width - viewportPadding); - const top = rect.top + rect.height / 2; + const top = rect.bottom + 10; setSidebarSearchPosition({ left, @@ -331,7 +331,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { ref={sidebarSearchButtonRef} type="button" className={cn( - "absolute left-0 top-0 z-[161] flex size-8 items-center justify-center rounded-full border border-white/8 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]" + "absolute left-0 top-0 z-[161] flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]" )} onClick={() => { if (isOpen) { @@ -374,7 +374,6 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { left: `${sidebarSearchPosition.left}px`, top: `${sidebarSearchPosition.top}px`, width: `${sidebarSearchPosition.width}px`, - transform: "translateY(-50%)", }} >
@@ -400,7 +399,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { )}
-
+
{t("power_k.search_menu.quick_access_title")} diff --git a/plane-src/apps/web/core/components/workspace/sidebar/user-menu-root.tsx b/plane-src/apps/web/core/components/workspace/sidebar/user-menu-root.tsx index 31d72fc..0c363d1 100644 --- a/plane-src/apps/web/core/components/workspace/sidebar/user-menu-root.tsx +++ b/plane-src/apps/web/core/components/workspace/sidebar/user-menu-root.tsx @@ -5,6 +5,7 @@ */ import { useState, useEffect } from "react"; +import { Menu } from "@headlessui/react"; import { observer } from "mobx-react"; import { useRouter } from "next/navigation"; import { LogOut, Settings, Settings2 } from "lucide-react"; @@ -23,7 +24,7 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useUser } from "@/hooks/store/user"; type TUserMenuRootProps = { - variant?: "default" | "sidebar-utility"; + variant?: "default" | "sidebar-utility" | "toolbar"; }; export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootProps) { @@ -42,6 +43,9 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP // translation const { t } = useTranslation(); + const isSidebarUtilityVariant = variant === "sidebar-utility"; + const isToolbarVariant = variant === "toolbar"; + const handleSignOut = () => { signOut().catch(() => setToast({ @@ -58,46 +62,8 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP else toggleAnySidebarDropdown(false); }, [isUserMenuOpen, toggleAnySidebarDropdown]); - return ( - - - - ) : ( - - ), - isActive: isUserMenuOpen, - }} - /> - ) - } - menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)} - onMenuClose={() => setIsUserMenuOpen(false)} - placement="bottom-end" - maxHeight="2xl" - optionsClassName="w-72 p-3 flex flex-col gap-y-3" - closeOnSelect - > + const menuContent = ( + <>
)} + + ); + + if (isToolbarVariant) { + return ( + + + + + + +
+ {menuContent} +
+
+
+ ); + } + + return ( + + + + ) : ( + + ), + isActive: isUserMenuOpen, + }} + /> + ) + } + menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)} + onMenuClose={() => setIsUserMenuOpen(false)} + placement="bottom-end" + maxHeight="2xl" + optionsClassName="w-72 p-3 flex flex-col gap-y-3" + closeOnSelect + > + {menuContent} ); }); diff --git a/plane-src/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx b/plane-src/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx index 15932da..6f42ff2 100644 --- a/plane-src/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx +++ b/plane-src/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx @@ -31,7 +31,7 @@ import { WorkspaceLogo } from "../logo"; import SidebarDropdownItem from "./dropdown-item"; type WorkspaceMenuRootProps = { - variant: "sidebar" | "top-navigation" | "sidebar-panel"; + variant: "sidebar" | "top-navigation" | "sidebar-panel" | "toolbar"; }; type WorkspaceMenuStateSyncProps = { @@ -46,7 +46,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) { const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props; const updateSidebarPanelMenuPosition = useCallback(() => { - if (variant !== "sidebar-panel" || !sidebarPanelButtonRef.current || typeof window === "undefined") return; + if (!["sidebar-panel", "toolbar"].includes(variant) || !sidebarPanelButtonRef.current || typeof window === "undefined") return; const rect = sidebarPanelButtonRef.current.getBoundingClientRect(); const width = 480; @@ -64,7 +64,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) { }, [onSidebarDropdownToggle, open]); useLayoutEffect(() => { - if (!open || variant !== "sidebar-panel") { + if (!open || !["sidebar-panel", "toolbar"].includes(variant)) { onSidebarPanelPositionChange(null); return; } @@ -133,6 +133,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work "w-full justify-center text-center": variant === "sidebar", "flex-grow justify-stretch text-left": variant === "top-navigation", "w-full max-w-none justify-stretch text-left": variant === "sidebar-panel", + "w-fit max-w-none justify-center text-center": variant === "toolbar", })} > {({ open, close }: { open: boolean; close: () => void }) => { @@ -220,6 +221,24 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work /> )} + {variant === "toolbar" && ( + + + + )} {(() => { const menuItems = ( @@ -324,7 +343,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work ); - if (variant === "sidebar-panel") { + if (["sidebar-panel", "toolbar"].includes(variant)) { if (!open || !sidebarPanelMenuPosition || typeof document === "undefined") return null; return createPortal(menuItems, document.body); }