2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

PUI location type (#7238)

* add location type to PUI

* fix add form bug where it contains the previous editing data when reusing the same fields

* fix sonarcloud issues
This commit is contained in:
Lukas 2024-05-16 00:57:11 +02:00 committed by GitHub
parent 20dc0380bd
commit 548ecf58a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 208 additions and 15 deletions

View File

@ -12,6 +12,7 @@ import {
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { getValueAtPath } from 'mantine-datatable';
import { Suspense, useCallback, useMemo } from 'react'; import { Suspense, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -43,7 +44,7 @@ export type PartIconsType = {
export type DetailsField = export type DetailsField =
| { | {
hidden?: boolean; hidden?: boolean;
icon?: string; icon?: InvenTreeIconType;
name: string; name: string;
label?: string; label?: string;
badge?: BadgeType; badge?: BadgeType;
@ -382,6 +383,11 @@ export function DetailsTableField({
const FieldType: any = getFieldType(field.type); const FieldType: any = getFieldType(field.type);
const fieldValue = useMemo(
() => getValueAtPath(item, field.name) as string,
[item, field.name]
);
return ( return (
<Table.Tr style={{ verticalAlign: 'top' }}> <Table.Tr style={{ verticalAlign: 'top' }}>
<Table.Td <Table.Td
@ -390,16 +396,16 @@ export function DetailsTableField({
maxWidth: '50' maxWidth: '50'
}} }}
> >
<InvenTreeIcon icon={(field.icon ?? field.name) as InvenTreeIconType} /> <InvenTreeIcon icon={field.icon ?? (field.name as InvenTreeIconType)} />
</Table.Td> </Table.Td>
<Table.Td style={{ maxWidth: '65%' }}> <Table.Td style={{ maxWidth: '65%' }}>
<Text>{field.label}</Text> <Text>{field.label}</Text>
</Table.Td> </Table.Td>
<Table.Td style={{}}> <Table.Td style={{}}>
<FieldType field_data={field} field_value={item[field.name]} /> <FieldType field_data={field} field_value={fieldValue} />
</Table.Td> </Table.Td>
<Table.Td style={{ width: '50' }}> <Table.Td style={{ width: '50' }}>
{field.copy && <CopyField value={item[field.name]} />} {field.copy && <CopyField value={fieldValue} />}
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
); );

View File

@ -25,7 +25,11 @@ import {
RenderPartParameterTemplate, RenderPartParameterTemplate,
RenderPartTestTemplate RenderPartTestTemplate
} from './Part'; } from './Part';
import { RenderStockItem, RenderStockLocation } from './Stock'; import {
RenderStockItem,
RenderStockLocation,
RenderStockLocationType
} from './Stock';
import { RenderOwner, RenderUser } from './User'; import { RenderOwner, RenderUser } from './User';
type EnumDictionary<T extends string | symbol | number, U> = { type EnumDictionary<T extends string | symbol | number, U> = {
@ -57,6 +61,7 @@ const RendererLookup: EnumDictionary<
[ModelType.salesorder]: RenderSalesOrder, [ModelType.salesorder]: RenderSalesOrder,
[ModelType.salesordershipment]: RenderSalesOrderShipment, [ModelType.salesordershipment]: RenderSalesOrderShipment,
[ModelType.stocklocation]: RenderStockLocation, [ModelType.stocklocation]: RenderStockLocation,
[ModelType.stocklocationtype]: RenderStockLocationType,
[ModelType.stockitem]: RenderStockItem, [ModelType.stockitem]: RenderStockItem,
[ModelType.stockhistory]: RenderStockItem, [ModelType.stockhistory]: RenderStockItem,
[ModelType.supplierpart]: RenderSupplierPart, [ModelType.supplierpart]: RenderSupplierPart,

View File

@ -79,6 +79,11 @@ export const ModelInformationDict: ModelDict = {
cui_detail: '/stock/location/:pk/', cui_detail: '/stock/location/:pk/',
api_endpoint: ApiEndpoints.stock_location_list api_endpoint: ApiEndpoints.stock_location_list
}, },
stocklocationtype: {
label: t`Stock Location Type`,
label_multiple: t`Stock Location Types`,
api_endpoint: ApiEndpoints.stock_location_type_list
},
stockhistory: { stockhistory: {
label: t`Stock History`, label: t`Stock History`,
label_multiple: t`Stock Histories`, label_multiple: t`Stock Histories`,

View File

@ -17,6 +17,21 @@ export function RenderStockLocation({
); );
} }
/**
* Inline rendering of a single StockLocationType instance
*/
export function RenderStockLocationType({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
<RenderInlineModel
primary={instance.name}
// TODO: render location icon here too (ref: #7237)
secondary={instance.description + ` (${instance.location_count})`}
/>
);
}
export function RenderStockItem({ export function RenderStockItem({
instance instance
}: Readonly<InstanceRenderInterface>): ReactNode { }: Readonly<InstanceRenderInterface>): ReactNode {

View File

@ -94,6 +94,7 @@ export enum ApiEndpoints {
stock_item_list = 'stock/', stock_item_list = 'stock/',
stock_tracking_list = 'stock/track/', stock_tracking_list = 'stock/track/',
stock_location_list = 'stock/location/', stock_location_list = 'stock/location/',
stock_location_type_list = 'stock/location-type/',
stock_location_tree = 'stock/location/tree/', stock_location_tree = 'stock/location/tree/',
stock_attachment_list = 'stock/attachment/', stock_attachment_list = 'stock/attachment/',
stock_test_result_list = 'stock/test/', stock_test_result_list = 'stock/test/',

View File

@ -11,6 +11,7 @@ export enum ModelType {
projectcode = 'projectcode', projectcode = 'projectcode',
stockitem = 'stockitem', stockitem = 'stockitem',
stocklocation = 'stocklocation', stocklocation = 'stocklocation',
stocklocationtype = 'stocklocationtype',
stockhistory = 'stockhistory', stockhistory = 'stockhistory',
build = 'build', build = 'build',
buildline = 'buildline', buildline = 'buildline',

View File

@ -819,6 +819,7 @@ export function stockLocationFields({}: {}): ApiFormFieldSet {
description: {}, description: {},
structural: {}, structural: {},
external: {}, external: {},
icon: {},
location_type: {} location_type: {}
}; };

View File

@ -7,6 +7,7 @@ import {
IconExclamationCircle, IconExclamationCircle,
IconList, IconList,
IconListDetails, IconListDetails,
IconPackages,
IconPlugConnected, IconPlugConnected,
IconScale, IconScale,
IconSitemap, IconSitemap,
@ -57,6 +58,10 @@ const PartCategoryTemplateTable = Loadable(
lazy(() => import('../../../../tables/part/PartCategoryTemplateTable')) lazy(() => import('../../../../tables/part/PartCategoryTemplateTable'))
); );
const LocationTypesTable = Loadable(
lazy(() => import('../../../../tables/stock/LocationTypesTable'))
);
const CurrencyTable = Loadable( const CurrencyTable = Loadable(
lazy(() => import('../../../../tables/settings/CurrencyTable')) lazy(() => import('../../../../tables/settings/CurrencyTable'))
); );
@ -122,6 +127,12 @@ export default function AdminCenter() {
icon: <IconSitemap />, icon: <IconSitemap />,
content: <PartCategoryTemplateTable /> content: <PartCategoryTemplateTable />
}, },
{
name: 'location-types',
label: t`Location types`,
icon: <IconPackages />,
content: <LocationTypesTable />
},
{ {
name: 'templates', name: 'templates',
label: t`Templates`, label: t`Templates`,

View File

@ -133,6 +133,14 @@ export default function Stock() {
type: 'boolean', type: 'boolean',
name: 'external', name: 'external',
label: t`External` label: t`External`
},
{
type: 'string',
// TODO: render location type icon here (ref: #7237)
name: 'location_type_detail.name',
label: t`Location Type`,
hidden: !location?.location_type,
icon: 'packages'
} }
]; ];

View File

@ -131,7 +131,7 @@ export default function ParametricPartTable({
const addParameter = useCreateApiFormModal({ const addParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,
title: t`Add Part Parameter`, title: t`Add Part Parameter`,
fields: partParameterFields, fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
focus: 'data', focus: 'data',
onFormSuccess: (parameter: any) => { onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedPart, parameter); updateParameterRecord(selectedPart, parameter);
@ -146,7 +146,7 @@ export default function ParametricPartTable({
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,
title: t`Edit Part Parameter`, title: t`Edit Part Parameter`,
pk: selectedParameter, pk: selectedParameter,
fields: partParameterFields, fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
focus: 'data', focus: 'data',
onFormSuccess: (parameter: any) => { onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedPart, parameter); updateParameterRecord(selectedPart, parameter);

View File

@ -36,7 +36,7 @@ export default function PartCategoryTemplateTable({}: {}) {
const newTemplate = useCreateApiFormModal({ const newTemplate = useCreateApiFormModal({
url: ApiEndpoints.category_parameter_list, url: ApiEndpoints.category_parameter_list,
title: t`Add Category Parameter`, title: t`Add Category Parameter`,
fields: formFields, fields: useMemo(() => ({ ...formFields }), [formFields]),
onFormSuccess: table.refreshTable onFormSuccess: table.refreshTable
}); });
@ -44,7 +44,7 @@ export default function PartCategoryTemplateTable({}: {}) {
url: ApiEndpoints.category_parameter_list, url: ApiEndpoints.category_parameter_list,
pk: selectedTemplate, pk: selectedTemplate,
title: t`Edit Category Parameter`, title: t`Edit Category Parameter`,
fields: formFields, fields: useMemo(() => ({ ...formFields }), [formFields]),
onFormSuccess: (record: any) => table.updateRecord(record) onFormSuccess: (record: any) => table.updateRecord(record)
}); });

View File

@ -110,7 +110,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
const newParameter = useCreateApiFormModal({ const newParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,
title: t`New Part Parameter`, title: t`New Part Parameter`,
fields: partParameterFields, fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
focus: 'template', focus: 'template',
initialData: { initialData: {
part: partId part: partId
@ -126,7 +126,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,
pk: selectedParameter, pk: selectedParameter,
title: t`Edit Part Parameter`, title: t`Edit Part Parameter`,
fields: partParameterFields, fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
onFormSuccess: table.refreshTable onFormSuccess: table.refreshTable
}); });

View File

@ -83,7 +83,10 @@ export default function PartParameterTemplateTable() {
const newTemplate = useCreateApiFormModal({ const newTemplate = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_template_list, url: ApiEndpoints.part_parameter_template_list,
title: t`Add Parameter Template`, title: t`Add Parameter Template`,
fields: partParameterTemplateFields, fields: useMemo(
() => ({ ...partParameterTemplateFields }),
[partParameterTemplateFields]
),
onFormSuccess: table.refreshTable onFormSuccess: table.refreshTable
}); });
@ -95,7 +98,10 @@ export default function PartParameterTemplateTable() {
url: ApiEndpoints.part_parameter_template_list, url: ApiEndpoints.part_parameter_template_list,
pk: selectedTemplate, pk: selectedTemplate,
title: t`Edit Parameter Template`, title: t`Edit Parameter Template`,
fields: partParameterTemplateFields, fields: useMemo(
() => ({ ...partParameterTemplateFields }),
[partParameterTemplateFields]
),
onFormSuccess: (record: any) => table.updateRecord(record) onFormSuccess: (record: any) => table.updateRecord(record)
}); });

View File

@ -124,7 +124,10 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
const newTestTemplate = useCreateApiFormModal({ const newTestTemplate = useCreateApiFormModal({
url: ApiEndpoints.part_test_template_list, url: ApiEndpoints.part_test_template_list,
title: t`Add Test Template`, title: t`Add Test Template`,
fields: partTestTemplateFields, fields: useMemo(
() => ({ ...partTestTemplateFields }),
[partTestTemplateFields]
),
initialData: { initialData: {
part: partId part: partId
}, },
@ -137,7 +140,10 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
url: ApiEndpoints.part_test_template_list, url: ApiEndpoints.part_test_template_list,
pk: selectedTest, pk: selectedTest,
title: t`Edit Test Template`, title: t`Edit Test Template`,
fields: partTestTemplateFields, fields: useMemo(
() => ({ ...partTestTemplateFields }),
[partTestTemplateFields]
),
onFormSuccess: (record: any) => table.updateRecord(record) onFormSuccess: (record: any) => table.updateRecord(record)
}); });

View File

@ -0,0 +1,128 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
export default function LocationTypesTable() {
const table = useTable('location-types');
const user = useUserState();
const formFields: ApiFormFieldSet = useMemo(() => {
return {
name: {},
description: {},
icon: {}
};
}, []);
const [selectedLocationType, setSelectedLocationType] = useState<number>(0);
const newLocationType = useCreateApiFormModal({
url: ApiEndpoints.stock_location_type_list,
title: t`Add Location Type`,
fields: useMemo(() => ({ ...formFields }), [formFields]),
onFormSuccess: table.refreshTable
});
const editLocationType = useEditApiFormModal({
url: ApiEndpoints.stock_location_type_list,
pk: selectedLocationType,
title: t`Edit Location Type`,
fields: useMemo(() => ({ ...formFields }), [formFields]),
onFormSuccess: (record: any) => table.updateRecord(record)
});
const deleteLocationType = useDeleteApiFormModal({
url: ApiEndpoints.stock_location_type_list,
pk: selectedLocationType,
title: t`Delete Location Type`,
onFormSuccess: table.refreshTable
});
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'name',
title: t`Name`,
sortable: true
},
{
accessor: 'description',
title: t`Description`
},
{
accessor: 'icon',
title: t`Icon`,
sortable: true
},
{
accessor: 'location_count',
sortable: true
}
];
}, []);
const rowActions = useCallback(
(record: any) => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.stock_location),
onClick: () => {
setSelectedLocationType(record.pk);
editLocationType.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.stock_location),
onClick: () => {
setSelectedLocationType(record.pk);
deleteLocationType.open();
}
})
];
},
[user]
);
const tableActions = useMemo(() => {
return [
<AddItemButton
key="add-location-type"
tooltip={t`Add Location Type`}
onClick={() => newLocationType.open()}
hidden={!user.hasAddRole(UserRoles.stock_location)}
/>
];
}, [user]);
return (
<>
{newLocationType.modal}
{editLocationType.modal}
{deleteLocationType.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.stock_location_type_list)}
tableState={table}
columns={tableColumns}
props={{
rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
);
}