From 038fbc72279f34e825bc6d596c32965c35405e46 Mon Sep 17 00:00:00 2001 From: vamsi krishna Date: Mon, 2 Dec 2024 12:28:38 +0530 Subject: [PATCH] added stacked notifications to store --- .../use-workspace-notifications.ts | 2 +- .../workspace-notifications.store.ts | 6 +- .../workspace-notifications/index.ts | 3 +- .../notification-card/index.ts | 2 + .../notification-card/item.tsx | 25 +++++ .../notification-card/root.tsx | 57 ++++++++++ .../workspace-notifications/root.tsx | 106 ++++++++++++++++++ web/ee/store/root.store.ts | 3 + .../notifications.store.ts | 95 ++++++++++++++++ 9 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 web/ee/components/workspace-notifications/notification-card/index.ts create mode 100644 web/ee/components/workspace-notifications/notification-card/item.tsx create mode 100644 web/ee/components/workspace-notifications/notification-card/root.tsx create mode 100644 web/ee/components/workspace-notifications/root.tsx create mode 100644 web/ee/store/workspace-notifications/notifications.store.ts diff --git a/web/core/hooks/store/notifications/use-workspace-notifications.ts b/web/core/hooks/store/notifications/use-workspace-notifications.ts index f882d67161..42d8f0f88d 100644 --- a/web/core/hooks/store/notifications/use-workspace-notifications.ts +++ b/web/core/hooks/store/notifications/use-workspace-notifications.ts @@ -2,7 +2,7 @@ import { useContext } from "react"; // context import { StoreContext } from "@/lib/store-context"; // mobx store -import { IWorkspaceNotificationStore } from "@/store/notifications/workspace-notifications.store"; +import { IWorkspaceNotificationStore } from "@/plane-web/store/workspace-notifications/notifications.store"; export const useWorkspaceNotifications = (): IWorkspaceNotificationStore => { const context = useContext(StoreContext); diff --git a/web/core/store/notifications/workspace-notifications.store.ts b/web/core/store/notifications/workspace-notifications.store.ts index a0de7bc0f6..5f41f0b3e0 100644 --- a/web/core/store/notifications/workspace-notifications.store.ts +++ b/web/core/store/notifications/workspace-notifications.store.ts @@ -27,8 +27,8 @@ import workspaceNotificationService from "@/services/workspace-notification.serv import { Notification, INotification } from "@/store/notifications/notification"; import { CoreRootStore } from "@/store/root.store"; -type TNotificationLoader = ENotificationLoader | undefined; -type TNotificationQueryParamType = ENotificationQueryParamType; +export type TNotificationLoader = ENotificationLoader | undefined; +export type TNotificationQueryParamType = ENotificationQueryParamType; export interface IWorkspaceNotificationStore { // observables @@ -104,7 +104,7 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore { updateBulkFilters: action, // actions getUnreadNotificationsCount: action, - getNotifications: action, + // getNotifications: action, markAllNotificationsAsRead: action, }); } diff --git a/web/ee/components/workspace-notifications/index.ts b/web/ee/components/workspace-notifications/index.ts index fb22da3e81..ff8925d4e1 100644 --- a/web/ee/components/workspace-notifications/index.ts +++ b/web/ee/components/workspace-notifications/index.ts @@ -1 +1,2 @@ -export * from "ce/components/workspace-notifications"; +export * from "./root"; +export * from "./notification-card"; diff --git a/web/ee/components/workspace-notifications/notification-card/index.ts b/web/ee/components/workspace-notifications/notification-card/index.ts new file mode 100644 index 0000000000..9936e7c4b4 --- /dev/null +++ b/web/ee/components/workspace-notifications/notification-card/index.ts @@ -0,0 +1,2 @@ +export * from './root'; +export * from './item' \ No newline at end of file diff --git a/web/ee/components/workspace-notifications/notification-card/item.tsx b/web/ee/components/workspace-notifications/notification-card/item.tsx new file mode 100644 index 0000000000..2d0ed680b9 --- /dev/null +++ b/web/ee/components/workspace-notifications/notification-card/item.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react' +import { observer } from "mobx-react"; +import { Row } from '@plane/ui'; +//store +import { useIssueDetail } from "@/hooks/store" +//components +import { IssueIdentifier } from '@/plane-web/components/issues'; +export interface INotificationItem { + issueId: string; + notificationsCount: number; +} +export const NotificationItem: FC = observer((props)=>{ + const { issueId, notificationsCount } = props; + const { issue: { getIssueById }} = useIssueDetail(); + const issueDetail = getIssueById(issueId); + + if(!issueDetail) return null + return ( +
+ + + +
+ ) +}) \ No newline at end of file diff --git a/web/ee/components/workspace-notifications/notification-card/root.tsx b/web/ee/components/workspace-notifications/notification-card/root.tsx new file mode 100644 index 0000000000..ab8196d877 --- /dev/null +++ b/web/ee/components/workspace-notifications/notification-card/root.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +// components +// constants +import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; +// hooks +import { useWorkspaceNotifications } from "@/hooks/store"; +import { NotificationItem } from "@/plane-web/components/workspace-notifications"; + +type TNotificationCardListRoot = { + workspaceSlug: string; + workspaceId: string; +}; + +export const NotificationCardListRoot: FC = observer((props) => { + const { workspaceSlug, workspaceId } = props; + // hooks + const { loader, paginationInfo, getNotifications, notificationIssueIdsByWorkspaceId, groupedNotifications } = useWorkspaceNotifications(); + const notificationIssueIds = notificationIssueIdsByWorkspaceId(workspaceId); + + const getNextNotifications = async () => { + try { + await getNotifications(workspaceSlug, ENotificationLoader.PAGINATION_LOADER, ENotificationQueryParamType.NEXT); + } catch (error) { + console.error(error); + } + }; + + if (!workspaceSlug || !workspaceId || !notificationIssueIds) return <>; + return ( +
+ {notificationIssueIds.map((issueId: string) => ( + // + + ))} + + {/* fetch next page notifications */} + {paginationInfo && paginationInfo?.next_page_results && ( + <> + {loader === ENotificationLoader.PAGINATION_LOADER ? ( +
+
Loading...
+
+ ) : ( +
+
+ Load more +
+
+ )} + + )} +
+ ); +}); diff --git a/web/ee/components/workspace-notifications/root.tsx b/web/ee/components/workspace-notifications/root.tsx new file mode 100644 index 0000000000..a1b3a27a2e --- /dev/null +++ b/web/ee/components/workspace-notifications/root.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// components +import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui"; +import { CountChip } from "@/components/common"; +import { + NotificationsLoader, + NotificationEmptyState, + NotificationSidebarHeader, + AppliedFilters, +} from "@/components/workspace-notifications"; +// constants +import { NOTIFICATION_TABS } from "@/constants/notification"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getNumberCount } from "@/helpers/string.helper"; +// hooks +import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +// plane web components +import { NotificationCardListRoot } from '@/plane-web/components/workspace-notifications' + +export const NotificationsSidebar: FC = observer(() => { + const { workspaceSlug } = useParams(); + // hooks + const { getWorkspaceBySlug } = useWorkspace(); + const { + currentSelectedNotificationId, + unreadNotificationsCount, + loader, + notificationIssueIdsByWorkspaceId, + currentNotificationTab, + setCurrentNotificationTab, + } = useWorkspaceNotifications(); + // derived values + const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; + const groupedNotificationIssueIds = workspace ? notificationIssueIdsByWorkspaceId(workspace.id) : undefined; + + if (!workspaceSlug || !workspace) return <>; + + return ( +
+
+ + + + +
+ {NOTIFICATION_TABS.map((tab) => ( +
currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)} + > +
+
{tab.label}
+ {tab.count(unreadNotificationsCount) > 0 && ( + + )} +
+ {currentNotificationTab === tab.value && ( +
+ )} +
+ ))} +
+ + {/* applied filters */} + + + {/* rendering notifications */} + {loader === "init-loader" ? ( +
+ +
+ ) : ( + <> + {groupedNotificationIssueIds && groupedNotificationIssueIds.length > 0 ? ( + + + + ) : ( +
+ +
+ )} + + )} +
+
+ ); +}); diff --git a/web/ee/store/root.store.ts b/web/ee/store/root.store.ts index d6f5cdaddf..1a9f5e078d 100644 --- a/web/ee/store/root.store.ts +++ b/web/ee/store/root.store.ts @@ -18,6 +18,7 @@ import { WorkspaceSubscriptionStore, } from "@/plane-web/store/subscription/subscription.store"; import { IWorkspaceFeatureStore, WorkspaceFeatureStore } from "@/plane-web/store/workspace-feature.store"; +import { IWorkspaceNotificationStore, WorkspaceNotificationStore } from "@/plane-web/store/workspace-notifications/notifications.store"; import { IProjectFilterStore, ProjectFilterStore, @@ -66,6 +67,7 @@ export class RootStore extends CoreRootStore { cycle: ICycleStore; piChat: IPiChatStore; timelineStore: ITimelineStore; + workspaceNotification: IWorkspaceNotificationStore; // importers jiraImporter: IJiraStore; jiraServerImporter: IJiraServerStore; @@ -91,6 +93,7 @@ export class RootStore extends CoreRootStore { this.cycle = new CycleStore(this); this.piChat = new PiChatStore(this); this.timelineStore = new TimeLineStore(this); + this.workspaceNotification = new WorkspaceNotificationStore(this); // importers this.jiraImporter = new JiraStore(this); this.jiraServerImporter = new JiraServerStore(this); diff --git a/web/ee/store/workspace-notifications/notifications.store.ts b/web/ee/store/workspace-notifications/notifications.store.ts new file mode 100644 index 0000000000..8c2b2fc5e7 --- /dev/null +++ b/web/ee/store/workspace-notifications/notifications.store.ts @@ -0,0 +1,95 @@ +import { orderBy, groupBy } from "lodash"; +import { action, makeObservable, observable, runInAction, set } from "mobx"; +//store +import { computedFn } from "mobx-utils"; +import { TNotification, TNotificationPaginatedInfo } from "@plane/types"; +import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; +import { convertToEpoch } from "@/helpers/date-time.helper"; +import {RootStore} from '@/plane-web/store/root.store' +import workspaceNotificationService from "@/services/workspace-notification.service"; +import { IWorkspaceNotificationStore as IWorkspaceNotificationStoreCore , TNotificationLoader, TNotificationQueryParamType, WorkspaceNotificationStore as WorkspaceNotificationStoreCore } from "@/store/notifications/workspace-notifications.store"; + +export type TGroupedNotifications = Record + +export interface IWorkspaceNotificationStore extends IWorkspaceNotificationStoreCore { + // observales + groupedNotifications: Record; + // actions + groupNotificationsById: (notifications: TNotification[]) => void; + notificationIssueIdsByWorkspaceId: (workspaceId: string) => string[] | undefined; +} + +export class WorkspaceNotificationStore extends WorkspaceNotificationStoreCore implements IWorkspaceNotificationStore { + // observables + groupedNotifications: Record = {}; + + constructor(protected store: RootStore) { + super(store); + makeObservable(this, { + groupedNotifications: observable, + groupNotificationsById: action, + }); + } + + // actions + /** + * @description Execute when notifications are fetched + * @param { INotification[] } notifications + */ + groupNotificationsById = action((notifications: TNotification[]) => { + this.groupedNotifications = groupBy(notifications, (n) => n.data?.issue?.id); + }); + + notificationIssueIdsByWorkspaceId = computedFn((workspaceId: string)=>{ + if(!workspaceId || Object.keys(this.groupedNotifications).length === 0) return undefined; + + const groupedNotificationIssueIds = orderBy( + Object.keys(this.groupedNotifications), + (issueId) => { + const notifications = this.groupedNotifications[issueId]; + const latestNotification = orderBy(notifications,(n)=>convertToEpoch(n.created_at),'desc')[0]; + return convertToEpoch(latestNotification.created_at) + }, + 'desc' + ) + + return groupedNotificationIssueIds; + }) + + + /** + * @description get all workspace notification + * @param { string } workspaceSlug, + * @param { TNotificationLoader } loader, + * @returns { TNotification | undefined } + */ + getNotifications = async ( + workspaceSlug: string, + loader: TNotificationLoader = ENotificationLoader.INIT_LOADER, + queryParamType: TNotificationQueryParamType = ENotificationQueryParamType.INIT + ): Promise => { + this.loader = loader; + try { + const queryParams = this.generateNotificationQueryParams(queryParamType); + await this.getUnreadNotificationsCount(workspaceSlug); + const notificationResponse = await workspaceNotificationService.fetchNotifications(workspaceSlug, queryParams); + if (notificationResponse) { + const { results, ...paginationInfo } = notificationResponse; + runInAction(() => { + if (results) { + this.groupNotificationsById(results) + } + set(this, "paginationInfo", paginationInfo); + }); + } + return notificationResponse; + } catch (error) { + console.error("WorkspaceNotificationStore -> getNotifications -> error", error); + throw error; + } finally { + runInAction(() => (this.loader = undefined)); + } + }; +} + +