[WEB-1792] chore: integrated inbox issue in notification peek view and handled increment/decrement of unread notifications (#5008)

* chore: added a boolean field in notification list

* chore: notification filters changed

* chore: handled inbox notification and typo on the card items

* chore: handled notification count increment and decrement

* chore: typos and ui updates

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
guru_sainath 2024-07-02 16:12:27 +05:30 committed by GitHub
parent 0fd36257d7
commit 26040144fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 202 additions and 97 deletions

View file

@ -3,11 +3,15 @@ from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import Notification, UserNotificationPreference
# Third Party imports
from rest_framework import serializers
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by"
)
is_inbox_issue = serializers.BooleanField(read_only=True)
class Meta:
model = Notification

View file

@ -47,10 +47,18 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
type = request.GET.get("type", "all")
q_filters = Q()
inbox_issue = Issue.objects.filter(
pk=OuterRef("entity_identifier"),
issue_inbox__status__in=[0, 2, -2],
workspace__slug=self.kwargs.get("slug"),
)
notifications = (
Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
)
.filter(entity_name="issue")
.annotate(is_inbox_issue=Exists(inbox_issue))
.select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at")
)
@ -202,46 +210,19 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
# Watching Issues Count
subscribed_issues_count = Notification.objects.filter(
unread_notifications_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
entity_identifier__in=IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# My Issues Count
my_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
entity_identifier__in=IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# Created Issues Count
created_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
entity_identifier__in=Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True),
).count()
return Response(
{
"subscribed_issues": subscribed_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
"total_unread_notifications_count": int(
unread_notifications_count
)
},
status=status.HTTP_200_OK,
)

View file

