[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:
Oliver 2023-09-14 23:44:35 +10:00 committed by GitHub
parent 1ed3d21a00
commit 004dcd04d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 317 additions and 6 deletions

View File

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

View File

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

View 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>
);
}

View File

@ -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}
/>
);
}

View 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>
</>
);
}

View File

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