[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 .user import UserLiteSerializer
from plane.db.models import Notification, UserNotificationPreference from plane.db.models import Notification, UserNotificationPreference
# Third Party imports
from rest_framework import serializers
class NotificationSerializer(BaseSerializer): class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer( triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by" read_only=True, source="triggered_by"
) )
is_inbox_issue = serializers.BooleanField(read_only=True)
class Meta: class Meta:
model = Notification model = Notification

View file

@ -47,10 +47,18 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
type = request.GET.get("type", "all") type = request.GET.get("type", "all")
q_filters = Q() 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 = ( notifications = (
Notification.objects.filter( Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id 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") .select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at") .order_by("snoozed_till", "-created_at")
) )
@ -202,46 +210,19 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView): class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
# Watching Issues Count # Watching Issues Count
subscribed_issues_count = Notification.objects.filter( unread_notifications_count = Notification.objects.filter(
workspace__slug=slug, workspace__slug=slug,
receiver_id=request.user.id, receiver_id=request.user.id,
read_at__isnull=True, read_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
snoozed_till__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() ).count()
return Response( return Response(
{ {
"subscribed_issues": subscribed_issues_count, "total_unread_notifications_count": int(
"my_issues": my_issues_count, unread_notifications_count
"created_issues": created_issues_count, )
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )

View file

@ -50,6 +50,7 @@ export type TNotification = {
read_at: string | undefined; read_at: string | undefined;
archived_at: string | undefined; archived_at: string | undefined;
snoozed_till: string | undefined; snoozed_till: string | undefined;
is_inbox_issue: boolean | undefined;
workspace: string | undefined; workspace: string | undefined;
project: string | undefined; project: string | undefined;
created_at: string | undefined; created_at: string | undefined;
@ -84,7 +85,13 @@ export type TNotificationPaginatedInfo = {
// notification count // notification count
export type TUnreadNotificationsCount = { export type TUnreadNotificationsCount = {
created_issues: number | undefined; total_unread_notifications_count: number;
my_issues: number | undefined; };
subscribed_issues: number | undefined;
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 { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { InboxContentRoot } from "@/components/inbox";
import { IssuePeekOverview } from "@/components/issues"; import { IssuePeekOverview } from "@/components/issues";
// constants // constants
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks // hooks
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; import { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
const WorkspaceDashboardPage = observer(() => { const WorkspaceDashboardPage = observer(() => {
// hooks // hooks
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications(); const { currentSelectedNotification, notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
const {
membership: { fetchUserProjectInfo },
} = useUser();
// derived values // derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
const { workspace_slug, project_id, issue_id, is_inbox_issue } = currentSelectedNotification;
// fetch workspace notifications // fetch workspace notifications
const notificationMutation = const notificationMutation =
@ -29,15 +35,42 @@ const WorkspaceDashboardPage = observer(() => {
useSWR( useSWR(
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null, currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
currentWorkspace?.slug currentWorkspace?.slug
? async () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader) ? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
: null : 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 ( return (
<> <>
<PageHead title={pageTitle} /> <PageHead title={pageTitle} />
<div className="w-full h-full overflow-hidden overflow-y-auto"> <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 /> <IssuePeekOverview embedIssue />
)}
</div> </div>
</> </>
); );

View file

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

View file

@ -15,10 +15,18 @@ type TInboxContentRoot = {
inboxIssueId: string; inboxIssueId: string;
isMobileSidebar: boolean; isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void; setIsMobileSidebar: (value: boolean) => void;
isNotificationEmbed?: boolean;
}; };
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => { export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props; const {
workspaceSlug,
projectId,
inboxIssueId,
isMobileSidebar,
setIsMobileSidebar,
isNotificationEmbed = false,
} = props;
/// router /// router
const router = useAppRouter(); const router = useAppRouter();
// states // states
@ -33,11 +41,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || ""); const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
useEffect(() => { useEffect(() => {
if (!isIssueAvailable && inboxIssueId) { if (!isIssueAvailable && inboxIssueId && !isNotificationEmbed) {
router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`); router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isIssueAvailable]); }, [isIssueAvailable, isNotificationEmbed]);
useSWR( useSWR(
workspaceSlug && projectId && inboxIssueId workspaceSlug && projectId && inboxIssueId
@ -69,6 +77,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
projectId={projectId} projectId={projectId}
inboxIssue={inboxIssue} inboxIssue={inboxIssue}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
isNotificationEmbed={isNotificationEmbed || false}
/> />
</div> </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"> <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(); const { isMobile } = usePlatformOS();
return ( 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 justify-between items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}> <Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>

View file

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

View file

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

View file

@ -1,6 +1,6 @@
export const NotificationsLoader = () => ( export const NotificationsLoader = () => (
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden"> <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"> <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" /> <span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
<div className="flex flex-col gap-2.5 w-full"> <div className="flex flex-col gap-2.5 w-full">

View file

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

View file

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

View file

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

View file

@ -41,6 +41,7 @@ export class Notification implements INotification {
read_at: string | undefined = undefined; read_at: string | undefined = undefined;
archived_at: string | undefined = undefined; archived_at: string | undefined = undefined;
snoozed_till: string | undefined = undefined; snoozed_till: string | undefined = undefined;
is_inbox_issue: boolean | undefined = undefined;
workspace: string | undefined = undefined; workspace: string | undefined = undefined;
project: string | undefined = undefined; project: string | undefined = undefined;
created_at: 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.archived_at = this.notification.archived_at;
this.snoozed_till = this.notification.snoozed_till; this.snoozed_till = this.notification.snoozed_till;
this.workspace = this.notification.workspace; this.workspace = this.notification.workspace;
this.is_inbox_issue = this.notification.is_inbox_issue;
this.project = this.notification.project; this.project = this.notification.project;
this.created_at = this.notification.created_at; this.created_at = this.notification.created_at;
this.updated_at = this.notification.updated_at; this.updated_at = this.notification.updated_at;
@ -131,6 +133,7 @@ export class Notification implements INotification {
archived_at: this.archived_at, archived_at: this.archived_at,
snoozed_till: this.snoozed_till, snoozed_till: this.snoozed_till,
workspace: this.workspace, workspace: this.workspace,
is_inbox_issue: this.is_inbox_issue,
project: this.project, project: this.project,
created_at: this.created_at, created_at: this.created_at,
updated_at: this.updated_at, updated_at: this.updated_at,
@ -187,6 +190,7 @@ export class Notification implements INotification {
const payload: Partial<TNotification> = { const payload: Partial<TNotification> = {
read_at: new Date().toISOString(), read_at: new Date().toISOString(),
}; };
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
runInAction(() => this.mutateNotification(payload)); runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.markNotificationAsRead(workspaceSlug, this.id); const notification = await workspaceNotificationService.markNotificationAsRead(workspaceSlug, this.id);
if (notification) { if (notification) {
@ -195,6 +199,7 @@ export class Notification implements INotification {
return notification; return notification;
} catch (error) { } catch (error) {
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt })); runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
throw error; throw error;
} }
}; };
@ -212,6 +217,7 @@ export class Notification implements INotification {
const payload: Partial<TNotification> = { const payload: Partial<TNotification> = {
read_at: undefined, read_at: undefined,
}; };
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
runInAction(() => this.mutateNotification(payload)); runInAction(() => this.mutateNotification(payload));
const notification = await workspaceNotificationService.markNotificationAsUnread(workspaceSlug, this.id); const notification = await workspaceNotificationService.markNotificationAsUnread(workspaceSlug, this.id);
if (notification) { if (notification) {
@ -219,6 +225,7 @@ export class Notification implements INotification {
} }
return notification; return notification;
} catch (error) { } catch (error) {
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt })); runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
throw error; throw error;
} }

View file

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

View file

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