From 411b35decdc3a2cd331170b19b819c947a7167d7 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 3 Sep 2023 16:36:44 +0200 Subject: [PATCH] notifications --- public/locales/de/translation.json | 10 ++ public/locales/en/translation.json | 10 ++ src/App.js | 47 +++---- src/Components/Header/index.js | 165 ++++++++++++++++++++++++ src/Components/PageContent/index.js | 33 +---- src/Contexts/HeaderContext.js | 29 +++++ src/Contexts/WebSocketContext.js | 4 + src/Handlers/WebSocketMessageHandler.js | 43 ++++++ 8 files changed, 293 insertions(+), 48 deletions(-) create mode 100644 src/Components/Header/index.js create mode 100644 src/Contexts/HeaderContext.js diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 44cca96..fa63377 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -392,5 +392,15 @@ }, "createEquipmentDocumentationModal": { "buttonCreateDocumentation": "Dokumentation erstellen" + }, + "header": { + "notificationDrawer": { + "title": "Benachrichtigungen", + "deleteAllButtonText": "Alle löschen", + "deleteAllPopconfirm": { + "title": "Sind Sie sicher, dass Sie alle Benachrichtigungen löschen möchten?" + }, + "noNotifications": "Keine Benachrichtigungen" + } } } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 580a30c..1502c93 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -391,5 +391,15 @@ }, "createEquipmentDocumentationModal": { "buttonCreateDocumentation": "Create Documentation" + }, + "header": { + "notificationDrawer": { + "title": "Notifications", + "deleteAllButtonText": "Delete All", + "deleteAllPopconfirm": { + "title": "Are you sure you want to delete all notifications?" + }, + "noNotifications": "No notifications" + } } } diff --git a/src/App.js b/src/App.js index d753e68..996f6af 100644 --- a/src/App.js +++ b/src/App.js @@ -12,6 +12,7 @@ import { GroupTasksProvider } from "./Contexts/GroupTasksContext"; import { AdminAreaRolesProvider } from "./Contexts/AdminAreaRolesContext"; import { UserProfileProvider } from "./Contexts/UserProfileContext"; import { UsersProvider } from "./Contexts/UsersContext"; +import HeaderProvider from "./Contexts/HeaderContext"; export default function App() { const [notificationApi, notificationContextHolder] = @@ -35,30 +36,32 @@ export default function App() { {notificationContextHolder} - - - - - - - - - + + + + + + - - - - - - + isWebSocketReady={isWebSocketReady} + setIsWebSocketReady={setIsWebSocketReady} + notificationApi={notificationApi} + > + + + + + + + + + + ); diff --git a/src/Components/Header/index.js b/src/Components/Header/index.js new file mode 100644 index 0000000..e6cfb6a --- /dev/null +++ b/src/Components/Header/index.js @@ -0,0 +1,165 @@ +import { + BellOutlined, + CheckCircleOutlined, + CheckOutlined, + CloseCircleOutlined, + CloseOutlined, + DeleteOutlined, + ExclamationCircleOutlined, + InboxOutlined, + InfoCircleOutlined, + MenuFoldOutlined, + MenuUnfoldOutlined, + QuestionCircleOutlined, +} from "@ant-design/icons"; +import { Badge, Button, Drawer, List, Popconfirm, Typography } from "antd"; +import { Header } from "antd/es/layout/layout"; +import { useEffect, useState } from "react"; +import { useHeaderContext } from "../../Contexts/HeaderContext"; +import { FormatDatetime, myFetch } from "../../utils"; +import { useWebSocketContext } from "../../Contexts/WebSocketContext"; +import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler"; +import { useTranslation } from "react-i18next"; + +export default function HeaderMenu({ + isSideMenuCollapsed, + setIsSideMenuCollapsed, +}) { + const webSocketContext = useWebSocketContext(); + const headerContext = useHeaderContext(); + const { t } = useTranslation(); + const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] = + useState(false); + + useEffect(() => { + // fetch will only be called if the drawer is open and there are no notifications + // further notifications will be fetched by the websocket + if (!isNotificationDrawerOpen || headerContext.notifications !== null) + return; + + myFetch("/notifications", "GET").then((data) => + headerContext.setNotifications(data.Notifications) + ); + }, [isNotificationDrawerOpen]); + + return ( +
+ + + ) + } + > + {headerContext.totalNotifications === 0 || + headerContext.notifications === null ? ( +
+ + + {t("header.notificationDrawer.noNotifications")} + +
+ ) : ( + <> + { + return new Date(b.CreatedAt) - new Date(a.CreatedAt); + })} + renderItem={(item) => ( + + } + title={item.Title} + description={FormatDatetime(item.CreatedAt)} + /> + + + webSocketContext.SendSocketMessage( + SentMessagesCommands.DeleteOneNotification, + { + notificationId: item.Id, + } + ) + } + /> + + )} + /> + + )} + +
+ ); +} + +function NotificationTypeIcon({ type }) { + switch (type) { + case 1: + return ; + case 2: + return ; + case 3: + return ( + + ); + case 4: + return ; + default: + return ; + } +} diff --git a/src/Components/PageContent/index.js b/src/Components/PageContent/index.js index e1c0c31..10eff7b 100644 --- a/src/Components/PageContent/index.js +++ b/src/Components/PageContent/index.js @@ -1,7 +1,7 @@ -import { Content, Header } from "antd/es/layout/layout"; +import { Content } from "antd/es/layout/layout"; import AppRoutes from "../AppRoutes"; -import { Button, Layout } from "antd"; -import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons"; +import { Layout } from "antd"; +import HeaderMenu from "../Header"; export default function PageContent({ isSideMenuCollapsed, @@ -9,29 +9,10 @@ export default function PageContent({ }) { return ( -
-
+ useContext(HeaderContext); + +export default function HeaderProvider({ children }) { + const [totalNotifications, setTotalNotifications] = useState(0); + // initially null, then set to [...] on first fetch + const [notifications, setNotifications] = useState(null); + + return ( + + {children} + + ); +} diff --git a/src/Contexts/WebSocketContext.js b/src/Contexts/WebSocketContext.js index f5977ab..311c22d 100644 --- a/src/Contexts/WebSocketContext.js +++ b/src/Contexts/WebSocketContext.js @@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom"; import { useUserProfileContext } from "./UserProfileContext"; import { useAdminAreaRolesContext } from "./AdminAreaRolesContext"; import { useUsersContext } from "./UsersContext"; +import { useHeaderContext } from "./HeaderContext"; const WebSocketContext = createContext(null); @@ -25,6 +26,7 @@ export default function WebSocketProvider({ const wsMessageCache = useRef([]); const navigate = useNavigate(); const appContext = useAppContext(); + const headerContext = useHeaderContext(); const sideBarContext = useSideBarContext(); const groupTasksContext = useGroupTasksContext(); const userProfileContext = useUserProfileContext(); @@ -45,6 +47,7 @@ export default function WebSocketProvider({ data.Permissions === null ? [] : data.Permissions ); appContext.setUsers(data.Users); + headerContext.setTotalNotifications(data.TotalNotifications); sideBarContext.setUsername(data.Username); sideBarContext.setAvatar(data.Avatar); sideBarContext.setAvailableCategoryGroups(data.AvailableCategoryGroups); @@ -67,6 +70,7 @@ export default function WebSocketProvider({ notificationApi, sideBarContext, appContext, + headerContext, groupTasksContext, userProfileContext, adminAreaRolesContext, diff --git a/src/Handlers/WebSocketMessageHandler.js b/src/Handlers/WebSocketMessageHandler.js index 5f23bac..712a540 100644 --- a/src/Handlers/WebSocketMessageHandler.js +++ b/src/Handlers/WebSocketMessageHandler.js @@ -42,6 +42,9 @@ export const ReceivedMessagesCommands = { InstallingGlobalPythonPackagesFinished: 38, UpdateUsers: 39, CheckingForGroupTasksCategoryGroupChanges: 40, + NewNotification: 41, + AllNotificationsDeleted: 42, + OneNotificationDeleted: 43, }; // commands sent to the backend server @@ -69,6 +72,8 @@ export const SentMessagesCommands = { GroupTasksInstallPythonPackages: 21, GroupTasksInstallGlobalPythonPackages: 22, SubscribeToTopic: 23, + DeleteAllNotifications: 24, + DeleteOneNotification: 25, }; /* @@ -83,6 +88,7 @@ export function handleWebSocketMessage( notificationApi, sideBarContext, appContext, + headerContext, groupTasksContext, userProfileContext, adminAreaRolesContext, @@ -962,6 +968,43 @@ export function handleWebSocketMessage( description: `This may take a while`, }); break; + case ReceivedMessagesCommands.NewNotification: + headerContext.setTotalNotifications( + (totalNotifications) => totalNotifications + 1 + ); + + headerContext.setNotifications((arr) => { + // only add notifications to the list if the list is not null + // this has to do with the get fetch that is executed when the list is empty + if (arr === null) return arr; + + const newArr = [...arr]; + + newArr.push(body); + + return newArr; + }); + break; + case ReceivedMessagesCommands.AllNotificationsDeleted: + headerContext.setTotalNotifications(0); + + headerContext.setNotifications([]); + break; + case ReceivedMessagesCommands.OneNotificationDeleted: + headerContext.setTotalNotifications( + (totalNotifications) => totalNotifications - 1 + ); + + headerContext.setNotifications((arr) => { + if (arr === null) return arr; + + let newArr = [...arr]; + + newArr = newArr.filter((notification) => notification.Id !== body); + + return newArr; + }); + break; default: console.error("unknown command", cmd); break;