added stacked notifications to store

This commit is contained in:
vamsi krishna
2024-12-02 12:28:38 +05:30
parent 208574bfa4
commit 038fbc7227
9 changed files with 294 additions and 5 deletions

View File

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

View File

@@ -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,
});
}

View File

@@ -1 +1,2 @@
export * from "ce/components/workspace-notifications";
export * from "./root";
export * from "./notification-card";

View File

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

View File

@@ -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<INotificationItem> = observer((props)=>{
const { issueId, notificationsCount } = props;
const { issue: { getIssueById }} = useIssueDetail();
const issueDetail = getIssueById(issueId);
if(!issueDetail) return null
return (
<div className="border-b">
<Row>
<IssueIdentifier issueId={issueDetail.id} projectId={issueDetail.project_id || ""} textContainerClassName="text-xs" />
</Row>
</div>
)
})

View File

@@ -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<TNotificationCardListRoot> = 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 (
<div>
{notificationIssueIds.map((issueId: string) => (
// <NotificationItem key={notificationId} workspaceSlug={workspaceSlug} notificationId={notificationId} />
<NotificationItem issueId={issueId} key={issueId} notificationsCount={groupedNotifications[issueId].length}/>
))}
{/* fetch next page notifications */}
{paginationInfo && paginationInfo?.next_page_results && (
<>
{loader === ENotificationLoader.PAGINATION_LOADER ? (
<div className="py-4 flex justify-center items-center text-sm font-medium">
<div className="text-custom-primary-90">Loading...</div>
</div>
) : (
<div className="py-4 flex justify-center items-center text-sm font-medium" onClick={getNextNotifications}>
<div className="text-custom-primary-90 hover:text-custom-primary-100 transition-all cursor-pointer">
Load more
</div>
</div>
)}
</>
)}
</div>
);
});

View File

@@ -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 (
<div
className={cn(
"relative border-0 md:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all overflow-hidden",
currentSelectedNotificationId ? "w-0 md:w-2/6" : "w-full md:w-2/6"
)}
>
<div className="relative w-full h-full overflow-hidden flex flex-col">
<Row className="h-[3.75rem] border-b border-custom-border-200 flex">
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
</Row>
<Header variant={EHeaderVariant.SECONDARY} className="justify-start">
{NOTIFICATION_TABS.map((tab) => (
<div
key={tab.value}
className="h-full px-3 relative cursor-pointer"
onClick={() => currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)}
>
<div
className={cn(
`relative h-full flex justify-center items-center gap-1 text-sm transition-all`,
currentNotificationTab === tab.value
? "text-custom-primary-100"
: "text-custom-text-100 hover:text-custom-text-200"
)}
>
<div className="font-medium">{tab.label}</div>
{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" />
)}
</div>
))}
</Header>
{/* applied filters */}
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
{/* rendering notifications */}
{loader === "init-loader" ? (
<div className="relative w-full h-full overflow-hidden">
<NotificationsLoader />
</div>
) : (
<>
{groupedNotificationIssueIds && groupedNotificationIssueIds.length > 0 ? (
<ContentWrapper variant={ERowVariant.HUGGING}>
<NotificationCardListRoot workspaceSlug={workspaceSlug.toString()} workspaceId={workspace?.id} />
</ContentWrapper>
) : (
<div className="relative w-full h-full flex justify-center items-center">
<NotificationEmptyState />
</div>
)}
</>
)}
</div>
</div>
);
});

View File

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

View File

@@ -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<string,TNotification[]>
export interface IWorkspaceNotificationStore extends IWorkspaceNotificationStoreCore {
// observales
groupedNotifications: Record<string, TNotification[]>;
// actions
groupNotificationsById: (notifications: TNotification[]) => void;
notificationIssueIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
}
export class WorkspaceNotificationStore extends WorkspaceNotificationStoreCore implements IWorkspaceNotificationStore {
// observables
groupedNotifications: Record<string, TNotification[]> = {};
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<TNotificationPaginatedInfo | undefined> => {
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));
}
};
}