diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index db586544e4..002f98c3fe 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -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
diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py
index 69b13e96ea..94d9952191 100644
--- a/src/backend/InvenTree/InvenTree/helpers.py
+++ b/src/backend/InvenTree/InvenTree/helpers.py
@@ -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')
diff --git a/src/frontend/src/components/nav/NotificationDrawer.tsx b/src/frontend/src/components/nav/NotificationDrawer.tsx
index ce3d4d43ea..2fa64791ee 100644
--- a/src/frontend/src/components/nav/NotificationDrawer.tsx
+++ b/src/frontend/src/components/nav/NotificationDrawer.tsx
@@ -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 (
+
+
+
+
+ {
+ if (link) {
+ // Mark the notification as read
+ onRead();
+ }
+
+ if (link.startsWith('/')) {
+ navigateToLink(link, navigate, event);
+ }
+ }}
+ >
+ {notification.name}
+
+ {notification.age_human}
+
+
+
+
+
+
+
+
+
+ );
+}
/**
* 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 (
}
>
-
-
- {!hasNotifications && (
-
- {t`You have no unread notifications.`}
-
- )}
- {hasNotifications &&
- notificationQuery.data?.results?.map((notification: any) => (
-
-
- {notification?.target?.link ? (
-
- {notification.target?.name ??
- notification.name ??
- t`Notification`}
-
- ) : (
-
- {notification.target?.name ??
- notification.name ??
- t`Notification`}
-
- )}
- {notification.age_human ?? ''}
-
-
- {
- let url = apiUrl(
- ApiEndpoints.notifications_list,
- notification.pk
- );
- api
- .patch(url, {
- read: true
- })
- .then((response) => {
- notificationQuery.refetch();
- });
- }}
- >
-
-
-
-
-
- ))}
- {notificationQuery.isFetching && (
-
-
-
- )}
-
+
+
+
+ {!hasNotifications && (
+
+ {t`You have no unread notifications.`}
+
+ )}
+ {hasNotifications &&
+ notificationQuery.data?.results?.map((notification: any) => (
+ markAsRead(notification)}
+ />
+ ))}
+ {notificationQuery.isFetching && (
+
+
+
+ )}
+
+
);
}
diff --git a/src/frontend/src/components/plugins/PluginDrawer.tsx b/src/frontend/src/components/plugins/PluginDrawer.tsx
index f8415cce28..a470930f90 100644
--- a/src/frontend/src/components/plugins/PluginDrawer.tsx
+++ b/src/frontend/src/components/plugins/PluginDrawer.tsx
@@ -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({
-
+
diff --git a/src/frontend/src/components/render/Generic.tsx b/src/frontend/src/components/render/Generic.tsx
index 3d04f990df..59a64236f1 100644
--- a/src/frontend/src/components/render/Generic.tsx
+++ b/src/frontend/src/components/render/Generic.tsx
@@ -21,6 +21,12 @@ export function RenderContentType({
return instance && ;
}
+export function RenderError({
+ instance
+}: Readonly): ReactNode {
+ return instance && ;
+}
+
export function RenderImportSession({
instance
}: {
diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx
index 5e01c8ff68..eda0cf3f3b 100644
--- a/src/frontend/src/components/render/Instance.tsx
+++ b/src/frontend/src/components/render/Instance.tsx
@@ -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 = {
diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx
index da3ef8eb8f..37cb068934 100644
--- a/src/frontend/src/components/render/ModelType.tsx
+++ b/src/frontend/src/components/render/ModelType.tsx
@@ -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/'
}
};
diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/src/enums/ModelType.tsx
index 15d36d2d36..1c78bd3e33 100644
--- a/src/frontend/src/enums/ModelType.tsx
+++ b/src/frontend/src/enums/ModelType.tsx
@@ -32,5 +32,6 @@ export enum ModelType {
reporttemplate = 'reporttemplate',
labeltemplate = 'labeltemplate',
pluginconfig = 'pluginconfig',
- contenttype = 'contenttype'
+ contenttype = 'contenttype',
+ error = 'error'
}
diff --git a/src/frontend/src/pages/Notifications.tsx b/src/frontend/src/pages/Notifications.tsx
index 4c4cc68a06..9d10dac123 100644
--- a/src/frontend/src/pages/Notifications.tsx
+++ b/src/frontend/src/pages/Notifications.tsx
@@ -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: ,
+ icon: ,
onClick: () => {
let url = apiUrl(ApiEndpoints.notifications_list, record.pk);
diff --git a/src/frontend/src/tables/settings/ErrorTable.tsx b/src/frontend/src/tables/settings/ErrorTable.tsx
index 9f3ebffc44..5d8accc4f8 100644
--- a/src/frontend/src/tables/settings/ErrorTable.tsx
+++ b/src/frontend/src/tables/settings/ErrorTable.tsx
@@ -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 ;
+ }
+
return (
@@ -47,7 +74,7 @@ function ErrorDetail({ error }: { error: any }) {
- {error.data.split('\n').map((line: string, index: number) => (
+ {error.data?.split('\n').map((line: string, index: number) => (
{line}
@@ -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}
- {t`Error Details`}}
- onClose={close}
- >
-
-
+ {
+ if (!pk) return;
+
+ return ;
+ }}
+ />
{
setSelectedError(row);
- open();
+ navigate(`${row.pk}/`);
}
}}
/>