diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx index 5fa81a8e1..c6003c259 100644 --- a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx @@ -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 ( <>
- {is_inbox_issue === true && workspace_slug && project_id && issue_id ? ( + {!currentSelectedNotificationId ? ( +
+ +
+ ) : ( <> - {projectMemberInfoLoader ? ( -
- -
+ {is_inbox_issue === true && workspace_slug && project_id && issue_id ? ( + <> + {projectMemberInfoLoader ? ( +
+ +
+ ) : ( + {}} + isMobileSidebar={false} + workspaceSlug={workspace_slug} + projectId={project_id} + inboxIssueId={issue_id} + isNotificationEmbed + embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)} + /> + )} + ) : ( - {}} - isMobileSidebar={false} - workspaceSlug={workspace_slug} - projectId={project_id} - inboxIssueId={issue_id} - isNotificationEmbed + setCurrentSelectedNotificationId(undefined)} /> )} - ) : ( - setCurrentSelectedNotificationId(undefined)} - /> )}
diff --git a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx index 3d33bdd72..cfffe1265 100644 --- a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx +++ b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx @@ -38,7 +38,7 @@ export const NotificationAppSidebarOption: FC = o return (
- +
); }); diff --git a/web/core/components/workspace-notifications/sidebar/filters/index.ts b/web/core/components/workspace-notifications/sidebar/filters/index.ts index 4f7dbf49f..b4d269c8a 100644 --- a/web/core/components/workspace-notifications/sidebar/filters/index.ts +++ b/web/core/components/workspace-notifications/sidebar/filters/index.ts @@ -1,2 +1,2 @@ -export * from "./root"; +export * from "./menu"; export * from "./applied-filter"; diff --git a/web/core/components/workspace-notifications/sidebar/filters/menu/index.ts b/web/core/components/workspace-notifications/sidebar/filters/menu/index.ts new file mode 100644 index 000000000..5d76f4263 --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/filters/menu/index.ts @@ -0,0 +1,2 @@ +export * from "./root"; +export * from "./menu-option-item"; diff --git a/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx b/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx new file mode 100644 index 000000000..cf894e187 --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx @@ -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 ( +
handleFilterTypeChange(value, !isSelected)} + > +
+ {isSelected && } +
+
+ {label} +
+
+ ); +}); diff --git a/web/core/components/workspace-notifications/sidebar/filters/menu/root.tsx b/web/core/components/workspace-notifications/sidebar/filters/menu/root.tsx new file mode 100644 index 000000000..09f8427d3 --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/filters/menu/root.tsx @@ -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 ( + +
+ +
+ + } + keyExtractor={(item: { label: string; value: ENotificationFilterType }) => item.value} + render={(item) => } + /> + ); +}); diff --git a/web/core/components/workspace-notifications/sidebar/filters/root.tsx b/web/core/components/workspace-notifications/sidebar/filters/root.tsx deleted file mode 100644 index 86994c819..000000000 --- a/web/core/components/workspace-notifications/sidebar/filters/root.tsx +++ /dev/null @@ -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 ( - - - (open ? "bg-custom-background-80" : "") - )} - > - - - - - - -
- {FILTER_TYPE_OPTIONS.map((filter) => { - const isSelected = filters?.type?.[filter?.value] || false; - return ( -
handleFilterTypeChange(filter?.value, !isSelected)} - > -
- {isSelected && } -
-
- {filter.label} -
-
- ); - })} -
-
-
-
- ); -}); diff --git a/web/core/components/workspace-notifications/sidebar/header/options/menu-option.tsx b/web/core/components/workspace-notifications/sidebar/header/options/menu-option.tsx deleted file mode 100644 index 509dd9de3..000000000 --- a/web/core/components/workspace-notifications/sidebar/header/options/menu-option.tsx +++ /dev/null @@ -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 = 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) => 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 ( - - - (open ? "bg-custom-background-80" : "") - )} - > - - - - - - -
-
-
{ - handleMarkAllNotificationsAsRead(); - captureEvent(NOTIFICATIONS_READ); - }} - > - -
Mark all as read
- {loader === ENotificationLoader.MARK_ALL_AS_READY && ( -
- -
- )} -
-
- -
- -
-
handleFilterChange("read", !filters?.read)} - > - -
- Show unread -
- {filters?.read && ( -
- -
- )} -
- -
- handleBulkFilterChange({ - archived: !filters?.archived, - snoozed: false, - }) - } - > - -
- Show Archived -
- {filters?.archived && ( -
- -
- )} -
- -
- handleBulkFilterChange({ - snoozed: !filters?.snoozed, - archived: false, - }) - } - > - -
- Show Snoozed -
- {filters?.snoozed && ( -
- -
- )} -
-
-
- - - - ); -}); diff --git a/web/core/components/workspace-notifications/sidebar/header/options/menu-option/index.ts b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/index.ts new file mode 100644 index 000000000..808d90c76 --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/index.ts @@ -0,0 +1,2 @@ +export * from "./root"; +export * from "./menu-item"; diff --git a/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx new file mode 100644 index 000000000..918966310 --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx @@ -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 = observer((props) => { + const { type, label = "", isActive, prependIcon, appendIcon, onClick } = props; + + if (type === "menu-item") + return ( +
onClick && onClick()} + > + {prependIcon && prependIcon} +
+ {label} +
+ {appendIcon &&
{appendIcon}
} +
+ ); + + return
; +}); diff --git a/web/core/components/workspace-notifications/sidebar/header/options/menu-option/root.tsx b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/root.tsx new file mode 100644 index 000000000..ea0eb130f --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/root.tsx @@ -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 = 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) => 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: , + appendIcon: loader === ENotificationLoader.MARK_ALL_AS_READY ? : undefined, + onClick: () => { + captureEvent(NOTIFICATIONS_READ); + handleMarkAllNotificationsAsRead(); + }, + }, + { + key: "menu-divider", + type: "divider", + }, + { + key: "menu-unread", + type: "menu-item", + label: "Show unread", + isActive: filters?.read, + prependIcon: , + appendIcon: filters?.read ? : undefined, + onClick: () => handleFilterChange("read", !filters?.read), + }, + { + key: "menu-archived", + type: "menu-item", + label: "Show archived", + isActive: filters?.archived, + prependIcon: , + appendIcon: filters?.archived ? : undefined, + onClick: () => + handleBulkFilterChange({ + archived: !filters?.archived, + snoozed: false, + }), + }, + { + key: "menu-snoozed", + type: "menu-item", + label: "Show snoozed", + isActive: filters?.snoozed, + prependIcon: , + appendIcon: filters?.snoozed ? : undefined, + onClick: () => + handleBulkFilterChange({ + snoozed: !filters?.snoozed, + archived: false, + }), + }, + ]; + + return ( + item.key} + panelClassName="p-0 py-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1" + render={(item: TPopoverMenuOptions) => } + /> + ); +}); diff --git a/web/core/components/workspace-notifications/sidebar/root.tsx b/web/core/components/workspace-notifications/sidebar/root.tsx index 679df0b31..26ad1d76a 100644 --- a/web/core/components/workspace-notifications/sidebar/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/root.tsx @@ -66,12 +66,9 @@ export const NotificationsSidebar: FC = observer(() => { )} >
{tab.label}
- + {tab.count(unreadNotificationsCount) > 0 && ( + + )}
{currentNotificationTab === tab.value && (
diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts index cad133dd0..56eb1d472 100644 --- a/web/core/constants/empty-state.ts +++ b/web/core/constants/empty-state.ts @@ -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",