[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:
parent
0fd36257d7
commit
26040144fc
16 changed files with 202 additions and 97 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
13
packages/types/src/workspace-notifications.d.ts
vendored
13
packages/types/src/workspace-notifications.d.ts
vendored
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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" />}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue