diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc6e48ac1..052cdafdcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [#12103](https://github.com/inventree/InvenTree/pull/12103) adds column-based filtering to table views in the user interface. This extends the existing table filtering functionality by allowing users to apply filters directly to individual columns. - [#12093](https://github.com/inventree/InvenTree/pull/12093) adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API) - [#12079](https://github.com/inventree/InvenTree/pull/12079) adds the ability to save filter groups for table and calendar views in the user interface. This allows users to save and reuse commonly used filter configurations, improving the usability and efficiency of the interface. - [#12077](https://github.com/inventree/InvenTree/pull/12077) adds "tags" fields to multiple new model types and a /api/tag/ endpoint for fetching tags. Also adds the ability to filter various model types by tags. diff --git a/docs/docs/assets/images/concepts/ui_table_column_filter_popover.png b/docs/docs/assets/images/concepts/ui_table_column_filter_popover.png new file mode 100644 index 0000000000..8e979ed999 Binary files /dev/null and b/docs/docs/assets/images/concepts/ui_table_column_filter_popover.png differ diff --git a/docs/docs/concepts/user_interface.md b/docs/docs/concepts/user_interface.md index 64255d6145..c9973c514c 100644 --- a/docs/docs/concepts/user_interface.md +++ b/docs/docs/concepts/user_interface.md @@ -158,6 +158,23 @@ Select the "table filters" button to open the filter selection menu Table filters are saved across browser sessions, allowing users to maintain their preferred filter settings when returning to the particular table view. +#### Column Filters + +Many table columns expose an inline filter icon directly in the column header, providing a quick way to filter by that column without opening the full filter drawer. Columns that support filtering display a small filter icon alongside the column title. The icon is highlighted when a filter for that column is currently active, giving an at-a-glance indication of which columns have active filters. + +Clicking the icon opens a compact popover anchored to the column header: + +{{ image("concepts/ui_table_column_filter_popover.png", "Column Filter Popover") }} + +**Single-filter columns** — for columns linked to one filter (e.g. *Active*, *Has IPN*, *Status*), selecting a value immediately applies the filter and the popover closes automatically. + +**Range columns** — for columns that represent a range concept (e.g. *Start Date*, *Target Date*, *Creation Date*), the popover stays open and presents multiple controls — for example *before* and *after* date pickers — so both bounds can be set in a single interaction. + +Once a filter is active, the popover shows a badge with the current value and a remove button (red ×) instead of the value picker. Clicking the × clears only that column's filter. + +!!! info "Column filters and the filter drawer share the same state" + Filters applied via a column popover appear immediately in the filter drawer's active-filter list, and filters added through the drawer are reflected in the column icons. Clearing all filters from the drawer also removes any filters set via column popovers. + #### Saved Filter Groups Frequently used combinations of filters can be saved as a named *filter group*, allowing them to be quickly recalled later without having to re-add each filter individually. diff --git a/src/frontend/lib/components/TableColumnSelect.tsx b/src/frontend/lib/components/TableColumnSelect.tsx index c686ce65c6..5b99ffc82f 100644 --- a/src/frontend/lib/components/TableColumnSelect.tsx +++ b/src/frontend/lib/components/TableColumnSelect.tsx @@ -23,7 +23,7 @@ export function TableColumnSelect({ {t`Select Columns`} {columns - .filter((col) => col.switchable ?? true) + .filter((col) => (col.switchable ?? true) && !col.propHidden) .map((col) => ( = { editable?: boolean; definition?: ApiFormFieldType; render?: (record: T, index?: number) => any; - filter?: any; + filter?: + | string + | string[] + | (({ close }: { close: () => void }) => ReactNode); filtering?: boolean; width?: number; minWidth?: string | number; diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index f3c61e8990..b2d4eb4f81 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -117,6 +117,7 @@ export function IPNColumn(props: TableColumnProps): TableColumn { switchable: true, title: t`IPN`, copyable: true, + filter: 'has_ipn', ...props }; } @@ -433,6 +434,7 @@ export function BooleanColumn(props: TableColumn): TableColumn { sortable: true, switchable: true, minWidth: '75px', + filter: props.filter ?? props.accessor, render: (record: any) => (
@@ -564,6 +566,7 @@ export function ProjectCodeColumn(props: TableColumnProps): TableColumn { sortable: true, title: t`Project Code`, hidden: !enabled, + filter: 'project_code', render: (record: any) => { const project_code = resolveItem( record, @@ -584,6 +587,7 @@ export function StatusColumn(props: StatusColumnProps): TableColumn { return { accessor: 'status', + filter: 'status', sortable: true, switchable: true, minWidth: '50px', @@ -636,6 +640,7 @@ export function CreatedByColumn(props: TableColumnProps): TableColumn { accessor: 'created_by', ordering: 'created_by', title: t`Created By`, + filter: 'created_by', ...props }); } @@ -665,6 +670,7 @@ export function ResponsibleColumn(props: TableColumnProps): TableColumn { accessor: 'responsible_detail', ordering: 'responsible', title: t`Responsible`, + filter: 'assigned_to', ...props }); } @@ -688,6 +694,7 @@ export function StartDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'start_date', title: t`Start Date`, + filter: ['has_start_date', 'start_date_before', 'start_date_after'], ...props }); } @@ -696,6 +703,7 @@ export function TargetDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'target_date', title: t`Target Date`, + filter: ['has_target_date', 'target_date_before', 'target_date_after'], ...props }); } @@ -704,6 +712,7 @@ export function CreationDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'creation_date', title: t`Creation Date`, + filter: ['created_before', 'created_after'], ...props }); } @@ -712,6 +721,7 @@ export function CompletionDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'completion_date', title: t`Completion Date`, + filter: ['completed_before', 'completed_after'], ...props }); } @@ -720,6 +730,7 @@ export function ShipmentDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'shipment_date', title: t`Shipment Date`, + filter: ['shipment_date_before', 'shipment_date_after'], ...props }); } @@ -729,6 +740,7 @@ export function UpdatedAtColumn(props: TableColumnProps): TableColumn { accessor: 'updated_at', title: t`Updated`, defaultVisible: false, + filter: ['updated_before', 'updated_after'], extra: { showTime: true }, ...props }); diff --git a/src/frontend/src/tables/FilterSelectDrawer.tsx b/src/frontend/src/tables/FilterSelectDrawer.tsx index 78eecb16ea..8be8e4f9f8 100644 --- a/src/frontend/src/tables/FilterSelectDrawer.tsx +++ b/src/frontend/src/tables/FilterSelectDrawer.tsx @@ -182,15 +182,17 @@ function MultiApiFilterElement({ ); } -function FilterElement({ +export function FilterElement({ filterName, filterProps, valueOptions, + brief = false, onValueChange }: { filterName: string; filterProps: TableFilter; valueOptions: TableFilterChoice[]; + brief?: boolean; onValueChange: (value: string | null, displayValue?: any) => void; }) { const setDateValue = useCallback( @@ -226,7 +228,7 @@ function FilterElement({ filters: filterProps.apiFilter, placeholder: t`Select filter value`, model: filterProps.model, - label: t`Select filter value`, + label: brief ? undefined : t`Select filter value`, onValueChange: (value: any, instance: any) => { if (filterProps.transform) { const choice = filterProps.transform(instance); @@ -244,9 +246,10 @@ function FilterElement({ case 'text': return ( } - onChange={(e) => setTextValue(e.currentTarget.value)} - onKeyDown={(e) => { + onChange={(e: React.ChangeEvent) => + setTextValue(e.currentTarget.value) + } + onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') { onValueChange(textValue); } @@ -267,9 +272,11 @@ function FilterElement({ case 'date': return ( ); case 'choice': @@ -278,8 +285,9 @@ function FilterElement({ return (