feature/IO-3096-GlobalNotifications - Checkpoint - clicking an individual notification will mark it read

This commit is contained in:
Dave Richer
2025-02-26 15:52:21 -05:00
parent 0767e290f4
commit e4d437018d
4 changed files with 98 additions and 12 deletions

View File

@@ -17,7 +17,8 @@ const NotificationCenterComponent = ({
showUnreadOnly,
toggleUnreadOnly,
markAllRead,
loadMore
loadMore,
onNotificationClick
}) => {
const { t } = useTranslation();
@@ -26,10 +27,10 @@ const NotificationCenterComponent = ({
<List.Item
key={`${notification.id}-${index}`}
className={notification.read ? "notification-read" : "notification-unread"}
onClick={() => !notification.read && onNotificationClick(notification.id)}
>
<Badge dot={!notification.read}>
<div>
{/* RO number as title/link */}
<Title
level={5}
style={{
@@ -39,7 +40,14 @@ const NotificationCenterComponent = ({
alignItems: "center"
}}
>
<Link to={`/manage/jobs/${notification.jobid}`} target="_blank">
<Link
to={`/manage/jobs/${notification.jobid}`}
target="_blank"
onClick={(e) => {
e.stopPropagation(); // Prevent List.Item click handler from firing
!notification.read && onNotificationClick(notification.id); // Mark as read when link clicked
}}
>
RO #{notification.roNumber}
</Link>
<Text type="secondary">{new Date(notification.created_at).toLocaleString()}</Text>
@@ -75,7 +83,6 @@ const NotificationCenterComponent = ({
style={{ height: "400px", width: "100%" }}
data={notifications}
totalCount={notifications.length}
overscan={200}
endReached={loadMore}
itemContent={renderNotification}
/>

View File

@@ -2,7 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useMutation, useQuery } from "@apollo/client";
import { connect } from "react-redux";
import NotificationCenterComponent from "./notification-center.component";
import { GET_NOTIFICATIONS, MARK_ALL_NOTIFICATIONS_READ } from "../../graphql/notifications.queries";
import {
GET_NOTIFICATIONS,
MARK_ALL_NOTIFICATIONS_READ,
MARK_NOTIFICATION_READ,
GET_UNREAD_COUNT
} from "../../graphql/notifications.queries";
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
@@ -38,7 +43,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
pollInterval: isConnected ? 0 : 30000,
skip: !userAssociationId, // Skip query if no userAssociationId
skip: !userAssociationId,
onError: (err) => {
setError(err.message);
console.error("GET_NOTIFICATIONS error:", err);
@@ -99,6 +104,53 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
}
});
const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, {
update: (cache, { data: { update_notifications } }) => {
const timestamp = new Date().toISOString();
const updatedNotification = update_notifications.returning[0];
// Update the notifications list
cache.modify({
fields: {
notifications(existing = [], { readField }) {
return existing.map((notif) => {
if (readField("id", notif) === updatedNotification.id) {
return { ...notif, read: timestamp };
}
return notif;
});
}
}
});
// Update the unread count in notifications_aggregate
const unreadCountQuery = cache.readQuery({
query: GET_UNREAD_COUNT,
variables: { associationid: userAssociationId }
});
if (unreadCountQuery?.notifications_aggregate?.aggregate?.count > 0) {
cache.writeQuery({
query: GET_UNREAD_COUNT,
variables: { associationid: userAssociationId },
data: {
notifications_aggregate: {
...unreadCountQuery.notifications_aggregate,
aggregate: {
...unreadCountQuery.notifications_aggregate.aggregate,
count: unreadCountQuery.notifications_aggregate.aggregate.count - 1
}
}
}
});
}
},
onError: (err) => {
setError(err.message);
console.error("MARK_NOTIFICATION_READ error:", err);
}
});
useEffect(() => {
if (data?.notifications) {
const processedNotifications = data.notifications
@@ -167,12 +219,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
const timestamp = new Date().toISOString();
setNotifications((prev) => {
const updatedNotifications = prev.map((notif) =>
notif.read === null && notif.associationid === userAssociationId
? {
...notif,
read: timestamp
}
: notif
notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif
);
return [...updatedNotifications];
});
@@ -180,6 +227,24 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`));
};
const handleNotificationClick = useCallback(
(notificationId) => {
markNotificationRead({
variables: { id: notificationId }
})
.then(() => {
const timestamp = new Date().toISOString();
setNotifications((prev) => {
return prev.map((notif) =>
notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif
);
});
})
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
},
[markNotificationRead]
);
useEffect(() => {
if (visible && !isConnected) {
refetch();
@@ -197,6 +262,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
toggleUnreadOnly={handleToggleUnreadOnly}
markAllRead={handleMarkAllRead}
loadMore={loadMore}
onNotificationClick={handleNotificationClick}
/>
);
}

View File

@@ -38,3 +38,14 @@ export const MARK_ALL_NOTIFICATIONS_READ = gql`
}
}
`;
export const MARK_NOTIFICATION_READ = gql`
mutation MarkNotificationRead($id: uuid!) {
update_notifications(where: { id: { _eq: $id } }, _set: { read: "now()" }) {
returning {
id
read
}
}
}
`;