UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: локальная привязка окна профиля в верхнем toolbar
This commit is contained in:
@@ -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() {
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-lg border border-subtle">
|
||||
<div id="full-screen-portal" className="absolute inset-0 w-full" />
|
||||
<div className="relative flex size-full overflow-hidden">
|
||||
<ProjectAppSidebar />
|
||||
<ExtendedProjectSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
||||
<Outlet />
|
||||
<main className="relative flex h-full w-full min-w-0 flex-col overflow-hidden bg-surface-1">
|
||||
<ProjectShellTopToolbar />
|
||||
<div className="relative flex min-h-0 w-full flex-1 overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+284
@@ -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 (
|
||||
<Tooltip tooltipContent={t(item.labelTranslationKey)} position="bottom">
|
||||
<Link
|
||||
href={item.href ?? "#"}
|
||||
className="nodedc-toolbar-icon-button flex h-8 w-8 items-center justify-center"
|
||||
data-active={item.active}
|
||||
aria-label={t(item.labelTranslationKey)}
|
||||
>
|
||||
<span className="nodedc-toolbar-icon-active-dot">{item.icon}</span>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const ToolbarIconButton = ({
|
||||
label,
|
||||
active = false,
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
active?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}) => (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-toolbar-icon-button flex h-8 w-8 items-center justify-center"
|
||||
data-active={active}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="nodedc-toolbar-icon-active-dot">{children}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
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 (
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button
|
||||
type="button"
|
||||
title={t("workspace_sidebar.projects.main")}
|
||||
className="nodedc-toolbar-icon-button grid h-8 w-8 place-items-center"
|
||||
aria-label={t("workspace_sidebar.projects.main")}
|
||||
>
|
||||
<span
|
||||
className={`nodedc-toolbar-icon-active-dot ${
|
||||
pathname.includes("/projects/") ? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]" : ""
|
||||
}`}
|
||||
>
|
||||
<ProjectIcon className="size-4" />
|
||||
</span>
|
||||
</Menu.Button>
|
||||
|
||||
<Menu.Items className="absolute top-full -right-2 z-[170] mt-2 origin-top-right">
|
||||
<div className="nodedc-glass-modal nodedc-glass-popup-surface flex max-h-[70vh] min-w-[26rem] flex-col overflow-hidden rounded-[1.5rem] border-0 p-2 shadow-none outline-none">
|
||||
<div className="vertical-scrollbar scrollbar-sm flex max-h-[70vh] flex-col gap-0.5 overflow-y-auto pr-1">
|
||||
{joinedProjectIds.map((projectId, index) => (
|
||||
<SidebarProjectsListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
projectListType="JOINED"
|
||||
disableDrag
|
||||
disableDrop
|
||||
isLastChild={index === joinedProjectIds.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<div className="z-20 w-full flex-shrink-0 px-4 pt-4 pb-3">
|
||||
<div className="nodedc-glass-modal flex w-full flex-wrap items-center justify-between gap-4 rounded-[1.6rem] px-4 py-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="nodedc-toolbar-group relative flex items-center gap-1 overflow-visible">
|
||||
<ToolbarIconButton
|
||||
label={t("app_header.add_task")}
|
||||
onClick={() => toggleCreateIssueModal(true)}
|
||||
disabled={!canCreateIssue || joinedProjectIds.length === 0}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</ToolbarIconButton>
|
||||
<TopNavPowerK variant="sidebar" />
|
||||
<Tooltip tooltipContent={t("notification.label")} position="bottom">
|
||||
<Link
|
||||
href={`/${workspaceSlug?.toString()}/notifications/`}
|
||||
className="nodedc-toolbar-icon-button relative flex h-8 w-8 items-center justify-center"
|
||||
data-active={pathname.includes("/notifications/")}
|
||||
aria-label={t("notification.label")}
|
||||
>
|
||||
<span className="nodedc-toolbar-icon-active-dot">
|
||||
<InboxIcon className="size-4" />
|
||||
</span>
|
||||
{totalNotifications > 0 && (
|
||||
<span className="absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
|
||||
)}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<UserMenuRoot variant="toolbar" />
|
||||
<WorkspaceMenuRoot variant="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 items-center justify-end gap-3">
|
||||
<div className="nodedc-toolbar-group flex items-center gap-1">
|
||||
{primaryItems.map((item) => (
|
||||
<ToolbarIconLink key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="nodedc-toolbar-group relative flex items-center gap-1 overflow-visible">
|
||||
<ProjectsToolbarMenu />
|
||||
{secondaryItems.map((item) => (
|
||||
<ToolbarIconLink key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user