@@ -1,5 +1,5 @@
|
|||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Alert, Badge, Button, Space, Spin, Tooltip, Typography } from "antd";
|
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -11,7 +11,7 @@ const { Text, Title } = Typography;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification Center Component
|
* Notification Center Component
|
||||||
* @type {React.ForwardRefExoticComponent<React.PropsWithoutRef<{readonly visible?: *, readonly onClose?: *, readonly notifications?: *, readonly loading?: *, readonly error?: *, readonly showUnreadOnly?: *, readonly toggleUnreadOnly?: *, readonly markAllRead?: *, readonly loadMore?: *, readonly onNotificationClick?: *, readonly unreadCount?: *}> & React.RefAttributes<unknown>>}
|
* @type {React.ForwardRefExoticComponent<React.PropsWithoutRef<{readonly visible?: *, readonly onClose?: *, readonly notifications?: *, readonly loading?: *, readonly showUnreadOnly?: *, readonly toggleUnreadOnly?: *, readonly markAllRead?: *, readonly loadMore?: *, readonly onNotificationClick?: *, readonly unreadCount?: *}> & React.RefAttributes<unknown>>}
|
||||||
*/
|
*/
|
||||||
const NotificationCenterComponent = forwardRef(
|
const NotificationCenterComponent = forwardRef(
|
||||||
(
|
(
|
||||||
@@ -20,7 +20,6 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
onClose,
|
onClose,
|
||||||
notifications,
|
notifications,
|
||||||
loading,
|
loading,
|
||||||
error,
|
|
||||||
showUnreadOnly,
|
showUnreadOnly,
|
||||||
toggleUnreadOnly,
|
toggleUnreadOnly,
|
||||||
markAllRead,
|
markAllRead,
|
||||||
@@ -79,20 +78,23 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
<div className="notification-header">
|
<div className="notification-header">
|
||||||
<Space direction="horizontal">
|
<Space direction="horizontal">
|
||||||
<h3>{t("notifications.labels.notification-center")}</h3>
|
<h3>{t("notifications.labels.notification-center")}</h3>
|
||||||
{loading && !error && <Spin spinning={loading} size="small"></Spin>}
|
{loading && <Spin spinning={loading} size="small"></Spin>}
|
||||||
</Space>
|
</Space>
|
||||||
<div className="notification-controls">
|
<div className="notification-controls">
|
||||||
<Tooltip title={t("notifications.labels.show-unread-only")}>
|
<Tooltip title={t("notifications.labels.show-unread-only")}>
|
||||||
<Button
|
<Space size={4} align="center" className="notification-toggle">
|
||||||
type="link"
|
{" "}
|
||||||
icon={showUnreadOnly ? <EyeFilled /> : <EyeOutlined />}
|
{showUnreadOnly ? (
|
||||||
onClick={() => toggleUnreadOnly(!showUnreadOnly)}
|
<EyeFilled className="notification-toggle-icon" />
|
||||||
className={showUnreadOnly ? "active" : ""}
|
) : (
|
||||||
/>
|
<EyeOutlined className="notification-toggle-icon" />
|
||||||
|
)}
|
||||||
|
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
|
||||||
|
</Space>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type={!unreadCount ? "default" : "primary"}
|
||||||
icon={!unreadCount ? <CheckCircleFilled /> : <CheckCircleOutlined />}
|
icon={!unreadCount ? <CheckCircleFilled /> : <CheckCircleOutlined />}
|
||||||
onClick={markAllRead}
|
onClick={markAllRead}
|
||||||
disabled={!unreadCount}
|
disabled={!unreadCount}
|
||||||
@@ -100,7 +102,6 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && <Alert message={error} type="error" closable onClose={() => onClose()} />}
|
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{ height: "400px", width: "100%" }}
|
style={{ height: "400px", width: "100%" }}
|
||||||
data={notifications}
|
data={notifications}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||||
const notificationRef = useRef(null); // Add ref for the notification center
|
const notificationRef = useRef(null); // Add ref for the notification center
|
||||||
|
|
||||||
@@ -37,13 +36,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
||||||
}, [baseWhereClause, showUnreadOnly]);
|
}, [baseWhereClause, showUnreadOnly]);
|
||||||
|
|
||||||
const {
|
const { data, fetchMore, loading, refetch } = useQuery(GET_NOTIFICATIONS, {
|
||||||
data,
|
|
||||||
fetchMore,
|
|
||||||
loading,
|
|
||||||
error: queryError,
|
|
||||||
refetch
|
|
||||||
} = useQuery(GET_NOTIFICATIONS, {
|
|
||||||
variables: {
|
variables: {
|
||||||
limit: INITIAL_NOTIFICATIONS,
|
limit: INITIAL_NOTIFICATIONS,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -54,7 +47,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
skip: !userAssociationId,
|
skip: !userAssociationId,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.message);
|
|
||||||
console.error(`Error polling Notifications in notification-center: ${err?.message || ""}`);
|
console.error(`Error polling Notifications in notification-center: ${err?.message || ""}`);
|
||||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||||
}
|
}
|
||||||
@@ -63,6 +55,11 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
// Handle click outside to close
|
// Handle click outside to close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
|
// Prevent open + close behavior from the header
|
||||||
|
if (event.target.closest("#header-notifications")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) {
|
if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -104,16 +101,9 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
setNotifications(processedNotifications);
|
setNotifications(processedNotifications);
|
||||||
setError(null);
|
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (queryError) {
|
|
||||||
setError(queryError.message);
|
|
||||||
}
|
|
||||||
}, [queryError]);
|
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (!loading && data?.notifications.length) {
|
if (!loading && data?.notifications.length) {
|
||||||
fetchMore({
|
fetchMore({
|
||||||
@@ -125,7 +115,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
setError(err.message);
|
|
||||||
console.error("Fetch more error:", err);
|
console.error("Fetch more error:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -187,7 +176,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
|
||||||
showUnreadOnly={showUnreadOnly}
|
showUnreadOnly={showUnreadOnly}
|
||||||
toggleUnreadOnly={handleToggleUnreadOnly}
|
toggleUnreadOnly={handleToggleUnreadOnly}
|
||||||
markAllRead={handleMarkAllRead}
|
markAllRead={handleMarkAllRead}
|
||||||
|
|||||||
@@ -36,6 +36,38 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
// Styles for the eye icon and switch (custom classes)
|
||||||
|
.notification-toggle {
|
||||||
|
align-items: center; // Ensure vertical alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toggle-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1677ff;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-switch {
|
||||||
|
&.ant-switch-small {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
.ant-switch-handle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-switch-checked {
|
||||||
|
background-color: #1677ff;
|
||||||
|
.ant-switch-handle {
|
||||||
|
left: calc(100% - 14px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles for the "Mark All Read" button (restore original link button style)
|
||||||
.ant-btn-link {
|
.ant-btn-link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
@@ -67,16 +99,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification-item {
|
.notification-item {
|
||||||
padding: 12px 16px; // Increased padding from 8px to 12px for more space
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer; // Add pointer cursor to indicate clickability
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #fafafa; // Optional: Add hover effect for better UX
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-content {
|
.notification-content {
|
||||||
|
|||||||
@@ -3779,8 +3779,8 @@
|
|||||||
"teams-search": "Search for a Team",
|
"teams-search": "Search for a Team",
|
||||||
"add-watchers-team": "Add Team Members",
|
"add-watchers-team": "Add Team Members",
|
||||||
"new-notification-title": "New Notification:",
|
"new-notification-title": "New Notification:",
|
||||||
"show-unread-only": "Show Unread",
|
"show-unread-only": "Show Unread Only",
|
||||||
"mark-all-read": "Mark Read",
|
"mark-all-read": "Mark All Read",
|
||||||
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
||||||
"ro-number": "RO #{{ro_number}}",
|
"ro-number": "RO #{{ro_number}}",
|
||||||
"no-watchers": "No Watchers",
|
"no-watchers": "No Watchers",
|
||||||
|
|||||||
Reference in New Issue
Block a user