From 53c16510a19d627d0bee93653ff5b02ba5609d07 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 26 Oct 2023 12:49:38 +0200 Subject: [PATCH] [PUI] Render status labels (#5759) * added deepsource coverage settings * Ignore missing coverage * trigger full CI run * typo * Added general status lookup endpoint * Bumped API version * cleaned up branch * Fixed PlaygroundArea accordion behaviour * Added dummy area for status labels * Added StatusRenderer skeleton * Fetch data from server * Made server api state session persistant * cleanup * Added StatusLabel lookups based on ModelType * Made use of translated status fields * Added new ModelTypes * Used new StatusRenderer * Simplified renderer * style fixes * revert style change * Squashed commit of the following: commit 5e8ea099068475fd257d8c172348dc6f3edf9bcf Author: Matthias Mair Date: Tue Oct 24 09:22:38 2023 +0200 Update ui_plattform.spec.ts commit 49da3312beff7fd6837ea741e621df221c445d19 Author: Matthias Mair Date: Tue Oct 24 07:56:25 2023 +0200 more logging commit 5337be4c3990051b805a6fce2e79ca4030b4afe5 Author: Matthias Mair Date: Tue Oct 24 07:56:11 2023 +0200 added filter method for undefined settings that overwrite defaults commit 5df8a0b3e77cd5dcf04c39ad7638ac845df75e4c Author: Matthias Mair Date: Tue Oct 24 03:05:06 2023 +0200 you do not need to string a string commit 0650d3b3a0132889c2a76de38db38224e974d205 Author: Matthias Mair Date: Tue Oct 24 03:04:34 2023 +0200 fix things that were borken for no good reason commit a40dbfd1364cf01465037350184f59d2a2a8afab Author: Matthias Mair Date: Tue Oct 24 02:39:34 2023 +0200 reduce unneeded blocking timeouts commit bf9046a5361ae919e70662e717d6156434b6fe43 Author: Matthias Mair Date: Tue Oct 24 02:34:10 2023 +0200 catch server fetching errors commit aa01e67e8c8e789fdf755ac4481e730fe5ea4183 Author: Matthias Mair Date: Tue Oct 24 02:33:29 2023 +0200 move init as things are now plugged together different commit 290c33bd3125d50779497d6fc5981d5813b58f5d Author: Matthias Mair Date: Tue Oct 24 01:49:32 2023 +0200 do not log a failed automatic login try - why would you? --- InvenTree/InvenTree/api_version.py | 5 +- InvenTree/common/api.py | 9 ++ InvenTree/generic/states/api.py | 22 ++++- .../src/components/nav/SearchDrawer.tsx | 6 +- .../src/components/render/Instance.tsx | 2 + .../src/components/render/ModelType.tsx | 14 ++- .../components/renderers/StatusRenderer.tsx | 96 +++++++++++++++++++ .../tables/build/BuildOrderTable.tsx | 8 +- .../tables/purchasing/PurchaseOrderTable.tsx | 10 +- .../tables/sales/ReturnOrderTable.tsx | 6 +- .../tables/sales/SalesOrderTable.tsx | 6 +- .../tables/stock/StockItemTable.tsx | 6 +- src/frontend/src/defaults/backendMappings.tsx | 29 ++++++ src/frontend/src/pages/Index/Playground.tsx | 30 +++++- src/frontend/src/states/ApiState.tsx | 53 +++++++--- 15 files changed, 266 insertions(+), 36 deletions(-) create mode 100644 src/frontend/src/components/renderers/StatusRenderer.tsx create mode 100644 src/frontend/src/defaults/backendMappings.tsx diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index c3dd8a1897..98a7c71143 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 141 +INVENTREE_API_VERSION = 142 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v142 -> 2023-10-20: https://github.com/inventree/InvenTree/pull/5759 + - Adds generic API endpoints for looking up status models + v141 -> 2023-10-23 : https://github.com/inventree/InvenTree/pull/5774 - Changed 'part.responsible' from User to Owner diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 23536f2aeb..3b9e15bbdc 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -18,6 +18,7 @@ from rest_framework.views import APIView import common.models import common.serializers +from generic.states.api import AllStatusViews, StatusView from InvenTree.api import BulkDeleteMixin, MetadataView from InvenTree.config import CONFIG_LOOKUPS from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER @@ -617,6 +618,14 @@ common_api_urls = [ path('/', FlagDetail.as_view(), name='api-flag-detail'), re_path(r'^.*$', FlagList.as_view(), name='api-flag-list'), ])), + + # Status + path('generic/status/', include([ + path(f'/', include([ + path('', StatusView.as_view(), name='api-status'), + ])), + path('', AllStatusViews.as_view(), name='api-status-all'), + ])), ] admin_api_urls = [ diff --git a/InvenTree/generic/states/api.py b/InvenTree/generic/states/api.py index e549d237d1..e5c07d5128 100644 --- a/InvenTree/generic/states/api.py +++ b/InvenTree/generic/states/api.py @@ -26,7 +26,7 @@ class StatusView(APIView): MODEL_REF = 'statusmodel' def get_status_model(self, *args, **kwargs): - """Return the StatusCode moedl based on extra parameters passed to the view""" + """Return the StatusCode model based on extra parameters passed to the view""" status_model = self.kwargs.get(self.MODEL_REF, None) if status_model is None: @@ -50,3 +50,23 @@ class StatusView(APIView): } return Response(data) + + +class AllStatusViews(StatusView): + """Endpoint for listing all defined status models.""" + + permission_classes = [ + permissions.IsAuthenticated, + ] + + def get(self, request, *args, **kwargs): + """Perform a GET request to learn information about status codes""" + data = {} + + for status_class in StatusCode.__subclasses__(): + data[status_class.__name__] = { + 'class': status_class.__name__, + 'values': status_class.dict(), + } + + return Response(data) diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index b2ffa64ad2..903ebb6456 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -324,9 +324,9 @@ export function SearchDrawer({ // Callback when one of the search results is clicked function onResultClick(query: ModelType, pk: number) { closeDrawer(); - navigate( - ModelInformationDict[query].url_detail.replace(':pk', pk.toString()) - ); + const targetModel = ModelInformationDict[query]; + if (targetModel.url_detail == undefined) return; + navigate(targetModel.url_detail.replace(':pk', pk.toString())); } return ( diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 47c6222bed..277a901364 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -46,11 +46,13 @@ const RendererLookup: EnumDictionary< [ModelType.partcategory]: RenderPartCategory, [ModelType.partparametertemplate]: RenderPartParameterTemplate, [ModelType.purchaseorder]: RenderPurchaseOrder, + [ModelType.purchaseorderline]: RenderPurchaseOrder, [ModelType.returnorder]: RenderReturnOrder, [ModelType.salesorder]: RenderSalesOrder, [ModelType.salesordershipment]: RenderSalesOrderShipment, [ModelType.stocklocation]: RenderStockLocation, [ModelType.stockitem]: RenderStockItem, + [ModelType.stockhistory]: RenderStockItem, [ModelType.supplierpart]: RenderSupplierPart, [ModelType.user]: RenderUser, [ModelType.manufacturerpart]: RenderPart diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 3a250c3f35..201ad8e39c 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -8,9 +8,11 @@ export enum ModelType { partparametertemplate = 'partparametertemplate', stockitem = 'stockitem', stocklocation = 'stocklocation', + stockhistory = 'stockhistory', build = 'build', company = 'company', purchaseorder = 'purchaseorder', + purchaseorderline = 'purchaseorderline', salesorder = 'salesorder', salesordershipment = 'salesordershipment', returnorder = 'returnorder', @@ -23,8 +25,8 @@ export enum ModelType { interface ModelInformatonInterface { label: string; label_multiple: string; - url_overview: string; - url_detail: string; + url_overview?: string; + url_detail?: string; } type ModelDictory = { @@ -74,6 +76,10 @@ export const ModelInformationDict: ModelDictory = { url_overview: '/stocklocation', url_detail: '/stocklocation/:pk/' }, + stockhistory: { + label: t`Stock History`, + label_multiple: t`Stock Histories` + }, build: { label: t`Build`, label_multiple: t`Builds`, @@ -92,6 +98,10 @@ export const ModelInformationDict: ModelDictory = { url_overview: '/purchaseorder', url_detail: '/purchaseorder/:pk/' }, + purchaseorderline: { + label: t`Purchase Order Line`, + label_multiple: t`Purchase Order Lines` + }, salesorder: { label: t`Sales Order`, label_multiple: t`Sales Orders`, diff --git a/src/frontend/src/components/renderers/StatusRenderer.tsx b/src/frontend/src/components/renderers/StatusRenderer.tsx new file mode 100644 index 0000000000..b0af1e1d76 --- /dev/null +++ b/src/frontend/src/components/renderers/StatusRenderer.tsx @@ -0,0 +1,96 @@ +import { Badge, MantineSize } from '@mantine/core'; + +import { colorMap } from '../../defaults/backendMappings'; +import { useServerApiState } from '../../states/ApiState'; +import { ModelType } from '../render/ModelType'; + +interface StatusCodeInterface { + key: string; + label: string; + color: string; +} + +export interface StatusCodeListInterface { + [key: string]: StatusCodeInterface; +} + +interface renderStatusLabelOptionsInterface { + size?: MantineSize; +} + +/* + * Generic function to render a status label + */ +function renderStatusLabel( + key: string, + codes: StatusCodeListInterface, + options: renderStatusLabelOptionsInterface = {} +) { + let text = null; + let color = null; + + // Find the entry which matches the provided key + for (let name in codes) { + let entry = codes[name]; + + if (entry.key == key) { + text = entry.label; + color = entry.color; + break; + } + } + + if (!text) { + console.error(`renderStatusLabel could not find match for code ${key}`); + } + + // Fallbacks + if (color == null) color = 'default'; + color = colorMap[color] || colorMap['default']; + const size = options.size || 'xs'; + + if (!text) { + text = key; + } + + return ( + + {text} + + ); +} +/* + * Render the status for a object. + * Uses the values specified in "status_codes.py" + */ +export const StatusRenderer = ({ + status, + type, + options +}: { + status: string; + type: ModelType; + options?: renderStatusLabelOptionsInterface; +}) => { + const [statusCodeList] = useServerApiState((state) => [state.status]); + if (statusCodeList === undefined) { + console.log('StatusRenderer: statusCodeList is undefined'); + return null; + } + const statusCodes = statusCodeList[type]; + if (statusCodes === undefined) { + console.log('StatusRenderer: statusCodes is undefined'); + return null; + } + + return renderStatusLabel(status, statusCodes, options); +}; + +/* + * Render the status badge in a table + */ +export function TableStatusRenderer( + type: ModelType +): ((record: any) => any) | undefined { + return (record: any) => StatusRenderer({ status: record.status, type: type }); +} diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index d45d69540d..e106cc5cde 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -1,11 +1,13 @@ import { t } from '@lingui/macro'; -import { Progress, Text } from '@mantine/core'; +import { Progress } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ThumbnailHoverCard } from '../../images/Thumbnail'; +import { ModelType } from '../../render/ModelType'; +import { TableStatusRenderer } from '../../renderers/StatusRenderer'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -82,8 +84,8 @@ function buildOrderTableColumns(): TableColumn[] { accessor: 'status', sortable: true, title: t`Status`, - switchable: true - // TODO: Custom render function here (status label) + switchable: true, + render: TableStatusRenderer(ModelType.build) }, { accessor: 'priority', diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx index 0ffd43d2f4..2f04bd4747 100644 --- a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx @@ -5,6 +5,8 @@ import { useMemo } from 'react'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { Thumbnail } from '../../images/Thumbnail'; +import { ModelType } from '../../render/ModelType'; +import { StatusRenderer } from '../../renderers/StatusRenderer'; import { InvenTreeTable } from '../InvenTreeTable'; export function PurchaseOrderTable({ params }: { params?: any }) { @@ -59,8 +61,12 @@ export function PurchaseOrderTable({ params }: { params?: any }) { accessor: 'status', title: t`Status`, sortable: true, - switchable: true - // TODO: Custom formatter + switchable: true, + render: (record: any) => + StatusRenderer({ + status: record.status, + type: ModelType.purchaseorder + }) }, { accessor: 'creation_date', diff --git a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx index 192feeb978..94bfcc523b 100644 --- a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx @@ -5,6 +5,8 @@ import { useMemo } from 'react'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { Thumbnail } from '../../images/Thumbnail'; +import { ModelType } from '../../render/ModelType'; +import { TableStatusRenderer } from '../../renderers/StatusRenderer'; import { InvenTreeTable } from '../InvenTreeTable'; export function ReturnOrderTable({ params }: { params?: any }) { @@ -58,8 +60,8 @@ export function ReturnOrderTable({ params }: { params?: any }) { accessor: 'status', title: t`Status`, sortable: true, - switchable: true - // TODO: Custom formatter + switchable: true, + render: TableStatusRenderer(ModelType.returnorder) } // TODO: Creation date // TODO: Target date diff --git a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx index 0375db21a0..3a25b75a5d 100644 --- a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx @@ -5,6 +5,8 @@ import { useMemo } from 'react'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { Thumbnail } from '../../images/Thumbnail'; +import { ModelType } from '../../render/ModelType'; +import { TableStatusRenderer } from '../../renderers/StatusRenderer'; import { InvenTreeTable } from '../InvenTreeTable'; export function SalesOrderTable({ params }: { params?: any }) { @@ -59,8 +61,8 @@ export function SalesOrderTable({ params }: { params?: any }) { accessor: 'status', title: t`Status`, sortable: true, - switchable: true - // TODO: Custom formatter + switchable: true, + render: TableStatusRenderer(ModelType.salesorder) } // TODO: Creation date diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx index 05c8994340..c29e5792ee 100644 --- a/src/frontend/src/components/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx @@ -7,6 +7,8 @@ import { notYetImplemented } from '../../../functions/notifications'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { Thumbnail } from '../../images/Thumbnail'; +import { ModelType } from '../../render/ModelType'; +import { TableStatusRenderer } from '../../renderers/StatusRenderer'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { RowAction } from '../RowActions'; @@ -52,8 +54,8 @@ function stockItemTableColumns(): TableColumn[] { sortable: true, switchable: true, filter: true, - title: t`Status` - // TODO: Custom renderer for stock status label + title: t`Status`, + render: TableStatusRenderer(ModelType.stockitem) }, { accessor: 'batch', diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx new file mode 100644 index 0000000000..7e8a13a5e3 --- /dev/null +++ b/src/frontend/src/defaults/backendMappings.tsx @@ -0,0 +1,29 @@ +import { ModelType } from '../components/render/ModelType'; + +/* Lookup tables for mapping backend responses to internal types */ + +/** + * List of status codes which are used in the backend + * and the model type they are associated with + */ +export const statusCodeList: Record = { + BuildStatus: ModelType.build, + PurchaseOrderStatus: ModelType.purchaseorder, + ReturnOrderLineStatus: ModelType.purchaseorderline, + ReturnOrderStatus: ModelType.returnorder, + SalesOrderStatus: ModelType.salesorder, + StockHistoryCode: ModelType.stockhistory, + StockStatus: ModelType.stockitem +}; + +/* + * Map the colors used in the backend to the colors used in the frontend + */ +export const colorMap: { [key: string]: string } = { + dark: 'dark', + warning: 'yellow', + success: 'green', + info: 'cyan', + danger: 'red', + default: 'gray' +}; diff --git a/src/frontend/src/pages/Index/Playground.tsx b/src/frontend/src/pages/Index/Playground.tsx index c2820d13d3..ef79e61475 100644 --- a/src/frontend/src/pages/Index/Playground.tsx +++ b/src/frontend/src/pages/Index/Playground.tsx @@ -1,13 +1,15 @@ import { Trans } from '@lingui/macro'; -import { Button } from '@mantine/core'; +import { Button, TextInput } from '@mantine/core'; import { Group, Text } from '@mantine/core'; import { Accordion } from '@mantine/core'; -import { ReactNode } from 'react'; +import { ReactNode, useState } from 'react'; import { ApiFormProps } from '../../components/forms/ApiForm'; import { ApiFormChangeCallback } from '../../components/forms/fields/ApiFormField'; import { PlaceholderPill } from '../../components/items/Placeholder'; import { StylishText } from '../../components/items/StylishText'; +import { ModelType } from '../../components/render/ModelType'; +import { StatusRenderer } from '../../components/renderers/StatusRenderer'; import { openCreateApiForm, openEditApiForm } from '../../functions/forms'; import { createPart, @@ -60,6 +62,23 @@ function ApiFormsPlayground() { ); } +// Show some example status labels +function StatusLabelPlayground() { + const [status, setStatus] = useState('10'); + return ( + <> + + Stock Status + setStatus(event.currentTarget.value)} + /> + + + + ); +} + /** Construct a simple accordion group with title and content */ function PlaygroundArea({ title, @@ -95,10 +114,11 @@ export default function Playground() { + } /> } - > + title="Status labels" + content={} + /> ); diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index d49ff31aa8..467d776230 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -1,28 +1,52 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; import { api } from '../App'; +import { ModelType } from '../components/render/ModelType'; +import { StatusCodeListInterface } from '../components/renderers/StatusRenderer'; +import { statusCodeList } from '../defaults/backendMappings'; import { emptyServerAPI } from '../defaults/defaults'; import { ServerAPIProps, UserProps } from './states'; +type StatusLookup = Record; + interface ServerApiStateProps { server: ServerAPIProps; setServer: (newServer: ServerAPIProps) => void; fetchServerApiState: () => void; + status: StatusLookup | undefined; } -export const useServerApiState = create((set, get) => ({ - server: emptyServerAPI, - setServer: (newServer: ServerAPIProps) => set({ server: newServer }), - fetchServerApiState: async () => { - // Fetch server data - await api - .get(apiUrl(ApiPaths.api_server_info)) - .then((response) => { - set({ server: response.data }); - }) - .catch(() => {}); - } -})); +export const useServerApiState = create()( + persist( + (set) => ({ + server: emptyServerAPI, + setServer: (newServer: ServerAPIProps) => set({ server: newServer }), + fetchServerApiState: async () => { + // Fetch server data + await api + .get(apiUrl(ApiPaths.api_server_info)) + .then((response) => { + set({ server: response.data }); + }) + .catch(() => {}); + // Fetch status data for rendering labels + await api.get(apiUrl(ApiPaths.global_status)).then((response) => { + const newStatusLookup: StatusLookup = {} as StatusLookup; + for (const key in response.data) { + newStatusLookup[statusCodeList[key]] = response.data[key].values; + } + set({ status: newStatusLookup }); + }); + }, + status: undefined + }), + { + name: 'server-api-state', + getStorage: () => sessionStorage + } + ) +); export enum ApiPaths { api_server_info = 'api-server-info', @@ -43,6 +67,7 @@ export enum ApiPaths { barcode = 'api-barcode', news = 'news', + global_status = 'api-global-status', // Build order URLs build_order_list = 'api-build-list', @@ -123,6 +148,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'barcode/'; case ApiPaths.news: return 'news/'; + case ApiPaths.global_status: + return 'generic/status/'; case ApiPaths.build_order_list: return 'build/'; case ApiPaths.build_order_attachment_list: