feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
/* eslint-disable no-useless-catch */
import { set } from "lodash-es";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import type { IUserLite, TNotification, TNotificationData } from "@plane/types";
// services
import workspaceNotificationService from "@/services/workspace-notification.service";
// store
import type { CoreRootStore } from "../root.store";
export interface INotification extends TNotification {
// observables
// computed
asJson: TNotification;
// computed functions
// helper functions
mutateNotification: (notification: Partial<TNotification>) => void;
// actions
updateNotification: (workspaceSlug: string, payload: Partial<TNotification>) => Promise<TNotification | undefined>;
markNotificationAsRead: (workspaceSlug: string) => Promise<TNotification | undefined>;
markNotificationAsUnRead: (workspaceSlug: string) => Promise<TNotification | undefined>;
archiveNotification: (workspaceSlug: string) => Promise<TNotification | undefined>;
unArchiveNotification: (workspaceSlug: string) => Promise<TNotification | undefined>;
snoozeNotification: (workspaceSlug: string, snoozeTill: Date) => Promise<TNotification | undefined>;
unSnoozeNotification: (workspaceSlug: string) => Promise<TNotification | undefined>;
}
export class Notification implements INotification {
// observables
id: string;
title: string | undefined = undefined;
data: TNotificationData | undefined = undefined;
entity_identifier: string | undefined = undefined;
entity_name: string | undefined = undefined;
message_html: string | undefined = undefined;
message: undefined = undefined;
message_stripped: undefined = undefined;
sender: string | undefined = undefined;
receiver: string | undefined = undefined;
triggered_by: string | undefined = undefined;
triggered_by_details: IUserLite | undefined = undefined;
read_at: string | undefined = undefined;
archived_at: string | undefined = undefined;
snoozed_till: string | undefined = undefined;
is_inbox_issue: boolean | undefined = undefined;
is_mentioned_notification: boolean | undefined = undefined;
workspace: string | undefined = undefined;
project: string | undefined = undefined;
created_at: string | undefined = undefined;
updated_at: string | undefined = undefined;
created_by: string | undefined = undefined;
updated_by: string | undefined = undefined;
constructor(
private store: CoreRootStore,
private notification: TNotification
) {
this.id = this.notification.id;
makeObservable(this, {
// observables
id: observable.ref,
title: observable.ref,
data: observable,
entity_identifier: observable.ref,
entity_name: observable.ref,
message_html: observable.ref,
message: observable.ref,
message_stripped: observable.ref,
sender: observable.ref,
receiver: observable.ref,
triggered_by: observable.ref,
triggered_by_details: observable,
read_at: observable.ref,
archived_at: observable.ref,
snoozed_till: observable.ref,
is_inbox_issue: observable.ref,
is_mentioned_notification: observable.ref,
workspace: observable.ref,
project: observable.ref,
created_at: observable.ref,
updated_at: observable.ref,
created_by: observable.ref,
updated_by: observable.ref,
// computed
asJson: computed,
// actions
updateNotification: action,
markNotificationAsRead: action,
markNotificationAsUnRead: action,
archiveNotification: action,
unArchiveNotification: action,
snoozeNotification: action,
unSnoozeNotification: action,
});
this.title = this.notification.title;
this.data = this.notification.data;
this.entity_identifier = this.notification.entity_identifier;
this.entity_name = this.notification.entity_name;
this.message_html = this.notification.message_html;
this.message = this.notification.message;
this.message_stripped = this.notification.message_stripped;
this.sender = this.notification.sender;
this.receiver = this.notification.receiver;
this.triggered_by = this.notification.triggered_by;
this.triggered_by_details = this.notification.triggered_by_details;
this.read_at = this.notification.read_at;
this.archived_at = this.notification.archived_at;
this.snoozed_till = this.notification.snoozed_till;
this.is_inbox_issue = this.notification.is_inbox_issue;
this.is_mentioned_notification = this.notification.is_mentioned_notification;
this.workspace = this.notification.workspace;
this.project = this.notification.project;
this.created_at = this.notification.created_at;
this.updated_at = this.notification.updated_at;
this.created_by = this.notification.created_by;
this.updated_by = this.notification.updated_by;
}
// computed
/**
* @description get notification as json
*/
get asJson() {
return {
id: this.id,
title: this.title,
data: this.data,
entity_identifier: this.entity_identifier,
entity_name: this.entity_name,
message_html: this.message_html,
message: this.message,
message_stripped: this.message_stripped,
sender: this.sender,
receiver: this.receiver,
triggered_by: this.triggered_by,
triggered_by_details: this.triggered_by_details,
read_at: this.read_at,
archived_at: this.archived_at,
snoozed_till: this.snoozed_till,
is_inbox_issue: this.is_inbox_issue,
is_mentioned_notification: this.is_mentioned_notification,
workspace: this.workspace,
project: this.project,
created_at: this.created_at,
updated_at: this.updated_at,
created_by: this.created_by,
updated_by: this.updated_by,
};
}
// computed functions
// helper functions
mutateNotification = (notification: Partial<TNotification>) => {
Object.entries(notification).forEach(([key, value]) => {
if (key in this) {
set(this, key, value);
}
});
};
// actions
/**
* @description update notification
* @param { string } workspaceSlug
* @param { Partial<TNotification> } payload
* @returns { TNotification | undefined }
*/
updateNotification = async (
workspaceSlug: string,
payload: Partial<TNotification>
): Promise<TNotification | undefined> => {
try {
const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
if (notification) {
runInAction(() => this.mutateNotification(notification));
}
return notification;
} catch (error) {
throw error;
}
};
/**
* @description mark notification as read
* @param { string } workspaceSlug
* @returns { TNotification | undefined }
*/
markNotificationAsRead = async (workspaceSlug: string): Promise<TNotification | undefined> => {
const currentNotificationReadAt = this.read_at;
try {
const payload: Partial<TNotification> = {
read_at: new Date().toISOString(),
};
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.markNotificationAsRead(workspaceSlug, this.id);
if (notification) {
runInAction(() => this.mutateNotification(notification));
}
return notification;
} catch (error) {
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
throw error;
}
};
/**
* @description mark notification as unread
* @param { string } workspaceSlug
* @returns { TNotification | undefined }
*/
markNotificationAsUnRead = async (workspaceSlug: string): Promise<TNotification | undefined> => {
const currentNotificationReadAt = this.read_at;
try {
const payload: Partial<TNotification> = {
read_at: undefined,
};
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.markNotificationAsUnread(workspaceSlug, this.id);
if (notification) {
runInAction(() => this.mutateNotification(notification));
}
return notification;
} catch (error) {
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
throw error;
}
};
/**
* @description archive notification
* @param { string } workspaceSlug
* @returns { TNotification | undefined }
*/
archiveNotification = async (workspaceSlug: string): Promise<TNotification | undefined> => {
const currentNotificationArchivedAt = this.archived_at;
try {
const payload: Partial<TNotification> = {
archived_at: new Date().toISOString(),
};
runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.markNotificationAsArchived(workspaceSlug, this.id);
if (notification) {
runInAction(() => this.mutateNotification(notification));
}
return notification;
} catch (error) {
runInAction(() => this.mutateNotification({ archived_at: currentNotificationArchivedAt }));
throw error;
}
};
/**
* @description unarchive notification
* @param { string } workspaceSlug
* @returns { TNotification | undefined }
*/
unArchiveNotification = async (workspaceSlug: string): Promise<TNotification | undefined> => {
const currentNotificationArchivedAt = this.archived_at;
try {
const payload: Partial<TNotification> = {
archived_at: undefined,
};
runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.markNotificationAsUnArchived(workspaceSlug, this.id);
if (notification) {
runInAction(() => this.mutateNotification(notification));
}
return notification;
} catch (error) {
runInAction(() => this.mutateNotification({ archived_at: currentNotificationArchivedAt }));
throw error;
}
};
/**
* @description snooze notification
* @param { string } workspaceSlug
* @param { Date } snoozeTill
* @returns { TNotification | undefined }
*/
snoozeNotification = async (workspaceSlug: string, snoozeTill: Date): Promise<TNotification | undefined> => {
const currentNotificationSnoozeTill = this.snoozed_till;
try {
const payload: Partial<TNotification> = {
snoozed_till: snoozeTill.toISOString(),
};
runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
return notification;
} catch (error) {
runInAction(() => this.mutateNotification({ snoozed_till: currentNotificationSnoozeTill }));
throw error;
}
};
/**
* @description un snooze notification
* @param { string } workspaceSlug
* @returns { TNotification | undefined }
*/
unSnoozeNotification = async (workspaceSlug: string): Promise<TNotification | undefined> => {
const currentNotificationSnoozeTill = this.snoozed_till;
try {
const payload: Partial<TNotification> = {
snoozed_till: undefined,
};
runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
return notification;
} catch (error) {
runInAction(() => this.mutateNotification({ snoozed_till: currentNotificationSnoozeTill }));
throw error;
}
};
}

