Merged in release/2024-03-14 (pull request #2152)

Release/2024 03 14
This commit is contained in:
Dave Richer
2025-03-05 18:56:08 +00:00
4 changed files with 56 additions and 35 deletions

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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",