mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	[PUI]: Basic notifications page (#5537)
* Add notification indicator to the navbar * Fetch every 30 seconds * Basic notifications drawer * Simple page for rendering notification tables * Implement notification table actions * Move table component
This commit is contained in:
		| @@ -1,14 +1,18 @@ | ||||
| import { ActionIcon, Container, Group, Tabs } from '@mantine/core'; | ||||
| import { ActionIcon, Container, Group, Indicator, Tabs } from '@mantine/core'; | ||||
| import { useDisclosure } from '@mantine/hooks'; | ||||
| import { IconSearch } from '@tabler/icons-react'; | ||||
| import { IconBell, IconSearch } from '@tabler/icons-react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { useState } from 'react'; | ||||
| import { useNavigate, useParams } from 'react-router-dom'; | ||||
|  | ||||
| import { api } from '../../App'; | ||||
| import { navTabs as mainNavTabs } from '../../defaults/links'; | ||||
| import { InvenTreeStyle } from '../../globalStyle'; | ||||
| import { ScanButton } from '../items/ScanButton'; | ||||
| import { MainMenu } from './MainMenu'; | ||||
| import { NavHoverMenu } from './NavHoverMenu'; | ||||
| import { NavigationDrawer } from './NavigationDrawer'; | ||||
| import { NotificationDrawer } from './NotificationDrawer'; | ||||
| import { SearchDrawer } from './SearchDrawer'; | ||||
|  | ||||
| export function Header() { | ||||
| @@ -20,10 +24,46 @@ export function Header() { | ||||
|     { open: openSearchDrawer, close: closeSearchDrawer } | ||||
|   ] = useDisclosure(false); | ||||
|  | ||||
|   const [ | ||||
|     notificationDrawerOpened, | ||||
|     { open: openNotificationDrawer, close: closeNotificationDrawer } | ||||
|   ] = useDisclosure(false); | ||||
|  | ||||
|   const [notificationCount, setNotificationCount] = useState<number>(0); | ||||
|  | ||||
|   // Fetch number of notifications for the current user | ||||
|   const notifications = useQuery({ | ||||
|     queryKey: ['notification-count'], | ||||
|     queryFn: async () => { | ||||
|       return api | ||||
|         .get('/notifications/', { | ||||
|           params: { | ||||
|             read: false, | ||||
|             limit: 1 | ||||
|           } | ||||
|         }) | ||||
|         .then((response) => { | ||||
|           setNotificationCount(response.data.count); | ||||
|           return response.data; | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Error fetching notifications:', error); | ||||
|           return error; | ||||
|         }); | ||||
|     }, | ||||
|     refetchInterval: 30000, | ||||
|     refetchOnMount: true, | ||||
|     refetchOnWindowFocus: false | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <div className={classes.layoutHeader}> | ||||
|       <SearchDrawer opened={searchDrawerOpened} onClose={closeSearchDrawer} /> | ||||
|       <NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} /> | ||||
|       <NotificationDrawer | ||||
|         opened={notificationDrawerOpened} | ||||
|         onClose={closeNotificationDrawer} | ||||
|       /> | ||||
|       <Container className={classes.layoutHeaderSection} size={'xl'}> | ||||
|         <Group position="apart"> | ||||
|           <Group> | ||||
| @@ -35,6 +75,17 @@ export function Header() { | ||||
|             <ActionIcon onClick={openSearchDrawer}> | ||||
|               <IconSearch /> | ||||
|             </ActionIcon> | ||||
|             <ActionIcon onClick={openNotificationDrawer}> | ||||
|               <Indicator | ||||
|                 radius="lg" | ||||
|                 size="18" | ||||
|                 label={notificationCount} | ||||
|                 color="red" | ||||
|                 disabled={notificationCount <= 0} | ||||
|               > | ||||
|                 <IconBell /> | ||||
|               </Indicator> | ||||
|             </ActionIcon> | ||||
|             <MainMenu /> | ||||
|           </Group> | ||||
|         </Group> | ||||
|   | ||||
| @@ -34,10 +34,6 @@ export function MainMenu() { | ||||
|         </UnstyledButton> | ||||
|       </Menu.Target> | ||||
|       <Menu.Dropdown> | ||||
|         <Menu.Item icon={<IconHeart />}> | ||||
|           <Trans>Notifications</Trans> | ||||
|           <PlaceholderPill /> | ||||
|         </Menu.Item> | ||||
|         <Menu.Item icon={<IconUserCircle />}> | ||||
|           <Trans>Profile</Trans> <PlaceholderPill /> | ||||
|         </Menu.Item> | ||||
|   | ||||
							
								
								
									
										111
									
								
								src/frontend/src/components/nav/NotificationDrawer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/frontend/src/components/nav/NotificationDrawer.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Divider, | ||||
|   Drawer, | ||||
|   LoadingOverlay, | ||||
|   Space, | ||||
|   Tooltip | ||||
| } from '@mantine/core'; | ||||
| import { Badge, Group, Stack, Text } from '@mantine/core'; | ||||
| import { IconBellCheck, IconBellPlus, IconBookmark } from '@tabler/icons-react'; | ||||
| import { IconMacro } from '@tabler/icons-react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| import { api } from '../../App'; | ||||
|  | ||||
| /** | ||||
|  * Construct a notification drawer. | ||||
|  */ | ||||
| export function NotificationDrawer({ | ||||
|   opened, | ||||
|   onClose | ||||
| }: { | ||||
|   opened: boolean; | ||||
|   onClose: () => void; | ||||
| }) { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const notificationQuery = useQuery({ | ||||
|     enabled: opened, | ||||
|     queryKey: ['notifications', opened], | ||||
|     queryFn: async () => | ||||
|       api | ||||
|         .get('/notifications/', { | ||||
|           params: { | ||||
|             read: false, | ||||
|             limit: 10 | ||||
|           } | ||||
|         }) | ||||
|         .then((response) => response.data) | ||||
|         .catch((error) => { | ||||
|           console.error('Error fetching notifications:', error); | ||||
|           return error; | ||||
|         }), | ||||
|     refetchOnMount: false, | ||||
|     refetchOnWindowFocus: false | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <Drawer | ||||
|       opened={opened} | ||||
|       size="md" | ||||
|       position="right" | ||||
|       onClose={onClose} | ||||
|       withCloseButton={false} | ||||
|       styles={{ | ||||
|         header: { | ||||
|           width: '100%' | ||||
|         }, | ||||
|         title: { | ||||
|           width: '100%' | ||||
|         } | ||||
|       }} | ||||
|       title={ | ||||
|         <Group position="apart" noWrap={true}> | ||||
|           <Text size="lg">{t`Notifications`}</Text> | ||||
|           <ActionIcon | ||||
|             onClick={() => { | ||||
|               onClose(); | ||||
|               navigate('/notifications/'); | ||||
|             }} | ||||
|           > | ||||
|             <IconBellPlus /> | ||||
|           </ActionIcon> | ||||
|         </Group> | ||||
|       } | ||||
|     > | ||||
|       <Stack spacing="xs"> | ||||
|         <Divider /> | ||||
|         <LoadingOverlay visible={notificationQuery.isFetching} /> | ||||
|         {notificationQuery.data?.results.map((notification: any) => ( | ||||
|           <Group position="apart"> | ||||
|             <Stack spacing="3"> | ||||
|               <Text size="sm">{notification.target?.name ?? 'target'}</Text> | ||||
|               <Text size="xs">{notification.age_human ?? 'name'}</Text> | ||||
|             </Stack> | ||||
|             <Space /> | ||||
|             <ActionIcon | ||||
|               color="gray" | ||||
|               variant="hover" | ||||
|               onClick={() => { | ||||
|                 api | ||||
|                   .patch(`/notifications/${notification.pk}/`, { | ||||
|                     read: true | ||||
|                   }) | ||||
|                   .then((response) => { | ||||
|                     notificationQuery.refetch(); | ||||
|                   }); | ||||
|               }} | ||||
|             > | ||||
|               <Tooltip label={t`Mark as read`}> | ||||
|                 <IconBellCheck /> | ||||
|               </Tooltip> | ||||
|             </ActionIcon> | ||||
|           </Group> | ||||
|         ))} | ||||
|       </Stack> | ||||
|     </Drawer> | ||||
|   ); | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { useMemo } from 'react'; | ||||
|  | ||||
| import { TableColumn } from '../Column'; | ||||
| import { InvenTreeTable } from '../InvenTreeTable'; | ||||
| import { RowAction } from '../RowActions'; | ||||
|  | ||||
| export function NotificationTable({ | ||||
|   params, | ||||
|   refreshId, | ||||
|   tableKey, | ||||
|   actions | ||||
| }: { | ||||
|   params: any; | ||||
|   refreshId: string; | ||||
|   tableKey: string; | ||||
|   actions: (record: any) => RowAction[]; | ||||
| }) { | ||||
|   const columns: TableColumn[] = useMemo(() => { | ||||
|     return [ | ||||
|       { | ||||
|         accessor: 'age_human', | ||||
|         title: t`Age`, | ||||
|         sortable: true | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'category', | ||||
|         title: t`Category`, | ||||
|         sortable: true | ||||
|       }, | ||||
|       { | ||||
|         accessor: `name`, | ||||
|         title: t`Notification` | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'message', | ||||
|         title: t`Message` | ||||
|       } | ||||
|     ]; | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <InvenTreeTable | ||||
|       url="/notifications/" | ||||
|       tableKey={tableKey} | ||||
|       refreshId={refreshId} | ||||
|       params={params} | ||||
|       rowActions={actions} | ||||
|       columns={columns} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										92
									
								
								src/frontend/src/pages/Notifications.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/frontend/src/pages/Notifications.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Stack } from '@mantine/core'; | ||||
| import { IconBellCheck, IconBellExclamation } from '@tabler/icons-react'; | ||||
| import { useMemo } from 'react'; | ||||
|  | ||||
| import { api } from '../App'; | ||||
| import { StylishText } from '../components/items/StylishText'; | ||||
| import { PanelGroup } from '../components/nav/PanelGroup'; | ||||
| import { NotificationTable } from '../components/tables/notifications/NotificationsTable'; | ||||
| import { useTableRefresh } from '../hooks/TableRefresh'; | ||||
|  | ||||
| export default function NotificationsPage() { | ||||
|   const unreadRefresh = useTableRefresh(); | ||||
|   const historyRefresh = useTableRefresh(); | ||||
|  | ||||
|   const notificationPanels = useMemo(() => { | ||||
|     return [ | ||||
|       { | ||||
|         name: 'notifications-unread', | ||||
|         label: t`Notifications`, | ||||
|         icon: <IconBellExclamation size="18" />, | ||||
|         content: ( | ||||
|           <NotificationTable | ||||
|             params={{ read: false }} | ||||
|             refreshId={unreadRefresh.refreshId} | ||||
|             tableKey="notifications-unread" | ||||
|             actions={(record) => [ | ||||
|               { | ||||
|                 title: t`Mark as read`, | ||||
|                 onClick: () => { | ||||
|                   api | ||||
|                     .patch(`/notifications/${record.pk}/`, { | ||||
|                       read: true | ||||
|                     }) | ||||
|                     .then((response) => { | ||||
|                       unreadRefresh.refreshTable(); | ||||
|                     }); | ||||
|                 } | ||||
|               } | ||||
|             ]} | ||||
|           /> | ||||
|         ) | ||||
|       }, | ||||
|       { | ||||
|         name: 'notifications-history', | ||||
|         label: t`History`, | ||||
|         icon: <IconBellCheck size="18" />, | ||||
|         content: ( | ||||
|           <NotificationTable | ||||
|             params={{ read: true }} | ||||
|             refreshId={historyRefresh.refreshId} | ||||
|             tableKey="notifications-history" | ||||
|             actions={(record) => [ | ||||
|               { | ||||
|                 title: t`Mark as unread`, | ||||
|                 onClick: () => { | ||||
|                   api | ||||
|                     .patch(`/notifications/${record.pk}/`, { | ||||
|                       read: false | ||||
|                     }) | ||||
|                     .then((response) => { | ||||
|                       historyRefresh.refreshTable(); | ||||
|                     }); | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 title: t`Delete`, | ||||
|                 color: 'red', | ||||
|                 onClick: () => { | ||||
|                   api | ||||
|                     .delete(`/notifications/${record.pk}/`) | ||||
|                     .then((response) => { | ||||
|                       historyRefresh.refreshTable(); | ||||
|                     }); | ||||
|                 } | ||||
|               } | ||||
|             ]} | ||||
|           /> | ||||
|         ) | ||||
|       } | ||||
|     ]; | ||||
|   }, [historyRefresh, unreadRefresh]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Stack spacing="xs"> | ||||
|         <StylishText>{t`Notifications`}</StylishText> | ||||
|         <PanelGroup panels={notificationPanels} /> | ||||
|       </Stack> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -19,6 +19,11 @@ export const Dashboard = Loadable( | ||||
|   lazy(() => import('./pages/Index/Dashboard')) | ||||
| ); | ||||
| export const ErrorPage = Loadable(lazy(() => import('./pages/ErrorPage'))); | ||||
|  | ||||
| export const Notifications = Loadable( | ||||
|   lazy(() => import('./pages/Notifications')) | ||||
| ); | ||||
|  | ||||
| export const Profile = Loadable( | ||||
|   lazy(() => import('./pages/Index/Profile/Profile')) | ||||
| ); | ||||
| @@ -60,6 +65,10 @@ export const router = createBrowserRouter( | ||||
|           path: 'dashboard/', | ||||
|           element: <Dashboard /> | ||||
|         }, | ||||
|         { | ||||
|           path: 'notifications/', | ||||
|           element: <Notifications /> | ||||
|         }, | ||||
|         { | ||||
|           path: 'playground/', | ||||
|           element: <Playground /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user