From cef5d4d9c0be230e580cb7a0dd426a08fa474d5b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 23 Feb 2026 19:46:17 +1100 Subject: [PATCH 01/28] [UI] Default table filters (#11405) * Support "default filters" for table views - User overrides apply in preference - Only when there is no stored value (null) * Correctly handle partially constructed filters - Reverse lookup on available filter set * Add default filters for order tables * Default filters for company tables * More default filters * Add some more default filters * Bump CHANGELOG * build fix * Tweaks for playwright testing * Tweak playwright test * Improve test flexibility --- CHANGELOG.md | 2 + src/frontend/lib/types/Filters.tsx | 2 +- src/frontend/src/hooks/UseFilterSet.tsx | 32 +++++++-- src/frontend/src/hooks/UseTable.tsx | 29 ++++++-- .../AdminCenter/CurrencyManagementPanel.tsx | 2 +- .../AdminCenter/UnitManagementPanel.tsx | 2 +- src/frontend/src/pages/build/BuildDetail.tsx | 1 + .../src/pages/company/CompanyDetail.tsx | 1 + .../src/pages/purchasing/PurchasingIndex.tsx | 2 + src/frontend/src/pages/sales/SalesIndex.tsx | 1 + src/frontend/src/tables/Filter.tsx | 42 ++++++++++++ .../src/tables/FilterSelectDrawer.tsx | 66 ++++++++++++++++--- .../src/tables/InvenTreeTableHeader.tsx | 16 ++--- .../src/tables/build/BuildOrderTable.tsx | 9 ++- .../src/tables/company/CompanyTable.tsx | 11 +++- .../src/tables/general/BarcodeScanTable.tsx | 2 +- src/frontend/src/tables/part/PartTable.tsx | 11 +++- .../src/tables/part/PartVariantTable.tsx | 1 + .../src/tables/plugin/PluginErrorTable.tsx | 4 +- .../purchasing/ManufacturerPartTable.tsx | 24 ++++++- .../tables/purchasing/PurchaseOrderTable.tsx | 9 ++- .../tables/purchasing/SupplierPartTable.tsx | 29 +++++++- .../src/tables/sales/ReturnOrderTable.tsx | 13 +++- .../src/tables/sales/SalesOrderTable.tsx | 9 ++- .../src/tables/settings/ApiTokenTable.tsx | 2 +- .../src/tables/settings/EmailTable.tsx | 2 +- .../src/tables/stock/StockItemTable.tsx | 29 +++++++- src/frontend/tests/pages/pui_build.spec.ts | 2 + src/frontend/tests/pages/pui_company.spec.ts | 7 ++ src/frontend/tests/pages/pui_part.spec.ts | 10 +-- .../tests/pages/pui_purchase_order.spec.ts | 8 +++ src/frontend/tests/pui_exporting.spec.ts | 2 +- src/frontend/tests/pui_printing.spec.ts | 2 +- 33 files changed, 331 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c135487674..b821f59bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +[#11405](https://github.com/inventree/InvenTree/pull/11405) adds default table filters, which hide inactive items by default. The default table filters are overridden by user filter selection, and only apply to the table view initially presented to the user. This means that users can still view inactive items if they choose to, but they will not be shown by default. + [#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs. [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API. diff --git a/src/frontend/lib/types/Filters.tsx b/src/frontend/lib/types/Filters.tsx index 1ef44a3a84..53d5a563ca 100644 --- a/src/frontend/lib/types/Filters.tsx +++ b/src/frontend/lib/types/Filters.tsx @@ -39,7 +39,7 @@ export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text' | 'api'; */ export type TableFilter = { name: string; - label: string; + label?: string; description?: string; type?: TableFilterType; choices?: TableFilterChoice[]; diff --git a/src/frontend/src/hooks/UseFilterSet.tsx b/src/frontend/src/hooks/UseFilterSet.tsx index 7638b91532..9d0300cb46 100644 --- a/src/frontend/src/hooks/UseFilterSet.tsx +++ b/src/frontend/src/hooks/UseFilterSet.tsx @@ -1,21 +1,43 @@ import type { FilterSetState, TableFilter } from '@lib/types/Filters'; import { useLocalStorage } from '@mantine/hooks'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; -export function useFilterSet(filterKey: string): FilterSetState { +export function useFilterSet( + filterKey: string, + initialFilters?: TableFilter[] +): FilterSetState { // Array of active filters (saved to local storage) - const [activeFilters, setActiveFilters] = useLocalStorage({ + const [storedFilters, setStoredFilters] = useLocalStorage< + TableFilter[] | null + >({ key: `inventree-filterset-${filterKey}`, - defaultValue: [], + defaultValue: null, sync: false, getInitialValueInEffect: false }); + const activeFilters: TableFilter[] = useMemo(() => { + if (storedFilters == null) { + // If there are no stored filters, set initial values + const filters = initialFilters || []; + setStoredFilters(filters); + return filters; + } + return storedFilters || []; + }, [storedFilters]); + // Callback to clear all active filters from the table const clearActiveFilters = useCallback(() => { - setActiveFilters([]); + setStoredFilters([]); }, []); + const setActiveFilters = useCallback( + (filters: TableFilter[]) => { + setStoredFilters(filters); + }, + [setStoredFilters] + ); + return { filterKey, activeFilters, diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index 79d10e87f4..49a2c50569 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -2,17 +2,28 @@ import { randomId } from '@mantine/hooks'; import { useCallback, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import type { FilterSetState } from '@lib/types/Filters'; +import type { FilterSetState, TableFilter } from '@lib/types/Filters'; import type { TableState } from '@lib/types/Tables'; import { useFilterSet } from './UseFilterSet'; +export type TableStateExtraProps = { + idAccessor?: string; + initialFilters?: TableFilter[]; +}; + /** * A custom hook for managing the state of an component. * * Refer to the TableState type definition for more information. */ -export function useTable(tableName: string, idAccessor = 'pk'): TableState { +export function useTable( + tableName: string, + tableProps: TableStateExtraProps = { + idAccessor: 'pk', + initialFilters: [] + } +): TableState { // Function to generate a new ID (to refresh the table) function generateTableName() { return `${tableName.replaceAll('-', '')}-${randomId()}`; @@ -38,7 +49,10 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { [generateTableName] ); - const filterSet: FilterSetState = useFilterSet(`table-${tableName}`); + const filterSet: FilterSetState = useFilterSet( + `table-${tableName}`, + tableProps.initialFilters + ); // Array of expanded records const [expandedRecords, setExpandedRecords] = useState([]); @@ -59,7 +73,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { // Array of selected primary key values const selectedIds = useMemo( - () => selectedRecords.map((r) => r[idAccessor || 'pk']), + () => selectedRecords.map((r) => r[tableProps.idAccessor || 'pk']), [selectedRecords] ); @@ -89,7 +103,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { // Find the matching record in the table const index = _records.findIndex( - (r) => r[idAccessor || 'pk'] === record.pk + (r) => r[tableProps.idAccessor || 'pk'] === record.pk ); if (index >= 0) { @@ -106,6 +120,11 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { [records] ); + const idAccessor = useMemo( + () => tableProps.idAccessor || 'pk', + [tableProps.idAccessor] + ); + const [isLoading, setIsLoading] = useState(false); return { diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx index a0972b445c..7e1ca77049 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx @@ -20,7 +20,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable'; export function CurrencyTable({ setInfo }: Readonly<{ setInfo: (info: any) => void }>) { - const table = useTable('currency', 'currency'); + const table = useTable('currency', { idAccessor: 'currency' }); const columns = useMemo(() => { return [ { diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx index 3df935a820..658d84840f 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx @@ -11,7 +11,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable'; import CustomUnitsTable from '../../../../tables/settings/CustomUnitsTable'; function AllUnitTable() { - const table = useTable('all-units', 'name'); + const table = useTable('all-units', { idAccessor: 'name' }); const columns = useMemo(() => { return [ { diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index f0542f3f9b..8f8bf0e60e 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -469,6 +469,7 @@ export default function BuildDetail() { tableName='build-consumed' showLocation={false} allowReturn + defaultInStock={null} params={{ consumed_by: id }} diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index ebc20200cf..bc51806281 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -248,6 +248,7 @@ export default function CompanyDetail(props: Readonly) { tableName='assigned-stock' showLocation={false} allowReturn + defaultInStock={null} params={{ customer: company.pk }} /> ) : ( diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index 706c7e737d..a4a2f70d47 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -108,6 +108,7 @@ export default function PurchasingIndex() { icon: , content: ( @@ -157,6 +158,7 @@ export default function PurchasingIndex() { icon: , content: ( diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 0fe4b8a31c..d225081ec4 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -141,6 +141,7 @@ export default function SalesIndex() { icon: , content: ( diff --git a/src/frontend/src/tables/Filter.tsx b/src/frontend/src/tables/Filter.tsx index 9970c72add..f093cb94f2 100644 --- a/src/frontend/src/tables/Filter.tsx +++ b/src/frontend/src/tables/Filter.tsx @@ -3,6 +3,7 @@ import { t } from '@lingui/core/macro'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; +import { isTrue } from '@lib/functions/Conversion'; import type { TableFilter, TableFilterChoice } from '@lib/types/Filters'; import type { StatusCodeInterface, @@ -14,6 +15,47 @@ import { } from '../states/GlobalStatusState'; import { useGlobalSettingsState } from '../states/SettingsStates'; +// Determine the appropriate display label for a given filter, based on its name and the list of available filters +export function filterDisplayLabel( + name: string, + filters?: TableFilter[] +): string { + const filter = filters?.find((f) => f.name === name); + return filter?.label ?? name; +} + +// Determine the appropriate display value for a filter, based on its type and value +// This is useful for recreating a display value if we only have a name:value pair +export function filterDisplayValue( + name: string, + value: any, + filters?: TableFilter[] +) { + const filterDef = filters?.find((f) => f.name === name); + + if (!filterDef) { + return value; + } + + if (!filterDef.type || filterDef.type == 'boolean') { + return isTrue(value) ? t`Yes` : t`No`; + } + + if (filterDef.type === 'choice' && filterDef.choices) { + const choice = filterDef.choices.find((c) => c.value === value); + return choice ? choice.label : value; + } + + if (filterDef.type === 'choice' && filterDef.choiceFunction) { + const choices = filterDef.choiceFunction(); + const choice = choices.find((c) => c.value === value); + return choice ? choice.label : value; + } + + // No obvious match - return the raw value + return value; +} + /** * Return list of available filter options for a given filter * @param filter - TableFilter object diff --git a/src/frontend/src/tables/FilterSelectDrawer.tsx b/src/frontend/src/tables/FilterSelectDrawer.tsx index eecacf9431..a737974403 100644 --- a/src/frontend/src/tables/FilterSelectDrawer.tsx +++ b/src/frontend/src/tables/FilterSelectDrawer.tsx @@ -28,17 +28,46 @@ import type { import { IconCheck } from '@tabler/icons-react'; import { StandaloneField } from '../components/forms/StandaloneField'; import { StylishText } from '../components/items/StylishText'; -import { getTableFilterOptions } from './Filter'; +import { + filterDisplayLabel, + filterDisplayValue, + getTableFilterOptions +} from './Filter'; + +/* + * Render a preview of a single filter + */ +export function FilterPreview({ + filter, + filters +}: Readonly<{ + filter: TableFilter; + filters?: TableFilter[]; +}>) { + return ( + + + {filter.label ?? filterDisplayLabel(filter.name, filters)} + + + {filter.displayValue ?? + filterDisplayValue(filter.name, filter.value, filters)} + + + ); +} /* * Render a single table filter item */ function FilterItem({ flt, - filterSet + filterSet, + availableFilters }: Readonly<{ flt: TableFilter; filterSet: FilterSetState; + availableFilters?: TableFilter[]; }>) { const removeFilter = useCallback(() => { const newFilters = filterSet.activeFilters.filter( @@ -47,17 +76,29 @@ function FilterItem({ filterSet.setActiveFilters(newFilters); }, [flt]); + // Find the matching filter definition + const filterProps: TableFilter | undefined = useMemo(() => { + return availableFilters?.find((f) => f.name === flt.name); + }, [availableFilters, flt]); + return ( - {flt.label} - {flt.description} + {flt.label ?? filterProps?.label ?? flt.name} + {flt.description ?? filterProps?.description} - {flt.displayValue ?? flt.value} - - + + {flt.displayValue ?? + filterDisplayValue(flt.name, flt.value, availableFilters)} + + + @@ -174,10 +215,10 @@ function FilterAddGroup({ return ( availableFilters ?.filter((flt) => !activeFilterNames.includes(flt.name)) - ?.sort((a, b) => a.label.localeCompare(b.label)) + ?.sort((a, b) => (a.label ?? a.name).localeCompare(b.label ?? b.name)) ?.map((flt) => ({ value: flt.name, - label: flt.label, + label: flt.label ?? flt.name, description: flt.description })) ?? [] ); @@ -317,7 +358,12 @@ export function FilterSelectDrawer({ {hasFilters && filterSet.activeFilters?.map((f) => ( - + ))} {addFilter && ( diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx index e052936137..d5d6b45c27 100644 --- a/src/frontend/src/tables/InvenTreeTableHeader.tsx +++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx @@ -9,7 +9,6 @@ import { Paper, Space, Stack, - Text, Tooltip } from '@mantine/core'; import { @@ -37,7 +36,7 @@ import { StylishText } from '../components/items/StylishText'; import useDataExport from '../hooks/UseDataExport'; import { useDeleteApiFormModal } from '../hooks/UseForm'; import { TableColumnSelect } from './ColumnSelect'; -import { FilterSelectDrawer } from './FilterSelectDrawer'; +import { FilterPreview, FilterSelectDrawer } from './FilterSelectDrawer'; /** * Render a composite header for an InvenTree table @@ -272,15 +271,10 @@ export default function InvenTreeTableHeader({ {t`Active Filters`} {tableState.filterSet.activeFilters?.map((filter) => ( - - {filter.label} - {filter.displayValue} - + ))} diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 242a81ef4e..77f071df84 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -64,7 +64,14 @@ export function BuildOrderTable({ salesOrderId?: number; }>) { const globalSettings = useGlobalSettingsState(); - const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index'); + const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index', { + initialFilters: [ + { + name: 'outstanding', + value: 'true' + } + ] + }); const tableColumns = useMemo(() => { return [ diff --git a/src/frontend/src/tables/company/CompanyTable.tsx b/src/frontend/src/tables/company/CompanyTable.tsx index ce02a7756d..e3c6125344 100644 --- a/src/frontend/src/tables/company/CompanyTable.tsx +++ b/src/frontend/src/tables/company/CompanyTable.tsx @@ -29,13 +29,22 @@ import { InvenTreeTable } from '../InvenTreeTable'; * based on the provided filter parameters */ export function CompanyTable({ + companyType, params, path }: Readonly<{ + companyType?: string; params?: any; path?: string; }>) { - const table = useTable('company'); + const table = useTable(`company-${companyType ?? 'index'}`, { + initialFilters: [ + { + name: 'active', + value: 'true' + } + ] + }); const navigate = useNavigate(); const user = useUserState(); diff --git a/src/frontend/src/tables/general/BarcodeScanTable.tsx b/src/frontend/src/tables/general/BarcodeScanTable.tsx index d13b5ec5f4..60b2546e73 100644 --- a/src/frontend/src/tables/general/BarcodeScanTable.tsx +++ b/src/frontend/src/tables/general/BarcodeScanTable.tsx @@ -26,7 +26,7 @@ export default function BarcodeScanTable({ const navigate = useNavigate(); const user = useUserState(); - const table = useTable('barcode-scan-results', 'id'); + const table = useTable('barcode-scan-results', { idAccessor: 'id' }); const tableColumns: TableColumn[] = useMemo(() => { return [ diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index 7093149492..e208b4035c 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -338,17 +338,26 @@ export function PartListTable({ enableImport = true, basePartInstance, props, + tableName = 'part-list', defaultPartData }: Readonly<{ enableImport?: boolean; props?: InvenTreeTableProps; basePartInstance?: any; + tableName?: string; defaultPartData?: any; }>) { const tableColumns = useMemo(() => partTableColumns(), []); const tableFilters = useMemo(() => partTableFilters(), []); - const table = useTable('part-list'); + const table = useTable(tableName ?? 'part-list', { + initialFilters: [ + { + name: 'active', + value: 'true' + } + ] + }); const user = useUserState(); const globalSettings = useGlobalSettingsState(); diff --git a/src/frontend/src/tables/part/PartVariantTable.tsx b/src/frontend/src/tables/part/PartVariantTable.tsx index 845e654b3b..2822476b59 100644 --- a/src/frontend/src/tables/part/PartVariantTable.tsx +++ b/src/frontend/src/tables/part/PartVariantTable.tsx @@ -43,6 +43,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) { ancestor: part.pk } }} + tableName='part-variants' basePartInstance={part} defaultPartData={{ ...part, diff --git a/src/frontend/src/tables/plugin/PluginErrorTable.tsx b/src/frontend/src/tables/plugin/PluginErrorTable.tsx index a7172f7071..4f3d5212da 100644 --- a/src/frontend/src/tables/plugin/PluginErrorTable.tsx +++ b/src/frontend/src/tables/plugin/PluginErrorTable.tsx @@ -19,7 +19,9 @@ export interface PluginRegistryErrorI { * Table displaying list of plugin registry errors */ export default function PluginErrorTable() { - const table = useTable('registryErrors', 'id'); + const table = useTable('registryErrors', { + idAccessor: 'id' + }); const registryErrorTableColumns: TableColumn[] = useMemo( diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx index d18f99dc87..dd1aaaa99c 100644 --- a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx +++ b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx @@ -54,7 +54,29 @@ export function ManufacturerPartTable({ return tId; }, [manufacturerId, partId]); - const table = useTable(tableId); + const initialFilters = useMemo(() => { + const filters: TableFilter[] = []; + + if (!manufacturerId) { + filters.push({ + name: 'manufacturer_active', + value: 'true' + }); + } + + if (!partId) { + filters.push({ + name: 'part_active', + value: 'true' + }); + } + + return filters; + }, [manufacturerId, partId]); + + const table = useTable(tableId, { + initialFilters: initialFilters + }); const user = useUserState(); diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index 81fe9db967..34c1f37ae6 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -60,7 +60,14 @@ export function PurchaseOrderTable({ supplierPartId?: number; externalBuildId?: number; }>) { - const table = useTable('purchase-order'); + const table = useTable('purchase-order', { + initialFilters: [ + { + name: 'outstanding', + value: 'true' + } + ] + }); const user = useUserState(); const tableFilters: TableFilter[] = useMemo(() => { diff --git a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx index ecdc42cf0b..9afc553d52 100644 --- a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx +++ b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx @@ -54,7 +54,34 @@ export function SupplierPartTable({ partId?: number; supplierId?: number; }>): ReactNode { - const table = useTable('supplierparts'); + const initialFilters = useMemo(() => { + const filters: TableFilter[] = [ + { + name: 'active', + value: 'true' + } + ]; + + if (!supplierId) { + filters.push({ + name: 'supplier_active', + value: 'true' + }); + } + + if (!partId) { + filters.push({ + name: 'part_active', + value: 'true' + }); + } + + return filters; + }, [supplierId, partId]); + + const table = useTable('supplierparts', { + initialFilters: initialFilters + }); const user = useUserState(); diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index f01dedfbf9..dfd0362e90 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -56,7 +56,18 @@ export function ReturnOrderTable({ partId?: number; customerId?: number; }>) { - const table = useTable(!!partId ? 'returnorders-part' : 'returnorders-index'); + const table = useTable( + !!partId ? 'returnorders-part' : 'returnorders-index', + { + initialFilters: [ + { + name: 'outstanding', + value: 'true' + } + ] + } + ); + const user = useUserState(); const tableFilters: TableFilter[] = useMemo(() => { diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index c1e9b0124a..c87e079767 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -58,7 +58,14 @@ export function SalesOrderTable({ partId?: number; customerId?: number; }>) { - const table = useTable(!!partId ? 'salesorder-part' : 'salesorder-index'); + const table = useTable(!!partId ? 'salesorder-part' : 'salesorder-index', { + initialFilters: [ + { + name: 'outstanding', + value: 'true' + } + ] + }); const user = useUserState(); const tableFilters: TableFilter[] = useMemo(() => { diff --git a/src/frontend/src/tables/settings/ApiTokenTable.tsx b/src/frontend/src/tables/settings/ApiTokenTable.tsx index ccb953a4ce..9736dded12 100644 --- a/src/frontend/src/tables/settings/ApiTokenTable.tsx +++ b/src/frontend/src/tables/settings/ApiTokenTable.tsx @@ -48,7 +48,7 @@ export function ApiTokenTable({ return []; }, [only_myself]); - const table = useTable('api-tokens', 'id'); + const table = useTable('api-tokens', { idAccessor: 'id' }); const tableColumns = useMemo(() => { const cols = [ diff --git a/src/frontend/src/tables/settings/EmailTable.tsx b/src/frontend/src/tables/settings/EmailTable.tsx index 2ea50d835d..87526a2e27 100644 --- a/src/frontend/src/tables/settings/EmailTable.tsx +++ b/src/frontend/src/tables/settings/EmailTable.tsx @@ -61,7 +61,7 @@ export function EmailTable() { ]; }, []); - const table = useTable('emails', 'pk'); + const table = useTable('emails', { idAccessor: 'pk' }); const [selectedEmailId, setSelectedEmailId] = useState(''); diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 26e0245319..5f04978063 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -315,6 +315,8 @@ export function StockItemTable({ showLocation = true, showPricing = true, allowReturn = false, + initialFilters, + defaultInStock = true, tableName = 'stockitems' }: Readonly<{ params?: any; @@ -322,9 +324,34 @@ export function StockItemTable({ showLocation?: boolean; showPricing?: boolean; allowReturn?: boolean; + defaultInStock?: boolean | null; + initialFilters?: TableFilter[]; tableName: string; }>) { - const table = useTable(tableName); + const initialStockFilters: TableFilter[] = useMemo(() => { + if (!!initialFilters) { + return initialFilters; + } + + const filters: TableFilter[] = []; + + // Optionally set the default "in_stock" filter + // Typically, we default to only displaying "in_stock" items, + // but this can be overridden by the caller if required + if (defaultInStock != undefined && defaultInStock != null) { + filters.push({ + name: 'in_stock', + value: defaultInStock ? 'true' : 'false' + }); + } + + return filters; + }, [defaultInStock, initialFilters]); + + const table = useTable(tableName, { + initialFilters: initialStockFilters + }); + const user = useUserState(); const settings = useGlobalSettingsState(); diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 67d517dfde..0a3a759d51 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -723,6 +723,8 @@ test('Build Order - External', async ({ browser }) => { await navigate(page, 'manufacturing/build-order/26/details'); await loadTab(page, 'External Orders'); + await clearTableFilters(page); + await page.getByRole('cell', { name: 'PO0017' }).waitFor(); await page.getByRole('cell', { name: 'PO0018' }).waitFor(); }); diff --git a/src/frontend/tests/pages/pui_company.spec.ts b/src/frontend/tests/pages/pui_company.spec.ts index 0d5834f1aa..a7f4545639 100644 --- a/src/frontend/tests/pages/pui_company.spec.ts +++ b/src/frontend/tests/pages/pui_company.spec.ts @@ -15,21 +15,28 @@ test('Company', async ({ browser }) => { await navigate(page, 'company/1/details'); await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor(); await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor(); + await loadTab(page, 'Supplied Parts'); await page .getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true }) .waitFor(); + await loadTab(page, 'Purchase Orders'); + await clearTableFilters(page); await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor(); + await loadTab(page, 'Stock Items'); await page .getByRole('cell', { name: 'Blue plastic enclosure' }) .first() .waitFor(); + await loadTab(page, 'Contacts'); await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor(); + await loadTab(page, 'Addresses'); await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor(); + await loadTab(page, 'Attachments'); await loadTab(page, 'Notes'); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index eef7e89e91..cb4fd3a9f7 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -66,14 +66,16 @@ test('Parts - Tabs', async ({ browser }) => { }); test('Parts - Manufacturer Parts', async ({ browser }) => { - const page = await doCachedLogin(browser, { url: 'part/84/suppliers' }); + const page = await doCachedLogin(browser, { url: 'part/84/' }); + // Load the "suppliers" tab await loadTab(page, 'Suppliers'); await page.getByText('Hammond Manufacturing').click(); - await loadTab(page, 'Parameters'); - await loadTab(page, 'Suppliers'); - await loadTab(page, 'Attachments'); + + // Wait for manufacturer part page to load await page.getByText('1551ACLR - 1551ACLR').waitFor(); + await loadTab(page, 'Parameters'); + await loadTab(page, 'Attachments'); }); test('Parts - Supplier Parts', async ({ browser }) => { diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts index f78fa30a02..c8ff3f9393 100644 --- a/src/frontend/tests/pages/pui_purchase_order.spec.ts +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -25,6 +25,14 @@ test('Purchasing - Index', async ({ browser }) => { await showCalendarView(page); await showTableView(page); + // Check default filters are applied + // By default, only outstanding orders are visible + await page.getByText(/1 - \d+ \/ \d+/).waitFor(); + + // Clearing the filters, more orders should be visible + await clearTableFilters(page); + await page.getByText(/1 - 1\d \/ 1\d/).waitFor(); + // Suppliers tab await loadTab(page, 'Suppliers'); await showParametricView(page); diff --git a/src/frontend/tests/pui_exporting.spec.ts b/src/frontend/tests/pui_exporting.spec.ts index 68ef9dc60c..70fbd7a9ad 100644 --- a/src/frontend/tests/pui_exporting.spec.ts +++ b/src/frontend/tests/pui_exporting.spec.ts @@ -33,7 +33,7 @@ test('Exporting - Orders', async ({ browser }) => { await page.getByText('Process completed successfully').waitFor(); // Download list of purchase order items - await page.getByRole('cell', { name: 'PO0011' }).click(); + await page.getByRole('cell', { name: 'PO0014' }).click(); await loadTab(page, 'Line Items'); await openExportDialog(page); await page.getByRole('button', { name: 'Export', exact: true }).click(); diff --git a/src/frontend/tests/pui_printing.spec.ts b/src/frontend/tests/pui_printing.spec.ts index abd25ce299..f149949187 100644 --- a/src/frontend/tests/pui_printing.spec.ts +++ b/src/frontend/tests/pui_printing.spec.ts @@ -62,7 +62,7 @@ test('Printing - Report Printing', async ({ browser }) => { await loadTab(page, 'Purchase Orders'); await activateTableView(page); - await page.getByRole('cell', { name: 'PO0009' }).click(); + await page.getByRole('cell', { name: 'PO0013' }).click(); // Select "print report" await page.getByLabel('action-menu-printing-actions').click(); From 33c57887adff616ca1be816f656ef7167055106d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 23 Feb 2026 09:55:09 +0100 Subject: [PATCH 02/28] bump django-allauth (#11401) * bump django-allauth * bump api --- src/backend/InvenTree/InvenTree/api_version.py | 5 ++++- src/backend/requirements-3.14.txt | 6 +++--- src/backend/requirements.txt | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 2871dd1ec4..493f4ae5db 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 = 457 +INVENTREE_API_VERSION = 458 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v458 -> 2026-02-22 : https://github.com/inventree/InvenTree/pull/11401 + - siwtches token refresh endpoint to use POST instead of GET (upstream allauth change) + v457 -> 2026-02-11 : https://github.com/inventree/InvenTree/pull/10887 - Extend the "auto allocate" wizard API to include tracked items diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index ac459aef18..0997935226 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -538,9 +538,9 @@ django==5.2.11 \ # djangorestframework # djangorestframework-simplejwt # drf-spectacular -django-allauth[headless, mfa, openid, saml, socialaccount]==65.13.1 \ - --hash=sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6 \ - --hash=sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15 +django-allauth[headless, mfa, openid, saml, socialaccount]==65.14.3 \ + --hash=sha256:1d8e1127bdffceb8001bdd9bafbf97661f81e92f4b7bd4f6e799167b0311286d \ + --hash=sha256:548eef76ab85f6e48f46f98437abf22acf0e834f73e9915fb6cc3f31a0dcdf4d # via # -c src/backend/requirements.txt # -r src/backend/requirements.in diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 7b5b028ca8..fdf5c05f38 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -518,9 +518,9 @@ django==5.2.11 \ # djangorestframework # djangorestframework-simplejwt # drf-spectacular -django-allauth[headless, mfa, openid, saml, socialaccount]==65.13.1 \ - --hash=sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6 \ - --hash=sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15 +django-allauth[headless, mfa, openid, saml, socialaccount]==65.14.3 \ + --hash=sha256:1d8e1127bdffceb8001bdd9bafbf97661f81e92f4b7bd4f6e799167b0311286d \ + --hash=sha256:548eef76ab85f6e48f46f98437abf22acf0e834f73e9915fb6cc3f31a0dcdf4d # via -r src/backend/requirements.in django-anymail[amazon-ses, postal]==14.0 \ --hash=sha256:98e5575bcff231952a92117b7d0a4dd37fab582333a343de302d415a31dc72b1 \ From edc639d55cd7f53573b3e8d4f34c5ea1e120e05e Mon Sep 17 00:00:00 2001 From: Tin Pham <72054441+tinpham5614@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:59:22 -0800 Subject: [PATCH 03/28] feat: add copyable cells feature with CopyButton component (#11406) --- src/frontend/lib/types/Tables.tsx | 4 ++ .../src/components/buttons/CopyButton.tsx | 6 ++- src/frontend/src/tables/CopyableCell.tsx | 40 +++++++++++++++++++ src/frontend/src/tables/InvenTreeTable.tsx | 26 ++++++++++++ .../purchasing/PurchaseOrderLineItemTable.tsx | 3 +- 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/frontend/src/tables/CopyableCell.tsx diff --git a/src/frontend/lib/types/Tables.tsx b/src/frontend/lib/types/Tables.tsx index ba0675e285..99f02ce06d 100644 --- a/src/frontend/lib/types/Tables.tsx +++ b/src/frontend/lib/types/Tables.tsx @@ -99,6 +99,8 @@ export type TableState = { * @param cellsStyle - The style of the cells in the column * @param extra - Extra data to pass to the render function * @param noContext - Disable context menu for this column + * @param copyable - Enable copy button on hover (uses accessor to get value, or custom function) + * @param copyAccessor - Custom accessor path for copy value (defaults to column accessor) */ export type TableColumnProps = { accessor?: string; @@ -123,6 +125,8 @@ export type TableColumnProps = { extra?: any; noContext?: boolean; style?: MantineStyleProp; + copyable?: boolean | ((record: T) => string); + copyAccessor?: string; }; /** diff --git a/src/frontend/src/components/buttons/CopyButton.tsx b/src/frontend/src/components/buttons/CopyButton.tsx index b9e283daa9..7479054cbc 100644 --- a/src/frontend/src/components/buttons/CopyButton.tsx +++ b/src/frontend/src/components/buttons/CopyButton.tsx @@ -46,7 +46,11 @@ export function CopyButton({ { + e.stopPropagation(); + e.preventDefault(); + copy(); + }} variant='transparent' size={size ?? 'sm'} > diff --git a/src/frontend/src/tables/CopyableCell.tsx b/src/frontend/src/tables/CopyableCell.tsx new file mode 100644 index 0000000000..4d7fc94712 --- /dev/null +++ b/src/frontend/src/tables/CopyableCell.tsx @@ -0,0 +1,40 @@ +import { Group } from '@mantine/core'; +import { useState } from 'react'; +import { CopyButton } from '../components/buttons/CopyButton'; + +/** + * A wrapper component that adds a copy button to cell content on hover + * This component is used to make table cells copyable without adding visual clutter + * + * @param children - The cell content to render + * @param value - The value to copy when the copy button is clicked + */ +export function CopyableCell({ + children, + value +}: Readonly<{ + children: React.ReactNode; + value: string; +}>) { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + justify='space-between' + > + {children} + {isHovered && value != null && ( + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + + + )} + + ); +} diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index d84c237721..ca3d43e35b 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -33,6 +33,7 @@ import { hashString } from '../functions/tables'; import { useLocalState } from '../states/LocalState'; import { useUserSettingsState } from '../states/SettingsStates'; import { useStoredTableState } from '../states/StoredTableState'; +import { CopyableCell } from './CopyableCell'; import InvenTreeTableHeader from './InvenTreeTableHeader'; const ACTIONS_COLUMN_ACCESSOR: string = '--actions--'; @@ -264,11 +265,36 @@ export function InvenTreeTable>({ ? false : (tableState.hiddenColumns?.includes(col.accessor) ?? false); + // Wrap the render function with CopyableCell if copyable is enabled + const originalRender = col.render; + let wrappedRender = originalRender; + + if (col.copyable) { + wrappedRender = (record: any, index?: number) => { + const content = + originalRender?.(record, index) ?? + resolveItem(record, col.accessor); + + // Determine the value to copy, ensuring it is always a string + let rawCopyValue: unknown; + if (typeof col.copyable === 'function') { + rawCopyValue = col.copyable(record); + } else { + const accessor = col.copyAccessor ?? col.accessor; + rawCopyValue = resolveItem(record, accessor); + } + const copyValue = rawCopyValue == null ? '' : String(rawCopyValue); + + return {content}; + }; + } + return { ...col, hidden: hidden, resizable: col.resizable ?? true, title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`, + render: wrappedRender, cellsStyle: (record: any, index: number) => { const width = (col as any).minWidth ?? 100; return { diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 3decd7c824..2f4198e459 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -237,7 +237,8 @@ export function PurchaseOrderLineItemTable({ title: t`Supplier Code`, switchable: false, sortable: true, - ordering: 'SKU' + ordering: 'SKU', + copyable: true }, LinkColumn({ accessor: 'supplier_part_detail.link', From 00be7abda27306e07952b22d6aa7251c99dbcdc0 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 23 Feb 2026 21:33:54 +0100 Subject: [PATCH 04/28] bump docker image to python 3.14 (#11414) --- .pre-commit-config.yaml | 2 +- contrib/container/Dockerfile | 4 ++-- contrib/container/requirements.txt | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6832270e7b..a4d6e46287 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: files: docs/requirements\.(in|txt)$ - id: pip-compile name: pip-compile requirements.txt - args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, -c, src/backend/requirements.txt] + args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.14, -c, src/backend/requirements.txt] files: contrib/container/requirements\.(in|txt)$ - repo: https://github.com/Riverside-Healthcare/djLint rev: v1.36.4 diff --git a/contrib/container/Dockerfile b/contrib/container/Dockerfile index f33549827c..b7ee19c4f6 100644 --- a/contrib/container/Dockerfile +++ b/contrib/container/Dockerfile @@ -9,8 +9,8 @@ # - Runs InvenTree web server under django development server # - Monitors source files for any changes, and live-reloads server -# Base image last bumped 2026-02-12 -FROM python:3.11-slim-trixie@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf AS inventree_base +# Base image last bumped 2026-02-23 +FROM python:3.14-slim-trixie@sha256:486b8092bfb12997e10d4920897213a06563449c951c5506c2a2cfaf591c599f AS inventree_base # Build arguments for this image ARG commit_tag="" diff --git a/contrib/container/requirements.txt b/contrib/container/requirements.txt index 17049dbde5..ccf76c5555 100644 --- a/contrib/container/requirements.txt +++ b/contrib/container/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.11 -c src/backend/requirements.txt +# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.14 -c src/backend/requirements.txt asgiref==3.11.1 \ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133 @@ -236,7 +236,6 @@ typing-extensions==4.15.0 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # -c src/backend/requirements.txt - # psycopg # psycopg-pool uv==0.9.22 \ --hash=sha256:012bdc5285a9cdb091ac514b7eb8a707e3b649af5355fe4afb4920bfe1958c00 \ From 8a2adda1e14556ceb352947dba4338ba92b110e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:05:24 +1100 Subject: [PATCH 05/28] chore(deps): bump the dependencies group across 1 directory with 4 updates (#11416) Bumps the dependencies group with 4 updates in the / directory: [depot/setup-action](https://github.com/depot/setup-action), [depot/build-push-action](https://github.com/depot/build-push-action), [anchore/sbom-action](https://github.com/anchore/sbom-action) and [actions/stale](https://github.com/actions/stale). Updates `depot/setup-action` from 1.6.0 to 1.7.1 - [Release notes](https://github.com/depot/setup-action/releases) - [Commits](https://github.com/depot/setup-action/compare/b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5...15c09a5f77a0840ad4bce955686522a257853461) Updates `depot/build-push-action` from 1.16.2 to 1.17.0 - [Release notes](https://github.com/depot/build-push-action/releases) - [Commits](https://github.com/depot/build-push-action/compare/9785b135c3c76c33db102e45be96a25ab55cd507...5f3b3c2e5a00f0093de47f657aeaefcedff27d18) Updates `anchore/sbom-action` from 0.21.1 to 0.22.2 - [Release notes](https://github.com/anchore/sbom-action/releases) - [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md) - [Commits](https://github.com/anchore/sbom-action/compare/0b82b0b1a22399a1c542d4d656f70cd903571b5c...28d71544de8eaf1b958d335707167c5f783590ad) Updates `actions/stale` from 10.1.1 to 10.2.0 - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/997185467fa4f803885201cee163a9f38240193d...b5d41d4e1d5dceea10e7104786b73624c18a190f) --- updated-dependencies: - dependency-name: depot/setup-action dependency-version: 1.7.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: depot/build-push-action dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: anchore/sbom-action dependency-version: 0.22.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/stale dependency-version: 10.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yaml | 4 ++-- .github/workflows/release.yaml | 2 +- .github/workflows/stale.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 89a46315a1..7b1a727718 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -206,11 +206,11 @@ jobs: images: | inventree/inventree ghcr.io/${{ github.repository }} - - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # pin@v1 + - uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # pin@v1 - name: Push Docker Images id: push-docker if: github.event_name != 'pull_request' - uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # pin@v1 + uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # pin@v1 with: project: jczzbjkk68 context: . diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 40f48e9723..bd1cdf8242 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -55,7 +55,7 @@ jobs: - name: Build frontend run: cd src/frontend && npm run compile && npm run build - name: Create SBOM for frontend - uses: anchore/sbom-action@0b82b0b1a22399a1c542d4d656f70cd903571b5c # pin@v0 + uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # pin@v0 with: artifact-name: frontend-build.spdx path: src/frontend diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index d8b024b852..fc770c386e 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -16,7 +16,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # pin@v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # pin@v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue seems stale. Please react to show this is still important." From 449bd4e5a0ee9cc8714acfea457f2d7c9b67e0ca Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 24 Feb 2026 11:44:14 +1100 Subject: [PATCH 06/28] [UI] Copy cells expansion (#11410) * Prevent copy button if copy value is null * Add "link" columns to order tables * Support copy for default column types * Tweak padding to avoid flickering issues * Refactor IPNColumn * Adjust visual styling * Copy for SKU and MPN columns * Add more copy columns * More tweaks * Tweak playwright testing * Further cleanup * More copy cols --- .../src/components/buttons/CopyButton.tsx | 7 +++++-- src/frontend/src/tables/ColumnRenderers.tsx | 17 +++++++++++++++++ src/frontend/src/tables/CopyableCell.tsx | 15 +++++++++++++-- src/frontend/src/tables/InvenTreeTable.tsx | 4 +++- src/frontend/src/tables/bom/BomTable.tsx | 10 ++++------ src/frontend/src/tables/bom/UsedInTable.tsx | 9 ++++----- .../tables/build/BuildAllocatedStockTable.tsx | 16 +++++++--------- .../src/tables/build/BuildLineTable.tsx | 12 +++++------- .../src/tables/build/BuildOrderTable.tsx | 13 +++++-------- .../tables/part/PartSalesAllocationsTable.tsx | 6 ++---- src/frontend/src/tables/part/PartTable.tsx | 11 ++++++----- .../src/tables/part/PartTestResultTable.tsx | 3 ++- .../ManufacturerPartParametricTable.tsx | 14 ++++++-------- .../tables/purchasing/ManufacturerPartTable.tsx | 12 ++++-------- .../purchasing/PurchaseOrderLineItemTable.tsx | 3 ++- .../tables/purchasing/PurchaseOrderTable.tsx | 7 +++++-- .../purchasing/SupplierPartParametricTable.tsx | 3 ++- .../src/tables/purchasing/SupplierPartTable.tsx | 16 +++++++--------- .../src/tables/sales/ReturnOrderTable.tsx | 7 +++++-- .../tables/sales/SalesOrderAllocationTable.tsx | 15 +++++++-------- .../tables/sales/SalesOrderLineItemTable.tsx | 9 ++------- .../tables/sales/SalesOrderShipmentTable.tsx | 16 +++++++++------- .../src/tables/sales/SalesOrderTable.tsx | 7 +++++-- .../src/tables/stock/StockItemTable.tsx | 17 ++++++++--------- .../src/tables/stock/StockTrackingTable.tsx | 9 +++------ .../tests/pages/pui_purchase_order.spec.ts | 6 ++++-- 26 files changed, 142 insertions(+), 122 deletions(-) diff --git a/src/frontend/src/components/buttons/CopyButton.tsx b/src/frontend/src/components/buttons/CopyButton.tsx index 7479054cbc..363487be5c 100644 --- a/src/frontend/src/components/buttons/CopyButton.tsx +++ b/src/frontend/src/components/buttons/CopyButton.tsx @@ -1,6 +1,7 @@ import { t } from '@lingui/core/macro'; import { ActionIcon, + type ActionIconVariant, Button, type DefaultMantineColor, type FloatingPosition, @@ -22,7 +23,8 @@ export function CopyButton({ tooltipPosition, content, size, - color = 'gray' + color = 'gray', + variant = 'transparent' }: Readonly<{ value: any; label?: string; @@ -32,6 +34,7 @@ export function CopyButton({ content?: JSX.Element; size?: MantineSize; color?: DefaultMantineColor; + variant?: ActionIconVariant; }>) { const ButtonComponent = label ? Button : ActionIcon; @@ -51,7 +54,7 @@ export function CopyButton({ e.preventDefault(); copy(); }} - variant='transparent' + variant={copied ? 'transparent' : (variant ?? 'transparent')} size={size ?? 'sm'} > {copied ? ( diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index d025e423a8..6247094b67 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -107,6 +107,18 @@ export function PartColumn(props: PartColumnProps): TableColumn { }; } +export function IPNColumn(props: TableColumnProps): TableColumn { + return { + accessor: 'part_detail.IPN', + sortable: true, + ordering: 'IPN', + switchable: true, + title: t`IPN`, + copyable: true, + ...props + }; +} + export type StockColumnProps = TableColumnProps & { nullMessage?: string | ReactNode; }; @@ -448,6 +460,7 @@ export function DescriptionColumn(props: TableColumnProps): TableColumn { sortable: false, switchable: true, minWidth: '200px', + copyable: true, ...props }; } @@ -457,6 +470,8 @@ export function LinkColumn(props: TableColumnProps): TableColumn { accessor: 'link', sortable: false, defaultVisible: false, + copyable: true, + copyAccessor: props.accessor ?? 'link', render: (record: any) => { const url = resolveItem(record, props.accessor ?? 'link'); @@ -490,6 +505,7 @@ export function ReferenceColumn(props: TableColumnProps): TableColumn { title: t`Reference`, sortable: true, switchable: true, + copyable: true, ...props }; } @@ -664,6 +680,7 @@ export function DateColumn(props: TableColumnProps): TableColumn { formatDate(resolveItem(record, props.accessor ?? 'date'), { showTime: props.extra?.showTime }), + copyable: true, ...props }; } diff --git a/src/frontend/src/tables/CopyableCell.tsx b/src/frontend/src/tables/CopyableCell.tsx index 4d7fc94712..e07dec3b96 100644 --- a/src/frontend/src/tables/CopyableCell.tsx +++ b/src/frontend/src/tables/CopyableCell.tsx @@ -20,19 +20,30 @@ export function CopyableCell({ return ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} justify='space-between' + align='center' > {children} {isHovered && value != null && ( e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > - +
+ +
)}
diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index ca3d43e35b..250683db76 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -285,7 +285,9 @@ export function InvenTreeTable>({ } const copyValue = rawCopyValue == null ? '' : String(rawCopyValue); - return {content}; + if (!!copyValue) { + return {content}; + } }; } diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index 6fdec77f7d..9062e12b23 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -44,6 +44,7 @@ import { BooleanColumn, CategoryColumn, DescriptionColumn, + IPNColumn, NoteColumn, ReferenceColumn } from '../ColumnRenderers'; @@ -129,12 +130,9 @@ export function BomTable({ ); } }, - { - accessor: 'sub_part_detail.IPN', - title: t`IPN`, - sortable: true, - ordering: 'IPN' - }, + IPNColumn({ + accessor: 'sub_part_detail.IPN' + }), CategoryColumn({ accessor: 'category_detail', defaultVisible: false, diff --git a/src/frontend/src/tables/bom/UsedInTable.tsx b/src/frontend/src/tables/bom/UsedInTable.tsx index d2e4481d14..0134d1a4ec 100644 --- a/src/frontend/src/tables/bom/UsedInTable.tsx +++ b/src/frontend/src/tables/bom/UsedInTable.tsx @@ -15,6 +15,7 @@ import { useTable } from '../../hooks/UseTable'; import { useUserState } from '../../states/UserState'; import { DescriptionColumn, + IPNColumn, PartColumn, ReferenceColumn } from '../ColumnRenderers'; @@ -40,11 +41,9 @@ export function UsedInTable({ title: t`Assembly`, part: 'part_detail' }), - { - accessor: 'part_detail.IPN', - sortable: false, - title: t`IPN` - }, + IPNColumn({ + sortable: false + }), { accessor: 'part_detail.revision', title: t`Revision`, diff --git a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx index acc0bfe67d..9932f64e16 100644 --- a/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx +++ b/src/frontend/src/tables/build/BuildAllocatedStockTable.tsx @@ -22,6 +22,7 @@ import { useTable } from '../../hooks/UseTable'; import { useUserState } from '../../states/UserState'; import { DecimalColumn, + IPNColumn, LocationColumn, PartColumn, ReferenceColumn, @@ -99,14 +100,9 @@ export default function BuildAllocatedStockTable({ hidden: !showPartInfo, switchable: false }), - { - accessor: 'part_detail.IPN', - ordering: 'IPN', - hidden: !showPartInfo, - title: t`IPN`, - sortable: true, - switchable: true - }, + IPNColumn({ + hidden: !showPartInfo + }), { hidden: !showPartInfo, accessor: 'bom_reference', @@ -119,7 +115,9 @@ export default function BuildAllocatedStockTable({ title: t`Batch Code`, sortable: false, switchable: true, - render: (record: any) => record?.stock_item_detail?.batch + render: (record: any) => record?.stock_item_detail?.batch, + copyable: true, + copyAccessor: 'stock_item_detail.batch' }, DecimalColumn({ accessor: 'stock_item_detail.quantity', diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index bd1cf3ebdf..d072df911b 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -44,6 +44,7 @@ import { CategoryColumn, DecimalColumn, DescriptionColumn, + IPNColumn, LocationColumn, PartColumn, RenderPartColumn @@ -91,7 +92,9 @@ export function BuildLineSubTable({ }, { accessor: 'stock_item_detail.batch', - title: t`Batch` + title: t`Batch`, + copyable: true, + copyAccessor: 'stock_item_detail.batch' }, LocationColumn({ accessor: 'location_detail' @@ -332,12 +335,7 @@ export default function BuildLineTable({ ); } }), - { - accessor: 'part_detail.IPN', - sortable: true, - ordering: 'IPN', - title: t`IPN` - }, + IPNColumn({}), CategoryColumn({ accessor: 'category_detail', defaultVisible: false, diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 77f071df84..115e07ab74 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -18,6 +18,8 @@ import { CreationDateColumn, DateColumn, DescriptionColumn, + IPNColumn, + LinkColumn, PartColumn, ProjectCodeColumn, ReferenceColumn, @@ -79,13 +81,7 @@ export function BuildOrderTable({ PartColumn({ switchable: false }), - { - accessor: 'part_detail.IPN', - sortable: true, - ordering: 'IPN', - switchable: true, - title: t`IPN` - }, + IPNColumn({}), { accessor: 'part_detail.revision', title: t`Revision`, @@ -150,7 +146,8 @@ export function BuildOrderTable({ ordering: 'issued_by', title: t`Issued By` }), - ResponsibleColumn({}) + ResponsibleColumn({}), + LinkColumn({}) ]; }, [parentBuildId, globalSettings]); diff --git a/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx b/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx index c4e7748ac8..516bb456de 100644 --- a/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx +++ b/src/frontend/src/tables/part/PartSalesAllocationsTable.tsx @@ -16,6 +16,7 @@ import { useTable } from '../../hooks/UseTable'; import { useUserState } from '../../states/UserState'; import { DescriptionColumn, + IPNColumn, PartColumn, ProjectCodeColumn, StatusColumn @@ -56,10 +57,7 @@ export default function PartSalesAllocationsTable({ PartColumn({ part: 'part_detail' }), - { - accessor: 'part_detail.IPN', - title: t`IPN` - }, + IPNColumn({}), ProjectCodeColumn({ accessor: 'order_detail.project_code_detail' }), diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index e208b4035c..c6380f9ac0 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -41,6 +41,7 @@ import { CategoryColumn, DefaultLocationColumn, DescriptionColumn, + IPNColumn, LinkColumn, PartColumn } from '../ColumnRenderers'; @@ -56,17 +57,17 @@ function partTableColumns(): TableColumn[] { part: '', accessor: 'name' }), - { - accessor: 'IPN', - sortable: true - }, + IPNColumn({ + accessor: 'IPN' + }), { accessor: 'revision', sortable: true }, { accessor: 'units', - sortable: true + sortable: true, + copyable: true }, DescriptionColumn({}), CategoryColumn({ diff --git a/src/frontend/src/tables/part/PartTestResultTable.tsx b/src/frontend/src/tables/part/PartTestResultTable.tsx index a0f1d08f9c..6a67fb4a20 100644 --- a/src/frontend/src/tables/part/PartTestResultTable.tsx +++ b/src/frontend/src/tables/part/PartTestResultTable.tsx @@ -288,7 +288,8 @@ export default function PartTestResultTable({ accessor: 'batch', title: t`Batch Code`, sortable: true, - switchable: true + switchable: true, + copyable: true }, LocationColumn({ accessor: 'location_detail' diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartParametricTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartParametricTable.tsx index d6c06ab75e..e250d564ab 100644 --- a/src/frontend/src/tables/purchasing/ManufacturerPartParametricTable.tsx +++ b/src/frontend/src/tables/purchasing/ManufacturerPartParametricTable.tsx @@ -3,7 +3,7 @@ import type { TableFilter } from '@lib/types/Filters'; import type { TableColumn } from '@lib/types/Tables'; import { t } from '@lingui/core/macro'; import { type ReactNode, useMemo } from 'react'; -import { CompanyColumn, PartColumn } from '../ColumnRenderers'; +import { CompanyColumn, IPNColumn, PartColumn } from '../ColumnRenderers'; import ParametricDataTable from '../general/ParametricDataTable'; export default function ManufacturerPartParametricTable({ @@ -16,12 +16,9 @@ export default function ManufacturerPartParametricTable({ PartColumn({ switchable: false }), - { - accessor: 'part_detail.IPN', - title: t`IPN`, - sortable: false, - switchable: true - }, + IPNColumn({ + sortable: false + }), { accessor: 'manufacturer', sortable: true, @@ -32,7 +29,8 @@ export default function ManufacturerPartParametricTable({ { accessor: 'MPN', title: t`MPN`, - sortable: true + sortable: true, + copyable: true } ]; }, []); diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx index dd1aaaa99c..674963ff80 100644 --- a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx +++ b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx @@ -25,6 +25,7 @@ import { useUserState } from '../../states/UserState'; import { CompanyColumn, DescriptionColumn, + IPNColumn, LinkColumn, PartColumn } from '../ColumnRenderers'; @@ -86,13 +87,7 @@ export function ManufacturerPartTable({ PartColumn({ switchable: !!partId }), - { - accessor: 'part_detail.IPN', - title: t`IPN`, - sortable: true, - ordering: 'IPN', - switchable: true - }, + IPNColumn({}), { accessor: 'manufacturer', sortable: true, @@ -103,7 +98,8 @@ export function ManufacturerPartTable({ { accessor: 'MPN', title: t`MPN`, - sortable: true + sortable: true, + copyable: true }, DescriptionColumn({}), LinkColumn({}) diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 2f4198e459..f792cec63b 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -251,7 +251,8 @@ export function PurchaseOrderLineItemTable({ ordering: 'MPN', title: t`Manufacturer Code`, sortable: true, - defaultVisible: false + defaultVisible: false, + copyable: true }, CurrencyColumn({ accessor: 'purchase_price', diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index 34c1f37ae6..18977c9103 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -19,6 +19,7 @@ import { CreationDateColumn, DescriptionColumn, LineItemsProgressColumn, + LinkColumn, ProjectCodeColumn, ReferenceColumn, ResponsibleColumn, @@ -120,7 +121,8 @@ export function PurchaseOrderTable({ ) }, { - accessor: 'supplier_reference' + accessor: 'supplier_reference', + copyable: true }, LineItemsProgressColumn({}), StatusColumn({ model: ModelType.purchaseorder }), @@ -150,7 +152,8 @@ export function PurchaseOrderTable({ }); } }, - ResponsibleColumn({}) + ResponsibleColumn({}), + LinkColumn({}) ]; }, []); diff --git a/src/frontend/src/tables/purchasing/SupplierPartParametricTable.tsx b/src/frontend/src/tables/purchasing/SupplierPartParametricTable.tsx index 08dd6cfe37..1ee71094a1 100644 --- a/src/frontend/src/tables/purchasing/SupplierPartParametricTable.tsx +++ b/src/frontend/src/tables/purchasing/SupplierPartParametricTable.tsx @@ -27,7 +27,8 @@ export default function SupplierPartParametricTable({ { accessor: 'SKU', title: t`Supplier Part`, - sortable: true + sortable: true, + copyable: true } ]; }, []); diff --git a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx index 9afc553d52..f0c99c870c 100644 --- a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx +++ b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx @@ -32,6 +32,7 @@ import { CompanyColumn, DecimalColumn, DescriptionColumn, + IPNColumn, LinkColumn, NoteColumn, PartColumn @@ -92,13 +93,7 @@ export function SupplierPartTable({ switchable: !!partId, part: 'part_detail' }), - { - accessor: 'part_detail.IPN', - title: t`IPN`, - sortable: true, - ordering: 'IPN', - switchable: true - }, + IPNColumn({}), { accessor: 'supplier', sortable: true, @@ -109,7 +104,8 @@ export function SupplierPartTable({ { accessor: 'SKU', title: t`Supplier Part`, - sortable: true + sortable: true, + copyable: true }, DescriptionColumn({}), { @@ -124,7 +120,9 @@ export function SupplierPartTable({ accessor: 'MPN', sortable: true, title: t`MPN`, - render: (record: any) => record?.manufacturer_part_detail?.MPN + render: (record: any) => record?.manufacturer_part_detail?.MPN, + copyable: true, + copyAccessor: 'manufacturer_part_detail.MPN' }, BooleanColumn({ accessor: 'primary', diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index dfd0362e90..498e0d2163 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -19,6 +19,7 @@ import { CreationDateColumn, DescriptionColumn, LineItemsProgressColumn, + LinkColumn, ProjectCodeColumn, ReferenceColumn, ResponsibleColumn, @@ -123,7 +124,8 @@ export function ReturnOrderTable({ ) }, { - accessor: 'customer_reference' + accessor: 'customer_reference', + copyable: true }, DescriptionColumn({}), LineItemsProgressColumn({}), @@ -154,7 +156,8 @@ export function ReturnOrderTable({ currency: record.order_currency || record.customer_detail?.currency }); } - } + }, + LinkColumn({}) ]; }, []); diff --git a/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx b/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx index 4355366ac9..a5726d5759 100644 --- a/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx @@ -29,6 +29,7 @@ import { useTable } from '../../hooks/UseTable'; import { useUserState } from '../../states/UserState'; import { DescriptionColumn, + IPNColumn, LocationColumn, PartColumn, ReferenceColumn, @@ -130,13 +131,9 @@ export default function SalesOrderAllocationTable({ accessor: 'part_detail.description', hidden: showPartInfo != true }), - { - accessor: 'part_detail.IPN', - title: t`IPN`, - hidden: showPartInfo != true, - sortable: true, - ordering: 'IPN' - }, + IPNColumn({ + hidden: showPartInfo != true + }), { accessor: 'serial', title: t`Serial Number`, @@ -149,7 +146,9 @@ export default function SalesOrderAllocationTable({ title: t`Batch Code`, sortable: true, switchable: true, - render: (record: any) => record?.item_detail?.batch + render: (record: any) => record?.item_detail?.batch, + copyable: true, + copyAccessor: 'item_detail.batch' }, { accessor: 'available', diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx index 7b2cb4b440..7bac4cb4f9 100644 --- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx @@ -47,6 +47,7 @@ import { DateColumn, DecimalColumn, DescriptionColumn, + IPNColumn, LinkColumn, ProjectCodeColumn, RenderPartColumn @@ -94,13 +95,7 @@ export default function SalesOrderLineItemTable({ ); } }, - { - accessor: 'part_detail.IPN', - title: t`IPN`, - sortable: true, - ordering: 'IPN', - switchable: true - }, + IPNColumn({}), DescriptionColumn({ accessor: 'part_detail.description' }), diff --git a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx index 52a90e6829..deed41fff1 100644 --- a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx @@ -142,7 +142,8 @@ export default function SalesOrderShipmentTable({ accessor: 'order_detail.reference', title: t`Sales Order`, hidden: !showOrderInfo, - sortable: false + sortable: false, + copyable: true }, StatusColumn({ switchable: true, @@ -155,7 +156,8 @@ export default function SalesOrderShipmentTable({ accessor: 'reference', title: t`Shipment Reference`, switchable: false, - sortable: true + sortable: true, + copyable: true }, { accessor: 'allocated_items', @@ -193,14 +195,14 @@ export default function SalesOrderShipmentTable({ title: t`Delivery Date` }), { - accessor: 'tracking_number' + accessor: 'tracking_number', + copyable: true }, { - accessor: 'invoice_number' + accessor: 'invoice_number', + copyable: true }, - LinkColumn({ - accessor: 'link' - }) + LinkColumn({}) ]; }, [showOrderInfo]); diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index c87e079767..6276a1a7d4 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -20,6 +20,7 @@ import { CreationDateColumn, DescriptionColumn, LineItemsProgressColumn, + LinkColumn, ProjectCodeColumn, ReferenceColumn, ResponsibleColumn, @@ -146,7 +147,8 @@ export function SalesOrderTable({ }, { accessor: 'customer_reference', - title: t`Customer Reference` + title: t`Customer Reference`, + copyable: true }, DescriptionColumn({}), LineItemsProgressColumn({}), @@ -190,7 +192,8 @@ export function SalesOrderTable({ currency: record.order_currency || record.customer_detail?.currency }); } - } + }, + LinkColumn({}) ]; }, []); diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 5f04978063..de0d125548 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -24,6 +24,7 @@ import { useUserState } from '../../states/UserState'; import { DateColumn, DescriptionColumn, + IPNColumn, LocationColumn, PartColumn, StatusColumn, @@ -59,12 +60,7 @@ function stockItemTableColumns({ accessor: 'part', part: 'part_detail' }), - { - accessor: 'part_detail.IPN', - title: t`IPN`, - sortable: true, - ordering: 'IPN' - }, + IPNColumn({}), { accessor: 'part_detail.revision', title: t`Revision`, @@ -83,7 +79,8 @@ function stockItemTableColumns({ StatusColumn({ model: ModelType.stockitem }), { accessor: 'batch', - sortable: true + sortable: true, + copyable: true }, LocationColumn({ hidden: !showLocation, @@ -101,13 +98,15 @@ function stockItemTableColumns({ accessor: 'SKU', title: t`Supplier Part`, sortable: true, - defaultVisible: false + defaultVisible: false, + copyable: true }, { accessor: 'MPN', title: t`Manufacturer Part`, sortable: true, - defaultVisible: false + defaultVisible: false, + copyable: true }, { accessor: 'purchase_price', diff --git a/src/frontend/src/tables/stock/StockTrackingTable.tsx b/src/frontend/src/tables/stock/StockTrackingTable.tsx index c901a23952..6019661760 100644 --- a/src/frontend/src/tables/stock/StockTrackingTable.tsx +++ b/src/frontend/src/tables/stock/StockTrackingTable.tsx @@ -27,6 +27,7 @@ import { useTable } from '../../hooks/UseTable'; import { DateColumn, DescriptionColumn, + IPNColumn, PartColumn, StockColumn } from '../ColumnRenderers'; @@ -238,14 +239,10 @@ export function StockTrackingTable({ switchable: true, hidden: !partId }), - { - title: t`IPN`, - accessor: 'part_detail.IPN', - sortable: true, + IPNColumn({ defaultVisible: false, - switchable: true, hidden: !partId - }, + }), StockColumn({ title: t`Stock Item`, accessor: 'item_detail', diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts index c8ff3f9393..d44f34aad0 100644 --- a/src/frontend/tests/pages/pui_purchase_order.spec.ts +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -31,7 +31,7 @@ test('Purchasing - Index', async ({ browser }) => { // Clearing the filters, more orders should be visible await clearTableFilters(page); - await page.getByText(/1 - 1\d \/ 1\d/).waitFor(); + await page.getByText(/1 - \d\d \/ \d\d/).waitFor(); // Suppliers tab await loadTab(page, 'Suppliers'); @@ -49,9 +49,11 @@ test('Purchasing - Index', async ({ browser }) => { // Check for expected values await clearTableFilters(page); + await page + .getByRole('textbox', { name: 'table-search-input' }) + .fill('R_100K_0402'); await page.getByText('R_100K_0402_1%').first().waitFor(); await page.getByRole('cell', { name: 'RR05P100KDTR-ND' }).first().waitFor(); - await page.getByRole('cell', { name: 'RT0402BRD07100KL' }).first().waitFor(); // Manufacturers tab await loadTab(page, 'Manufacturers'); From 246108e732f50a49b8abaed24b106cbb74df0b3e Mon Sep 17 00:00:00 2001 From: JustusRijke <53965859+JustusRijke@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:58:01 +0100 Subject: [PATCH 07/28] Fix auto pricing overwriting manual purchase price #10846 (#11411) * Fix auto pricing overwriting manual purchase price #10846 * Added entry to api_version.py --------- Co-authored-by: Oliver --- src/backend/InvenTree/InvenTree/api_version.py | 7 +++++-- src/backend/InvenTree/order/serializers.py | 2 +- src/frontend/src/forms/PurchaseOrderForms.tsx | 12 ++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 493f4ae5db..4d4b72abd2 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 458 +INVENTREE_API_VERSION = 459 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v459 -> 2026-02-23 : https://github.com/inventree/InvenTree/pull/11411 + - Changed PurchaseOrderLine "auto_pricing" default value from true to false + v458 -> 2026-02-22 : https://github.com/inventree/InvenTree/pull/11401 - - siwtches token refresh endpoint to use POST instead of GET (upstream allauth change) + - Switches token refresh endpoint to use POST instead of GET (upstream allauth change) v457 -> 2026-02-11 : https://github.com/inventree/InvenTree/pull/10887 - Extend the "auto allocate" wizard API to include tracked items diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index c62ce69d90..5d0fd37d5b 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -639,7 +639,7 @@ class PurchaseOrderLineItemSerializer( help_text=_( 'Automatically calculate purchase price based on supplier part data' ), - default=True, + default=False, ) destination_detail = enable_filter( diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index cb22aa5c86..b83f1e8d8b 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -113,16 +113,6 @@ export function usePurchaseOrderLineItemFields({ } }, [create, part, quantity, priceBreaks]); - useEffect(() => { - if (autoPricing) { - setPurchasePrice(''); - } - }, [autoPricing]); - - useEffect(() => { - setAutoPricing(purchasePrice === ''); - }, [purchasePrice]); - const fields = useMemo(() => { const fields: ApiFormFieldSet = { order: { @@ -159,6 +149,7 @@ export function usePurchaseOrderLineItemFields({ purchase_price: { icon: , value: purchasePrice, + disabled: autoPricing, placeholder: suggestedPurchasePrice, placeholderAutofill: true, onValueChange: setPurchasePrice @@ -169,6 +160,7 @@ export function usePurchaseOrderLineItemFields({ onValueChange: setPurchasePriceCurrency }, auto_pricing: { + default: create !== false, value: autoPricing, onValueChange: setAutoPricing }, From 8a4ad4ff6238b218fd86595db43eb022353c2101 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 24 Feb 2026 16:08:26 +1100 Subject: [PATCH 08/28] [UI] Default locale (#11412) * [UI] Support default server language * Handle faulty theme * Add option for default language * Improve language selection * Brief docs entry * Fix typo * Fix yarn build * Remove debug msg * Fix calendar locale --- docs/docs/concepts/user_interface.md | 8 +++ .../src/components/calendar/Calendar.tsx | 18 ++++++- .../src/components/items/LanguageSelect.tsx | 16 +++++- .../src/components/plugins/PluginContext.tsx | 5 +- src/frontend/src/contexts/LanguageContext.tsx | 54 ++++++++++++++++--- src/frontend/src/contexts/ThemeContext.tsx | 42 +++++++++------ src/frontend/src/states/LocalState.tsx | 6 +-- 7 files changed, 119 insertions(+), 30 deletions(-) diff --git a/docs/docs/concepts/user_interface.md b/docs/docs/concepts/user_interface.md index a7a20f99f0..665b9fcf02 100644 --- a/docs/docs/concepts/user_interface.md +++ b/docs/docs/concepts/user_interface.md @@ -291,3 +291,11 @@ Users may opt to disable the spotlight search functionality if they do not find Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization. If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability. + +## Language Support + +The InvenTree user interface supports multiple languages, allowing users to interact with the system in their preferred language. + +The default system language can be configured by the system administrator in the [server configuration options](../start/config.md#basic-options). + +Additionally, users can select their preferred language in their [user settings](../settings/user.md), allowing them to override the system default language with their own choice. This provides a personalized experience for each user, ensuring that they can interact with the system in the language they are most comfortable with. diff --git a/src/frontend/src/components/calendar/Calendar.tsx b/src/frontend/src/components/calendar/Calendar.tsx index d93195d725..50f416a175 100644 --- a/src/frontend/src/components/calendar/Calendar.tsx +++ b/src/frontend/src/components/calendar/Calendar.tsx @@ -29,6 +29,10 @@ import { } from '@tabler/icons-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; +import { + defaultLocale, + getPriorityLocale +} from '../../contexts/LanguageContext'; import type { CalendarState } from '../../hooks/UseCalendar'; import { useLocalState } from '../../states/LocalState'; import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer'; @@ -60,7 +64,19 @@ export default function Calendar({ const [locale] = useLocalState(useShallow((s) => [s.language])); // Ensure underscore is replaced with dash - const calendarLocale = useMemo(() => locale.replace('_', '-'), [locale]); + const calendarLocale = useMemo(() => { + let _locale: string | null = locale; + + if (!_locale) { + _locale = getPriorityLocale(); + } + + _locale = _locale || defaultLocale; + + _locale = _locale.replace('_', '-'); + + return _locale; + }, [locale]); const selectMonth = useCallback( (date: DateValue) => { diff --git a/src/frontend/src/components/items/LanguageSelect.tsx b/src/frontend/src/components/items/LanguageSelect.tsx index 6ef3cdfc0a..3650603670 100644 --- a/src/frontend/src/components/items/LanguageSelect.tsx +++ b/src/frontend/src/components/items/LanguageSelect.tsx @@ -1,8 +1,12 @@ import { Select } from '@mantine/core'; import { useEffect, useState } from 'react'; +import { t } from '@lingui/core/macro'; import { useShallow } from 'zustand/react/shallow'; -import { getSupportedLanguages } from '../../contexts/LanguageContext'; +import { + activateLocale, + getSupportedLanguages +} from '../../contexts/LanguageContext'; import { useLocalState } from '../../states/LocalState'; export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) { @@ -28,13 +32,21 @@ export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) { })); setLangOptions(newLangOptions); setValue(locale); + activateLocale(locale); // Ensure the locale is activated on component load }, [locale]); return (