From a9b932cc320946b17ac6f9e1e503834386e04d22 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 30 Apr 2024 16:21:38 +1000 Subject: [PATCH] PUI tweaks (#7144) * Default progress bars a bit thicker * Implement useFilters hook - Adds "project code" filter for order tables * Add "responsible" filters to backend * Add more filters to tables * Bump API version * Typo fix * Tweak PartTable * Tweaks * remove unused imports --- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/order/api.py | 4 + src/backend/InvenTree/order/serializers.py | 12 ++- .../src/components/items/ProgressBar.tsx | 5 +- src/frontend/src/hooks/UseFilter.tsx | 91 +++++++++++++++++++ .../src/tables/build/BuildOrderTable.tsx | 47 ++++++++-- .../src/tables/build/BuildOutputTable.tsx | 16 ++-- src/frontend/src/tables/part/PartTable.tsx | 10 +- .../tables/purchasing/PurchaseOrderTable.tsx | 27 +++++- .../src/tables/sales/ReturnOrderTable.tsx | 25 ++++- .../src/tables/sales/SalesOrderTable.tsx | 27 +++++- .../src/tables/stock/StockItemTable.tsx | 2 +- 12 files changed, 226 insertions(+), 45 deletions(-) create mode 100644 src/frontend/src/hooks/UseFilter.tsx diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 5f43e0305c..0d405b6de0 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 192 +INVENTREE_API_VERSION = 193 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v193 - 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144 + - Adds "assigned_to" filter to PurchaseOrder / SalesOrder / ReturnOrder API endpoints + v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106 - Adds 'trackable' ordering option to BuildLineLabel API endpoint diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 1d6692793e..9ee4df9974 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -148,6 +148,10 @@ class OrderFilter(rest_filters.FilterSet): return queryset.exclude(project_code=None) return queryset.filter(project_code=None) + assigned_to = rest_filters.ModelChoiceFilter( + queryset=Owner.objects.all(), field_name='responsible' + ) + class LineItemFilter(rest_filters.FilterSet): """Base class for custom API filters for order line item list(s).""" diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 42f6a3c69e..a1da09f21b 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -77,16 +77,18 @@ class AbstractOrderSerializer(serializers.Serializer): """Abstract serializer class which provides fields common to all order types.""" # Number of line items in this order - line_items = serializers.IntegerField(read_only=True) + line_items = serializers.IntegerField(read_only=True, label=_('Line Items')) # Number of completed line items (this is an annotated field) - completed_lines = serializers.IntegerField(read_only=True) + completed_lines = serializers.IntegerField( + read_only=True, label=_('Completed Lines') + ) # Human-readable status text (read-only) status_text = serializers.CharField(source='get_status_display', read_only=True) # status field cannot be set directly - status = serializers.IntegerField(read_only=True) + status = serializers.IntegerField(read_only=True, label=_('Order Status')) # Reference string is *required* reference = serializers.CharField(required=True) @@ -114,7 +116,9 @@ class AbstractOrderSerializer(serializers.Serializer): barcode_hash = serializers.CharField(read_only=True) - creation_date = serializers.DateField(required=False, allow_null=True) + creation_date = serializers.DateField( + required=False, allow_null=True, label=_('Creation Date') + ) def validate_reference(self, reference): """Custom validation for the reference field.""" diff --git a/src/frontend/src/components/items/ProgressBar.tsx b/src/frontend/src/components/items/ProgressBar.tsx index b2938cfdeb..2369da7faa 100644 --- a/src/frontend/src/components/items/ProgressBar.tsx +++ b/src/frontend/src/components/items/ProgressBar.tsx @@ -6,6 +6,7 @@ export type ProgressBarProps = { maximum?: number; label?: string; progressLabel?: boolean; + size?: string; }; /** @@ -31,8 +32,8 @@ export function ProgressBar(props: ProgressBarProps) { 100 ? 'blue' : 'green'} - size="sm" - radius="xs" + size={props.size ?? 'md'} + radius="sm" /> ); diff --git a/src/frontend/src/hooks/UseFilter.tsx b/src/frontend/src/hooks/UseFilter.tsx new file mode 100644 index 0000000000..9d36a6e426 --- /dev/null +++ b/src/frontend/src/hooks/UseFilter.tsx @@ -0,0 +1,91 @@ +/* + * Custom hook for retrieving a list of items from the API, + * and turning them into "filters" for use in the frontend table framework. + */ +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { api } from '../App'; +import { ApiEndpoints } from '../enums/ApiEndpoints'; +import { resolveItem } from '../functions/conversion'; +import { apiUrl } from '../states/ApiState'; +import { TableFilterChoice } from '../tables/Filter'; + +type UseFilterProps = { + url: string; + method?: 'GET' | 'POST' | 'OPTIONS'; + params?: any; + accessor?: string; + transform: (item: any) => TableFilterChoice; +}; + +export function useFilters(props: UseFilterProps) { + const query = useQuery({ + enabled: true, + queryKey: [props.url, props.method, props.params], + queryFn: async () => { + return await api + .request({ + url: props.url, + method: props.method || 'GET', + params: props.params + }) + .then((response) => { + let data = resolveItem(response, props.accessor ?? 'data'); + + if (data == null || data == undefined) { + return []; + } + + return data; + }) + .catch((error) => []); + } + }); + + const choices: TableFilterChoice[] = useMemo(() => { + return query.data?.map(props.transform) ?? []; + }, [props.transform, query.data]); + + const refresh = useCallback(() => { + query.refetch(); + }, []); + + return { + choices, + refresh + }; +} + +// Provide list of project code filters +export function useProjectCodeFilters() { + return useFilters({ + url: apiUrl(ApiEndpoints.project_code_list), + transform: (item) => ({ + value: item.pk, + label: item.code + }) + }); +} + +// Provide list of user filters +export function useUserFilters() { + return useFilters({ + url: apiUrl(ApiEndpoints.user_list), + transform: (item) => ({ + value: item.pk, + label: item.username + }) + }); +} + +// Provide list of owner filters +export function useOwnerFilters() { + return useFilters({ + url: apiUrl(ApiEndpoints.owner_list), + transform: (item) => ({ + value: item.pk, + label: item.name + }) + }); +} diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 34ff1c14a5..c5e730b550 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -10,6 +10,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useBuildOrderFields } from '../../forms/BuildForms'; +import { + useOwnerFilters, + useProjectCodeFilters, + useUserFilters +} from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -92,6 +97,10 @@ export function BuildOrderTable({ }) { const tableColumns = useMemo(() => buildOrderTableColumns(), []); + const projectCodeFilters = useProjectCodeFilters(); + const userFilters = useUserFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -115,18 +124,36 @@ export function BuildOrderTable({ type: 'boolean', label: t`Assigned to me`, description: t`Show orders assigned to me` + }, + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'issued_by', + label: t`Issued By`, + description: t`Filter by user who issued this order`, + choices: userFilters.choices + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices } - // TODO: 'assigned to' filter - // TODO: 'issued by' filter - // { - // name: 'has_project_code', - // title: t`Has Project Code`, - // description: t`Show orders with project code`, - // } - // TODO: 'has project code' filter (see table_filters.js) - // TODO: 'project code' filter (see table_filters.js) ]; - }, []); + }, [ + projectCodeFilters.choices, + userFilters.choices, + responsibleFilters.choices + ]); const user = useUserState(); diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index dec3deac37..31ccdd8238 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -116,13 +116,13 @@ export default function BuildOutputTable({ />, } + icon={} color="red" disabled={!table.hasSelectedRecords} />, } + icon={} color="red" disabled={!table.hasSelectedRecords} /> @@ -153,14 +153,14 @@ export default function BuildOutputTable({ { title: t`Scrap`, tooltip: t`Scrap build output`, - color: 'red', - icon: + icon: , + color: 'red' }, { - title: t`Delete`, - tooltip: t`Delete build output`, - color: 'red', - icon: + title: t`Cancel`, + tooltip: t`Cancel build output`, + icon: , + color: 'red' } ]; diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index 8407aadf2d..0c2151527b 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -1,7 +1,6 @@ import { t } from '@lingui/macro'; import { Group, Text } from '@mantine/core'; import { ReactNode, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { formatPriceRange } from '../../defaults/formatters'; @@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { usePartFields } from '../../forms/PartForms'; -import { shortenString } from '../../functions/tables'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -43,13 +41,7 @@ function partTableColumns(): TableColumn[] { { accessor: 'category', sortable: true, - - render: function (record: any) { - // TODO: Link to the category detail page - return shortenString({ - str: record.category_detail?.pathstring - }); - } + render: (record: any) => record.category_detail?.pathstring }, { accessor: 'total_in_stock', diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index dd0df11d7f..845cf77f5c 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; +import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -44,6 +45,9 @@ export function PurchaseOrderTable({ const table = useTable('purchase-order'); const user = useUserState(); + const projectCodeFilters = useProjectCodeFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -54,11 +58,26 @@ export function PurchaseOrderTable({ }, OutstandingFilter(), OverdueFilter(), - AssignedToMeFilter() - // TODO: has_project_code - // TODO: project_code + AssignedToMeFilter(), + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices + } ]; - }, []); + }, [projectCodeFilters.choices, responsibleFilters.choices]); const tableColumns = useMemo(() => { return [ diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index 73bcbfed3f..b4f77d1413 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useReturnOrderFields } from '../../forms/SalesOrderForms'; +import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -35,6 +36,9 @@ export function ReturnOrderTable({ params }: { params?: any }) { const table = useTable('return-orders'); const user = useUserState(); + const projectCodeFilters = useProjectCodeFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -45,9 +49,26 @@ export function ReturnOrderTable({ params }: { params?: any }) { }, OutstandingFilter(), OverdueFilter(), - AssignedToMeFilter() + AssignedToMeFilter(), + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices + } ]; - }, []); + }, [projectCodeFilters.choices, responsibleFilters.choices]); const tableColumns = useMemo(() => { return [ diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index c3556e6278..d48f72a4b9 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useSalesOrderFields } from '../../forms/SalesOrderForms'; +import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -41,6 +42,9 @@ export function SalesOrderTable({ const table = useTable('sales-order'); const user = useUserState(); + const projectCodeFilters = useProjectCodeFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -51,11 +55,26 @@ export function SalesOrderTable({ }, OutstandingFilter(), OverdueFilter(), - AssignedToMeFilter() - // TODO: has_project_code - // TODO: project_code + AssignedToMeFilter(), + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices + } ]; - }, []); + }, [projectCodeFilters.choices, responsibleFilters.choices]); const salesOrderFields = useSalesOrderFields(); diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 818620e217..b3c2a84195 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -4,7 +4,7 @@ import { ReactNode, useMemo } from 'react'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { ActionDropdown } from '../../components/items/ActionDropdown'; -import { formatCurrency, renderDate } from '../../defaults/formatters'; +import { formatCurrency } from '../../defaults/formatters'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles';