[PUI] refactor table filter selector (#6047)

* Add FilterSelectDrawer component

* Add descriptions for build order table filters

* Pass active filters through via UseTable hook

* Remove old FilterGroup component

* Add callback to remove selected filter

* Implement interface for adding new filters

* Prevent duplication of filters

* Hide "add filter" elements after creating new filter

* Improved rendering

* Implement more filters for stock item table

* Add some filters for stock location table

* Refactor filter choice method

- Add StatusFilterOptions callback
- Update filters for existing tables

* purchase order table filters

* Implement more table filters

* Fix unused imports

* Render display value, not raw value

* Cleanup

* UI improvements
This commit is contained in:
Oliver 2023-12-08 11:56:35 +11:00 committed by GitHub
parent 3a7b1510b3
commit 048a06ce19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 719 additions and 391 deletions

View File

@ -820,8 +820,9 @@ class PartFilter(rest_filters.FilterSet):
def filter_has_units(self, queryset, name, value):
"""Filter by whether the Part has units or not"""
if str2bool(value):
return queryset.exclude(units='')
return queryset.filter(units='')
return queryset.exclude(Q(units=None) | Q(units=''))
return queryset.filter(Q(units=None) | Q(units='')).distinct()
# Filter by parts which have (or not) an IPN value
has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn')

View File

@ -1,3 +1,8 @@
import { t } from '@lingui/macro';
import { ModelType } from '../../enums/ModelType';
import { useServerApiState } from '../../states/ApiState';
/**
* Interface for the table filter choice
*/
@ -7,15 +12,96 @@ export type TableFilterChoice = {
};
/**
* Interface for the table filter,
* Interface for the table filter type. Provides a number of options for selecting filter value:
*
* choices: A list of TableFilterChoice objects
* choiceFunction: A function which returns a list of TableFilterChoice objects
* statusType: A ModelType which is used to generate a list of status codes
*/
export type TableFilter = {
name: string;
label: string;
description?: string;
type: string;
type?: string;
choices?: TableFilterChoice[];
choiceFunction?: () => TableFilterChoice[];
defaultValue?: any;
value?: any;
displayValue?: any;
};
/**
* Return list of available filter options for a given filter
* @param filter - TableFilter object
* @returns - A list of TableFilterChoice objects
*/
export function getTableFilterOptions(
filter: TableFilter
): TableFilterChoice[] {
if (filter.choices) {
return filter.choices;
}
if (filter.choiceFunction) {
return filter.choiceFunction();
}
// Default fallback is a boolean filter
return [
{ value: 'true', label: t`Yes` },
{ value: 'false', label: t`No` }
];
}
/*
* Construct a table filter which allows filtering by status code
*/
export function StatusFilterOptions(
model: ModelType
): () => TableFilterChoice[] {
return () => {
const statusCodeList = useServerApiState.getState().status;
if (!statusCodeList) {
return [];
}
const codes = statusCodeList[model];
if (codes) {
return Object.keys(codes).map((key) => {
const entry = codes[key];
return {
value: entry.key,
label: entry.label ?? entry.key
};
});
}
return [];
};
}
export function AssignedToMeFilter(): TableFilter {
return {
name: 'assigned_to_me',
label: t`Assigned to me`,
description: t`Show orders assigned to me`
};
}
export function OutstandingFilter(): TableFilter {
return {
name: 'outstanding',
label: t`Outstanding`,
description: t`Show outstanding orders`
};
}
export function OverdueFilter(): TableFilter {
return {
name: 'overdue',
label: t`Overdue`,
description: t`Show overdue orders`
};
}

View File

@ -1,50 +0,0 @@
import { t } from '@lingui/macro';
import { Badge, CloseButton } from '@mantine/core';
import { Text, Tooltip } from '@mantine/core';
import { Group } from '@mantine/core';
import { TableFilter } from './Filter';
export function FilterBadge({
filter,
onFilterRemove
}: {
filter: TableFilter;
onFilterRemove: () => void;
}) {
/**
* Construct text to display for the given badge ID
*/
function filterDescription() {
let text = filter.label || filter.name;
text += ' = ';
text += filter.value;
return text;
}
return (
<Badge
size="lg"
radius="lg"
variant="outline"
color="gray"
styles={(theme) => ({
root: {
paddingRight: '4px'
},
inner: {
textTransform: 'none'
}
})}
>
<Group spacing={1}>
<Text>{filterDescription()}</Text>
<Tooltip label={t`Remove filter`}>
<CloseButton color="red" onClick={() => onFilterRemove()} />
</Tooltip>
</Group>
</Badge>
);
}

View File

@ -1,58 +0,0 @@
import { t } from '@lingui/macro';
import { ActionIcon, Group, Text, Tooltip } from '@mantine/core';
import { IconFilterMinus } from '@tabler/icons-react';
import { IconFilterPlus } from '@tabler/icons-react';
import { TableFilter } from './Filter';
import { FilterBadge } from './FilterBadge';
/**
* Return a table filter group component:
* - Displays a list of active filters for the table
* - Allows the user to add/remove filters
* - Allows the user to clear all filters
*/
export function FilterGroup({
activeFilters,
onFilterAdd,
onFilterRemove,
onFilterClearAll
}: {
activeFilters: TableFilter[];
onFilterAdd: () => void;
onFilterRemove: (filterName: string) => void;
onFilterClearAll: () => void;
}) {
return (
<Group position="right" spacing={5}>
{activeFilters.length == 0 && (
<Text italic={true} size="sm">{t`Add table filter`}</Text>
)}
{activeFilters.map((f) => (
<FilterBadge
key={f.name}
filter={f}
onFilterRemove={() => onFilterRemove(f.name)}
/>
))}
{activeFilters.length && (
<ActionIcon
radius="sm"
variant="outline"
onClick={() => onFilterClearAll()}
>
<Tooltip label={t`Clear all filters`}>
<IconFilterMinus color="red" />
</Tooltip>
</ActionIcon>
)}
{
<ActionIcon radius="sm" variant="outline" onClick={() => onFilterAdd()}>
<Tooltip label={t`Add filter`}>
<IconFilterPlus color="green" />
</Tooltip>
</ActionIcon>
}
</Group>
);
}

View File

@ -0,0 +1,234 @@
import { t } from '@lingui/macro';
import {
Badge,
Button,
CloseButton,
Divider,
Drawer,
Group,
Paper,
Select,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { TableState } from '../../hooks/UseTable';
import { StylishText } from '../items/StylishText';
import {
TableFilter,
TableFilterChoice,
getTableFilterOptions
} from './Filter';
/*
* Render a single table filter item
*/
function FilterItem({
flt,
tableState
}: {
flt: TableFilter;
tableState: TableState;
}) {
const removeFilter = useCallback(() => {
let newFilters = tableState.activeFilters.filter(
(f) => f.name !== flt.name
);
tableState.setActiveFilters(newFilters);
}, [flt]);
return (
<Paper p="sm" shadow="sm" radius="xs">
<Group position="apart" key={flt.name}>
<Stack spacing="xs">
<Text size="sm">{flt.label}</Text>
<Text size="xs">{flt.description}</Text>
</Stack>
<Group position="right">
<Badge>{flt.displayValue ?? flt.value}</Badge>
<Tooltip label={t`Remove filter`} withinPortal={true}>
<CloseButton size="md" color="red" onClick={removeFilter} />
</Tooltip>
</Group>
</Group>
</Paper>
);
}
interface FilterProps extends React.ComponentPropsWithoutRef<'div'> {
name: string;
label: string;
description?: string;
}
/*
* Custom component for the filter select
*/
const FilterSelectItem = forwardRef<HTMLDivElement, FilterProps>(
({ label, description, ...others }, ref) => (
<div ref={ref} {...others}>
<Text size="sm">{label}</Text>
<Text size="xs">{description}</Text>
</div>
)
);
function FilterAddGroup({
tableState,
availableFilters
}: {
tableState: TableState;
availableFilters: TableFilter[];
}) {
const filterOptions = useMemo(() => {
let activeFilterNames = tableState.activeFilters.map((flt) => flt.name);
return availableFilters
.filter((flt) => !activeFilterNames.includes(flt.name))
.map((flt) => ({
value: flt.name,
label: flt.label,
description: flt.description
}));
}, [tableState.activeFilters, availableFilters]);
const [selectedFilter, setSelectedFilter] = useState<string | null>(null);
const valueOptions: TableFilterChoice[] = useMemo(() => {
// Find the matching filter
let filter: TableFilter | undefined = availableFilters.find(
(flt) => flt.name === selectedFilter
);
if (!filter) {
return [];
}
return getTableFilterOptions(filter);
}, [selectedFilter]);
const setSelectedValue = useCallback(
(value: string | null) => {
// Find the matching filter
let filter: TableFilter | undefined = availableFilters.find(
(flt) => flt.name === selectedFilter
);
if (!filter) {
return;
}
let filters = tableState.activeFilters.filter(
(flt) => flt.name !== selectedFilter
);
let newFilter: TableFilter = {
...filter,
value: value,
displayValue: valueOptions.find((v) => v.value === value)?.label
};
tableState.setActiveFilters([...filters, newFilter]);
},
[selectedFilter]
);
return (
<Stack spacing="xs">
<Divider />
<Select
data={filterOptions}
itemComponent={FilterSelectItem}
searchable={true}
placeholder={t`Select filter`}
label={t`Filter`}
onChange={(value: string | null) => setSelectedFilter(value)}
maxDropdownHeight={800}
/>
{selectedFilter && (
<Select
data={valueOptions}
label={t`Value`}
placeholder={t`Select filter value`}
onChange={(value: string | null) => setSelectedValue(value)}
maxDropdownHeight={800}
/>
)}
</Stack>
);
}
export function FilterSelectDrawer({
availableFilters,
tableState,
opened,
onClose
}: {
availableFilters: TableFilter[];
tableState: TableState;
opened: boolean;
onClose: () => void;
}) {
const [addFilter, setAddFilter] = useState<boolean>(false);
// Hide the "add filter" selection whenever the selected filters change
useEffect(() => {
setAddFilter(false);
}, [tableState.activeFilters]);
return (
<Drawer
size="sm"
position="right"
withCloseButton={true}
opened={opened}
onClose={onClose}
title={<StylishText size="lg">{t`Table Filters`}</StylishText>}
>
<Stack spacing="xs">
{tableState.activeFilters.map((f) => (
<FilterItem flt={f} tableState={tableState} />
))}
{tableState.activeFilters.length > 0 && <Divider />}
{addFilter && (
<Stack spacing="xs">
<FilterAddGroup
tableState={tableState}
availableFilters={availableFilters}
/>
</Stack>
)}
{addFilter && (
<Button
onClick={() => setAddFilter(false)}
color="orange"
variant="subtle"
>
<Text>{t`Cancel`}</Text>
</Button>
)}
{!addFilter &&
tableState.activeFilters.length < availableFilters.length && (
<Button
onClick={() => setAddFilter(true)}
color="green"
variant="subtle"
>
<Text>{t`Add Filter`}</Text>
</Button>
)}
{!addFilter && tableState.activeFilters.length > 0 && (
<Button
onClick={tableState.clearActiveFilters}
color="red"
variant="subtle"
>
<Text>{t`Clear Filters`}</Text>
</Button>
)}
</Stack>
</Drawer>
);
}

View File

@ -1,178 +0,0 @@
import { t } from '@lingui/macro';
import { Modal } from '@mantine/core';
import { Select } from '@mantine/core';
import { Stack } from '@mantine/core';
import { Button, Group, Text } from '@mantine/core';
import { forwardRef, useMemo, useState } from 'react';
import { TableFilter, TableFilterChoice } from './Filter';
/**
* Construct the selection of filters
*/
function constructAvailableFilters(
activeFilters: TableFilter[],
availableFilters: TableFilter[]
) {
// Collect a list of active filters
let activeFilterNames = activeFilters.map((flt) => flt.name);
let options = availableFilters
.filter((flt) => !activeFilterNames.includes(flt.name))
.map((flt) => ({
value: flt.name,
label: flt.label,
description: flt.description
}));
return options;
}
/**
* Construct the selection of available values for the selected filter
*/
function constructValueOptions(
availableFilters: TableFilter[],
selectedFilter: string | null
) {
// No options if no filter is selected
if (!selectedFilter) {
return [];
}
let filter = availableFilters.find((flt) => flt.name === selectedFilter);
if (!filter) {
console.error(`Could not find filter ${selectedFilter}`);
return [];
}
let options: TableFilterChoice[] = [];
switch (filter.type) {
case 'boolean':
// Boolean filter values True / False
options = [
{ value: 'true', label: t`True` },
{ value: 'false', label: t`False` }
];
break;
default:
// Choices are supplied by the filter definition
if (filter.choices) {
options = filter.choices;
} else if (filter.choiceFunction) {
options = filter.choiceFunction();
} else {
console.error(`Filter choices not supplied for filter ${filter.name}`);
}
break;
}
return options;
}
interface FilterProps extends React.ComponentPropsWithoutRef<'div'> {
name: string;
label: string;
description?: string;
}
/*
* Custom component for the filter select
*/
const FilterSelectItem = forwardRef<HTMLDivElement, FilterProps>(
({ name, label, description, ...others }, ref) => (
<div ref={ref} {...others}>
<Text size="sm">{label}</Text>
<Text size="xs">{description}</Text>
</div>
)
);
/**
* Modal dialog to add a} new filter for a particular table
* @param opened : boolean - Whether the modal is opened or not
* @param onClose : () => void - Function called when the modal is closed
* @returns
*/
export function FilterSelectModal({
availableFilters,
activeFilters,
opened,
onCreateFilter,
onClose
}: {
availableFilters: TableFilter[];
activeFilters: TableFilter[];
opened: boolean;
onCreateFilter: (name: string, value: string) => void;
onClose: () => void;
}) {
let filterOptions = useMemo(
() => constructAvailableFilters(activeFilters, availableFilters),
[activeFilters, availableFilters]
);
// Internal state variable for the selected filter
let [selectedFilter, setSelectedFilter] = useState<string | null>(null);
// Internal state variable for the selected filter value
let [value, setValue] = useState<string | null>(null);
let valueOptions = useMemo(
() => constructValueOptions(availableFilters, selectedFilter),
[availableFilters, activeFilters, selectedFilter]
);
// Callback when the modal is closed. Ensure that the internal state is reset
function closeModal() {
setSelectedFilter(null);
setValue(null);
onClose();
}
function createFilter() {
if (selectedFilter && value) {
onCreateFilter(selectedFilter, value);
}
closeModal();
}
return (
<Modal title={t`Add Table Filter`} opened={opened} onClose={closeModal}>
<Stack>
<Text>{t`Select from the available filters`}</Text>
<Select
data={filterOptions}
itemComponent={FilterSelectItem}
label={t`Filter`}
placeholder={t`Select filter`}
searchable={true}
onChange={(value) => setSelectedFilter(value)}
withinPortal={true}
maxDropdownHeight={400}
/>
<Select
data={valueOptions}
disabled={valueOptions.length == 0}
label={t`Value`}
placeholder={t`Select filter value`}
onChange={(value) => setValue(value)}
withinPortal={true}
maxDropdownHeight={400}
/>
<Group position="right">
<Button color="red" onClick={closeModal}>{t`Cancel`}</Button>
<Button
color="green"
onClick={createFilter}
disabled={!(selectedFilter && value)}
>
{t`Add Filter`}
</Button>
</Group>
</Stack>
</Modal>
);
}

View File

@ -15,8 +15,7 @@ import { TableColumn } from './Column';
import { TableColumnSelect } from './ColumnSelect';
import { DownloadAction } from './DownloadAction';
import { TableFilter } from './Filter';
import { FilterGroup } from './FilterGroup';
import { FilterSelectModal } from './FilterSelectModal';
import { FilterSelectDrawer } from './FilterSelectDrawer';
import { RowAction, RowActions } from './RowActions';
import { TableSearchInput } from './Search';
@ -126,13 +125,6 @@ export function InvenTreeTable<T = any>({
defaultValue: []
});
// Active filters (saved to local storage)
const [activeFilters, setActiveFilters] = useLocalStorage<any[]>({
key: `inventree-active-table-filters-${tableName}`,
defaultValue: [],
getInitialValueInEffect: false
});
// Data selection
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
@ -198,50 +190,12 @@ export function InvenTreeTable<T = any>({
);
}
// Filter selection open state
const [filterSelectOpen, setFilterSelectOpen] = useState<boolean>(false);
// Pagination
const [page, setPage] = useState(1);
// Filter list visibility
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
/*
* Callback for the "add filter" button.
* Launches a modal dialog to add a new filter
*/
function onFilterAdd(name: string, value: string) {
let filters = [...activeFilters];
let newFilter = tableProps.customFilters?.find((flt) => flt.name == name);
if (newFilter) {
filters.push({
...newFilter,
value: value
});
setActiveFilters(filters);
}
}
/*
* Callback function when a specified filter is removed from the table
*/
function onFilterRemove(filterName: string) {
let filters = activeFilters.filter((flt) => flt.name != filterName);
setActiveFilters(filters);
}
/*
* Callback function when all custom filters are removed from the table
*/
function onFilterClearAll() {
setActiveFilters([]);
}
// Search term
const [searchTerm, setSearchTerm] = useState<string>('');
@ -259,7 +213,9 @@ export function InvenTreeTable<T = any>({
};
// Add custom filters
activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value));
tableState.activeFilters.forEach(
(flt) => (queryParams[flt.name] = flt.value)
);
// Add custom search term
if (searchTerm) {
@ -398,11 +354,12 @@ export function InvenTreeTable<T = any>({
const { data, isError, isFetching, isLoading, refetch } = useQuery({
queryKey: [
`table-${tableName}`,
tableState.tableKey,
props.params,
sortStatus.columnAccessor,
sortStatus.direction,
page,
activeFilters,
tableState.activeFilters,
searchTerm
],
queryFn: fetchTableData,
@ -412,23 +369,17 @@ export function InvenTreeTable<T = any>({
const [recordCount, setRecordCount] = useState<number>(0);
/*
* Reload the table whenever the tableKey changes
* this allows us to programmatically refresh the table
*/
useEffect(() => {
refetch();
}, [tableState?.tableKey, props.params]);
return (
<>
<FilterSelectModal
availableFilters={tableProps.customFilters ?? []}
activeFilters={activeFilters}
opened={filterSelectOpen}
onCreateFilter={onFilterAdd}
onClose={() => setFilterSelectOpen(false)}
/>
{tableProps.enableFilters &&
(tableProps.customFilters?.length ?? 0) > 0 && (
<FilterSelectDrawer
availableFilters={tableProps.customFilters ?? []}
tableState={tableState}
opened={filtersVisible}
onClose={() => setFiltersVisible(false)}
/>
)}
<Stack spacing="sm">
<Group position="apart">
<Group position="left" key="custom-actions" spacing={5}>
@ -478,8 +429,8 @@ export function InvenTreeTable<T = any>({
(tableProps.customFilters?.length ?? 0 > 0) && (
<Indicator
size="xs"
label={activeFilters.length}
disabled={activeFilters.length == 0}
label={tableState.activeFilters.length}
disabled={tableState.activeFilters.length == 0}
>
<ActionIcon>
<Tooltip label={t`Table filters`}>
@ -498,14 +449,6 @@ export function InvenTreeTable<T = any>({
)}
</Group>
</Group>
{filtersVisible && (
<FilterGroup
activeFilters={activeFilters}
onFilterAdd={() => setFilterSelectOpen(true)}
onFilterRemove={onFilterRemove}
onFilterClearAll={onFilterClearAll}
/>
)}
<DataTable
withBorder
striped

View File

@ -238,12 +238,51 @@ export function BomTable({
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'sub_part_trackable',
label: t`Trackable Part`,
description: t`Show trackable items`
},
{
name: 'sub_part_assembly',
label: t`Assembled Part`,
description: t`Show asssmbled items`
},
{
name: 'available_stock',
label: t`Has Available Stock`,
description: t`Show items with available stock`
},
{
name: 'on_order',
label: t`On Order`,
description: t`Show items on order`
},
{
name: 'validated',
label: t`Validated`,
description: t`Show validated items`
},
{
name: 'inherited',
label: t`Gets Inherited`,
description: t`Show inherited items`
},
{
name: 'optional',
label: t`Optional`,
description: t`Show optional items`
},
{
name: 'consumable',
label: t`Consumable`,
type: 'boolean'
description: t`Show consumable items`
},
{
name: 'has_pricing',
label: t`Has Pricing`,
description: t`Show items with pricing`
}
// TODO: More BOM table filters here
];
}, [partId, params]);

View File

@ -80,7 +80,28 @@ export function UsedInTable({
}, [partId]);
const tableFilters: TableFilter[] = useMemo(() => {
return [];
return [
{
name: 'inherited',
label: t`Gets Inherited`,
description: t`Show inherited items`
},
{
name: 'optional',
label: t`Optional`,
description: t`Show optional items`
},
{
name: 'part_active',
label: t`Active`,
description: t`Show active assemblies`
},
{
name: 'part_trackable',
label: t`Trackable`,
description: t`Show trackable assemblies`
}
];
}, [partId]);
return (

View File

@ -18,6 +18,7 @@ import {
StatusColumn,
TargetDateColumn
} from '../ColumnRenderers';
import { StatusFilterOptions, TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -101,26 +102,39 @@ function buildOrderTableColumns(): TableColumn[] {
export function BuildOrderTable({ params = {} }: { params?: any }) {
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
const tableFilters = useMemo(() => {
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
// TODO: Filter by status code
name: 'active',
type: 'boolean',
label: t`Active`
label: t`Active`,
description: t`Show active orders`
},
{
name: 'status',
label: t`Status`,
description: t`Filter by order status`,
choiceFunction: StatusFilterOptions(ModelType.build)
},
{
name: 'overdue',
type: 'boolean',
label: t`Overdue`
label: t`Overdue`,
description: t`Show overdue status`
},
{
name: 'assigned_to_me',
type: 'boolean',
label: t`Assigned to me`
label: t`Assigned to me`,
description: t`Show orders assigned to me`
}
// 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)
];

View File

@ -5,8 +5,10 @@ import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -31,6 +33,14 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
title: t`Path`,
sortable: false
},
{
accessor: 'structural',
title: t`Structural`,
sortable: true,
render: (record: any) => {
return <YesNoButton value={record.structural} />;
}
},
{
accessor: 'part_count',
title: t`Parts`,
@ -39,6 +49,21 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
];
}, []);
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'cascade',
label: t`Include Subcategories`,
description: t`Include subcategories in results`
},
{
name: 'structural',
label: t`Structural`,
description: t`Show structural categories`
}
];
}, []);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.category_list)}
@ -50,6 +75,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
params: {
...params
},
customFilters: tableFilters,
onRowClick: (record, index, event) => {
navigate(`/part/category/${record.pk}`);
}

View File

@ -14,6 +14,7 @@ import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
@ -22,6 +23,26 @@ export default function PartParameterTemplateTable() {
const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'checkbox',
label: t`Checkbox`,
description: t`Show checkbox templates`
},
{
name: 'has_choices',
label: t`Has choices`,
description: t`Show templates with choices`
},
{
name: 'has_units',
label: t`Has Units`,
description: t`Show templates with units`
}
];
}, []);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
@ -113,6 +134,7 @@ export default function PartParameterTemplateTable() {
columns={tableColumns}
props={{
rowActions: rowActions,
customFilters: tableFilters,
customActionGroups: tableActions
}}
/>

View File

@ -1,14 +1,43 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { TableFilter } from '../Filter';
import { PartListTable } from './PartTable';
/**
* Display variant parts for the specified parent part
*/
export function PartVariantTable({ partId }: { partId: string }) {
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'active',
label: t`Active`,
description: t`Show active variants`
},
{
name: 'template',
label: t`Template`,
description: t`Show template variants`
},
{
name: 'virtual',
label: t`Virtual`,
description: t`Show virtual variants`
},
{
name: 'trackable',
label: t`Trackable`,
description: t`Show trackable variants`
}
];
}, []);
return (
<PartListTable
props={{
enableDownload: false,
customFilters: [],
customFilters: tableFilters,
params: {
ancestor: partId
}

View File

@ -17,6 +17,13 @@ import {
TargetDateColumn,
TotalPriceColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
TableFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -27,7 +34,21 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
const table = useTable('purchase-order');
// TODO: Custom filters
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'status',
label: t`Status`,
description: t`Filter by order status`,
choiceFunction: StatusFilterOptions(ModelType.purchaseorder)
},
OutstandingFilter(),
OverdueFilter(),
AssignedToMeFilter()
// TODO: has_project_code
// TODO: project_code
];
}, []);
// TODO: Row actions
@ -83,6 +104,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
...params,
supplier_detail: true
},
customFilters: tableFilters,
onRowClick: (row: any) => {
if (row.pk) {
navigate(`/purchasing/purchase-order/${row.pk}`);

View File

@ -16,6 +16,13 @@ import {
StatusColumn,
TargetDateColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
TableFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) {
@ -23,7 +30,19 @@ export function ReturnOrderTable({ params }: { params?: any }) {
const navigate = useNavigate();
// TODO: Custom filters
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'status',
label: t`Status`,
description: t`Filter by order status`,
choiceFunction: StatusFilterOptions(ModelType.returnorder)
},
OutstandingFilter(),
OverdueFilter(),
AssignedToMeFilter()
];
}, []);
// TODO: Row actions
@ -81,6 +100,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
...params,
customer_detail: true
},
customFilters: tableFilters,
onRowClick: (row: any) => {
if (row.pk) {
navigate(`/sales/return-order/${row.pk}/`);

View File

@ -17,6 +17,13 @@ import {
TargetDateColumn,
TotalPriceColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
TableFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
export function SalesOrderTable({ params }: { params?: any }) {
@ -24,7 +31,21 @@ export function SalesOrderTable({ params }: { params?: any }) {
const navigate = useNavigate();
// TODO: Custom filters
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'status',
label: t`Status`,
description: t`Filter by order status`,
choiceFunction: StatusFilterOptions(ModelType.salesorder)
},
OutstandingFilter(),
OverdueFilter(),
AssignedToMeFilter()
// TODO: has_project_code
// TODO: project_code
];
}, []);
// TODO: Row actions
@ -80,6 +101,7 @@ export function SalesOrderTable({ params }: { params?: any }) {
...params,
customer_detail: true
},
customFilters: tableFilters,
onRowClick: (row: any) => {
if (row.pk) {
navigate(`/sales/sales-order/${row.pk}/`);

View File

@ -11,7 +11,7 @@ import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { StatusColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { StatusFilterOptions, TableFilter } from '../Filter';
import { TableHoverCard } from '../TableHoverCard';
import { InvenTreeTable } from './../InvenTreeTable';
@ -243,15 +243,98 @@ function stockItemTableColumns(): TableColumn[] {
function stockItemTableFilters(): TableFilter[] {
return [
{
name: 'test_filter',
label: t`Test Filter`,
description: t`This is a test filter`,
type: 'choice',
choiceFunction: () => [
{ value: '1', label: 'One' },
{ value: '2', label: 'Two' },
{ value: '3', label: 'Three' }
]
name: 'active',
label: t`Active`,
description: t`Show stock for active parts`
},
{
name: 'status',
label: t`Status`,
description: t`Filter by stock status`,
choiceFunction: StatusFilterOptions(ModelType.stockitem)
},
{
name: 'assembly',
label: t`Assembly`,
description: t`Show stock for assmebled parts`
},
{
name: 'allocated',
label: t`Allocated`,
description: t`Show items which have been allocated`
},
{
name: 'available',
label: t`Available`,
description: t`Show items which are available`
},
{
name: 'cascade',
label: t`Include Sublocations`,
description: t`Include stock in sublocations`
},
{
name: 'depleted',
label: t`Depleted`,
description: t`Show depleted stock items`
},
{
name: 'in_stock',
label: t`In Stock`,
description: t`Show items which are in stock`
},
{
name: 'is_building',
label: t`In Production`,
description: t`Show items which are in production`
},
{
name: 'include_variants',
label: t`Include Variants`,
description: t`Include stock items for variant parts`
},
{
name: 'installed',
label: t`Installed`,
description: t`Show stock items which are installed in other items`
},
{
name: 'sent_to_customer',
label: t`Sent to Customer`,
description: t`Show items which have been sent to a customer`
},
{
name: 'serialized',
label: t`Is Serialized`,
description: t`Show items which have a serial number`
},
// TODO: serial
// TODO: serial_gte
// TODO: serial_lte
{
name: 'has_batch',
label: t`Has Batch Code`,
description: t`Show items which have a batch code`
},
// TODO: batch
{
name: 'tracked',
label: t`Tracked`,
description: t`Show tracked items`
},
{
name: 'has_purchase_price',
label: t`Has Purchase Price`,
description: t`Show items which have a purchase price`
},
// TODO: Expired
// TODO: stale
// TODO: expiry_date_lte
// TODO: expiry_date_gte
{
name: 'external',
label: t`External Location`,
description: t`Show items in an external location`
}
];
}

View File

@ -8,6 +8,7 @@ import { apiUrl } from '../../../states/ApiState';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
/**
@ -18,6 +19,31 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'cascade',
label: t`Include Sublocations`,
description: t`Include sublocations in results`
},
{
name: 'structural',
label: t`Structural`,
description: t`Show structural locations`
},
{
name: 'external',
label: t`External`,
description: t`Show external locations`
},
{
name: 'has_location_type',
label: t`Has location type`
}
// TODO: location_type
];
}, []);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
@ -69,6 +95,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
props={{
enableDownload: true,
params: params,
customFilters: tableFilters,
onRowClick: (record) => {
navigate(`/stock/location/${record.pk}`);
}

View File

@ -1,17 +1,27 @@
import { randomId } from '@mantine/hooks';
import { randomId, useLocalStorage } from '@mantine/hooks';
import { useCallback, useState } from 'react';
import { TableFilter } from '../components/tables/Filter';
/*
* Type definition for representing the state of a table:
*
* tableKey: A unique key for the table. When this key changes, the table will be refreshed.
* refreshTable: A callback function to externally refresh the table.
* activeFilters: An array of active filters (saved to local storage)
*/
export type TableState = {
tableKey: string;
activeFilters: TableFilter[];
setActiveFilters: (filters: TableFilter[]) => void;
clearActiveFilters: () => void;
refreshTable: () => void;
};
/**
* A custom hook for managing the state of an <InvenTreeTable> component.
*
* tableKey: A unique key for the table. When this key changes, the table will be refreshed.
* refreshTable: A callback function to externally refresh the table.
*
* Refer to the TableState type definition for more information.
*/
export function useTable(tableName: string): TableState {
@ -27,8 +37,23 @@ export function useTable(tableName: string): TableState {
setTableKey(generateTableName());
}, []);
// Array of active filters (saved to local storage)
const [activeFilters, setActiveFilters] = useLocalStorage<TableFilter[]>({
key: `inventree-table-filters-${tableName}`,
defaultValue: [],
getInitialValueInEffect: false
});
// Callback to clear all active filters from the table
const clearActiveFilters = useCallback(() => {
setActiveFilters([]);
}, []);
return {
tableKey,
activeFilters,
setActiveFilters,
clearActiveFilters,
refreshTable
};
}