UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: расширенный home layout и аналитические панели
This commit is contained in:
@@ -15,12 +15,15 @@ import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
// hooks
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
export const WorkspaceDashboardHeader = observer(function WorkspaceDashboardHeader() {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { toggleWidgetSettings } = useHome();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -36,17 +39,19 @@ export const WorkspaceDashboardHeader = observer(function WorkspaceDashboardHead
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={() => toggleWidgetSettings(true)}
|
||||
className="nodedc-toolbar-pill my-auto mb-0"
|
||||
prependIcon={<Shapes />}
|
||||
>
|
||||
<div className="hidden sm:hidden md:block">{t("home.manage_widgets")}</div>
|
||||
</Button>
|
||||
</Header.RightItem>
|
||||
{isCompactToolbar && (
|
||||
<Header.RightItem>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={() => toggleWidgetSettings(true)}
|
||||
className="nodedc-toolbar-pill my-auto mb-0"
|
||||
prependIcon={<Shapes />}
|
||||
>
|
||||
<div className="hidden sm:hidden md:block">{t("home.manage_widgets")}</div>
|
||||
</Button>
|
||||
</Header.RightItem>
|
||||
)}
|
||||
</Header>
|
||||
</>
|
||||
);
|
||||
|
||||
+30
-193
@@ -7,8 +7,6 @@
|
||||
*/
|
||||
|
||||
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";
|
||||
@@ -19,157 +17,31 @@ import {
|
||||
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 { cn, copyUrlToClipboard, joinUrlPath } from "@plane/utils";
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { joinUrlPath } from "@plane/utils";
|
||||
import { openWorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal.utils";
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
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 { useUser, useUserPermissions, useUserProfile } 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 { openWorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal.utils";
|
||||
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 { toggleCreateProjectModal } = useCommandPalette();
|
||||
|
||||
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-[rgb(var(--nodedc-on-accent-rgb))]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<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 flex scrollbar-sm 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}
|
||||
renderInToolbarMenu
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 border-t border-white/8 px-1 pt-2">
|
||||
<Menu.Item>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-13 font-medium text-secondary transition-colors hover:bg-layer-transparent-hover hover:text-primary"
|
||||
onClick={() => toggleCreateProjectModal(true)}
|
||||
>
|
||||
<span className="grid size-8 flex-shrink-0 place-items-center">
|
||||
<PlusIcon className="size-4" />
|
||||
</span>
|
||||
<span>{t("create_project")}</span>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
import {
|
||||
DEFAULT_PROJECT_SHELL_TOOLBAR_LAYOUT,
|
||||
PROJECT_SHELL_TOOLBAR_LAYOUTS,
|
||||
type TProjectShellToolbarLayout,
|
||||
type TToolbarItem,
|
||||
} from "./top-toolbar";
|
||||
|
||||
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 { data: userProfile } = useUserProfile();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||
const { preferences: personalPreferences } = usePersonalNavigationPreferences();
|
||||
@@ -186,7 +58,7 @@ export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar()
|
||||
);
|
||||
|
||||
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
|
||||
const totalNotifications = isMentionsEnabled
|
||||
const notificationsCount = isMentionsEnabled
|
||||
? unreadNotificationsCount.mention_unread_notifications_count
|
||||
: unreadNotificationsCount.total_unread_notifications_count;
|
||||
|
||||
@@ -249,62 +121,27 @@ export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar()
|
||||
}).sort((a, b) => a.sort_order - b.sort_order),
|
||||
[pathname, workspacePreferences, workspaceSlug]
|
||||
);
|
||||
|
||||
const workspaceSlugValue = workspaceSlug?.toString();
|
||||
const isWorkspaceHome = pathname === `/${workspaceSlugValue}` || pathname === `/${workspaceSlugValue}/`;
|
||||
const toolbarLayout: TProjectShellToolbarLayout =
|
||||
userProfile?.theme?.nodedcCompactToolbar === true ? "compact" : DEFAULT_PROJECT_SHELL_TOOLBAR_LAYOUT;
|
||||
const ToolbarLayout = PROJECT_SHELL_TOOLBAR_LAYOUTS[toolbarLayout];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("z-20 w-full flex-shrink-0 px-4 pt-4 pb-3", {
|
||||
"nodedc-home-top-toolbar": isWorkspaceHome,
|
||||
})}
|
||||
>
|
||||
<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">
|
||||
<WorkspaceMenuRoot variant="toolbar" />
|
||||
<TopNavPowerK variant="sidebar" />
|
||||
<UserMenuRoot variant="toolbar" />
|
||||
<Tooltip tooltipContent={t("notification.label")} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-toolbar-icon-button relative flex h-8 w-8 items-center justify-center"
|
||||
data-active={false}
|
||||
aria-label={t("notification.label")}
|
||||
onClick={() => openWorkspaceNotificationsModal()}
|
||||
>
|
||||
<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" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ToolbarIconButton
|
||||
label={t("app_header.add_task")}
|
||||
onClick={() => toggleCreateIssueModal(true)}
|
||||
disabled={!canCreateIssue || joinedProjectIds.length === 0}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</ToolbarIconButton>
|
||||
</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>
|
||||
<ToolbarLayout
|
||||
canCreateIssue={canCreateIssue}
|
||||
draftsItem={primaryItems.find((item) => item.key === "drafts")}
|
||||
homeItem={primaryItems.find((item) => item.key === "home")}
|
||||
isWorkspaceHome={isWorkspaceHome}
|
||||
joinedProjectIdsCount={joinedProjectIds.length}
|
||||
notificationsCount={notificationsCount}
|
||||
primaryItems={primaryItems}
|
||||
profileItem={primaryItems.find((item) => item.key === "your_work")}
|
||||
secondaryItems={secondaryItems}
|
||||
stickiesItem={primaryItems.find((item) => item.key === "stickies")}
|
||||
onCreateIssue={() => toggleCreateIssueModal(true)}
|
||||
onOpenNotifications={() => openWorkspaceNotificationsModal()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
+3
-2
@@ -21,6 +21,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
@@ -68,7 +69,7 @@ export const PageDetailsHeader = observer(function PageDetailsHeader() {
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ExpandedToolbarBreadcrumbs isLoading={loader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
@@ -94,7 +95,7 @@ export const PageDetailsHeader = observer(function PageDetailsHeader() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
|
||||
+4
-3
@@ -14,13 +14,14 @@ import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TPage } from "@plane/types";
|
||||
// plane ui
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { Header } from "@plane/ui";
|
||||
// components
|
||||
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
||||
@@ -62,7 +63,7 @@ export const PagesListHeader = observer(function PagesListHeader() {
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ExpandedToolbarBreadcrumbs isLoading={loader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
@@ -70,7 +71,7 @@ export const PagesListHeader = observer(function PagesListHeader() {
|
||||
featureKey={EProjectFeatureKey.PAGES}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
</Header.LeftItem>
|
||||
{canCurrentUserCreatePage && (
|
||||
<Header.RightItem>
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"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 { useTranslation } from "@plane/i18n";
|
||||
import { PlusIcon } from "@plane/propel/icons";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
|
||||
import { ToolbarIconButton, ToolbarIconLink, ToolbarNotificationsButton } from "./toolbar-controls";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const CompactProjectShellToolbarLayout = ({
|
||||
canCreateIssue,
|
||||
isWorkspaceHome,
|
||||
joinedProjectIdsCount,
|
||||
notificationsCount,
|
||||
primaryItems,
|
||||
secondaryItems,
|
||||
onCreateIssue,
|
||||
onOpenNotifications,
|
||||
}: TProjectShellToolbarLayoutProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("z-20 w-full flex-shrink-0 px-4 pt-4 pb-3", {
|
||||
"nodedc-home-top-toolbar": isWorkspaceHome,
|
||||
})}
|
||||
>
|
||||
<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">
|
||||
<WorkspaceMenuRoot variant="toolbar" />
|
||||
<TopNavPowerK variant="sidebar" />
|
||||
<UserMenuRoot variant="toolbar" />
|
||||
<ToolbarNotificationsButton
|
||||
label={t("notification.label")}
|
||||
notificationsCount={notificationsCount}
|
||||
onClick={onOpenNotifications}
|
||||
/>
|
||||
<ToolbarIconButton
|
||||
label={t("app_header.add_task")}
|
||||
onClick={onCreateIssue}
|
||||
disabled={!canCreateIssue || joinedProjectIdsCount === 0}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</ToolbarIconButton>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
"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 { useTranslation } from "@plane/i18n";
|
||||
import { Shapes } from "lucide-react";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
|
||||
import { ExpandedToolbarLink, ExpandedToolbarToolButton, ToolbarNotificationsButton } from "./toolbar-controls";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const ExpandedProjectShellToolbarLayout = ({
|
||||
draftsItem,
|
||||
homeItem,
|
||||
isWorkspaceHome,
|
||||
notificationsCount,
|
||||
profileItem,
|
||||
stickiesItem,
|
||||
onOpenNotifications,
|
||||
}: TProjectShellToolbarLayoutProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toggleWidgetSettings } = useHome();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("z-20 w-full flex-shrink-0 px-5 pt-4 pb-3", {
|
||||
"nodedc-home-top-toolbar": isWorkspaceHome,
|
||||
})}
|
||||
>
|
||||
<div className="nodedc-expanded-toolbar">
|
||||
<div className="nodedc-expanded-toolbar-top">
|
||||
<div className="nodedc-expanded-toolbar-left">
|
||||
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-center">
|
||||
<WorkspaceMenuRoot variant="expanded-toolbar" />
|
||||
<div className="nodedc-expanded-nav-group">
|
||||
<ExpandedToolbarLink item={homeItem} label="Главная" />
|
||||
<ProjectsToolbarMenu variant="expanded" />
|
||||
<ExpandedToolbarLink item={stickiesItem} label="Стикеры" />
|
||||
<ExpandedToolbarLink item={draftsItem} label="Черновики" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-right">
|
||||
<div className="nodedc-expanded-user-group">
|
||||
<ExpandedToolbarLink item={profileItem} label="Профиль" />
|
||||
<ToolbarNotificationsButton
|
||||
label={t("notification.label")}
|
||||
notificationsCount={notificationsCount}
|
||||
onClick={onOpenNotifications}
|
||||
variant="expanded"
|
||||
/>
|
||||
<UserMenuRoot variant="expanded-toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-tools-row">
|
||||
<div className="nodedc-expanded-breadcrumbs-slot" data-nodedc-expanded-breadcrumbs-slot />
|
||||
{!isWorkspaceHome && (
|
||||
<div className="nodedc-expanded-main-tool-cluster">
|
||||
<TopNavPowerK variant="expanded-toolbar" />
|
||||
<div className="nodedc-expanded-header-filters-slot" data-nodedc-expanded-header-filters-slot />
|
||||
</div>
|
||||
)}
|
||||
<div className="nodedc-expanded-action-tool-cluster">
|
||||
<div className="nodedc-expanded-tool-slot" data-nodedc-voice-task-toolbar-slot />
|
||||
{isWorkspaceHome && (
|
||||
<ExpandedToolbarToolButton label={t("home.manage_widgets")} onClick={() => toggleWidgetSettings(true)}>
|
||||
<Shapes className="size-4" />
|
||||
</ExpandedToolbarToolButton>
|
||||
)}
|
||||
<div className="nodedc-expanded-primary-action-slot" data-nodedc-expanded-primary-action-slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./layout-registry";
|
||||
export * from "./types";
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from "react";
|
||||
// components
|
||||
import { CompactProjectShellToolbarLayout } from "./compact-layout";
|
||||
import { ExpandedProjectShellToolbarLayout } from "./expanded-layout";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayout, TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const DEFAULT_PROJECT_SHELL_TOOLBAR_LAYOUT: TProjectShellToolbarLayout = "expanded";
|
||||
|
||||
export const PROJECT_SHELL_TOOLBAR_LAYOUTS: Record<
|
||||
TProjectShellToolbarLayout,
|
||||
ComponentType<TProjectShellToolbarLayoutProps>
|
||||
> = {
|
||||
compact: CompactProjectShellToolbarLayout,
|
||||
expanded: ExpandedProjectShellToolbarLayout,
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
"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 { Menu } from "@headlessui/react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PlusIcon, ProjectIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { cn, copyUrlToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// components
|
||||
import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item";
|
||||
|
||||
export const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu({
|
||||
variant = "compact",
|
||||
}: {
|
||||
variant?: "compact" | "expanded";
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
const { workspaceSlug } = useParams();
|
||||
const { joinedProjectIds } = useProject();
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
|
||||
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={cn(
|
||||
variant === "expanded"
|
||||
? "nodedc-expanded-nav-button"
|
||||
: "nodedc-toolbar-icon-button grid h-8 w-8 place-items-center"
|
||||
)}
|
||||
data-active={pathname.includes("/projects/")}
|
||||
aria-label={t("workspace_sidebar.projects.main")}
|
||||
>
|
||||
{variant === "expanded" ? null : (
|
||||
<span className="nodedc-toolbar-icon-active-dot">
|
||||
<ProjectIcon className="size-4" />
|
||||
</span>
|
||||
)}
|
||||
{variant === "expanded" && <span>Проекты</span>}
|
||||
</Menu.Button>
|
||||
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
"absolute top-full z-[170] mt-2",
|
||||
variant === "expanded" ? "left-0 origin-top-left" : "-right-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 flex scrollbar-sm 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}
|
||||
renderInToolbarMenu
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 border-t border-white/8 px-1 pt-2">
|
||||
<Menu.Item>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-13 font-medium text-secondary transition-colors hover:bg-layer-transparent-hover hover:text-primary"
|
||||
onClick={() => toggleCreateProjectModal(true)}
|
||||
>
|
||||
<span className="grid size-8 flex-shrink-0 place-items-center">
|
||||
<PlusIcon className="size-4" />
|
||||
</span>
|
||||
<span>{t("create_project")}</span>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
"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 Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { InboxIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import type { TToolbarItem } from "./types";
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToolbarIconButton = ({
|
||||
label,
|
||||
active = false,
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
active?: boolean;
|
||||
children: 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>
|
||||
);
|
||||
|
||||
export const ToolbarNotificationsButton = ({
|
||||
label,
|
||||
notificationsCount,
|
||||
onClick,
|
||||
variant = "compact",
|
||||
}: {
|
||||
label: string;
|
||||
notificationsCount: number;
|
||||
onClick: () => void;
|
||||
variant?: "compact" | "expanded";
|
||||
}) => (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"nodedc-toolbar-icon-button relative flex items-center justify-center",
|
||||
variant === "expanded" ? "nodedc-expanded-notification-button" : "h-8 w-8"
|
||||
)}
|
||||
data-active={false}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="nodedc-toolbar-icon-active-dot">
|
||||
<InboxIcon className={variant === "expanded" ? "size-5" : "size-4"} />
|
||||
</span>
|
||||
{notificationsCount > 0 && (
|
||||
<span className="nodedc-toolbar-notification-dot absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const ExpandedToolbarLink = ({ item, label }: { item?: TToolbarItem; label: string }) => {
|
||||
if (!item?.href) return null;
|
||||
|
||||
return (
|
||||
<Link href={item.href} className="nodedc-expanded-nav-button" data-active={item.active}>
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandedToolbarToolButton = ({
|
||||
label,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<button type="button" className="nodedc-expanded-tool-button" aria-label={label} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type TProjectShellToolbarLayout = "compact" | "expanded";
|
||||
|
||||
export type TToolbarItem = {
|
||||
key: string;
|
||||
href?: string;
|
||||
labelTranslationKey: string;
|
||||
active: boolean;
|
||||
icon: ReactNode;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export type TProjectShellToolbarLayoutProps = {
|
||||
canCreateIssue: boolean;
|
||||
draftsItem?: TToolbarItem;
|
||||
homeItem?: TToolbarItem;
|
||||
isWorkspaceHome: boolean;
|
||||
joinedProjectIdsCount: number;
|
||||
notificationsCount: number;
|
||||
primaryItems: TToolbarItem[];
|
||||
profileItem?: TToolbarItem;
|
||||
secondaryItems: TToolbarItem[];
|
||||
stickiesItem?: TToolbarItem;
|
||||
onCreateIssue: () => void;
|
||||
onOpenNotifications: () => void;
|
||||
};
|
||||
Reference in New Issue
Block a user