UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: упрощение верхней шапки God Mode
This commit is contained in:
@@ -4,82 +4,179 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ChevronRight, Menu, Settings } from "lucide-react";
|
||||
// components
|
||||
import { BreadcrumbLink } from "../breadcrumb-link";
|
||||
import { BrainCog, ChevronDown, ExternalLink, Image, LogOut, Mail, Palette, Settings, UserCog2 } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { LockIcon, WorkspaceIcon } from "@plane/propel/icons";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
// assets
|
||||
import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import { useUser } from "@/hooks/store";
|
||||
// local imports
|
||||
import { CORE_HEADER_SEGMENT_LABELS } from "./core";
|
||||
import { EXTENDED_HEADER_SEGMENT_LABELS } from "./extended";
|
||||
|
||||
export const HamburgerToggle = observer(function HamburgerToggle() {
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
return (
|
||||
<button
|
||||
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
|
||||
aria-label="Открыть меню"
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
|
||||
</button>
|
||||
);
|
||||
});
|
||||
const authService = new AuthService();
|
||||
|
||||
const HEADER_SEGMENT_LABELS = {
|
||||
...CORE_HEADER_SEGMENT_LABELS,
|
||||
...EXTENDED_HEADER_SEGMENT_LABELS,
|
||||
};
|
||||
const PRIMARY_NAVIGATION = [
|
||||
{ label: "Основное", href: "/general/", Icon: Settings },
|
||||
{ label: "Почта", href: "/email/", Icon: Mail },
|
||||
{ label: "Аутентификация", href: "/authentication/", Icon: LockIcon },
|
||||
{ label: "Воркспейсы", href: "/workspace/", Icon: WorkspaceIcon },
|
||||
];
|
||||
|
||||
const FEATURE_NAVIGATION = [
|
||||
{ label: "ИИ", href: "/ai/", Icon: BrainCog, description: "OpenAI модель и API-ключ" },
|
||||
{ label: "Изображения", href: "/image/", Icon: Image, description: "Внешние библиотеки изображений" },
|
||||
];
|
||||
|
||||
export const AdminHeader = observer(function AdminHeader() {
|
||||
const pathName = usePathname();
|
||||
const { currentUser, signOut } = useUser();
|
||||
const { resolvedTheme, setTheme } = useNextTheme();
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
|
||||
// Function to dynamically generate breadcrumb items based on pathname
|
||||
const generateBreadcrumbItems = (pathname: string) => {
|
||||
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
|
||||
pathSegments.pop();
|
||||
const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href));
|
||||
const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ";
|
||||
const avatarName = currentUser?.display_name || currentUser?.email || "DC";
|
||||
|
||||
let currentUrl = "";
|
||||
const breadcrumbItems = pathSegments.map((segment) => {
|
||||
currentUrl += "/" + segment;
|
||||
return {
|
||||
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
|
||||
href: currentUrl,
|
||||
};
|
||||
});
|
||||
return breadcrumbItems;
|
||||
const handleThemeSwitch = () => {
|
||||
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
return (
|
||||
<div className="nodedc-admin-header nodedc-glass-modal relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<HamburgerToggle />
|
||||
{breadcrumbItems.length >= 0 && (
|
||||
<nav className="min-w-0" aria-label="Навигация God Mode">
|
||||
<ol className="nodedc-admin-breadcrumbs">
|
||||
<BreadcrumbLink href="/general/" label="Настройки" icon={<Settings className="h-4 w-4" />} />
|
||||
{breadcrumbItems.map((item, index) => {
|
||||
if (!item.title) return null;
|
||||
const isCurrent = index === breadcrumbItems.length - 1;
|
||||
<header className="nodedc-admin-header relative z-30 flex w-full flex-shrink-0 flex-col gap-4">
|
||||
<div className="nodedc-admin-header-top grid w-full items-center gap-4">
|
||||
<a href="/" className="nodedc-admin-logo-link inline-flex w-fit items-center" aria-label="NODE.DC">
|
||||
<img src={NodeDcLogo} alt="NODE.DC" className="nodedc-admin-logo" />
|
||||
</a>
|
||||
|
||||
return (
|
||||
<Fragment key={`${item.href}-${item.title}`}>
|
||||
<li className="nodedc-admin-breadcrumb-separator" aria-hidden="true">
|
||||
<ChevronRight className="size-4" />
|
||||
</li>
|
||||
<BreadcrumbLink href={item.href} label={item.title} isCurrent={isCurrent} />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
<nav className="nodedc-admin-top-nav justify-self-center" aria-label="Основная навигация God Mode">
|
||||
{PRIMARY_NAVIGATION.map((item) => {
|
||||
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||
return (
|
||||
<Link key={item.href} href={item.href} className="nodedc-admin-top-nav-item" data-active={isActive}>
|
||||
<item.Icon className="size-3.5 stroke-[1.7]" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="nodedc-admin-top-nav-item" data-active={isFeatureRoute}>
|
||||
<BrainCog className="size-3.5 stroke-[1.7]" />
|
||||
<span>Возможности</span>
|
||||
<ChevronDown className="size-3 stroke-[2]" />
|
||||
</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="nodedc-glass-popup-surface absolute top-full left-1/2 z-[120] mt-3 flex w-64 -translate-x-1/2 flex-col gap-1 p-2 outline-none">
|
||||
{FEATURE_NAVIGATION.map((item) => {
|
||||
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||
return (
|
||||
<Menu.Item key={item.href}>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn("nodedc-admin-feature-menu-item", {
|
||||
"is-active": isActive,
|
||||
"is-hovered": active,
|
||||
})}
|
||||
>
|
||||
<span className="grid size-9 place-items-center rounded-full bg-white/6">
|
||||
<item.Icon className="size-4 stroke-[1.7]" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-13 font-medium text-primary">{item.label}</span>
|
||||
<span className="block truncate text-11 text-tertiary">{item.description}</span>
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
<a href="/" className="nodedc-admin-top-nav-item">
|
||||
<ExternalLink className="size-3.5 stroke-[1.7]" />
|
||||
<span>В приложение</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative justify-self-end">
|
||||
<Menu.Button className="nodedc-admin-user-button">
|
||||
<span className="min-w-0 text-right">
|
||||
<span className="block max-w-40 truncate text-14 font-medium text-primary">{adminName}</span>
|
||||
</span>
|
||||
<span className="grid size-10 place-items-center rounded-full bg-white/7">
|
||||
{currentUser ? (
|
||||
<Avatar
|
||||
name={avatarName}
|
||||
src={getFileURL(currentUser.avatar_url)}
|
||||
size={32}
|
||||
shape="circle"
|
||||
className="!text-body-sm-medium"
|
||||
/>
|
||||
) : (
|
||||
<UserCog2 className="size-5 text-[rgb(var(--nodedc-card-active-rgb))]" />
|
||||
)}
|
||||
</span>
|
||||
</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="nodedc-glass-popup-surface absolute top-full right-0 z-[120] mt-3 flex w-60 flex-col divide-y divide-white/6 p-2 text-12 outline-none">
|
||||
<div className="flex flex-col gap-1 px-2 pb-2">
|
||||
<span className="truncate text-13 font-medium text-primary">{adminName}</span>
|
||||
<span className="truncate text-11 text-tertiary">{currentUser?.email}</span>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Menu.Item as="button" type="button" className="nodedc-admin-menu-action" onClick={handleThemeSwitch}>
|
||||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||
{resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={signOut}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<Menu.Item as="button" type="submit" className="nodedc-admin-menu-action">
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Выйти
|
||||
</Menu.Item>
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user