UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: модальное окно уведомлений workspace
This commit is contained in:
parent
be7929deec
commit
3b0c75bee6
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue