mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +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 { 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 { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { api } from '../../App';
|
||||||
import { navTabs as mainNavTabs } from '../../defaults/links';
|
import { navTabs as mainNavTabs } from '../../defaults/links';
|
||||||
import { InvenTreeStyle } from '../../globalStyle';
|
import { InvenTreeStyle } from '../../globalStyle';
|
||||||
import { ScanButton } from '../items/ScanButton';
|
import { ScanButton } from '../items/ScanButton';
|
||||||
import { MainMenu } from './MainMenu';
|
import { MainMenu } from './MainMenu';
|
||||||
import { NavHoverMenu } from './NavHoverMenu';
|
import { NavHoverMenu } from './NavHoverMenu';
|
||||||
import { NavigationDrawer } from './NavigationDrawer';
|
import { NavigationDrawer } from './NavigationDrawer';
|
||||||
|
import { NotificationDrawer } from './NotificationDrawer';
|
||||||
import { SearchDrawer } from './SearchDrawer';
|
import { SearchDrawer } from './SearchDrawer';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
@ -20,10 +24,46 @@ export function Header() {
|
|||||||
{ open: openSearchDrawer, close: closeSearchDrawer }
|
{ open: openSearchDrawer, close: closeSearchDrawer }
|
||||||
] = useDisclosure(false);
|
] = 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 (
|
return (
|
||||||
<div className={classes.layoutHeader}>
|
<div className={classes.layoutHeader}>
|
||||||
<SearchDrawer opened={searchDrawerOpened} onClose={closeSearchDrawer} />
|
<SearchDrawer opened={searchDrawerOpened} onClose={closeSearchDrawer} />
|
||||||
<NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} />
|
<NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} />
|
||||||
|
<NotificationDrawer
|
||||||
|
opened={notificationDrawerOpened}
|
||||||
|
onClose={closeNotificationDrawer}
|
||||||
|
/>
|
||||||
<Container className={classes.layoutHeaderSection} size={'xl'}>
|
<Container className={classes.layoutHeaderSection} size={'xl'}>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group>
|
<Group>
|
||||||
@ -35,6 +75,17 @@ export function Header() {
|
|||||||
<ActionIcon onClick={openSearchDrawer}>
|
<ActionIcon onClick={openSearchDrawer}>
|
||||||
<IconSearch />
|
<IconSearch />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
<ActionIcon onClick={openNotificationDrawer}>
|
||||||
|
<Indicator
|
||||||
|
radius="lg"
|
||||||
|
size="18"
|
||||||
|
label={notificationCount}
|
||||||
|
color="red"
|
||||||
|
disabled={notificationCount <= 0}
|
||||||
|
>
|
||||||
|
<IconBell />
|
||||||
|
</Indicator>
|
||||||
|
</ActionIcon>
|
||||||
<MainMenu />
|
<MainMenu />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -34,10 +34,6 @@ export function MainMenu() {
|
|||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item icon={<IconHeart />}>
|
|
||||||
<Trans>Notifications</Trans>
|
|
||||||
<PlaceholderPill />
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item icon={<IconUserCircle />}>
|
<Menu.Item icon={<IconUserCircle />}>
|
||||||
<Trans>Profile</Trans> <PlaceholderPill />
|
<Trans>Profile</Trans> <PlaceholderPill />
|
||||||
</Menu.Item>
|
</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'))
|
lazy(() => import('./pages/Index/Dashboard'))
|
||||||
);
|
);
|
||||||
export const ErrorPage = Loadable(lazy(() => import('./pages/ErrorPage')));
|
export const ErrorPage = Loadable(lazy(() => import('./pages/ErrorPage')));
|
||||||
|
|
||||||
|
export const Notifications = Loadable(
|
||||||
|
lazy(() => import('./pages/Notifications'))
|
||||||
|
);
|
||||||
|
|
||||||
export const Profile = Loadable(
|
export const Profile = Loadable(
|
||||||
lazy(() => import('./pages/Index/Profile/Profile'))
|
lazy(() => import('./pages/Index/Profile/Profile'))
|
||||||
);
|
);
|
||||||
@ -60,6 +65,10 @@ export const router = createBrowserRouter(
|
|||||||
path: 'dashboard/',
|
path: 'dashboard/',
|
||||||
element: <Dashboard />
|
element: <Dashboard />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'notifications/',
|
||||||
|
element: <Notifications />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'playground/',
|
path: 'playground/',
|
||||||
element: <Playground />
|
element: <Playground />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user