diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index 2161a3a4dd..374f35b556 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -12,6 +12,7 @@ import { Tooltip } from '@mantine/core'; import { useSuspenseQuery } from '@tanstack/react-query'; +import { getValueAtPath } from 'mantine-datatable'; import { Suspense, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -43,7 +44,7 @@ export type PartIconsType = { export type DetailsField = | { hidden?: boolean; - icon?: string; + icon?: InvenTreeIconType; name: string; label?: string; badge?: BadgeType; @@ -382,6 +383,11 @@ export function DetailsTableField({ const FieldType: any = getFieldType(field.type); + const fieldValue = useMemo( + () => getValueAtPath(item, field.name) as string, + [item, field.name] + ); + return ( - + {field.label} - + - {field.copy && } + {field.copy && } ); diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index e3c4bbde97..fefb2ddb91 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -25,7 +25,11 @@ import { RenderPartParameterTemplate, RenderPartTestTemplate } from './Part'; -import { RenderStockItem, RenderStockLocation } from './Stock'; +import { + RenderStockItem, + RenderStockLocation, + RenderStockLocationType +} from './Stock'; import { RenderOwner, RenderUser } from './User'; type EnumDictionary = { @@ -57,6 +61,7 @@ const RendererLookup: EnumDictionary< [ModelType.salesorder]: RenderSalesOrder, [ModelType.salesordershipment]: RenderSalesOrderShipment, [ModelType.stocklocation]: RenderStockLocation, + [ModelType.stocklocationtype]: RenderStockLocationType, [ModelType.stockitem]: RenderStockItem, [ModelType.stockhistory]: RenderStockItem, [ModelType.supplierpart]: RenderSupplierPart, diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index cc9c66fd73..421a1945a6 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -79,6 +79,11 @@ export const ModelInformationDict: ModelDict = { cui_detail: '/stock/location/:pk/', 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: { label: t`Stock History`, label_multiple: t`Stock Histories`, diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index 1f8a60b995..4555cee4c6 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -17,6 +17,21 @@ export function RenderStockLocation({ ); } +/** + * Inline rendering of a single StockLocationType instance + */ +export function RenderStockLocationType({ + instance +}: Readonly): ReactNode { + return ( + + ); +} + export function RenderStockItem({ instance }: Readonly): ReactNode { diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 43c9371052..8f703f1624 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -94,6 +94,7 @@ export enum ApiEndpoints { stock_item_list = 'stock/', stock_tracking_list = 'stock/track/', stock_location_list = 'stock/location/', + stock_location_type_list = 'stock/location-type/', stock_location_tree = 'stock/location/tree/', stock_attachment_list = 'stock/attachment/', stock_test_result_list = 'stock/test/', diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/src/enums/ModelType.tsx index e5d5155883..8f0574ef91 100644 --- a/src/frontend/src/enums/ModelType.tsx +++ b/src/frontend/src/enums/ModelType.tsx @@ -11,6 +11,7 @@ export enum ModelType { projectcode = 'projectcode', stockitem = 'stockitem', stocklocation = 'stocklocation', + stocklocationtype = 'stocklocationtype', stockhistory = 'stockhistory', build = 'build', buildline = 'buildline', diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index eb8e0bad27..fca1e2834c 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -819,6 +819,7 @@ export function stockLocationFields({}: {}): ApiFormFieldSet { description: {}, structural: {}, external: {}, + icon: {}, location_type: {} }; diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 876651f886..cca88e567b 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -7,6 +7,7 @@ import { IconExclamationCircle, IconList, IconListDetails, + IconPackages, IconPlugConnected, IconScale, IconSitemap, @@ -57,6 +58,10 @@ const PartCategoryTemplateTable = Loadable( lazy(() => import('../../../../tables/part/PartCategoryTemplateTable')) ); +const LocationTypesTable = Loadable( + lazy(() => import('../../../../tables/stock/LocationTypesTable')) +); + const CurrencyTable = Loadable( lazy(() => import('../../../../tables/settings/CurrencyTable')) ); @@ -122,6 +127,12 @@ export default function AdminCenter() { icon: , content: }, + { + name: 'location-types', + label: t`Location types`, + icon: , + content: + }, { name: 'templates', label: t`Templates`, diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index 2f6e09cc44..2c8ce29b89 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -133,6 +133,14 @@ export default function Stock() { type: 'boolean', name: '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' } ]; diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx index 378b572991..18bd3855ec 100644 --- a/src/frontend/src/tables/part/ParametricPartTable.tsx +++ b/src/frontend/src/tables/part/ParametricPartTable.tsx @@ -131,7 +131,7 @@ export default function ParametricPartTable({ const addParameter = useCreateApiFormModal({ url: ApiEndpoints.part_parameter_list, title: t`Add Part Parameter`, - fields: partParameterFields, + fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]), focus: 'data', onFormSuccess: (parameter: any) => { updateParameterRecord(selectedPart, parameter); @@ -146,7 +146,7 @@ export default function ParametricPartTable({ url: ApiEndpoints.part_parameter_list, title: t`Edit Part Parameter`, pk: selectedParameter, - fields: partParameterFields, + fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]), focus: 'data', onFormSuccess: (parameter: any) => { updateParameterRecord(selectedPart, parameter); diff --git a/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx index 6a262fd4d3..861747cb9b 100644 --- a/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx +++ b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx @@ -36,7 +36,7 @@ export default function PartCategoryTemplateTable({}: {}) { const newTemplate = useCreateApiFormModal({ url: ApiEndpoints.category_parameter_list, title: t`Add Category Parameter`, - fields: formFields, + fields: useMemo(() => ({ ...formFields }), [formFields]), onFormSuccess: table.refreshTable }); @@ -44,7 +44,7 @@ export default function PartCategoryTemplateTable({}: {}) { url: ApiEndpoints.category_parameter_list, pk: selectedTemplate, title: t`Edit Category Parameter`, - fields: formFields, + fields: useMemo(() => ({ ...formFields }), [formFields]), onFormSuccess: (record: any) => table.updateRecord(record) }); diff --git a/src/frontend/src/tables/part/PartParameterTable.tsx b/src/frontend/src/tables/part/PartParameterTable.tsx index 65c3740812..f6550d642d 100644 --- a/src/frontend/src/tables/part/PartParameterTable.tsx +++ b/src/frontend/src/tables/part/PartParameterTable.tsx @@ -110,7 +110,7 @@ export function PartParameterTable({ partId }: { partId: any }) { const newParameter = useCreateApiFormModal({ url: ApiEndpoints.part_parameter_list, title: t`New Part Parameter`, - fields: partParameterFields, + fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]), focus: 'template', initialData: { part: partId @@ -126,7 +126,7 @@ export function PartParameterTable({ partId }: { partId: any }) { url: ApiEndpoints.part_parameter_list, pk: selectedParameter, title: t`Edit Part Parameter`, - fields: partParameterFields, + fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]), onFormSuccess: table.refreshTable }); diff --git a/src/frontend/src/tables/part/PartParameterTemplateTable.tsx b/src/frontend/src/tables/part/PartParameterTemplateTable.tsx index 0442eb36de..5b2256c3ca 100644 --- a/src/frontend/src/tables/part/PartParameterTemplateTable.tsx +++ b/src/frontend/src/tables/part/PartParameterTemplateTable.tsx @@ -83,7 +83,10 @@ export default function PartParameterTemplateTable() { const newTemplate = useCreateApiFormModal({ url: ApiEndpoints.part_parameter_template_list, title: t`Add Parameter Template`, - fields: partParameterTemplateFields, + fields: useMemo( + () => ({ ...partParameterTemplateFields }), + [partParameterTemplateFields] + ), onFormSuccess: table.refreshTable }); @@ -95,7 +98,10 @@ export default function PartParameterTemplateTable() { url: ApiEndpoints.part_parameter_template_list, pk: selectedTemplate, title: t`Edit Parameter Template`, - fields: partParameterTemplateFields, + fields: useMemo( + () => ({ ...partParameterTemplateFields }), + [partParameterTemplateFields] + ), onFormSuccess: (record: any) => table.updateRecord(record) }); diff --git a/src/frontend/src/tables/part/PartTestTemplateTable.tsx b/src/frontend/src/tables/part/PartTestTemplateTable.tsx index f96fda50b0..b92140e0f3 100644 --- a/src/frontend/src/tables/part/PartTestTemplateTable.tsx +++ b/src/frontend/src/tables/part/PartTestTemplateTable.tsx @@ -124,7 +124,10 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) { const newTestTemplate = useCreateApiFormModal({ url: ApiEndpoints.part_test_template_list, title: t`Add Test Template`, - fields: partTestTemplateFields, + fields: useMemo( + () => ({ ...partTestTemplateFields }), + [partTestTemplateFields] + ), initialData: { part: partId }, @@ -137,7 +140,10 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) { url: ApiEndpoints.part_test_template_list, pk: selectedTest, title: t`Edit Test Template`, - fields: partTestTemplateFields, + fields: useMemo( + () => ({ ...partTestTemplateFields }), + [partTestTemplateFields] + ), onFormSuccess: (record: any) => table.updateRecord(record) }); diff --git a/src/frontend/src/tables/stock/LocationTypesTable.tsx b/src/frontend/src/tables/stock/LocationTypesTable.tsx new file mode 100644 index 0000000000..c2b5afcae5 --- /dev/null +++ b/src/frontend/src/tables/stock/LocationTypesTable.tsx @@ -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(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 [ + newLocationType.open()} + hidden={!user.hasAddRole(UserRoles.stock_location)} + /> + ]; + }, [user]); + + return ( + <> + {newLocationType.modal} + {editLocationType.modal} + {deleteLocationType.modal} + + + ); +}