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 ( + + + + + + + + + + + ); +} /** * 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}/`); } }} />