mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
1ed3d21a00
commit
004dcd04d5
@ -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 />
|
||||
|
Loading…
Reference in New Issue
Block a user