mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
3a7b1510b3
commit
048a06ce19
@ -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')
|
||||
|
@ -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`
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
234
src/frontend/src/components/tables/FilterSelectDrawer.tsx
Normal file
234
src/frontend/src/components/tables/FilterSelectDrawer.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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)
|
||||
];
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}`);
|
||||
|
@ -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}/`);
|
||||
|
@ -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}/`);
|
||||
|
@ -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`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user