@ -50,6 +50,7 @@ export type TNotification = {
read_at: string | undefined;
archived_at: string | undefined;
snoozed_till: string | undefined;
is_inbox_issue: boolean | undefined;
workspace: string | undefined;
project: string | undefined;
created_at: string | undefined;
@ -84,7 +85,13 @@ export type TNotificationPaginatedInfo = {
// notification count
export type TUnreadNotificationsCount = {
created_issues: number | undefined;
my_issues: number | undefined;
subscribed_issues: number | undefined;
total_unread_notifications_count: number;
};
export type TCurrentSelectedNotification = {
workspace_slug: string | undefined;
project_id: string | undefined;
notification_id: string | undefined;
issue_id: string | undefined;
is_inbox_issue: boolean | undefined;
};

View file

@ -3,19 +3,25 @@
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core";
import { InboxContentRoot } from "@/components/inbox";
import { IssuePeekOverview } from "@/components/issues";
// constants
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
import { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
const WorkspaceDashboardPage = observer(() => {
// hooks
const { currentWorkspace } = useWorkspace();
const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
const { currentSelectedNotification, notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
const {
membership: { fetchUserProjectInfo },
} = useUser();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
const { workspace_slug, project_id, issue_id, is_inbox_issue } = currentSelectedNotification;
// fetch workspace notifications
const notificationMutation =
@ -29,15 +35,42 @@ const WorkspaceDashboardPage = observer(() => {
useSWR(
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
currentWorkspace?.slug
? async () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
: null
);
// fetching user project member info
const { isLoading: projectMemberInfoLoader } = useSWR(
workspace_slug && project_id && is_inbox_issue
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
: null,
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
);
return (
<>
<PageHead title={pageTitle} />
<div className="w-full h-full overflow-hidden overflow-y-auto">
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
<>
{projectMemberInfoLoader ? (
<div className="w-full h-full flex justify-center items-center">
<LogoSpinner />
</div>
) : (
<InboxContentRoot
setIsMobileSidebar={() => {}}
isMobileSidebar={false}
workspaceSlug={workspace_slug}
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
/>
)}
</>
) : (
<IssuePeekOverview embedIssue />
)}
</div>
</>
);

View file

@ -44,10 +44,19 @@ type TInboxIssueActionsHeader = {
isSubmitting: "submitting" | "submitted" | "saved";
isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void;
isNotificationEmbed: boolean;
};
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
const { workspaceSlug, projectId, inboxIssue, isSubmitting, isMobileSidebar, setIsMobileSidebar } = props;
const {
workspaceSlug,
projectId,
inboxIssue,
isSubmitting,
isMobileSidebar,
setIsMobileSidebar,
isNotificationEmbed = false,
} = props;
// states
const [isSnoozeDateModalOpen, setIsSnoozeDateModalOpen] = useState(false);
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
@ -58,7 +67,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox();
const { data: currentUser } = useUser();
const {
membership: { currentProjectRole },
membership: { currentProjectRoleByProjectId },
} = useUser();
const router = useAppRouter();
@ -66,6 +75,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const issue = inboxIssue?.issue;
// derived values
const currentProjectRole = currentProjectRoleByProjectId(projectId) || undefined;
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
@ -240,6 +250,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
</div>
<div className="flex items-center gap-2">
{!isNotificationEmbed && (
<div className="flex items-center gap-x-2">
<button
type="button"
@ -256,6 +267,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
<ChevronDown size={14} strokeWidth={2} />
</button>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
{canMarkAsAccepted && (

View file

@ -15,10 +15,18 @@ type TInboxContentRoot = {
inboxIssueId: string;
isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void;
isNotificationEmbed?: boolean;
};
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props;
const {
workspaceSlug,
projectId,
inboxIssueId,
isMobileSidebar,
setIsMobileSidebar,
isNotificationEmbed = false,
} = props;
/// router
const router = useAppRouter();
// states
@ -33,11 +41,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
useEffect(() => {
if (!isIssueAvailable && inboxIssueId) {
if (!isIssueAvailable && inboxIssueId && !isNotificationEmbed) {
router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isIssueAvailable]);
}, [isIssueAvailable, isNotificationEmbed]);
useSWR(
workspaceSlug && projectId && inboxIssueId
@ -69,6 +77,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
projectId={projectId}
inboxIssue={inboxIssue}
isSubmitting={isSubmitting}
isNotificationEmbed={isNotificationEmbed || false}
/>
</div>
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">

View file

@ -16,7 +16,7 @@ export const IssuePeekOverviewLoader: FC<TIssuePeekOverviewLoader> = (props) =>
const { isMobile } = usePlatformOS();
return (
<Loader className="w-full h-full overflow-hidden p-5 space-y-6">
<Loader className="w-full h-screen overflow-hidden p-5 space-y-6">
<div className="flex justify-between items-center gap-2">
<div className="flex items-center gap-2">
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>

View file

@ -16,21 +16,21 @@ type TNotificationAppSidebarOption = {
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
const { workspaceSlug, isSidebarCollapsed } = props;
// hooks
const { totalUnreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
useSWR(
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null
);
if (totalUnreadNotificationsCount <= 0) return <></>;
if (unreadNotificationsCount.total_unread_notifications_count <= 0) return <></>;
if (isSidebarCollapsed)
return <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />;
return (
<div className="text-[8px] ml-auto bg-custom-primary-100 text-white p-1 py-0.5 rounded-full">
{getNumberCount(totalUnreadNotificationsCount)}
{getNumberCount(unreadNotificationsCount.total_unread_notifications_count)}
</div>
);
});

View file

@ -32,9 +32,7 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
label={
<div className="flex items-center gap-2">
<div className="font-medium">Notifications</div>
<div className="rounded-full text-xs px-1.5 py-0.5 bg-custom-primary-100/20">
{notificationsCount}
</div>
<div className="rounded-full text-xs px-1.5 py-0.5 bg-custom-primary-100">{notificationsCount}</div>
</div>
}
icon={<Bell className="h-4 w-4 text-custom-text-300" />}

View file

@ -1,6 +1,6 @@
export const NotificationsLoader = () => (
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
{[...Array(3)].map((i) => (
{[...Array(8)].map((i) => (
<div key={i} className="flex w-full items-center gap-4 p-3">
<span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
<div className="flex flex-col gap-2.5 w-full">

View file

@ -3,6 +3,7 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Clock } from "lucide-react";
import { TCurrentSelectedNotification } from "@plane/types";
import { Avatar } from "@plane/ui";
// components
import { NotificationOption } from "@/components/workspace-notifications";
@ -12,7 +13,7 @@ import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/he
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
// hooks
import { useIssueDetail, useNotification } from "@/hooks/store";
import { useIssueDetail, useNotification, useWorkspaceNotifications } from "@/hooks/store";
type TNotificationItem = {
workspaceSlug: string;
@ -22,6 +23,7 @@ type TNotificationItem = {
export const NotificationItem: FC<TNotificationItem> = observer((props) => {
const { workspaceSlug, notificationId } = props;
// hooks
const { currentSelectedNotification, setCurrentSelectedNotification } = useWorkspaceNotifications();
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
// states
@ -44,15 +46,29 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
!isSnoozeStateModalOpen &&
!customSnoozeModal
) {
setPeekIssue({ workspaceSlug, projectId, issueId });
const currentSelectedNotificationPayload: TCurrentSelectedNotification = {
workspace_slug: workspaceSlug,
project_id: projectId,
issue_id: issueId,
notification_id: notification?.id,
is_inbox_issue: notification?.is_inbox_issue || false,
};
setCurrentSelectedNotification(currentSelectedNotificationPayload);
// make the notification as read
if (notification.read_at === null)
if (notification.read_at === null) {
try {
await markNotificationAsRead(workspaceSlug);
} catch (error) {
console.error(error);
}
}
if (notification?.is_inbox_issue === false) {
setPeekIssue({ workspaceSlug, projectId, issueId });
} else {
}
}
};
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <></>;
@ -61,8 +77,10 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
<div
className={cn(
"relative p-3 py-4 flex items-center gap-2 border-b border-custom-border-200 cursor-pointer transition-all group",
currentSelectedNotification && currentSelectedNotification?.notification_id === notification?.id
? "bg-custom-background-80/30"
: "",
notification.read_at === null ? "bg-custom-primary-100/5" : ""
// peekIssue && peekIssue?.issueId === issueId ? "bg-custom-background-80" : "
)}
onClick={handleNotificationIssuePeekOverview}
>

View file

@ -45,7 +45,7 @@ export const NotificationItemArchiveOption: FC<TNotificationItemArchiveOption> =
return (
<NotificationItemOptionButton
tooltipContent={data.read_at ? "Mark as unread" : "Mark as read"}
tooltipContent={data.archived_at ? "Un archive" : "Archive"}
callBack={handleNotificationUpdate}
>
{data.archived_at ? (

View file

@ -21,7 +21,7 @@ export const NotificationsSidebar: FC = observer(() => {
const { workspaceSlug } = useParams();
// hooks
const { getWorkspaceBySlug } = useWorkspace();
const { paginationInfo, loader, notificationIdsByWorkspaceId } = useWorkspaceNotifications();
const { unreadNotificationsCount, loader, notificationIdsByWorkspaceId } = useWorkspaceNotifications();
// derived values
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
@ -30,7 +30,7 @@ export const NotificationsSidebar: FC = observer(() => {
const currentTabEmptyState = ENotificationTab.ALL
? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE
: EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE;
const totalNotificationCount = paginationInfo?.total_count || 0;
const totalNotificationCount = unreadNotificationsCount.total_unread_notifications_count;
if (!workspaceSlug || !workspace) return <></>;
return (

View file

@ -41,6 +41,7 @@ export class Notification implements INotification {
read_at: string | undefined = undefined;
archived_at: string | undefined = undefined;
snoozed_till: string | undefined = undefined;
is_inbox_issue: boolean | undefined = undefined;
workspace: string | undefined = undefined;
project: string | undefined = undefined;
created_at: string | undefined = undefined;
@ -102,6 +103,7 @@ export class Notification implements INotification {
this.archived_at = this.notification.archived_at;
this.snoozed_till = this.notification.snoozed_till;
this.workspace = this.notification.workspace;
this.is_inbox_issue = this.notification.is_inbox_issue;
this.project = this.notification.project;
this.created_at = this.notification.created_at;
this.updated_at = this.notification.updated_at;
@ -131,6 +133,7 @@ export class Notification implements INotification {
archived_at: this.archived_at,
snoozed_till: this.snoozed_till,
workspace: this.workspace,
is_inbox_issue: this.is_inbox_issue,
project: this.project,
created_at: this.created_at,
updated_at: this.updated_at,
@ -187,6 +190,7 @@ export class Notification implements INotification {
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) {
@ -195,6 +199,7 @@ export class Notification implements INotification {
return notification;
} catch (error) {
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
throw error;
}
};
@ -212,6 +217,7 @@ export class Notification implements INotification {
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) {
@ -219,6 +225,7 @@ export class Notification implements INotification {
}
return notification;
} catch (error) {
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
throw error;
}

View file

@ -1,8 +1,10 @@
import isEmpty from "lodash/isEmpty";
import set from "lodash/set";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import update from "lodash/update";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import {
TCurrentSelectedNotification,
TNotification,
TNotificationFilter,
TNotificationPaginatedInfo,
@ -28,13 +30,13 @@ type TNotificationQueryParamType = ENotificationQueryParamType;
export interface IWorkspaceNotificationStore {
// observables
loader: TNotificationLoader;
unreadNotificationsCount: TUnreadNotificationsCount | undefined;
unreadNotificationsCount: TUnreadNotificationsCount;
notifications: Record<string, INotification>; // notification_id -> notification
currentNotificationTab: TNotificationTab;
currentSelectedNotification: TCurrentSelectedNotification;
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined;
filters: TNotificationFilter;
// computed
totalUnreadNotificationsCount: number;
// computed functions
notificationIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
// helper actions
@ -43,6 +45,8 @@ export interface IWorkspaceNotificationStore {
updateBulkFilters: (filters: Partial<TNotificationFilter>) => void;
// actions
setCurrentNotificationTab: (tab: TNotificationTab) => void;
setCurrentSelectedNotification: (notification: TCurrentSelectedNotification) => void;
setUnreadNotificationsCount: (type: "increment" | "decrement") => void;
getUnreadNotificationsCount: (workspaceSlug: string) => Promise<TUnreadNotificationsCount | undefined>;
getNotifications: (
workspaceSlug: string,
@ -57,9 +61,18 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
paginatedCount = 30;
// observables
loader: TNotificationLoader = undefined;
unreadNotificationsCount: TUnreadNotificationsCount | undefined = undefined;
unreadNotificationsCount: TUnreadNotificationsCount = {
total_unread_notifications_count: 0,
};
notifications: Record<string, INotification> = {};
currentNotificationTab: TNotificationTab = ENotificationTab.ALL;
currentSelectedNotification: TCurrentSelectedNotification = {
workspace_slug: undefined,
project_id: undefined,
notification_id: undefined,
issue_id: undefined,
is_inbox_issue: false,
};
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined = undefined;
filters: TNotificationFilter = {
type: {
@ -76,15 +89,17 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
makeObservable(this, {
// observables
loader: observable.ref,
unreadNotificationsCount: observable.ref,
unreadNotificationsCount: observable,
notifications: observable,
currentNotificationTab: observable.ref,
currentSelectedNotification: observable,
paginationInfo: observable,
filters: observable,
// computed
totalUnreadNotificationsCount: computed,
// helper actions
setCurrentNotificationTab: action,
setCurrentSelectedNotification: action,
setUnreadNotificationsCount: action,
mutateNotifications: action,
updateFilters: action,
updateBulkFilters: action,
@ -96,15 +111,6 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
}
// computed
get totalUnreadNotificationsCount() {
let count: number = 0;
if (!this.unreadNotificationsCount) return count;
Object.values(this.unreadNotificationsCount).forEach((value) => {
count += value || 0;
});
return count;
}
// computed functions
/**
@ -151,16 +157,14 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
.map(([key]) => key)
.join(",") || undefined;
const currentPage = this.paginationInfo ? Number(this.paginationInfo?.prev_cursor?.split(":")[1] || 0) + 1 : 0;
const queryCursorNext =
paramType === ENotificationQueryParamType.INIT
? `${this.paginatedCount}:0:0`
: paramType === ENotificationQueryParamType.CURRENT
? `${this.paginatedCount}:${currentPage}:0`
? `${this.paginatedCount}:${0}:0`
: paramType === ENotificationQueryParamType.NEXT && this.paginationInfo
? this.paginationInfo?.next_cursor
: `${this.paginatedCount}:${currentPage}:0`;
: `${this.paginatedCount}:${0}:0`;
const queryParams: TNotificationPaginatedInfoQueryParams = {
type: queryParamsType,
@ -232,6 +236,27 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
set(this, "currentNotificationTab", tab);
};
/**
* @description set current selected notification
* @param { TCurrentSelectedNotification } notification
* @returns { void }
*/
setCurrentSelectedNotification = (notification: TCurrentSelectedNotification): void => {
set(this, "currentSelectedNotification", notification);
};
/**
* @description set unread notifications count
* @param { "increment" | "decrement" } type
* @returns { void }
*/
setUnreadNotificationsCount = (type: "increment" | "decrement"): void =>
runInAction(() => {
update(this.unreadNotificationsCount, "total_unread_notifications_count", (count: 0) =>
type === "increment" ? count + 1 : count - 1
);
});
/**
* @description get unread notifications count
* @param { string } workspaceSlug,

View file

@ -34,6 +34,9 @@ export interface IUserMembershipStore {
currentWorkspaceRole: EUserWorkspaceRoles | undefined;
currentWorkspaceAllProjectsRole: IUserProjectsRole | undefined;
// computed functions
currentProjectRoleByProjectId: (projectId: string) => EUserProjectRoles | undefined;
hasPermissionToCurrentWorkspace: boolean | undefined;
hasPermissionToCurrentProject: boolean | undefined;
// fetch actions
@ -156,6 +159,14 @@ export class UserMembershipStore implements IUserMembershipStore {
return this.hasPermissionToProject[this.router.projectId];
}
// computed functions
/**
* Returns the current project role by project id
* @param projectId
* @returns EUserProjectRoles
*/
currentProjectRoleByProjectId = (projectId: string) => this.projectMemberInfo[projectId]?.role || undefined;
/**
* Fetches the current user workspace info
* @param workspaceSlug