mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 05:49:40 +02:00
added stacked notifications to store
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "ce/components/workspace-notifications";
|
||||
export * from "./root";
|
||||
export * from "./notification-card";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './root';
|
||||
export * from './item'
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
106
web/ee/components/workspace-notifications/root.tsx
Normal file
106
web/ee/components/workspace-notifications/root.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
95
web/ee/store/workspace-notifications/notifications.store.ts
Normal file
95
web/ee/store/workspace-notifications/notifications.store.ts
Normal 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user