feature/IO-3096-GlobalNotifications - Checkpoint - clicking an individual notification will mark it read
This commit is contained in:
@@ -17,7 +17,8 @@ const NotificationCenterComponent = ({
|
|||||||
showUnreadOnly,
|
showUnreadOnly,
|
||||||
toggleUnreadOnly,
|
toggleUnreadOnly,
|
||||||
markAllRead,
|
markAllRead,
|
||||||
loadMore
|
loadMore,
|
||||||
|
onNotificationClick
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -26,10 +27,10 @@ const NotificationCenterComponent = ({
|
|||||||
<List.Item
|
<List.Item
|
||||||
key={`${notification.id}-${index}`}
|
key={`${notification.id}-${index}`}
|
||||||
className={notification.read ? "notification-read" : "notification-unread"}
|
className={notification.read ? "notification-read" : "notification-unread"}
|
||||||
|
onClick={() => !notification.read && onNotificationClick(notification.id)}
|
||||||
>
|
>
|
||||||
<Badge dot={!notification.read}>
|
<Badge dot={!notification.read}>
|
||||||
<div>
|
<div>
|
||||||
{/* RO number as title/link */}
|
|
||||||
<Title
|
<Title
|
||||||
level={5}
|
level={5}
|
||||||
style={{
|
style={{
|
||||||
@@ -39,7 +40,14 @@ const NotificationCenterComponent = ({
|
|||||||
alignItems: "center"
|
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}
|
RO #{notification.roNumber}
|
||||||
</Link>
|
</Link>
|
||||||
<Text type="secondary">{new Date(notification.created_at).toLocaleString()}</Text>
|
<Text type="secondary">{new Date(notification.created_at).toLocaleString()}</Text>
|
||||||
@@ -75,7 +83,6 @@ const NotificationCenterComponent = ({
|
|||||||
style={{ height: "400px", width: "100%" }}
|
style={{ height: "400px", width: "100%" }}
|
||||||
data={notifications}
|
data={notifications}
|
||||||
totalCount={notifications.length}
|
totalCount={notifications.length}
|
||||||
overscan={200}
|
|
||||||
endReached={loadMore}
|
endReached={loadMore}
|
||||||
itemContent={renderNotification}
|
itemContent={renderNotification}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import NotificationCenterComponent from "./notification-center.component";
|
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 { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
@@ -38,7 +43,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
|||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: "cache-and-network",
|
||||||
notifyOnNetworkStatusChange: true,
|
notifyOnNetworkStatusChange: true,
|
||||||
pollInterval: isConnected ? 0 : 30000,
|
pollInterval: isConnected ? 0 : 30000,
|
||||||
skip: !userAssociationId, // Skip query if no userAssociationId
|
skip: !userAssociationId,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
console.error("GET_NOTIFICATIONS error:", err);
|
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(() => {
|
useEffect(() => {
|
||||||
if (data?.notifications) {
|
if (data?.notifications) {
|
||||||
const processedNotifications = data.notifications
|
const processedNotifications = data.notifications
|
||||||
@@ -167,12 +219,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
|||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
setNotifications((prev) => {
|
setNotifications((prev) => {
|
||||||
const updatedNotifications = prev.map((notif) =>
|
const updatedNotifications = prev.map((notif) =>
|
||||||
notif.read === null && notif.associationid === userAssociationId
|
notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif
|
||||||
? {
|
|
||||||
...notif,
|
|
||||||
read: timestamp
|
|
||||||
}
|
|
||||||
: notif
|
|
||||||
);
|
);
|
||||||
return [...updatedNotifications];
|
return [...updatedNotifications];
|
||||||
});
|
});
|
||||||
@@ -180,6 +227,24 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
|||||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`));
|
.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(() => {
|
useEffect(() => {
|
||||||
if (visible && !isConnected) {
|
if (visible && !isConnected) {
|
||||||
refetch();
|
refetch();
|
||||||
@@ -197,6 +262,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
|||||||
toggleUnreadOnly={handleToggleUnreadOnly}
|
toggleUnreadOnly={handleToggleUnreadOnly}
|
||||||
markAllRead={handleMarkAllRead}
|
markAllRead={handleMarkAllRead}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
onNotificationClick={handleNotificationClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -118,10 +118,12 @@ const notificationScenarios = [
|
|||||||
{
|
{
|
||||||
key: "supplement-imported",
|
key: "supplement-imported",
|
||||||
builder: supplementImportedBuilder
|
builder: supplementImportedBuilder
|
||||||
|
// spans multiple tables,
|
||||||
},
|
},
|
||||||
// This one may be tricky as the jobid is not directly in the event data (this is probably wrong)
|
// This one may be tricky as the jobid is not directly in the event data (this is probably wrong)
|
||||||
// (should otherwise)
|
// (should otherwise)
|
||||||
// Status needs to mark meta data 'md_backorderd' for example
|
// Status needs to mark meta data 'md_backorderd' for example
|
||||||
|
// Double check Jobid
|
||||||
{
|
{
|
||||||
key: "part-marked-back-ordered",
|
key: "part-marked-back-ordered",
|
||||||
table: "joblines",
|
table: "joblines",
|
||||||
|
|||||||
Reference in New Issue
Block a user