[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 <code@mjmair.com>
Date:   Tue Oct 24 09:22:38 2023 +0200

    Update ui_plattform.spec.ts

commit 49da3312beff7fd6837ea741e621df221c445d19
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 07:56:25 2023 +0200

    more logging

commit 5337be4c3990051b805a6fce2e79ca4030b4afe5
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 07:56:11 2023 +0200

    added filter method for undefined settings that overwrite defaults

commit 5df8a0b3e77cd5dcf04c39ad7638ac845df75e4c
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 03:05:06 2023 +0200

    you do not need to string a string

commit 0650d3b3a0132889c2a76de38db38224e974d205
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 03:04:34 2023 +0200

    fix things that were borken for no good reason

commit a40dbfd1364cf01465037350184f59d2a2a8afab
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 02:39:34 2023 +0200

    reduce unneeded blocking timeouts

commit bf9046a5361ae919e70662e717d6156434b6fe43
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 02:34:10 2023 +0200

    catch server fetching errors

commit aa01e67e8c8e789fdf755ac4481e730fe5ea4183
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 02:33:29 2023 +0200

    move init as things are now plugged together different

commit 290c33bd3125d50779497d6fc5981d5813b58f5d
Author: Matthias Mair <code@mjmair.com>
Date:   Tue Oct 24 01:49:32 2023 +0200

    do not log a failed automatic login try - why would you?
This commit is contained in:
Matthias Mair 2023-10-26 12:49:38 +02:00 committed by GitHub
parent 2ff2c0801a
commit 53c16510a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 266 additions and 36 deletions

View File

@ -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

View File

@ -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('<str:key>/', FlagDetail.as_view(), name='api-flag-detail'),
re_path(r'^.*$', FlagList.as_view(), name='api-flag-list'),
])),
# Status
path('generic/status/', include([
path(f'<str:{StatusView.MODEL_REF}>/', include([
path('', StatusView.as_view(), name='api-status'),
])),
path('', AllStatusViews.as_view(), name='api-status-all'),
])),
]
admin_api_urls = [

View File

@ -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)

View File

@ -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 (

View File

@ -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

View File

@ -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`,

View File

@ -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 (
<Badge color={color} variant="filled" size={size}>
{text}
</Badge>
);
}
/*
* 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 });
}

View File

@ -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',

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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<string, ModelType> = {
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'
};

View File

@ -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<string>('10');
return (
<>
<Group>
<Text>Stock Status</Text>
<TextInput
value={status}
onChange={(event) => setStatus(event.currentTarget.value)}
/>
<StatusRenderer type={ModelType.stockitem} status={status} />
</Group>
</>
);
}
/** Construct a simple accordion group with title and content */
function PlaygroundArea({
title,
@ -95,10 +114,11 @@ export default function Playground() {
</Trans>
</Text>
<Accordion defaultValue="">
<PlaygroundArea title="API Forms" content={<ApiFormsPlayground />} />
<PlaygroundArea
title="API Forms"
content={<ApiFormsPlayground />}
></PlaygroundArea>
title="Status labels"
content={<StatusLabelPlayground />}
/>
</Accordion>
</>
);

View File

@ -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<ModelType, StatusCodeListInterface>;
interface ServerApiStateProps {
server: ServerAPIProps;
setServer: (newServer: ServerAPIProps) => void;
fetchServerApiState: () => void;
status: StatusLookup | undefined;
}
export const useServerApiState = create<ServerApiStateProps>((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<ServerApiStateProps>()(
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: