Merge branch 'preview' of github.com:makeplane/plane into develop

This commit is contained in:
sriram veeraghanta 2024-02-09 16:22:41 +05:30
commit 99975d0ba0
4 changed files with 360 additions and 235 deletions

View File

@ -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}

View File

@ -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

View File

@ -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>
</>
);

View File

@ -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"