UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: модальное окно уведомлений workspace

This commit is contained in:
DCCONSTRUCTIONS 2026-04-28 20:50:58 +03:00
parent be7929deec
commit 3b0c75bee6
6 changed files with 180 additions and 10 deletions

View File

@ -36,6 +36,7 @@ import {
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 = {
@ -264,19 +265,20 @@ export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar()
<TopNavPowerK variant="sidebar" />
<UserMenuRoot variant="toolbar" />
<Tooltip tooltipContent={t("notification.label")} position="bottom">
<Link
href={`/${workspaceSlug?.toString()}/notifications/`}
<button
type="button"
className="nodedc-toolbar-icon-button relative flex h-8 w-8 items-center justify-center"
data-active={pathname.includes("/notifications/")}
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" />
)}
</Link>
<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")}

View File

@ -11,6 +11,7 @@ import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
import { GlobalModals } from "@/plane-web/components/common/modal/global";
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper";
import { WorkspaceSettingsModal } from "@/components/workspace/settings/workspace-settings-modal";
import { WorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal";
import type { Route } from "./+types/layout";
export default function WorkspaceLayout(props: Route.ComponentProps) {
@ -23,6 +24,7 @@ export default function WorkspaceLayout(props: Route.ComponentProps) {
<WorkspaceContentWrapper workspaceSlug={workspaceSlug}>
<GlobalModals workspaceSlug={workspaceSlug} />
<WorkspaceSettingsModal />
<WorkspaceNotificationsModal />
<Outlet />
</WorkspaceContentWrapper>
</AppRailVisibilityProvider>

View File

@ -0,0 +1,94 @@
"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 { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { X } from "lucide-react";
// plane imports
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
// local imports
import { NotificationsRoot } from "./root";
import { NotificationsSidebarRoot } from "./sidebar";
import {
closeWorkspaceNotificationsModal,
getWorkspaceNotificationsModalOpenFromSearch,
WORKSPACE_NOTIFICATIONS_MODAL_EVENT,
} from "./notifications-modal.utils";
const getInitialOpenState = () => {
if (typeof window === "undefined") return false;
return getWorkspaceNotificationsModalOpenFromSearch(window.location.search);
};
export const WorkspaceNotificationsModal = observer(function WorkspaceNotificationsModal() {
const [isOpen, setIsOpen] = useState(getInitialOpenState);
const { currentWorkspace } = useWorkspace();
useEffect(() => {
const syncFromLocation = () => {
setIsOpen(getWorkspaceNotificationsModalOpenFromSearch(window.location.search));
};
const handleModalEvent = (event: Event) => {
const detail = (event as CustomEvent<{ isOpen: boolean }>).detail;
setIsOpen(detail.isOpen);
};
window.addEventListener(WORKSPACE_NOTIFICATIONS_MODAL_EVENT, handleModalEvent);
window.addEventListener("popstate", syncFromLocation);
syncFromLocation();
return () => {
window.removeEventListener(WORKSPACE_NOTIFICATIONS_MODAL_EVENT, handleModalEvent);
window.removeEventListener("popstate", syncFromLocation);
};
}, []);
const handleClose = () => closeWorkspaceNotificationsModal();
return (
<ModalCore
isOpen={isOpen}
handleClose={handleClose}
position={EModalPosition.CENTER}
width={EModalWidth.VIIXL}
className="h-[88vh] max-h-[920px] overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] shadow-[0_28px_80px_rgba(0,0,0,0.42)]"
>
<div className="flex h-full min-h-0 flex-col">
<div className="flex shrink-0 items-center justify-between gap-4 px-6 py-5">
<div className="min-w-0">
<div className="text-18 font-semibold text-primary">Уведомления</div>
<div className="mt-1 truncate text-12 text-tertiary">{currentWorkspace?.name ?? "Workspace"}</div>
</div>
<button
type="button"
onClick={handleClose}
className="grid size-10 flex-shrink-0 place-items-center rounded-full bg-white/6 text-secondary transition hover:bg-white/10 hover:text-primary"
aria-label="Закрыть уведомления"
>
<X className="size-5" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-hidden border-t border-white/6">
<div className="flex h-full min-h-0 overflow-hidden">
<NotificationsSidebarRoot />
<div className="min-w-0 flex-1 overflow-hidden">
<NotificationsRoot workspaceSlug={currentWorkspace?.slug} />
</div>
</div>
</div>
</div>
</ModalCore>
);
});

View File

@ -0,0 +1,50 @@
export const WORKSPACE_NOTIFICATIONS_MODAL_QUERY_KEY = "workspaceNotifications";
export const WORKSPACE_NOTIFICATIONS_MODAL_EVENT = "nodedc:workspace-notifications-modal";
type TWorkspaceNotificationsModalEventDetail = {
isOpen: boolean;
};
const dispatchWorkspaceNotificationsModalEvent = (detail: TWorkspaceNotificationsModalEventDetail) => {
window.dispatchEvent(
new CustomEvent<TWorkspaceNotificationsModalEventDetail>(WORKSPACE_NOTIFICATIONS_MODAL_EVENT, { detail })
);
};
export const getWorkspaceNotificationsModalOpenFromSearch = (search: string): boolean => {
const value = new URLSearchParams(search).get(WORKSPACE_NOTIFICATIONS_MODAL_QUERY_KEY);
return value === "open";
};
export const setWorkspaceNotificationsModalSearch = (replace = false) => {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.set(WORKSPACE_NOTIFICATIONS_MODAL_QUERY_KEY, "open");
window.history[replace ? "replaceState" : "pushState"](window.history.state, "", url);
};
export const clearWorkspaceNotificationsModalSearch = () => {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.delete(WORKSPACE_NOTIFICATIONS_MODAL_QUERY_KEY);
window.history.replaceState(window.history.state, "", url);
};
export const openWorkspaceNotificationsModal = (replace = false) => {
if (typeof window === "undefined") return;
setWorkspaceNotificationsModalSearch(replace);
dispatchWorkspaceNotificationsModalEvent({ isOpen: true });
};
export const closeWorkspaceNotificationsModal = () => {
if (typeof window === "undefined") return;
clearWorkspaceNotificationsModalSearch();
dispatchWorkspaceNotificationsModalEvent({ isOpen: false });
};

View File

@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
import Link from "next/link";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
@ -17,6 +16,7 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
import { openWorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal.utils";
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
export const SidebarUtilityRail = observer(function SidebarUtilityRail() {
@ -63,16 +63,18 @@ export const SidebarUtilityRail = observer(function SidebarUtilityRail() {
)}
<TopNavPowerK variant="sidebar" />
<Tooltip tooltipContent="Уведомления" position="right">
<Link
href={`/${workspaceSlug?.toString()}/notifications/`}
<button
type="button"
className="relative flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] text-tertiary backdrop-blur-[18px] transition-all hover:bg-white/[0.07] hover:text-primary"
onClick={() => openWorkspaceNotificationsModal()}
aria-label="Уведомления"
>
<InboxIcon className="size-4" />
{totalNotifications > 0 && (
<span className="absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
)}
<span className="sr-only">Уведомления</span>
</Link>
</button>
</Tooltip>
<Tooltip tooltipContent="Профиль" position="right">
<div>

View File

@ -14,6 +14,7 @@ import type { EUserWorkspaceRoles } from "@plane/types";
// components
import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
import { NotificationAppSidebarOption } from "@/components/workspace-notifications/notification-app-sidebar-option";
import { openWorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal.utils";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useUserPermissions } from "@/hooks/store/user";
@ -54,6 +55,25 @@ export const SidebarUserMenuItem = observer(function SidebarUserMenuItem(props:
}
};
const handleNotificationsClick = () => {
handleLinkClick();
openWorkspaceNotificationsModal();
};
if (item.key === "notifications") {
return (
<button type="button" className="w-full text-left" onClick={handleNotificationsClick}>
<SidebarNavItem isActive={false}>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon className="size-4 flex-shrink-0" />
<p className="text-13 leading-5 font-medium">{t(item.labelTranslationKey)}</p>
</div>
<NotificationAppSidebarOption workspaceSlug={workspaceSlug.toString()} />
</SidebarNavItem>
</button>
);
}
return (
<Link href={item.href} onClick={handleLinkClick}>
<SidebarNavItem isActive={isActive}>