From 608ca75763869b18bb8c0d39dad3928037125783 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sun, 8 Oct 2023 11:20:32 +0200 Subject: [PATCH] Type model information in SearchDrawer (#5597) * moved search query to strongly typed model reference * move title and link to reusable typed section * typed ApiFormFieldType.model too * renamed symbol * switched to lookup --- .../components/forms/fields/ApiFormField.tsx | 3 +- .../forms/fields/RelatedModelField.tsx | 2 +- .../src/components/nav/SearchDrawer.tsx | 52 +++---- src/frontend/src/components/render/Build.tsx | 12 +- .../src/components/render/Company.tsx | 60 ++++---- .../src/components/render/Instance.tsx | 86 ++++++------ .../src/components/render/ModelType.tsx | 130 ++++++++++++++++++ src/frontend/src/components/render/Order.tsx | 36 ++--- src/frontend/src/components/render/Part.tsx | 16 +-- src/frontend/src/components/render/Stock.tsx | 16 +-- src/frontend/src/components/render/User.tsx | 10 +- 11 files changed, 282 insertions(+), 141 deletions(-) create mode 100644 src/frontend/src/components/render/ModelType.tsx diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index 19a9655183..de772fbeff 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -14,6 +14,7 @@ import { IconX } from '@tabler/icons-react'; import { ReactNode } from 'react'; import { useMemo } from 'react'; +import { ModelType } from '../../render/ModelType'; import { ApiFormProps } from '../ApiForm'; import { ChoiceField } from './ChoiceField'; import { RelatedModelField } from './RelatedModelField'; @@ -63,7 +64,7 @@ export type ApiFormFieldType = { fieldType?: string; api_url?: string; read_only?: boolean; - model?: string; + model?: ModelType; filters?: any; required?: boolean; choices?: any[]; diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index 5341d43b09..c13cac5d9b 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -160,7 +160,7 @@ export function RelatedModelField({ // TODO: If a custom render function is provided, use that return ( - + ); } diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index 19f34a6120..a8bf7de3a3 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -31,11 +31,11 @@ import { useNavigate } from 'react-router-dom'; import { api } from '../../App'; import { RenderInstance } from '../render/Instance'; +import { ModelInformationDict, ModelType } from '../render/ModelType'; // Define type for handling individual search queries type SearchQuery = { - name: string; - title: string; + name: ModelType; enabled: boolean; parameters: any; results?: any; @@ -57,16 +57,14 @@ function settingsCheck(setting: string) { function buildSearchQueries(): SearchQuery[] { return [ { - name: 'part', - title: t`Parts`, + name: ModelType.part, parameters: {}, enabled: permissionCheck('part.view') && settingsCheck('SEARCH_PREVIEW_SHOW_PARTS') }, { - name: 'supplierpart', - title: t`Supplier Parts`, + name: ModelType.supplierpart, parameters: { part_detail: true, supplier_detail: true, @@ -78,8 +76,7 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck('SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS') }, { - name: 'manufacturerpart', - title: t`Manufacturer Parts`, + name: ModelType.manufacturerpart, parameters: { part_detail: true, supplier_detail: true, @@ -91,16 +88,14 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck('SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS') }, { - name: 'partcategory', - title: t`Part Categories`, + name: ModelType.partcategory, parameters: {}, enabled: permissionCheck('part_category.view') && settingsCheck('SEARCH_PREVIEW_SHOW_CATEGORIES') }, { - name: 'stockitem', - title: t`Stock Items`, + name: ModelType.stockitem, parameters: { part_detail: true, location_detail: true @@ -110,16 +105,14 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck('SEARCH_PREVIEW_SHOW_STOCK') }, { - name: 'stocklocation', - title: t`Stock Locations`, + name: ModelType.stocklocation, parameters: {}, enabled: permissionCheck('stock_location.view') && settingsCheck('SEARCH_PREVIEW_SHOW_LOCATIONS') }, { - name: 'build', - title: t`Build Orders`, + name: ModelType.build, parameters: { part_detail: true }, @@ -128,8 +121,7 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck('SEARCH_PREVIEW_SHOW_BUILD_ORDERS') }, { - name: 'company', - title: t`Companies`, + name: ModelType.company, parameters: {}, enabled: (permissionCheck('sales_order.view') || @@ -137,8 +129,7 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck('SEARCH_PREVIEW_SHOW_COMPANIES') }, { - name: 'purchaseorder', - title: t`Purchase Orders`, + name: ModelType.purchaseorder, parameters: { supplier_detail: true }, @@ -147,8 +138,7 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck(`SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS`) }, { - name: 'salesorder', - title: t`Sales Orders`, + name: ModelType.salesorder, parameters: { customer_detail: true }, @@ -157,8 +147,7 @@ function buildSearchQueries(): SearchQuery[] { settingsCheck(`SEARCH_PREVIEW_SHOW_SALES_ORDERS`) }, { - name: 'returnorder', - title: t`Return Orders`, + name: ModelType.returnorder, parameters: { customer_detail: true }, @@ -178,19 +167,20 @@ function QueryResultGroup({ onResultClick }: { query: SearchQuery; - onRemove: (query: string) => void; - onResultClick: (query: string, pk: number) => void; + onRemove: (query: ModelType) => void; + onResultClick: (query: ModelType, pk: number) => void; }) { if (query.results.count == 0) { return null; } + const model = ModelInformationDict[query.name]; return ( - {query.title} + {model.label_multiple} {' '} - {query.results.count} results @@ -320,7 +310,7 @@ export function SearchDrawer({ }, [searchQuery.data]); // Callback to remove a set of results from the list - function removeResults(query: string) { + function removeResults(query: ModelType) { setQueryResults(queryResults.filter((q) => q.name != query)); } @@ -333,9 +323,11 @@ export function SearchDrawer({ const navigate = useNavigate(); // Callback when one of the search results is clicked - function onResultClick(query: string, pk: number) { + function onResultClick(query: ModelType, pk: number) { closeDrawer(); - navigate(`/${query}/${pk}/`); + navigate( + ModelInformationDict[query].url_detail.replace(':pk', pk.toString()) + ); } return ( diff --git a/src/frontend/src/components/render/Build.tsx b/src/frontend/src/components/render/Build.tsx index 43cf73767a..175a07b9cb 100644 --- a/src/frontend/src/components/render/Build.tsx +++ b/src/frontend/src/components/render/Build.tsx @@ -5,16 +5,12 @@ import { RenderInlineModel } from './Instance'; /** * Inline rendering of a single BuildOrder instance */ -export function RenderBuildOrder({ - buildorder -}: { - buildorder: any; -}): ReactNode { +export function RenderBuildOrder({ instance }: { instance: any }): ReactNode { return ( ); } diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index 7b4689afa3..06485839f2 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -5,23 +5,23 @@ import { RenderInlineModel } from './Instance'; /** * Inline rendering of a single Address instance */ -export function RenderAddress({ address }: { address: any }): ReactNode { +export function RenderAddress({ instance }: { instance: any }): ReactNode { let text = [ - address.title, - address.country, - address.postal_code, - address.postal_city, - address.province, - address.line1, - address.line2 + instance.title, + instance.country, + instance.postal_code, + instance.postal_city, + instance.province, + instance.line1, + instance.line2 ] .filter(Boolean) .join(', '); return ( ); } @@ -29,14 +29,14 @@ export function RenderAddress({ address }: { address: any }): ReactNode { /** * Inline rendering of a single Company instance */ -export function RenderCompany({ company }: { company: any }): ReactNode { +export function RenderCompany({ instance }: { instance: any }): ReactNode { // TODO: Handle URL return ( ); } @@ -44,25 +44,37 @@ export function RenderCompany({ company }: { company: any }): ReactNode { /** * Inline rendering of a single Contact instance */ -export function RenderContact({ contact }: { contact: any }): ReactNode { - return ; +export function RenderContact({ instance }: { instance: any }): ReactNode { + return ; } /** * Inline rendering of a single SupplierPart instance */ -export function RenderSupplierPart({ - supplierpart -}: { - supplierpart: any; -}): ReactNode { +export function RenderSupplierPart({ instance }: { instance: any }): ReactNode { // TODO: Handle image // TODO: handle URL - let supplier = supplierpart.supplier_detail ?? {}; - let part = supplierpart.part_detail ?? {}; + let supplier = instance.supplier_detail ?? {}; + let part = instance.part_detail ?? {}; - let text = supplierpart.SKU; + let text = instance.SKU; + + if (supplier.name) { + text = `${supplier.name} | ${text}`; + } + + return ; +} + +/** + * Inline rendering of a single ManufacturerPart instance + */ +export function ManufacturerPart({ instance }: { instance: any }): ReactNode { + let supplier = instance.supplier_detail ?? {}; + let part = instance.part_detail ?? {}; + + let text = instance.SKU; if (supplier.name) { text = `${supplier.name} | ${text}`; diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index b42151784a..0b8b0416f3 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -11,6 +11,7 @@ import { RenderContact, RenderSupplierPart } from './Company'; +import { ModelType } from './ModelType'; import { RenderPurchaseOrder, RenderReturnOrder, @@ -21,6 +22,35 @@ import { RenderPart, RenderPartCategory } from './Part'; import { RenderStockItem, RenderStockLocation } from './Stock'; import { RenderOwner, RenderUser } from './User'; +type EnumDictionary = { + [K in T]: U; +}; + +/** + * Lookup table for rendering a model instance + */ +const RendererLookup: EnumDictionary< + ModelType, + (props: { instance: any }) => ReactNode +> = { + [ModelType.address]: RenderAddress, + [ModelType.build]: RenderBuildOrder, + [ModelType.company]: RenderCompany, + [ModelType.contact]: RenderContact, + [ModelType.owner]: RenderOwner, + [ModelType.part]: RenderPart, + [ModelType.partcategory]: RenderPartCategory, + [ModelType.purchaseorder]: RenderPurchaseOrder, + [ModelType.returnorder]: RenderReturnOrder, + [ModelType.salesorder]: RenderSalesOrder, + [ModelType.salesordershipment]: RenderSalesOrderShipment, + [ModelType.stocklocation]: RenderStockLocation, + [ModelType.stockitem]: RenderStockItem, + [ModelType.supplierpart]: RenderSupplierPart, + [ModelType.user]: RenderUser, + [ModelType.manufacturerpart]: RenderPart +}; + // import { ApiFormFieldType } from "../forms/fields/ApiFormField"; /** @@ -30,48 +60,12 @@ export function RenderInstance({ model, instance }: { - model: string; + model: ModelType | undefined; instance: any; }): ReactNode { - switch (model) { - case 'address': - return ; - case 'build': - return ; - case 'company': - return ; - case 'contact': - return ; - case 'owner': - return ; - case 'part': - return ; - case 'partcategory': - return ; - case 'purchaseorder': - return ; - case 'returnorder': - return ; - case 'salesoder': - return ; - case 'salesordershipment': - return ; - case 'stocklocation': - return ; - case 'stockitem': - return ; - case 'supplierpart': - return ; - case 'user': - return ; - default: - // Unknown model - return ( - - <> - - ); - } + if (model === undefined) return ; + const RenderComponent = RendererLookup[model]; + return ; } /** @@ -101,3 +95,15 @@ export function RenderInlineModel({ ); } + +export function UnknownRenderer({ + model +}: { + model: ModelType | undefined; +}): ReactNode { + return ( + + <> + + ); +} diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx new file mode 100644 index 0000000000..17a671fd40 --- /dev/null +++ b/src/frontend/src/components/render/ModelType.tsx @@ -0,0 +1,130 @@ +import { t } from '@lingui/macro'; + +export enum ModelType { + part = 'part', + supplierpart = 'supplierpart', + manufacturerpart = 'manufacturerpart', + partcategory = 'partcategory', + stockitem = 'stockitem', + stocklocation = 'stocklocation', + build = 'build', + company = 'company', + purchaseorder = 'purchaseorder', + salesorder = 'salesorder', + salesordershipment = 'salesordershipment', + returnorder = 'returnorder', + address = 'address', + contact = 'contact', + owner = 'owner', + user = 'user' +} + +interface ModelInformatonInterface { + label: string; + label_multiple: string; + url_overview: string; + url_detail: string; +} + +type ModelDictory = { + [key in keyof typeof ModelType]: ModelInformatonInterface; +}; + +export const ModelInformationDict: ModelDictory = { + part: { + label: t`Part`, + label_multiple: t`Parts`, + url_overview: '/part', + url_detail: '/part/:pk/' + }, + supplierpart: { + label: t`Supplier Part`, + label_multiple: t`Supplier Parts`, + url_overview: '/supplierpart', + url_detail: '/supplierpart/:pk/' + }, + manufacturerpart: { + label: t`Manufacturer Part`, + label_multiple: t`Manufacturer Parts`, + url_overview: '/manufacturerpart', + url_detail: '/manufacturerpart/:pk/' + }, + partcategory: { + label: t`Part Category`, + label_multiple: t`Part Categories`, + url_overview: '/partcategory', + url_detail: '/partcategory/:pk/' + }, + stockitem: { + label: t`Stock Item`, + label_multiple: t`Stock Items`, + url_overview: '/stockitem', + url_detail: '/stockitem/:pk/' + }, + stocklocation: { + label: t`Stock Location`, + label_multiple: t`Stock Locations`, + url_overview: '/stocklocation', + url_detail: '/stocklocation/:pk/' + }, + build: { + label: t`Build`, + label_multiple: t`Builds`, + url_overview: '/build', + url_detail: '/build/:pk/' + }, + company: { + label: t`Company`, + label_multiple: t`Companies`, + url_overview: '/company', + url_detail: '/company/:pk/' + }, + purchaseorder: { + label: t`Purchase Order`, + label_multiple: t`Purchase Orders`, + url_overview: '/purchaseorder', + url_detail: '/purchaseorder/:pk/' + }, + salesorder: { + label: t`Sales Order`, + label_multiple: t`Sales Orders`, + url_overview: '/salesorder', + url_detail: '/salesorder/:pk/' + }, + salesordershipment: { + label: t`Sales Order Shipment`, + label_multiple: t`Sales Order Shipments`, + url_overview: '/salesordershipment', + url_detail: '/salesordershipment/:pk/' + }, + returnorder: { + label: t`Return Order`, + label_multiple: t`Return Orders`, + url_overview: '/returnorder', + url_detail: '/returnorder/:pk/' + }, + address: { + label: t`Address`, + label_multiple: t`Addresses`, + url_overview: '/address', + url_detail: '/address/:pk/' + }, + contact: { + label: t`Contact`, + label_multiple: t`Contacts`, + url_overview: '/contact', + url_detail: '/contact/:pk/' + }, + owner: { + label: t`Owner`, + label_multiple: t`Owners`, + url_overview: '/owner', + url_detail: '/owner/:pk/' + }, + user: { + label: t`User`, + label_multiple: t`Users`, + url_overview: '/user', + url_detail: '/user/:pk/' + } +}; diff --git a/src/frontend/src/components/render/Order.tsx b/src/frontend/src/components/render/Order.tsx index 92157cf70b..e9d990be5e 100644 --- a/src/frontend/src/components/render/Order.tsx +++ b/src/frontend/src/components/render/Order.tsx @@ -6,14 +6,18 @@ import { RenderInlineModel } from './Instance'; /** * Inline rendering of a single PurchaseOrder instance */ -export function RenderPurchaseOrder({ order }: { order: any }): ReactNode { - let supplier = order.supplier_detail || {}; +export function RenderPurchaseOrder({ + instance +}: { + instance: any; +}): ReactNode { + let supplier = instance.supplier_detail || {}; // TODO: Handle URL return ( ); @@ -22,13 +26,13 @@ export function RenderPurchaseOrder({ order }: { order: any }): ReactNode { /** * Inline rendering of a single ReturnOrder instance */ -export function RenderReturnOrder({ order }: { order: any }): ReactNode { - let customer = order.customer_detail || {}; +export function RenderReturnOrder({ instance }: { instance: any }): ReactNode { + let customer = instance.customer_detail || {}; return ( ); @@ -37,15 +41,15 @@ export function RenderReturnOrder({ order }: { order: any }): ReactNode { /** * Inline rendering of a single SalesOrder instance */ -export function RenderSalesOrder({ order }: { order: any }): ReactNode { - let customer = order.customer_detail || {}; +export function RenderSalesOrder({ instance }: { instance: any }): ReactNode { + let customer = instance.customer_detail || {}; // TODO: Handle URL return ( ); @@ -55,16 +59,16 @@ export function RenderSalesOrder({ order }: { order: any }): ReactNode { * Inline rendering of a single SalesOrderAllocation instance */ export function RenderSalesOrderShipment({ - shipment + instance }: { - shipment: any; + instance: any; }): ReactNode { - let order = shipment.sales_order_detail || {}; + let order = instance.sales_order_detail || {}; return ( ); } diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx index bd570085cc..cf0ec5d282 100644 --- a/src/frontend/src/components/render/Part.tsx +++ b/src/frontend/src/components/render/Part.tsx @@ -5,12 +5,12 @@ import { RenderInlineModel } from './Instance'; /** * Inline rendering of a single Part instance */ -export function RenderPart({ part }: { part: any }): ReactNode { +export function RenderPart({ instance }: { instance: any }): ReactNode { return ( ); } @@ -18,15 +18,15 @@ export function RenderPart({ part }: { part: any }): ReactNode { /** * Inline rendering of a PartCategory instance */ -export function RenderPartCategory({ category }: { category: any }): ReactNode { +export function RenderPartCategory({ instance }: { instance: any }): ReactNode { // TODO: Handle URL - let lvl = '-'.repeat(category.level || 0); + let lvl = '-'.repeat(instance.level || 0); return ( ); } diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index 4894b2f255..9537480749 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -6,24 +6,24 @@ import { RenderInlineModel } from './Instance'; * Inline rendering of a single StockLocation instance */ export function RenderStockLocation({ - location + instance }: { - location: any; + instance: any; }): ReactNode { return ( ); } -export function RenderStockItem({ item }: { item: any }): ReactNode { +export function RenderStockItem({ instance }: { instance: any }): ReactNode { return ( ); } diff --git a/src/frontend/src/components/render/User.tsx b/src/frontend/src/components/render/User.tsx index ca099c04e6..6bad2a3011 100644 --- a/src/frontend/src/components/render/User.tsx +++ b/src/frontend/src/components/render/User.tsx @@ -2,17 +2,17 @@ import { ReactNode } from 'react'; import { RenderInlineModel } from './Instance'; -export function RenderOwner({ owner }: { owner: any }): ReactNode { +export function RenderOwner({ instance }: { instance: any }): ReactNode { // TODO: Icon based on user / group status? - return ; + return ; } -export function RenderUser({ user }: { user: any }): ReactNode { +export function RenderUser({ instance }: { instance: any }): ReactNode { return ( ); }