forked from github/plane
Merge branch 'preview' of github.com:makeplane/plane into develop
This commit is contained in:
commit
99975d0ba0
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
|
||||
import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
@ -14,6 +14,7 @@ import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from
|
||||
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
||||
// type
|
||||
import type { IUserNotification } from "@plane/types";
|
||||
import { Menu } from "@headlessui/react";
|
||||
|
||||
type NotificationCardProps = {
|
||||
notification: IUserNotification;
|
||||
@ -40,8 +41,73 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// states
|
||||
const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// refs
|
||||
const snoozeRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const moreOptions = [
|
||||
{
|
||||
id: 1,
|
||||
name: notification.read_at ? "Mark as unread" : "Mark as read",
|
||||
icon: <MessageSquare className="h-3.5 w-3.5 text-custom-text-300" />,
|
||||
onClick: () => {
|
||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: notification.archived_at ? "Unarchive" : "Archive",
|
||||
icon: notification.archived_at ? (
|
||||
<ArchiveRestore className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
) : (
|
||||
<ArchiveIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
),
|
||||
onClick: () => {
|
||||
markNotificationArchivedStatus(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const snoozeOptionOnClick = (date: Date | null) => {
|
||||
if (!date) {
|
||||
setSelectedNotificationForSnooze(notification.id);
|
||||
return;
|
||||
}
|
||||
markSnoozeNotification(notification.id, date).then(() => {
|
||||
setToastAlert({
|
||||
title: `Notification snoozed till ${renderFormattedDate(date)}`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// close snooze options on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: any) => {
|
||||
if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) {
|
||||
setshowSnoozeOptions(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside, true);
|
||||
document.addEventListener("touchend", handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside, true);
|
||||
document.removeEventListener("touchend", handleClickOutside, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
|
||||
|
||||
@ -87,6 +153,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full space-y-2.5 overflow-hidden">
|
||||
<div className="flex items-start">
|
||||
{!notification.message ? (
|
||||
<div className="w-full break-words text-sm">
|
||||
<span className="font-semibold">
|
||||
@ -135,9 +202,87 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
<span className="semi-bold">{notification.message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex md:hidden items-start">
|
||||
<Menu as="div" className={" w-min text-left"}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button as={React.Fragment}>
|
||||
<button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex w-full items-center gap-x-2 rounded p-0.5 text-sm"
|
||||
>
|
||||
<MoreVertical className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
</button>
|
||||
</Menu.Button>
|
||||
{open && (
|
||||
<Menu.Items className={"absolute right-0 z-10"} static>
|
||||
<div
|
||||
className={
|
||||
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap"
|
||||
}
|
||||
>
|
||||
{moreOptions.map((item) => (
|
||||
<Menu.Item as="div">
|
||||
{({ close }) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
item.onClick();
|
||||
close();
|
||||
}}
|
||||
className="flex gap-x-2 items-center p-1.5"
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Menu.Item as="div">
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setshowSnoozeOptions(true);
|
||||
}}
|
||||
className="flex gap-x-2 items-center p-1.5"
|
||||
>
|
||||
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
Snooze
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
{showSnoozeOptions && (
|
||||
<div
|
||||
ref={snoozeRef}
|
||||
className="absolute right-36 z-20 my-1 top-24 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap"
|
||||
>
|
||||
{snoozeOptions.map((item) => (
|
||||
<p
|
||||
className="p-1.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setshowSnoozeOptions(false);
|
||||
snoozeOptionOnClick(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 text-xs">
|
||||
<p className="text-custom-text-300">
|
||||
<p className="text-custom-text-300 line-clamp-1">
|
||||
{truncateText(
|
||||
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
||||
50
|
||||
@ -152,43 +297,12 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex-shrink-0 text-custom-text-300">{calculateTimeAgo(notification.created_at)}</p>
|
||||
<p className="flex-shrink-0 text-custom-text-300 mt-auto">{calculateTimeAgo(notification.created_at)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-3 top-3 hidden gap-x-3 py-1 group-hover:flex">
|
||||
{[
|
||||
{
|
||||
id: 1,
|
||||
name: notification.read_at ? "Mark as unread" : "Mark as read",
|
||||
icon: <MessageSquare className="h-3.5 w-3.5 text-custom-text-300" />,
|
||||
onClick: () => {
|
||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: notification.archived_at ? "Unarchive" : "Archive",
|
||||
icon: notification.archived_at ? (
|
||||
<ArchiveRestore className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
) : (
|
||||
<ArchiveIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
),
|
||||
onClick: () => {
|
||||
markNotificationArchivedStatus(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
title: notification.archived_at ? "Notification un-archived" : "Notification archived",
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
].map((item) => (
|
||||
<div className="absolute right-3 top-3 hidden gap-x-3 py-1 md:group-hover:flex">
|
||||
{moreOptions.map((item) => (
|
||||
<Tooltip tooltipContent={item.name}>
|
||||
<button
|
||||
type="button"
|
||||
@ -221,18 +335,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!item.value) {
|
||||
setSelectedNotificationForSnooze(notification.id);
|
||||
return;
|
||||
}
|
||||
|
||||
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||
setToastAlert({
|
||||
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
snoozeOptionOnClick(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
|
||||
// components
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
// ui
|
||||
import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
@ -65,7 +67,11 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-5 pt-5">
|
||||
<h2 className="mb-2 text-xl font-semibold">Notifications</h2>
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<SidebarHamburgerToggle />
|
||||
<h2 className="md:text-xl md:font-semibold">Notifications</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-x-4 text-custom-text-200">
|
||||
<Tooltip tooltipContent="Refresh">
|
||||
<button
|
||||
@ -128,6 +134,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<div className="hidden md:block">
|
||||
<Tooltip tooltipContent="Close">
|
||||
<button type="button" onClick={() => closePopover()}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
@ -135,6 +142,7 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 w-full border-b border-custom-border-300 px-5">
|
||||
{snoozed || archived || readNotification ? (
|
||||
<button
|
||||
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useApplication } from "hooks/store";
|
||||
import useUserNotification from "hooks/use-user-notifications";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||
@ -15,8 +16,12 @@ import emptyNotification from "public/empty-state/notification.svg";
|
||||
import { getNumberCount } from "helpers/string.helper";
|
||||
|
||||
export const NotificationPopover = observer(() => {
|
||||
// states
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
// store hooks
|
||||
const { theme: themeStore } = useApplication();
|
||||
// refs
|
||||
const notificationPopoverRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
notifications,
|
||||
@ -44,8 +49,11 @@ export const NotificationPopover = observer(() => {
|
||||
setFetchNotifications,
|
||||
markAllNotificationsAsRead,
|
||||
} = useUserNotification();
|
||||
|
||||
const isSidebarCollapsed = themeStore.sidebarCollapsed;
|
||||
useOutsideClickDetector(notificationPopoverRef, () => {
|
||||
// if snooze modal is open, then don't close the popover
|
||||
if (selectedNotificationForSnooze === null) setIsActive(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -54,23 +62,22 @@ export const NotificationPopover = observer(() => {
|
||||
onClose={() => setSelectedNotificationForSnooze(null)}
|
||||
onSubmit={markSnoozeNotification}
|
||||
notification={notifications?.find((notification) => notification.id === selectedNotificationForSnooze) || null}
|
||||
onSuccess={() => {
|
||||
setSelectedNotificationForSnooze(null);
|
||||
}}
|
||||
onSuccess={() => setSelectedNotificationForSnooze(null)}
|
||||
/>
|
||||
<Popover className="relative w-full">
|
||||
{({ open: isActive, close: closePopover }) => {
|
||||
if (isActive) setFetchNotifications(true);
|
||||
|
||||
return (
|
||||
<Popover ref={notificationPopoverRef} className="md:relative w-full">
|
||||
<>
|
||||
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||
<Popover.Button
|
||||
<button
|
||||
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
isActive
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${isSidebarCollapsed ? "justify-center" : ""}`}
|
||||
onClick={() => {
|
||||
if (window.innerWidth < 768) themeStore.toggleSidebar();
|
||||
if (!isActive) setFetchNotifications(true);
|
||||
setIsActive(!isActive);
|
||||
}}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
{isSidebarCollapsed ? null : <span>Notifications</span>}
|
||||
@ -83,9 +90,10 @@ export const NotificationPopover = observer(() => {
|
||||
</span>
|
||||
)
|
||||
) : null}
|
||||
</Popover.Button>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Transition
|
||||
show={isActive}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
@ -94,11 +102,14 @@ export const NotificationPopover = observer(() => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute -top-36 left-0 z-10 ml-8 flex h-[50vh] w-[20rem] flex-col rounded-xl border border-custom-border-300 bg-custom-background-100 shadow-lg md:left-full md:w-[36rem]">
|
||||
<Popover.Panel
|
||||
className="absolute top-0 left-[280px] md:-top-36 md:ml-8 md:h-[50vh] z-10 flex h-full w-[100vw] flex-col rounded-xl md:border border-custom-border-300 bg-custom-background-100 shadow-lg md:left-full md:w-[36rem]"
|
||||
static
|
||||
>
|
||||
<NotificationHeader
|
||||
notificationCount={notificationCount}
|
||||
notificationMutate={notificationMutate}
|
||||
closePopover={closePopover}
|
||||
closePopover={() => setIsActive(false)}
|
||||
isRefreshing={isRefreshing}
|
||||
snoozed={snoozed}
|
||||
archived={archived}
|
||||
@ -119,7 +130,7 @@ export const NotificationPopover = observer(() => {
|
||||
<NotificationCard
|
||||
key={notification.id}
|
||||
isSnoozedTabOpen={snoozed}
|
||||
closePopover={closePopover}
|
||||
closePopover={() => setIsActive(false)}
|
||||
notification={notification}
|
||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||
markNotificationReadStatus={markNotificationAsRead}
|
||||
@ -187,8 +198,6 @@ export const NotificationPopover = observer(() => {
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
|
@ -109,7 +109,12 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// This is a workaround to fix the issue of the Notification popover modal close on closing this modal
|
||||
const closeTimeout = setTimeout(() => {
|
||||
onClose();
|
||||
clearTimeout(closeTimeout);
|
||||
}, 50);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reset({ ...defaultValues });
|
||||
clearTimeout(timeout);
|
||||
@ -142,7 +147,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all w-full sm:w-full sm:!max-w-2xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
@ -156,8 +161,8 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="mt-5 flex flex-col md:!flex-row md:items-center gap-3">
|
||||
<div className="flex-1 pb-3 md:pb-0">
|
||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">Pick a date</h6>
|
||||
<Controller
|
||||
name="date"
|
||||
|
Loading…
Reference in New Issue
Block a user