View File

@@ -0,0 +1,396 @@
import { orderBy, isEmpty, update, set } from "lodash-es";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// plane imports
import type { TNotificationTab } from "@plane/constants";
import { ENotificationTab, ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
import type {
TNotification,
TNotificationFilter,
TNotificationLite,
TNotificationPaginatedInfo,
TNotificationPaginatedInfoQueryParams,
TUnreadNotificationsCount,
} from "@plane/types";
// helpers
import { convertToEpoch } from "@plane/utils";
// services
import workspaceNotificationService from "@/services/workspace-notification.service";
// store
import type { INotification } from "@/store/notifications/notification";
import { Notification } from "@/store/notifications/notification";
import type { CoreRootStore } from "@/store/root.store";
type TNotificationLoader = ENotificationLoader | undefined;
type TNotificationQueryParamType = ENotificationQueryParamType;
export interface IWorkspaceNotificationStore {
// observables
loader: TNotificationLoader;
unreadNotificationsCount: TUnreadNotificationsCount;
notifications: Record<string, INotification>; // notification_id -> notification
currentNotificationTab: TNotificationTab;
currentSelectedNotificationId: string | undefined;
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined;
filters: TNotificationFilter;
// computed
// computed functions
notificationIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
notificationLiteByNotificationId: (notificationId: string | undefined) => TNotificationLite;
// helper actions
mutateNotifications: (notifications: TNotification[]) => void;
updateFilters: <T extends keyof TNotificationFilter>(key: T, value: TNotificationFilter[T]) => void;
updateBulkFilters: (filters: Partial<TNotificationFilter>) => void;
// actions
setCurrentNotificationTab: (tab: TNotificationTab) => void;
setCurrentSelectedNotificationId: (notificationId: string | undefined) => void;
setUnreadNotificationsCount: (type: "increment" | "decrement", newCount?: number) => void;
getUnreadNotificationsCount: (workspaceSlug: string) => Promise<TUnreadNotificationsCount | undefined>;
getNotifications: (
workspaceSlug: string,
loader?: TNotificationLoader,
queryCursorType?: TNotificationQueryParamType
) => Promise<TNotificationPaginatedInfo | undefined>;
markAllNotificationsAsRead: (workspaceId: string) => Promise<void>;
}
export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
// constants
paginatedCount = 300;
// observables
loader: TNotificationLoader = undefined;
unreadNotificationsCount: TUnreadNotificationsCount = {
total_unread_notifications_count: 0,
mention_unread_notifications_count: 0,
};
notifications: Record<string, INotification> = {};
currentNotificationTab: TNotificationTab = ENotificationTab.ALL;
currentSelectedNotificationId: string | undefined = undefined;
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined = undefined;
filters: TNotificationFilter = {
type: {
assigned: false,
created: false,
subscribed: false,
},
snoozed: false,
archived: false,
read: false,
};
constructor(protected store: CoreRootStore) {
makeObservable(this, {
// observables
loader: observable.ref,
unreadNotificationsCount: observable,
notifications: observable,
currentNotificationTab: observable.ref,
currentSelectedNotificationId: observable,
paginationInfo: observable,
filters: observable,
// computed
// helper actions
setCurrentNotificationTab: action,
setCurrentSelectedNotificationId: action,
setUnreadNotificationsCount: action,
mutateNotifications: action,
updateFilters: action,
updateBulkFilters: action,
// actions
getUnreadNotificationsCount: action,
getNotifications: action,
markAllNotificationsAsRead: action,
});
}
// computed
// computed functions
/**
* @description get notification ids by workspace id
* @param { string } workspaceId
*/
notificationIdsByWorkspaceId = computedFn((workspaceId: string) => {
if (!workspaceId || isEmpty(this.notifications)) return undefined;
const workspaceNotifications = orderBy(
Object.values(this.notifications || []),
(n) => convertToEpoch(n.created_at),
["desc"]
);
const workspaceNotificationIds = workspaceNotifications
.filter((n) => n.workspace === workspaceId)
.filter((n) =>
this.currentNotificationTab === ENotificationTab.MENTIONS
? n.is_mentioned_notification
: !n.is_mentioned_notification
)
.filter((n) => {
if (!this.filters.archived && !this.filters.snoozed) {
if (n.archived_at) {
return false;
} else if (n.snoozed_till) {
return false;
} else {
return true;
}
} else {
if (this.filters.snoozed) {
return n.snoozed_till ? true : false;
} else if (this.filters.archived) {
return n.archived_at ? true : false;
} else {
return true;
}
}
})
// .filter((n) => (this.filters.read ? (n.read_at ? true : false) : n.read_at ? false : true))
.map((n) => n.id) as string[];
return workspaceNotificationIds;
});
/**
* @description get notification lite by notification id
* @param { string } notificationId
*/
notificationLiteByNotificationId = computedFn((notificationId: string | undefined) => {
if (!notificationId) return {} as TNotificationLite;
const { workspaceSlug } = this.store.router;
const notification = this.notifications[notificationId];
if (!notification || !workspaceSlug) return {} as TNotificationLite;
return {
workspace_slug: workspaceSlug,
project_id: notification.project,
notification_id: notification.id,
issue_id: notification.data?.issue?.id,
is_inbox_issue: notification.is_inbox_issue || false,
};
});
// helper functions
/**
* @description generate notification query params
* @returns { object }
*/
generateNotificationQueryParams = (paramType: TNotificationQueryParamType): TNotificationPaginatedInfoQueryParams => {
const queryParamsType =
Object.entries(this.filters.type)
.filter(([, value]) => value)
.map(([key]) => key)
.join(",") || undefined;
const queryCursorNext =
paramType === ENotificationQueryParamType.INIT
? `${this.paginatedCount}:0:0`
: paramType === ENotificationQueryParamType.CURRENT
? `${this.paginatedCount}:${0}:0`
: paramType === ENotificationQueryParamType.NEXT && this.paginationInfo
? this.paginationInfo?.next_cursor
: `${this.paginatedCount}:${0}:0`;
const queryParams: TNotificationPaginatedInfoQueryParams = {
type: queryParamsType,
snoozed: this.filters.snoozed || false,
archived: this.filters.archived || false,
read: undefined,
per_page: this.paginatedCount,
cursor: queryCursorNext,
};
// NOTE: This validation is required to show all the read and unread notifications in a single place it may change in future.
queryParams.read = this.filters.read === true ? false : undefined;
if (this.currentNotificationTab === ENotificationTab.MENTIONS) queryParams.mentioned = true;
return queryParams;
};
// helper actions
/**
* @description mutate and validate current existing and new notifications
* @param { TNotification[] } notifications
*/
mutateNotifications = (notifications: TNotification[]) => {
(notifications || []).forEach((notification) => {
if (!notification.id) return;
if (this.notifications[notification.id]) {
this.notifications[notification.id].mutateNotification(notification);
} else {
set(this.notifications, notification.id, new Notification(this.store, notification));
}
});
};
/**
* @description update filters
* @param { T extends keyof TNotificationFilter } key
* @param { TNotificationFilter[T] } value
*/
updateFilters = <T extends keyof TNotificationFilter>(key: T, value: TNotificationFilter[T]) => {
set(this.filters, key, value);
const { workspaceSlug } = this.store.router;
if (!workspaceSlug) return;
set(this, "notifications", {});
this.getNotifications(workspaceSlug, ENotificationLoader.INIT_LOADER, ENotificationQueryParamType.INIT);
};
/**
* @description update bulk filters
* @param { Partial<TNotificationFilter> } filters
*/
updateBulkFilters = (filters: Partial<TNotificationFilter>) => {
Object.entries(filters).forEach(([key, value]) => {
set(this.filters, key, value);
});
const { workspaceSlug } = this.store.router;
if (!workspaceSlug) return;
set(this, "notifications", {});
this.getNotifications(workspaceSlug, ENotificationLoader.INIT_LOADER, ENotificationQueryParamType.INIT);
};
// actions
/**
* @description set notification tab
* @returns { void }
*/
setCurrentNotificationTab = (tab: TNotificationTab): void => {
set(this, "currentNotificationTab", tab);
const { workspaceSlug } = this.store.router;
if (!workspaceSlug) return;
set(this, "notifications", {});
this.getNotifications(workspaceSlug, ENotificationLoader.INIT_LOADER, ENotificationQueryParamType.INIT);
};
/**
* @description set current selected notification
* @param { string | undefined } notificationId
* @returns { void }
*/
setCurrentSelectedNotificationId = (notificationId: string | undefined): void => {
set(this, "currentSelectedNotificationId", notificationId);
};
/**
* @description set unread notifications count
* @param { "increment" | "decrement" } type
* @returns { void }
*/
setUnreadNotificationsCount = (type: "increment" | "decrement", newCount: number = 1): void => {
const validCount = Math.max(0, Math.abs(newCount));
switch (this.currentNotificationTab) {
case ENotificationTab.ALL:
update(
this.unreadNotificationsCount,
"total_unread_notifications_count",
(count: number) => +Math.max(0, type === "increment" ? count + validCount : count - validCount)
);
break;
case ENotificationTab.MENTIONS:
update(
this.unreadNotificationsCount,
"mention_unread_notifications_count",
(count: number) => +Math.max(0, type === "increment" ? count + validCount : count - validCount)
);
break;
default:
break;
}
};
/**
* @description get unread notifications count
* @param { string } workspaceSlug,
* @param { TNotificationQueryParamType } queryCursorType,
* @returns { number | undefined }
*/
getUnreadNotificationsCount = async (workspaceSlug: string): Promise<TUnreadNotificationsCount | undefined> => {
try {
const unreadNotificationCount = await workspaceNotificationService.fetchUnreadNotificationsCount(workspaceSlug);
if (unreadNotificationCount)
runInAction(() => {
set(this, "unreadNotificationsCount", unreadNotificationCount);
});
return unreadNotificationCount || undefined;
} catch (error) {
console.error("WorkspaceNotificationStore -> getUnreadNotificationsCount -> error", error);
throw error;
}
};
/**
* @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.mutateNotifications(results);
}
set(this, "paginationInfo", paginationInfo);
});
}
return notificationResponse;
} catch (error) {
console.error("WorkspaceNotificationStore -> getNotifications -> error", error);
throw error;
} finally {
runInAction(() => (this.loader = undefined));
}
};
/**
* @description mark all notifications as read
* @param { string } workspaceSlug,
* @returns { void }
*/
markAllNotificationsAsRead = async (workspaceSlug: string): Promise<void> => {
try {
this.loader = ENotificationLoader.MARK_ALL_AS_READY;
const queryParams = this.generateNotificationQueryParams(ENotificationQueryParamType.INIT);
const params = {
type: queryParams.type,
snoozed: queryParams.snoozed,
archived: queryParams.archived,
read: queryParams.read,
};
await workspaceNotificationService.markAllNotificationsAsRead(workspaceSlug, params);
runInAction(() => {
update(
this.unreadNotificationsCount,
this.currentNotificationTab === ENotificationTab.ALL
? "total_unread_notifications_count"
: "mention_unread_notifications_count",
() => 0
);
Object.values(this.notifications).forEach((notification) =>
notification.mutateNotification({
read_at: new Date().toUTCString(),
})
);
});
} catch (error) {
console.error("WorkspaceNotificationStore -> markAllNotificationsAsRead -> error", error);
throw error;
} finally {
runInAction(() => (this.loader = undefined));
}
};
}