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
This commit is contained in:
Matthias Mair 2023-10-08 11:20:32 +02:00 committed by GitHub
parent e76fa11e63
commit 608ca75763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 282 additions and 141 deletions

View File

@ -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[];

View File

@ -160,7 +160,7 @@ export function RelatedModelField({
// TODO: If a custom render function is provided, use that
return (
<RenderInstance instance={data} model={definition.model ?? 'undefined'} />
<RenderInstance instance={data} model={definition.model ?? undefined} />
);
}

View File

@ -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 (
<Paper shadow="sm" radius="xs" p="md">
<Stack key={query.name}>
<Group position="apart" noWrap={true}>
<Group position="left" spacing={5} noWrap={true}>
<Text size="lg">{query.title}</Text>
<Text size="lg">{model.label_multiple}</Text>
<Text size="sm" italic>
{' '}
- {query.results.count} <Trans>results</Trans>
@ -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 (

View File

@ -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 (
<RenderInlineModel
primary={buildorder.reference}
secondary={buildorder.title}
image={buildorder.part_detail?.thumbnail || buildorder.part_detail?.image}
primary={instance.reference}
secondary={instance.title}
image={instance.part_detail?.thumbnail || instance.part_detail?.image}
/>
);
}

View File

@ -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 (
<RenderInlineModel
primary={address.description}
secondary={address.address}
primary={instance.description}
secondary={instance.address}
/>
);
}
@ -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 (
<RenderInlineModel
image={company.thumnbnail || company.image}
primary={company.name}
secondary={company.description}
image={instance.thumnbnail || instance.image}
primary={instance.name}
secondary={instance.description}
/>
);
}
@ -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 <RenderInlineModel primary={contact.name} />;
export function RenderContact({ instance }: { instance: any }): ReactNode {
return <RenderInlineModel primary={instance.name} />;
}
/**
* 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 <RenderInlineModel primary={text} secondary={part.full_name} />;
}
/**
* 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}`;

View File

@ -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<T extends string | symbol | number, U> = {
[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 <RenderAddress address={instance} />;
case 'build':
return <RenderBuildOrder buildorder={instance} />;
case 'company':
return <RenderCompany company={instance} />;
case 'contact':
return <RenderContact contact={instance} />;
case 'owner':
return <RenderOwner owner={instance} />;
case 'part':
return <RenderPart part={instance} />;
case 'partcategory':
return <RenderPartCategory category={instance} />;
case 'purchaseorder':
return <RenderPurchaseOrder order={instance} />;
case 'returnorder':
return <RenderReturnOrder order={instance} />;
case 'salesoder':
return <RenderSalesOrder order={instance} />;
case 'salesordershipment':
return <RenderSalesOrderShipment shipment={instance} />;
case 'stocklocation':
return <RenderStockLocation location={instance} />;
case 'stockitem':
return <RenderStockItem item={instance} />;
case 'supplierpart':
return <RenderSupplierPart supplierpart={instance} />;
case 'user':
return <RenderUser user={instance} />;
default:
// Unknown model
return (
<Alert color="red" title={t`Unknown model: ${model}`}>
<></>
</Alert>
);
}
if (model === undefined) return <UnknownRenderer model={model} />;
const RenderComponent = RendererLookup[model];
return <RenderComponent instance={instance} />;
}
/**
@ -101,3 +95,15 @@ export function RenderInlineModel({
</Group>
);
}
export function UnknownRenderer({
model
}: {
model: ModelType | undefined;
}): ReactNode {
return (
<Alert color="red" title={t`Unknown model: ${model}`}>
<></>
</Alert>
);
}

View File

@ -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/'
}
};

View File

@ -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 (
<RenderInlineModel
primary={order.reference}
secondary={order.description}
primary={instance.reference}
secondary={instance.description}
image={supplier.thumnbnail || supplier.image}
/>
);
@ -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 (
<RenderInlineModel
primary={order.reference}
secondary={order.description}
primary={instance.reference}
secondary={instance.description}
image={customer.thumnbnail || customer.image}
/>
);
@ -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 (
<RenderInlineModel
primary={order.reference}
secondary={order.description}
primary={instance.reference}
secondary={instance.description}
image={customer.thumnbnail || customer.image}
/>
);
@ -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 (
<RenderInlineModel
primary={order.reference}
secondary={t`Shipment` + ` ${shipment.description}`}
secondary={t`Shipment` + ` ${instance.description}`}
/>
);
}

View File

@ -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 (
<RenderInlineModel
primary={part.name}
secondary={part.description}
image={part.thumnbnail || part.image}
primary={instance.name}
secondary={instance.description}
image={instance.thumnbnail || instance.image}
/>
);
}
@ -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 (
<RenderInlineModel
primary={`${lvl} ${category.name}`}
secondary={category.description}
primary={`${lvl} ${instance.name}`}
secondary={instance.description}
/>
);
}

View File

@ -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 (
<RenderInlineModel
primary={location.name}
secondary={location.description}
primary={instance.name}
secondary={instance.description}
/>
);
}
export function RenderStockItem({ item }: { item: any }): ReactNode {
export function RenderStockItem({ instance }: { instance: any }): ReactNode {
return (
<RenderInlineModel
primary={item.part_detail?.full_name}
secondary={item.quantity}
image={item.part_detail?.thumbnail || item.part_detail?.image}
primary={instance.part_detail?.full_name}
secondary={instance.quantity}
image={instance.part_detail?.thumbnail || instance.part_detail?.image}
/>
);
}

View File

@ -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 <RenderInlineModel primary={owner.name} />;
return <RenderInlineModel primary={instance.name} />;
}
export function RenderUser({ user }: { user: any }): ReactNode {
export function RenderUser({ instance }: { instance: any }): ReactNode {
return (
<RenderInlineModel
primary={user.username}
secondary={`${user.first_name} ${user.last_name}`}
primary={instance.username}
secondary={`${instance.first_name} ${instance.last_name}`}
/>
);
}