From dc741b61837d06d1fcdc45854cccf6762bf947c8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 17 May 2024 12:25:47 +1000 Subject: [PATCH] Admin tweaks (#7248) * Update admin site - Implement 'autocomplete' for more fields - Improves admin loading time * Add "admin" buttons to the PUI interface * Only allow superuser access --- src/backend/InvenTree/common/admin.py | 9 ++ src/backend/InvenTree/company/admin.py | 4 + src/backend/InvenTree/order/admin.py | 6 +- src/backend/InvenTree/part/admin.py | 4 +- src/backend/InvenTree/stock/admin.py | 1 + .../src/components/buttons/ActionButton.tsx | 9 +- .../src/components/buttons/AdminButton.tsx | 88 +++++++++++++++++++ .../src/components/render/ModelType.tsx | 34 ++++--- src/frontend/src/pages/build/BuildDetail.tsx | 6 +- .../src/pages/company/CompanyDetail.tsx | 3 + .../pages/company/ManufacturerPartDetail.tsx | 7 +- .../src/pages/company/SupplierPartDetail.tsx | 4 +- .../src/pages/part/CategoryDetail.tsx | 4 +- src/frontend/src/pages/part/PartDetail.tsx | 2 + .../pages/purchasing/PurchaseOrderDetail.tsx | 2 + .../src/pages/sales/ReturnOrderDetail.tsx | 4 +- .../src/pages/sales/SalesOrderDetail.tsx | 4 +- .../src/pages/stock/LocationDetail.tsx | 2 + src/frontend/src/pages/stock/StockDetail.tsx | 2 + 19 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 src/frontend/src/components/buttons/AdminButton.tsx diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index 528f705c3a..9dd3a05018 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -7,6 +7,15 @@ from import_export.admin import ImportExportModelAdmin import common.models +@admin.register(common.models.ProjectCode) +class ProjectCodeAdmin(ImportExportModelAdmin): + """Admin settings for ProjectCode.""" + + list_display = ('code', 'description') + + search_fields = ('code', 'description') + + class SettingsAdmin(ImportExportModelAdmin): """Admin settings for InvenTreeSetting.""" diff --git a/src/backend/InvenTree/company/admin.py b/src/backend/InvenTree/company/admin.py index 69136ad80c..7caf7f3b16 100644 --- a/src/backend/InvenTree/company/admin.py +++ b/src/backend/InvenTree/company/admin.py @@ -213,6 +213,8 @@ class AddressAdmin(ImportExportModelAdmin): search_fields = ['company', 'country', 'postal_code'] + autocomplete_fields = ['company'] + class ContactResource(InvenTreeResource): """Class for managing Contact data import/export.""" @@ -237,3 +239,5 @@ class ContactAdmin(ImportExportModelAdmin): list_display = ('company', 'name', 'role', 'email', 'phone') search_fields = ['company', 'name', 'email'] + + autocomplete_fields = ['company'] diff --git a/src/backend/InvenTree/order/admin.py b/src/backend/InvenTree/order/admin.py index 2345e8ca2a..a26d7499a3 100644 --- a/src/backend/InvenTree/order/admin.py +++ b/src/backend/InvenTree/order/admin.py @@ -114,7 +114,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin): inlines = [PurchaseOrderLineItemInlineAdmin] - autocomplete_fields = ('supplier',) + autocomplete_fields = ['supplier', 'project_code', 'contact', 'address'] class SalesOrderResource( @@ -152,7 +152,7 @@ class SalesOrderAdmin(ImportExportModelAdmin): search_fields = ['reference', 'customer__name', 'description'] - autocomplete_fields = ('customer',) + autocomplete_fields = ['customer', 'project_code', 'contact', 'address'] class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource): @@ -317,7 +317,7 @@ class ReturnOrderAdmin(ImportExportModelAdmin): search_fields = ['reference', 'customer__name', 'description'] - autocomplete_fields = ['customer'] + autocomplete_fields = ['customer', 'project_code', 'contact', 'address'] class ReturnOrderLineItemResource(PriceResourceMixin, InvenTreeResource): diff --git a/src/backend/InvenTree/part/admin.py b/src/backend/InvenTree/part/admin.py index c0d32fb0a2..e526dfc7ab 100644 --- a/src/backend/InvenTree/part/admin.py +++ b/src/backend/InvenTree/part/admin.py @@ -250,6 +250,8 @@ class PartAdmin(ImportExportModelAdmin): 'category', 'default_location', 'default_supplier', + 'bom_checked_by', + 'creation_user', ] inlines = [PartParameterInline] @@ -260,7 +262,7 @@ class PartPricingAdmin(admin.ModelAdmin): list_display = ('part', 'overall_min', 'overall_max') - autcomplete_fields = ['part'] + autocomplete_fields = ['part'] class PartStocktakeAdmin(admin.ModelAdmin): diff --git a/src/backend/InvenTree/stock/admin.py b/src/backend/InvenTree/stock/admin.py index f97c281f52..3601e992da 100644 --- a/src/backend/InvenTree/stock/admin.py +++ b/src/backend/InvenTree/stock/admin.py @@ -292,6 +292,7 @@ class StockItemAdmin(ImportExportModelAdmin): 'sales_order', 'stocktake_user', 'supplier_part', + 'consumed_by', ] diff --git a/src/frontend/src/components/buttons/ActionButton.tsx b/src/frontend/src/components/buttons/ActionButton.tsx index 1a4eb08a31..ba541b6ad3 100644 --- a/src/frontend/src/components/buttons/ActionButton.tsx +++ b/src/frontend/src/components/buttons/ActionButton.tsx @@ -4,13 +4,13 @@ import { ReactNode } from 'react'; import { notYetImplemented } from '../../functions/notifications'; export type ActionButtonProps = { - key?: string; icon?: ReactNode; text?: string; color?: string; tooltip?: string; variant?: string; size?: number | string; + radius?: number | string; disabled?: boolean; onClick?: any; hidden?: boolean; @@ -26,15 +26,16 @@ export function ActionButton(props: ActionButtonProps) { return ( !hidden && ( { + // Only users with superuser permission will see this button + if (!user || !user.isLoggedIn() || !user.isSuperuser()) { + return false; + } + + // TODO: Check if the server has the admin interface enabled + + const modelDef = ModelInformationDict[props.model]; + + // No admin URL associated with the model + if (!modelDef.admin_url) { + return false; + } + + // No primary key provided + if (!props.pk) { + return false; + } + + return true; + }, [user, props.model, props.pk]); + + const openAdmin = useCallback( + (event: any) => { + const modelDef = ModelInformationDict[props.model]; + const host = useLocalState.getState().host; + + if (!modelDef.admin_url) { + return; + } + + // TODO: Check the actual "admin" URL (it may be custom) + const url = `${host}/admin${modelDef.admin_url}${props.pk}/`; + + if (event?.ctrlKey || event?.shiftKey) { + // Open the link in a new tab + window.open(url, '_blank'); + } else { + window.open(url, '_self'); + } + }, + [props.model, props.pk] + ); + + return ( + } + color="blue" + size="lg" + radius="sm" + variant="filled" + tooltip={t`Open in admin interface`} + hidden={!enabled} + onClick={openAdmin} + /> + ); +} diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 421a1945a6..e4c9848f7e 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -10,6 +10,7 @@ export interface ModelInformationInterface { url_detail?: string; api_endpoint: ApiEndpoints; cui_detail?: string; + admin_url?: string; } export type ModelDict = { @@ -23,7 +24,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/part', url_detail: '/part/:pk/', cui_detail: '/part/:pk/', - api_endpoint: ApiEndpoints.part_list + api_endpoint: ApiEndpoints.part_list, + admin_url: '/part/part/' }, partparametertemplate: { label: t`Part Parameter Template`, @@ -45,7 +47,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/supplierpart', url_detail: '/purchasing/supplier-part/:pk/', cui_detail: '/supplier-part/:pk/', - api_endpoint: ApiEndpoints.supplier_part_list + api_endpoint: ApiEndpoints.supplier_part_list, + admin_url: '/company/supplierpart/' }, manufacturerpart: { label: t`Manufacturer Part`, @@ -53,7 +56,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/manufacturerpart', url_detail: '/purchasing/manufacturer-part/:pk/', cui_detail: '/manufacturer-part/:pk/', - api_endpoint: ApiEndpoints.manufacturer_part_list + api_endpoint: ApiEndpoints.manufacturer_part_list, + admin_url: '/company/manufacturerpart/' }, partcategory: { label: t`Part Category`, @@ -61,7 +65,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/part/category', url_detail: '/part/category/:pk/', cui_detail: '/part/category/:pk/', - api_endpoint: ApiEndpoints.category_list + api_endpoint: ApiEndpoints.category_list, + admin_url: '/part/partcategory/' }, stockitem: { label: t`Stock Item`, @@ -69,7 +74,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/stock/item', url_detail: '/stock/item/:pk/', cui_detail: '/stock/item/:pk/', - api_endpoint: ApiEndpoints.stock_item_list + api_endpoint: ApiEndpoints.stock_item_list, + admin_url: '/stock/stockitem/' }, stocklocation: { label: t`Stock Location`, @@ -77,7 +83,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/stock/location', url_detail: '/stock/location/:pk/', cui_detail: '/stock/location/:pk/', - api_endpoint: ApiEndpoints.stock_location_list + api_endpoint: ApiEndpoints.stock_location_list, + admin_url: '/stock/stocklocation/' }, stocklocationtype: { label: t`Stock Location Type`, @@ -95,7 +102,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/build', url_detail: '/build/:pk/', cui_detail: '/build/:pk/', - api_endpoint: ApiEndpoints.build_order_list + api_endpoint: ApiEndpoints.build_order_list, + admin_url: '/build/build/' }, buildline: { label: t`Build Line`, @@ -111,7 +119,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/company', url_detail: '/company/:pk/', cui_detail: '/company/:pk/', - api_endpoint: ApiEndpoints.company_list + api_endpoint: ApiEndpoints.company_list, + admin_url: '/company/company/' }, projectcode: { label: t`Project Code`, @@ -126,7 +135,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/purchasing/purchase-order', url_detail: '/purchasing/purchase-order/:pk/', cui_detail: '/order/purchase-order/:pk/', - api_endpoint: ApiEndpoints.purchase_order_list + api_endpoint: ApiEndpoints.purchase_order_list, + admin_url: '/order/purchaseorder/' }, purchaseorderline: { label: t`Purchase Order Line`, @@ -139,7 +149,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/sales/sales-order', url_detail: '/sales/sales-order/:pk/', cui_detail: '/order/sales-order/:pk/', - api_endpoint: ApiEndpoints.sales_order_list + api_endpoint: ApiEndpoints.sales_order_list, + admin_url: '/order/salesorder/' }, salesordershipment: { label: t`Sales Order Shipment`, @@ -154,7 +165,8 @@ export const ModelInformationDict: ModelDict = { url_overview: '/sales/return-order', url_detail: '/sales/return-order/:pk/', cui_detail: '/order/return-order/:pk/', - api_endpoint: ApiEndpoints.return_order_list + api_endpoint: ApiEndpoints.return_order_list, + admin_url: '/order/returnorder/' }, address: { label: t`Address`, diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index fbbffa5e81..f78f7620aa 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -17,6 +17,7 @@ import { import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; @@ -347,8 +348,8 @@ export default function BuildDetail() { }); const buildActions = useMemo(() => { - // TODO: Disable certain actions based on user permissions return [ + , cancelBuild.open() + onClick: () => cancelBuild.open(), + hidden: !user.hasChangeRole(UserRoles.build) // TODO: Hide if build cannot be cancelled }), DuplicateItemAction({ diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index 22b64740f3..bdfe135f27 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -18,6 +18,7 @@ import { import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -32,6 +33,7 @@ import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { companyFields } from '../../forms/CompanyForms'; import { useEditApiFormModal } from '../../hooks/UseForm'; @@ -285,6 +287,7 @@ export default function CompanyDetail(props: Readonly) { const companyActions = useMemo(() => { return [ + , { return [ + , ]; - }, [user]); + }, [user, manufacturerPart]); const breadcrumbs = useMemo(() => { return [ diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index 8afe9dced7..28a08425d4 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -10,6 +10,7 @@ import { import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -243,6 +244,7 @@ export default function SupplierPartDetail() { const supplierPartActions = useMemo(() => { return [ + , ]; - }, [user]); + }, [user, supplierPart]); const supplierPartFields = useSupplierPartFields(); diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index d82ad4c29a..6d03903d4a 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -10,6 +10,7 @@ import { import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { @@ -201,6 +202,7 @@ export default function CategoryDetail({}: {}) { const categoryActions = useMemo(() => { return [ + , ]; - }, [id, user]); + }, [id, user, category.pk]); const categoryPanels: PanelType[] = useMemo( () => [ diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 839ab2e6c5..672c4c15aa 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -34,6 +34,7 @@ import { ReactNode, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { api } from '../../App'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -746,6 +747,7 @@ export default function PartDetail() { const partActions = useMemo(() => { return [ + , { return [ + , { return [ + , ]; - }, [user]); + }, [user, order]); return ( <> diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index bdd01d04f4..4f540fca14 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -13,6 +13,7 @@ import { import { ReactNode, useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; @@ -297,6 +298,7 @@ export default function SalesOrderDetail() { const soActions = useMemo(() => { return [ + , ]; - }, [user]); + }, [user, order]); const orderBadges: ReactNode[] = useMemo(() => { return instanceQuery.isLoading diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index 2c8ce29b89..a7db455caf 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -10,6 +10,7 @@ import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { ActionButton } from '../../components/buttons/ActionButton'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { @@ -266,6 +267,7 @@ export default function Stock() { const locationActions = useMemo( () => [ + , } variant="outline" diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index b76ba94be0..6cc5abbf38 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -15,6 +15,7 @@ import { import { ReactNode, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import AdminButton from '../../components/buttons/AdminButton'; import { DetailsField, DetailsTable } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -408,6 +409,7 @@ export default function StockDetail() { const stockActions = useMemo( () => [ + ,