[WEB-1916] ui: updated the empty state design in workspace notifications and ui changes (#5093)

* ui: updated the empty state design in workspace notifications and ui changes

* chore: updated the popover custom components

* ui: updated the badge ui on the sidrbar options

* ui: broken down the menu components
This commit is contained in:
guru_sainath 2024-07-11 13:19:07 +05:30 committed by GitHub
parent a90724516b
commit 2136872351
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 263 additions and 266 deletions

View file

@ -6,9 +6,11 @@ import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { InboxContentRoot } from "@/components/inbox";
import { IssuePeekOverview } from "@/components/issues";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks
import { useIssueDetail, useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
@ -62,36 +64,44 @@ const WorkspaceDashboardPage = observer(() => {
setCurrentSelectedNotificationId(undefined);
setPeekIssue(undefined);
},
[]
[setCurrentSelectedNotificationId, setPeekIssue]
);
return (
<>
<PageHead title={pageTitle} />
<div className="w-full h-full overflow-hidden overflow-y-auto">
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
{!currentSelectedNotificationId ? (
<div className="w-full h-screen flex justify-center items-center">
<EmptyState type={EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE} layout="screen-simple" />
</div>
) : (
<>
{projectMemberInfoLoader ? (
<div className="w-full h-full flex justify-center items-center">
<LogoSpinner />
</div>
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
<>
{projectMemberInfoLoader ? (
<div className="w-full h-full flex justify-center items-center">
<LogoSpinner />
</div>
) : (
<InboxContentRoot
setIsMobileSidebar={() => {}}
isMobileSidebar={false}
workspaceSlug={workspace_slug}
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
) : (
<InboxContentRoot
setIsMobileSidebar={() => {}}
isMobileSidebar={false}
workspaceSlug={workspace_slug}
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
<IssuePeekOverview
embedIssue
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
) : (
<IssuePeekOverview
embedIssue
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</div>
</>

View file

@ -38,7 +38,7 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = o
return (
<div className="ml-auto">
<CountChip count={`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`} />
<CountChip count={`${isMentionsEnabled ? `@ ` : ``}${getNumberCount(totalNotifications)}`} />
</div>
);
});

View file

@ -1,2 +1,2 @@
export * from "./root";
export * from "./menu";
export * from "./applied-filter";

View file

@ -0,0 +1,2 @@
export * from "./root";
export * from "./menu-option-item";

View file

@ -0,0 +1,46 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { Check } from "lucide-react";
// constants
import { ENotificationFilterType } from "@/constants/notification";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useWorkspaceNotifications } from "@/hooks/store";
export const NotificationFilterOptionItem: FC<{ label: string; value: ENotificationFilterType }> = observer((props) => {
const { value, label } = props;
// hooks
const { filters, updateFilters } = useWorkspaceNotifications();
const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
updateFilters("type", {
...filters.type,
[filterType]: filterValue,
});
// derived values
const isSelected = filters?.type?.[value] || false;
return (
<div
key={value}
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() => handleFilterTypeChange(value, !isSelected)}
>
<div
className={cn(
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
)}
>
{isSelected && <Check className="h-2 w-2" />}
</div>
<div className={cn("whitespace-nowrap text-sm", isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
{label}
</div>
</div>
);
});

View file

@ -0,0 +1,32 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { ListFilter } from "lucide-react";
import { PopoverMenu, Tooltip } from "@plane/ui";
// components
import { NotificationFilterOptionItem } from "@/components/workspace-notifications";
// constants
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
export const NotificationFilter: FC = observer(() => {
// hooks
const { isMobile } = usePlatformOS();
return (
<PopoverMenu
data={FILTER_TYPE_OPTIONS}
button={
<Tooltip tooltipContent="Notification Filters" isMobile={isMobile} position="bottom">
<div className="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none">
<ListFilter className="h-3 w-3" />
</div>
</Tooltip>
}
keyExtractor={(item: { label: string; value: ENotificationFilterType }) => item.value}
render={(item) => <NotificationFilterOptionItem {...item} />}
/>
);
});

View file

@ -1,80 +0,0 @@
"use client";
import { FC, Fragment } from "react";
import { observer } from "mobx-react";
import { Check, ListFilter } from "lucide-react";
import { Popover, Transition } from "@headlessui/react";
import { Tooltip } from "@plane/ui";
// constants
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useWorkspaceNotifications } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
export const NotificationFilter: FC = observer(() => {
// hooks
const { isMobile } = usePlatformOS();
const { filters, updateFilters } = useWorkspaceNotifications();
const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
updateFilters("type", {
...filters.type,
[filterType]: filterValue,
});
return (
<Popover className="relative">
<Tooltip tooltipContent="Notification Filters" isMobile={isMobile} position="bottom">
<Popover.Button
className={cn(
"flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none",
({ open }: { open: boolean }) => (open ? "bg-custom-background-80" : "")
)}
>
<ListFilter className="h-3 w-3" />
</Popover.Button>
</Tooltip>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute mt-2 right-0 z-10 min-w-44">
<div className="p-2 rounded-md border border-custom-border-200 bg-custom-background-100">
{FILTER_TYPE_OPTIONS.map((filter) => {
const isSelected = filters?.type?.[filter?.value] || false;
return (
<div
key={filter.value}
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
>
<div
className={cn(
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
)}
>
{isSelected && <Check className="h-2 w-2" />}
</div>
<div
className={cn("whitespace-nowrap", isSelected ? "text-custom-text-100" : "text-custom-text-200")}
>
{filter.label}
</div>
</div>
);
})}
</div>
</Popover.Panel>
</Transition>
</Popover>
);
});

View file

@ -1,160 +0,0 @@
"use client";
import { FC, Fragment } from "react";
import { observer } from "mobx-react";
import { Check, CheckCheck, CheckCircle, Clock, MoreVertical } from "lucide-react";
import { Popover, Transition } from "@headlessui/react";
import { TNotificationFilter } from "@plane/types";
import { ArchiveIcon, Spinner, Tooltip } from "@plane/ui";
// constants
import { NOTIFICATIONS_READ } from "@/constants/event-tracker";
import { ENotificationLoader } from "@/constants/notification";
import { cn } from "@/helpers/common.helper";
// hooks
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TNotificationHeaderMenuOption = {
workspaceSlug: string;
};
export const NotificationHeaderMenuOption: FC<TNotificationHeaderMenuOption> = observer((props) => {
const { workspaceSlug } = props;
// hooks
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const { loader, filters, updateFilters, updateBulkFilters, markAllNotificationsAsRead } = useWorkspaceNotifications();
const handleFilterChange = (filterType: keyof TNotificationFilter, filterValue: boolean) =>
updateFilters(filterType, filterValue);
const handleBulkFilterChange = (filter: Partial<TNotificationFilter>) => updateBulkFilters(filter);
const handleMarkAllNotificationsAsRead = async () => {
// NOTE: We are using loader to prevent continues request when we are making all the notification to read
if (loader) return;
try {
await markAllNotificationsAsRead(workspaceSlug);
} catch (error) {
console.error(error);
}
};
return (
<Popover className="relative">
<Tooltip tooltipContent="Notification Filters" isMobile={isMobile} position="bottom">
<Popover.Button
className={cn(
"flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none",
({ open }: { open: boolean }) => (open ? "bg-custom-background-80" : "")
)}
>
<MoreVertical className="h-3 w-3" />
</Popover.Button>
</Tooltip>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute mt-2 right-0 z-10 min-w-44 select-none">
<div className="py-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1">
<div className="px-2">
<div
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() => {
handleMarkAllNotificationsAsRead();
captureEvent(NOTIFICATIONS_READ);
}}
>
<CheckCheck className="h-3 w-3" />
<div>Mark all as read</div>
{loader === ENotificationLoader.MARK_ALL_AS_READY && (
<div className="ml-auto">
<Spinner height="14px" width="14px" />
</div>
)}
</div>
</div>
<div className="border-b border-custom-border-200 " />
<div className="px-2">
<div
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() => handleFilterChange("read", !filters?.read)}
>
<CheckCircle className="flex-shrink-0 h-3 w-3" />
<div
className={cn("whitespace-nowrap", filters?.read ? "text-custom-text-100" : "text-custom-text-200")}
>
Show unread
</div>
{filters?.read && (
<div className="ml-auto">
<Check className="w-3 h-3" />
</div>
)}
</div>
<div
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() =>
handleBulkFilterChange({
archived: !filters?.archived,
snoozed: false,
})
}
>
<ArchiveIcon className="flex-shrink-0 h-3 w-3" />
<div
className={cn(
"whitespace-nowrap",
filters?.archived ? "text-custom-text-100" : "text-custom-text-200"
)}
>
Show Archived
</div>
{filters?.archived && (
<div className="ml-auto">
<Check className="w-3 h-3" />
</div>
)}
</div>
<div
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() =>
handleBulkFilterChange({
snoozed: !filters?.snoozed,
archived: false,
})
}
>
<Clock className="flex-shrink-0 h-3 w-3" />
<div
className={cn(
"whitespace-nowrap",
filters?.snoozed ? "text-custom-text-100" : "text-custom-text-200"
)}
>
Show Snoozed
</div>
{filters?.snoozed && (
<div className="ml-auto">
<Check className="w-3 h-3" />
</div>
)}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</Popover>
);
});

View file

@ -0,0 +1,2 @@
export * from "./root";
export * from "./menu-item";

View file

@ -0,0 +1,28 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
// components
import type { TPopoverMenuOptions } from "@/components/workspace-notifications";
// helpers
import { cn } from "@/helpers/common.helper";
export const NotificationMenuOptionItem: FC<TPopoverMenuOptions> = observer((props) => {
const { type, label = "", isActive, prependIcon, appendIcon, onClick } = props;
if (type === "menu-item")
return (
<div
className="flex items-center gap-2 cursor-pointer mx-2 px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() => onClick && onClick()}
>
{prependIcon && prependIcon}
<div className={cn("whitespace-nowrap text-sm", isActive ? "text-custom-text-100" : "text-custom-text-200")}>
{label}
</div>
{appendIcon && <div className="ml-auto">{appendIcon}</div>}
</div>
);
return <div className="border-b border-custom-border-200" />;
});

View file

@ -0,0 +1,114 @@
"use client";
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import { Check, CheckCheck, CheckCircle, Clock } from "lucide-react";
import { TNotificationFilter } from "@plane/types";
import { ArchiveIcon, PopoverMenu, Spinner } from "@plane/ui";
// components
import { NotificationMenuOptionItem } from "@/components/workspace-notifications";
// constants
import { NOTIFICATIONS_READ } from "@/constants/event-tracker";
import { ENotificationLoader } from "@/constants/notification";
// hooks
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
type TNotificationHeaderMenuOption = {
workspaceSlug: string;
};
export type TPopoverMenuOptions = {
key: string;
type: string;
label?: string | undefined;
isActive?: boolean | undefined;
prependIcon?: ReactNode | undefined;
appendIcon?: ReactNode | undefined;
onClick?: (() => void) | undefined;
};
export const NotificationHeaderMenuOption: FC<TNotificationHeaderMenuOption> = observer((props) => {
const { workspaceSlug } = props;
// hooks
const { captureEvent } = useEventTracker();
const { loader, filters, updateFilters, updateBulkFilters, markAllNotificationsAsRead } = useWorkspaceNotifications();
const handleFilterChange = (filterType: keyof TNotificationFilter, filterValue: boolean) =>
updateFilters(filterType, filterValue);
const handleBulkFilterChange = (filter: Partial<TNotificationFilter>) => updateBulkFilters(filter);
const handleMarkAllNotificationsAsRead = async () => {
// NOTE: We are using loader to prevent continues request when we are making all the notification to read
if (loader) return;
try {
await markAllNotificationsAsRead(workspaceSlug);
} catch (error) {
console.error(error);
}
};
const popoverMenuOptions: TPopoverMenuOptions[] = [
{
key: "menu-mark-all-read",
type: "menu-item",
label: "Mark all as read",
isActive: true,
prependIcon: <CheckCheck className="h-3 w-3" />,
appendIcon: loader === ENotificationLoader.MARK_ALL_AS_READY ? <Spinner height="14px" width="14px" /> : undefined,
onClick: () => {
captureEvent(NOTIFICATIONS_READ);
handleMarkAllNotificationsAsRead();
},
},
{
key: "menu-divider",
type: "divider",
},
{
key: "menu-unread",
type: "menu-item",
label: "Show unread",
isActive: filters?.read,
prependIcon: <CheckCircle className="flex-shrink-0 h-3 w-3" />,
appendIcon: filters?.read ? <Check className="w-3 h-3" /> : undefined,
onClick: () => handleFilterChange("read", !filters?.read),
},
{
key: "menu-archived",
type: "menu-item",
label: "Show archived",
isActive: filters?.archived,
prependIcon: <ArchiveIcon className="flex-shrink-0 h-3 w-3" />,
appendIcon: filters?.archived ? <Check className="w-3 h-3" /> : undefined,
onClick: () =>
handleBulkFilterChange({
archived: !filters?.archived,
snoozed: false,
}),
},
{
key: "menu-snoozed",
type: "menu-item",
label: "Show snoozed",
isActive: filters?.snoozed,
prependIcon: <Clock className="flex-shrink-0 h-3 w-3" />,
appendIcon: filters?.snoozed ? <Check className="w-3 h-3" /> : undefined,
onClick: () =>
handleBulkFilterChange({
snoozed: !filters?.snoozed,
archived: false,
}),
},
];
return (
<PopoverMenu
data={popoverMenuOptions}
buttonClassName="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none"
keyExtractor={(item: TPopoverMenuOptions) => item.key}
panelClassName="p-0 py-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1"
render={(item: TPopoverMenuOptions) => <NotificationMenuOptionItem {...item} />}
/>
);
});

View file

@ -66,12 +66,9 @@ export const NotificationsSidebar: FC = observer(() => {
)}
>
<div className="font-medium">{tab.label}</div>
<CountChip
count={getNumberCount(tab.count(unreadNotificationsCount))}
className={
currentNotificationTab === tab.value ? `bg-custom-primary-100/20` : `bg-custom-background-80/50`
}
/>
{tab.count(unreadNotificationsCount) > 0 && (
<CountChip count={getNumberCount(tab.count(unreadNotificationsCount))} />
)}
</div>
{currentNotificationTab === tab.value && (
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />

View file

@ -80,6 +80,7 @@ export enum EmptyStateType {
ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state",
ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state",
NOTIFICATION_DETAIL_EMPTY_STATE = "notification-detail-empty-state",
NOTIFICATION_ALL_EMPTY_STATE = "notification-all-empty-state",
NOTIFICATION_MENTIONS_EMPTY_STATE = "notification-mentions-empty-state",
NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state",
@ -595,6 +596,11 @@ const emptyStateDetails = {
path: "/empty-state/search/comments",
},
[EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE]: {
key: EmptyStateType.INBOX_DETAIL_EMPTY_STATE,
title: "Select to view details.",
path: "/empty-state/inbox/issue-detail",
},
[EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE]: {
key: EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE,
title: "No issues assigned",