mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[PUI] notifications fix (#8392)
* Include actual model name in notification repot * Include model_id in API * Refactoring for NotificationDrawer - Add error boundary - Separate rendering func for individual entry - Allow router navigation - Better link introspection - Add tooltip for notification description * Add URL support for errors * Allow navigation to an individual error in the admin center * Updates * Add rendering method for 'error' model type
This commit is contained in:
parent
40f456fbc9
commit
8a7a2da07b
@ -1,13 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 273
|
||||
INVENTREE_API_VERSION = 274
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v274 - 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392
|
||||
- Add more detailed information to NotificationEntry API serializer
|
||||
|
||||
v273 - 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376
|
||||
- Fixes for the BuildLine API endpoint
|
||||
|
||||
|
@ -1022,7 +1022,14 @@ def get_objectreference(
|
||||
ret = {}
|
||||
if url_fnc:
|
||||
ret['link'] = url_fnc()
|
||||
return {'name': str(item), 'model': str(model_cls._meta.verbose_name), **ret}
|
||||
|
||||
return {
|
||||
'name': str(item),
|
||||
'model_name': str(model_cls._meta.verbose_name),
|
||||
'model_type': str(model_cls._meta.model_name),
|
||||
'model_id': getattr(item, 'pk', None),
|
||||
**ret,
|
||||
}
|
||||
|
||||
|
||||
Inheritors_T = TypeVar('Inheritors_T')
|
||||
|
@ -2,12 +2,13 @@ import { t } from '@lingui/macro';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Anchor,
|
||||
Center,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
Loader,
|
||||
Space,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip
|
||||
@ -15,14 +16,84 @@ import {
|
||||
import { IconArrowRight, IconBellCheck } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { navigateToLink } from '../../functions/navigation';
|
||||
import { getDetailUrl } from '../../functions/urls';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import { ModelInformationDict } from '../render/ModelType';
|
||||
|
||||
/**
|
||||
* Render a single notification entry in the drawer
|
||||
*/
|
||||
function NotificationEntry({
|
||||
notification,
|
||||
onRead
|
||||
}: {
|
||||
notification: any;
|
||||
onRead: () => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
let link = notification.target?.link;
|
||||
|
||||
let model_type = notification.target?.model_type;
|
||||
let model_id = notification.target?.model_id;
|
||||
|
||||
// If a valid model type is provided, that overrides the specified link
|
||||
if (model_type as ModelType) {
|
||||
let model_info = ModelInformationDict[model_type as ModelType];
|
||||
if (model_info?.url_detail && model_id) {
|
||||
link = getDetailUrl(model_type as ModelType, model_id);
|
||||
} else if (model_info?.url_overview) {
|
||||
link = model_info.url_overview;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper p="xs" shadow="xs">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Tooltip
|
||||
label={notification.message}
|
||||
position="bottom-end"
|
||||
hidden={!notification.message}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Anchor
|
||||
href={link}
|
||||
underline="hover"
|
||||
target="_blank"
|
||||
onClick={(event: any) => {
|
||||
if (link) {
|
||||
// Mark the notification as read
|
||||
onRead();
|
||||
}
|
||||
|
||||
if (link.startsWith('/')) {
|
||||
navigateToLink(link, navigate, event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text size="sm">{notification.name}</Text>
|
||||
</Anchor>
|
||||
<Text size="xs">{notification.age_human}</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
<Tooltip label={t`Mark as read`} position="bottom-end">
|
||||
<ActionIcon variant="transparent" onClick={onRead}>
|
||||
<IconBellCheck />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a notification drawer.
|
||||
@ -46,7 +117,8 @@ export function NotificationDrawer({
|
||||
.get(apiUrl(ApiEndpoints.notifications_list), {
|
||||
params: {
|
||||
read: false,
|
||||
limit: 10
|
||||
limit: 10,
|
||||
ordering: '-creation'
|
||||
}
|
||||
})
|
||||
.then((response) => response.data)
|
||||
@ -73,6 +145,19 @@ export function NotificationDrawer({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const markAsRead = useCallback((notification: any) => {
|
||||
api
|
||||
.patch(apiUrl(ApiEndpoints.notifications_list, notification.pk), {
|
||||
read: true
|
||||
})
|
||||
.then(() => {
|
||||
notificationQuery.refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
notificationQuery.refetch();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
@ -117,67 +202,29 @@ export function NotificationDrawer({
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Divider />
|
||||
{!hasNotifications && (
|
||||
<Alert color="green">
|
||||
<Text size="sm">{t`You have no unread notifications.`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{hasNotifications &&
|
||||
notificationQuery.data?.results?.map((notification: any) => (
|
||||
<Group justify="space-between" key={notification.pk}>
|
||||
<Stack gap="3">
|
||||
{notification?.target?.link ? (
|
||||
<Text
|
||||
size="sm"
|
||||
component={Link}
|
||||
to={notification?.target?.link}
|
||||
target="_blank"
|
||||
>
|
||||
{notification.target?.name ??
|
||||
notification.name ??
|
||||
t`Notification`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm">
|
||||
{notification.target?.name ??
|
||||
notification.name ??
|
||||
t`Notification`}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs">{notification.age_human ?? ''}</Text>
|
||||
</Stack>
|
||||
<Space />
|
||||
<ActionIcon
|
||||
color="gray"
|
||||
variant="hover"
|
||||
onClick={() => {
|
||||
let url = apiUrl(
|
||||
ApiEndpoints.notifications_list,
|
||||
notification.pk
|
||||
);
|
||||
api
|
||||
.patch(url, {
|
||||
read: true
|
||||
})
|
||||
.then((response) => {
|
||||
notificationQuery.refetch();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t`Mark as read`}>
|
||||
<IconBellCheck />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
{notificationQuery.isFetching && (
|
||||
<Center>
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
<Boundary label="NotificationDrawer">
|
||||
<Stack gap="xs">
|
||||
<Divider />
|
||||
{!hasNotifications && (
|
||||
<Alert color="green">
|
||||
<Text size="sm">{t`You have no unread notifications.`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{hasNotifications &&
|
||||
notificationQuery.data?.results?.map((notification: any) => (
|
||||
<NotificationEntry
|
||||
key={`notification-${notification.pk}`}
|
||||
notification={notification}
|
||||
onRead={() => markAsRead(notification)}
|
||||
/>
|
||||
))}
|
||||
{notificationQuery.isFetching && (
|
||||
<Center>
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Boundary>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { t } from '@lingui/macro';
|
||||
import { Accordion, Alert, Card, Stack, Text } from '@mantine/core';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
@ -18,12 +19,18 @@ export default function PluginDrawer({
|
||||
pluginKey,
|
||||
pluginInstance
|
||||
}: {
|
||||
pluginKey: string;
|
||||
pluginKey?: string;
|
||||
pluginInstance: PluginInterface;
|
||||
}) {
|
||||
const { id } = useParams();
|
||||
|
||||
const pluginPrimaryKey: string = useMemo(() => {
|
||||
return pluginKey || id || '';
|
||||
}, [pluginKey, id]);
|
||||
|
||||
const { instance: pluginAdmin } = useInstance({
|
||||
endpoint: ApiEndpoints.plugin_admin,
|
||||
pathParams: { key: pluginKey },
|
||||
pathParams: { key: pluginPrimaryKey },
|
||||
defaultValue: {},
|
||||
hasPrimaryKey: false,
|
||||
refetchOnMount: true
|
||||
@ -128,7 +135,7 @@ export default function PluginDrawer({
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Card withBorder>
|
||||
<PluginSettingList pluginKey={pluginKey} />
|
||||
<PluginSettingList pluginKey={pluginPrimaryKey} />
|
||||
</Card>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
@ -21,6 +21,12 @@ export function RenderContentType({
|
||||
return instance && <RenderInlineModel primary={instance.app_labeled_name} />;
|
||||
}
|
||||
|
||||
export function RenderError({
|
||||
instance
|
||||
}: Readonly<InstanceRenderInterface>): ReactNode {
|
||||
return instance && <RenderInlineModel primary={instance.name} />;
|
||||
}
|
||||
|
||||
export function RenderImportSession({
|
||||
instance
|
||||
}: {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
} from './Company';
|
||||
import {
|
||||
RenderContentType,
|
||||
RenderError,
|
||||
RenderImportSession,
|
||||
RenderProjectCode
|
||||
} from './Generic';
|
||||
@ -92,7 +93,8 @@ const RendererLookup: EnumDictionary<
|
||||
[ModelType.reporttemplate]: RenderReportTemplate,
|
||||
[ModelType.labeltemplate]: RenderLabelTemplate,
|
||||
[ModelType.pluginconfig]: RenderPlugin,
|
||||
[ModelType.contenttype]: RenderContentType
|
||||
[ModelType.contenttype]: RenderContentType,
|
||||
[ModelType.error]: RenderError
|
||||
};
|
||||
|
||||
export type RenderInstanceProps = {
|
||||
|
@ -252,6 +252,13 @@ export const ModelInformationDict: ModelDict = {
|
||||
label: () => t`Content Type`,
|
||||
label_multiple: () => t`Content Types`,
|
||||
api_endpoint: ApiEndpoints.content_type_list
|
||||
},
|
||||
error: {
|
||||
label: () => t`Error`,
|
||||
label_multiple: () => t`Errors`,
|
||||
api_endpoint: ApiEndpoints.error_report_list,
|
||||
url_overview: '/settings/admin/errors',
|
||||
url_detail: '/settings/admin/errors/:pk/'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -32,5 +32,6 @@ export enum ModelType {
|
||||
reporttemplate = 'reporttemplate',
|
||||
labeltemplate = 'labeltemplate',
|
||||
pluginconfig = 'pluginconfig',
|
||||
contenttype = 'contenttype'
|
||||
contenttype = 'contenttype',
|
||||
error = 'error'
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
IconBellCheck,
|
||||
IconBellExclamation,
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconMail,
|
||||
IconMailOpened,
|
||||
IconTrash
|
||||
} from '@tabler/icons-react';
|
||||
@ -106,7 +106,7 @@ export default function NotificationsPage() {
|
||||
actions={(record) => [
|
||||
{
|
||||
title: t`Mark as unread`,
|
||||
icon: <IconCircleX />,
|
||||
icon: <IconMail />,
|
||||
onClick: () => {
|
||||
let url = apiUrl(ApiEndpoints.notifications_list, record.pk);
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Drawer, Group, Stack, Table, Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { Group, Loader, Stack, Table, Text } from '@mantine/core';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { CopyButton } from '../../components/buttons/CopyButton';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { useDeleteApiFormModal } from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@ -14,7 +15,33 @@ import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction, RowDeleteAction } from '../RowActions';
|
||||
|
||||
function ErrorDetail({ error }: { error: any }) {
|
||||
function ErrorDetail({ errorId }: { errorId?: number }) {
|
||||
const { id } = useParams();
|
||||
|
||||
const errorPrimaryKey = useMemo(() => {
|
||||
return errorId ?? id;
|
||||
}, [errorId, id]);
|
||||
|
||||
const errorInstance = useInstance({
|
||||
endpoint: ApiEndpoints.error_report_list,
|
||||
pk: errorPrimaryKey,
|
||||
defaultValue: {},
|
||||
hasPrimaryKey: true,
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const error = useMemo(
|
||||
() => errorInstance.instance || {},
|
||||
[errorInstance.instance]
|
||||
);
|
||||
|
||||
if (
|
||||
errorInstance.instanceQuery.isFetching ||
|
||||
errorInstance.instanceQuery.isLoading
|
||||
) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Table>
|
||||
@ -47,7 +74,7 @@ function ErrorDetail({ error }: { error: any }) {
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<Stack gap={3}>
|
||||
{error.data.split('\n').map((line: string, index: number) => (
|
||||
{error.data?.split('\n').map((line: string, index: number) => (
|
||||
<Text size="xs" key={`error-line-${index}`}>
|
||||
{line}
|
||||
</Text>
|
||||
@ -67,8 +94,7 @@ function ErrorDetail({ error }: { error: any }) {
|
||||
export default function ErrorReportTable() {
|
||||
const table = useTable('error-report');
|
||||
const user = useUserState();
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
@ -116,15 +142,15 @@ export default function ErrorReportTable() {
|
||||
return (
|
||||
<>
|
||||
{deleteErrorModal.modal}
|
||||
<Drawer
|
||||
opened={opened}
|
||||
size="xl"
|
||||
position="right"
|
||||
title={<StylishText>{t`Error Details`}</StylishText>}
|
||||
onClose={close}
|
||||
>
|
||||
<ErrorDetail error={selectedError} />
|
||||
</Drawer>
|
||||
<DetailDrawer
|
||||
title={t`Error Details`}
|
||||
size={'xl'}
|
||||
renderContent={(pk) => {
|
||||
if (!pk) return;
|
||||
|
||||
return <ErrorDetail errorId={selectedError.pk} />;
|
||||
}}
|
||||
/>
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.error_report_list)}
|
||||
tableState={table}
|
||||
@ -135,7 +161,7 @@ export default function ErrorReportTable() {
|
||||
rowActions: rowActions,
|
||||
onRowClick: (row) => {
|
||||
setSelectedError(row);
|
||||
open();
|
||||
navigate(`${row.pk}/`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user