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):
|
def filter_has_units(self, queryset, name, value):
|
||||||
"""Filter by whether the Part has units or not"""
|
"""Filter by whether the Part has units or not"""
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(units='')
|
return queryset.exclude(Q(units=None) | Q(units=''))
|
||||||
return queryset.filter(units='')
|
|
||||||
|
return queryset.filter(Q(units=None) | Q(units='')).distinct()
|
||||||
|
|
||||||
# Filter by parts which have (or not) an IPN value
|
# Filter by parts which have (or not) an IPN value
|
||||||
has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn')
|
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
|
* 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 = {
|
export type TableFilter = {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
type: string;
|
type?: string;
|
||||||
choices?: TableFilterChoice[];
|
choices?: TableFilterChoice[];
|
||||||
choiceFunction?: () => TableFilterChoice[];
|
choiceFunction?: () => TableFilterChoice[];
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
value?: 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 { TableColumnSelect } from './ColumnSelect';
|
||||||
import { DownloadAction } from './DownloadAction';
|
import { DownloadAction } from './DownloadAction';
|
||||||
import { TableFilter } from './Filter';
|
import { TableFilter } from './Filter';
|
||||||
import { FilterGroup } from './FilterGroup';
|
import { FilterSelectDrawer } from './FilterSelectDrawer';
|
||||||
import { FilterSelectModal } from './FilterSelectModal';
|
|
||||||
import { RowAction, RowActions } from './RowActions';
|
import { RowAction, RowActions } from './RowActions';
|
||||||
import { TableSearchInput } from './Search';
|
import { TableSearchInput } from './Search';
|
||||||
|
|
||||||
@ -126,13 +125,6 @@ export function InvenTreeTable<T = any>({
|
|||||||
defaultValue: []
|
defaultValue: []
|
||||||
});
|
});
|
||||||
|
|
||||||
// Active filters (saved to local storage)
|
|
||||||
const [activeFilters, setActiveFilters] = useLocalStorage<any[]>({
|
|
||||||
key: `inventree-active-table-filters-${tableName}`,
|
|
||||||
defaultValue: [],
|
|
||||||
getInitialValueInEffect: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Data selection
|
// Data selection
|
||||||
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
|
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
|
// Pagination
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
// Filter list visibility
|
// Filter list visibility
|
||||||
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
|
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
|
// Search term
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
|
||||||
@ -259,7 +213,9 @@ export function InvenTreeTable<T = any>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add custom filters
|
// Add custom filters
|
||||||
activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value));
|
tableState.activeFilters.forEach(
|
||||||
|
(flt) => (queryParams[flt.name] = flt.value)
|
||||||
|
);
|
||||||
|
|
||||||
// Add custom search term
|
// Add custom search term
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
@ -398,11 +354,12 @@ export function InvenTreeTable<T = any>({
|
|||||||
|
|
||||||
const { data, isError, isFetching, isLoading, refetch } = useQuery({
|
const { data, isError, isFetching, isLoading, refetch } = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
`table-${tableName}`,
|
tableState.tableKey,
|
||||||
|
props.params,
|
||||||
sortStatus.columnAccessor,
|
sortStatus.columnAccessor,
|
||||||
sortStatus.direction,
|
sortStatus.direction,
|
||||||
page,
|
page,
|
||||||
activeFilters,
|
tableState.activeFilters,
|
||||||
searchTerm
|
searchTerm
|
||||||
],
|
],
|
||||||
queryFn: fetchTableData,
|
queryFn: fetchTableData,
|
||||||
@ -412,23 +369,17 @@ export function InvenTreeTable<T = any>({
|
|||||||
|
|
||||||
const [recordCount, setRecordCount] = useState<number>(0);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<FilterSelectModal
|
{tableProps.enableFilters &&
|
||||||
availableFilters={tableProps.customFilters ?? []}
|
(tableProps.customFilters?.length ?? 0) > 0 && (
|
||||||
activeFilters={activeFilters}
|
<FilterSelectDrawer
|
||||||
opened={filterSelectOpen}
|
availableFilters={tableProps.customFilters ?? []}
|
||||||
onCreateFilter={onFilterAdd}
|
tableState={tableState}
|
||||||
onClose={() => setFilterSelectOpen(false)}
|
opened={filtersVisible}
|
||||||
/>
|
onClose={() => setFiltersVisible(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Stack spacing="sm">
|
<Stack spacing="sm">
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group position="left" key="custom-actions" spacing={5}>
|
<Group position="left" key="custom-actions" spacing={5}>
|
||||||
@ -478,8 +429,8 @@ export function InvenTreeTable<T = any>({
|
|||||||
(tableProps.customFilters?.length ?? 0 > 0) && (
|
(tableProps.customFilters?.length ?? 0 > 0) && (
|
||||||
<Indicator
|
<Indicator
|
||||||
size="xs"
|
size="xs"
|
||||||
label={activeFilters.length}
|
label={tableState.activeFilters.length}
|
||||||
disabled={activeFilters.length == 0}
|
disabled={tableState.activeFilters.length == 0}
|
||||||
>
|
>
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<Tooltip label={t`Table filters`}>
|
<Tooltip label={t`Table filters`}>
|
||||||
@ -498,14 +449,6 @@ export function InvenTreeTable<T = any>({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
{filtersVisible && (
|
|
||||||
<FilterGroup
|
|
||||||
activeFilters={activeFilters}
|
|
||||||
onFilterAdd={() => setFilterSelectOpen(true)}
|
|
||||||
onFilterRemove={onFilterRemove}
|
|
||||||
onFilterClearAll={onFilterClearAll}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<DataTable
|
<DataTable
|
||||||
withBorder
|
withBorder
|
||||||
striped
|
striped
|
||||||
|
@ -238,12 +238,51 @@ export function BomTable({
|
|||||||
|
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [
|
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',
|
name: 'consumable',
|
||||||
label: t`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]);
|
}, [partId, params]);
|
||||||
|
|
||||||
|
@ -80,7 +80,28 @@ export function UsedInTable({
|
|||||||
}, [partId]);
|
}, [partId]);
|
||||||
|
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
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]);
|
}, [partId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
StatusColumn,
|
StatusColumn,
|
||||||
TargetDateColumn
|
TargetDateColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
|
import { StatusFilterOptions, TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,26 +102,39 @@ function buildOrderTableColumns(): TableColumn[] {
|
|||||||
export function BuildOrderTable({ params = {} }: { params?: any }) {
|
export function BuildOrderTable({ params = {} }: { params?: any }) {
|
||||||
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
||||||
|
|
||||||
const tableFilters = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
// TODO: Filter by status code
|
|
||||||
name: 'active',
|
name: 'active',
|
||||||
type: 'boolean',
|
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',
|
name: 'overdue',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: t`Overdue`
|
label: t`Overdue`,
|
||||||
|
description: t`Show overdue status`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'assigned_to_me',
|
name: 'assigned_to_me',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: t`Assigned to me`
|
label: t`Assigned to me`,
|
||||||
|
description: t`Show orders assigned to me`
|
||||||
}
|
}
|
||||||
// TODO: 'assigned to' filter
|
// TODO: 'assigned to' filter
|
||||||
// TODO: 'issued by' 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: 'has project code' filter (see table_filters.js)
|
||||||
// TODO: '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 { ApiPaths } from '../../../enums/ApiEndpoints';
|
||||||
import { useTable } from '../../../hooks/UseTable';
|
import { useTable } from '../../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../../states/ApiState';
|
import { apiUrl } from '../../../states/ApiState';
|
||||||
|
import { YesNoButton } from '../../items/YesNoButton';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { DescriptionColumn } from '../ColumnRenderers';
|
import { DescriptionColumn } from '../ColumnRenderers';
|
||||||
|
import { TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +33,14 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
|
|||||||
title: t`Path`,
|
title: t`Path`,
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessor: 'structural',
|
||||||
|
title: t`Structural`,
|
||||||
|
sortable: true,
|
||||||
|
render: (record: any) => {
|
||||||
|
return <YesNoButton value={record.structural} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_count',
|
accessor: 'part_count',
|
||||||
title: t`Parts`,
|
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 (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiPaths.category_list)}
|
url={apiUrl(ApiPaths.category_list)}
|
||||||
@ -50,6 +75,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
|
|||||||
params: {
|
params: {
|
||||||
...params
|
...params
|
||||||
},
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
onRowClick: (record, index, event) => {
|
onRowClick: (record, index, event) => {
|
||||||
navigate(`/part/category/${record.pk}`);
|
navigate(`/part/category/${record.pk}`);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import { apiUrl } from '../../../states/ApiState';
|
|||||||
import { useUserState } from '../../../states/UserState';
|
import { useUserState } from '../../../states/UserState';
|
||||||
import { AddItemButton } from '../../buttons/AddItemButton';
|
import { AddItemButton } from '../../buttons/AddItemButton';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
|
import { TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { RowDeleteAction, RowEditAction } from '../RowActions';
|
import { RowDeleteAction, RowEditAction } from '../RowActions';
|
||||||
|
|
||||||
@ -22,6 +23,26 @@ export default function PartParameterTemplateTable() {
|
|||||||
|
|
||||||
const user = useUserState();
|
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(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -113,6 +134,7 @@ export default function PartParameterTemplateTable() {
|
|||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
props={{
|
props={{
|
||||||
rowActions: rowActions,
|
rowActions: rowActions,
|
||||||
|
customFilters: tableFilters,
|
||||||
customActionGroups: tableActions
|
customActionGroups: tableActions
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,14 +1,43 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { TableFilter } from '../Filter';
|
||||||
import { PartListTable } from './PartTable';
|
import { PartListTable } from './PartTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display variant parts for the specified parent part
|
* Display variant parts for the specified parent part
|
||||||
*/
|
*/
|
||||||
export function PartVariantTable({ partId }: { partId: string }) {
|
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 (
|
return (
|
||||||
<PartListTable
|
<PartListTable
|
||||||
props={{
|
props={{
|
||||||
enableDownload: false,
|
enableDownload: false,
|
||||||
customFilters: [],
|
customFilters: tableFilters,
|
||||||
params: {
|
params: {
|
||||||
ancestor: partId
|
ancestor: partId
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,13 @@ import {
|
|||||||
TargetDateColumn,
|
TargetDateColumn,
|
||||||
TotalPriceColumn
|
TotalPriceColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
|
import {
|
||||||
|
AssignedToMeFilter,
|
||||||
|
OutstandingFilter,
|
||||||
|
OverdueFilter,
|
||||||
|
StatusFilterOptions,
|
||||||
|
TableFilter
|
||||||
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,7 +34,21 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
|
|||||||
|
|
||||||
const table = useTable('purchase-order');
|
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
|
// TODO: Row actions
|
||||||
|
|
||||||
@ -83,6 +104,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
|
|||||||
...params,
|
...params,
|
||||||
supplier_detail: true
|
supplier_detail: true
|
||||||
},
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
onRowClick: (row: any) => {
|
onRowClick: (row: any) => {
|
||||||
if (row.pk) {
|
if (row.pk) {
|
||||||
navigate(`/purchasing/purchase-order/${row.pk}`);
|
navigate(`/purchasing/purchase-order/${row.pk}`);
|
||||||
|
@ -16,6 +16,13 @@ import {
|
|||||||
StatusColumn,
|
StatusColumn,
|
||||||
TargetDateColumn
|
TargetDateColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
|
import {
|
||||||
|
AssignedToMeFilter,
|
||||||
|
OutstandingFilter,
|
||||||
|
OverdueFilter,
|
||||||
|
StatusFilterOptions,
|
||||||
|
TableFilter
|
||||||
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
export function ReturnOrderTable({ params }: { params?: any }) {
|
export function ReturnOrderTable({ params }: { params?: any }) {
|
||||||
@ -23,7 +30,19 @@ export function ReturnOrderTable({ params }: { params?: any }) {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
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
|
// TODO: Row actions
|
||||||
|
|
||||||
@ -81,6 +100,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
|
|||||||
...params,
|
...params,
|
||||||
customer_detail: true
|
customer_detail: true
|
||||||
},
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
onRowClick: (row: any) => {
|
onRowClick: (row: any) => {
|
||||||
if (row.pk) {
|
if (row.pk) {
|
||||||
navigate(`/sales/return-order/${row.pk}/`);
|
navigate(`/sales/return-order/${row.pk}/`);
|
||||||
|
@ -17,6 +17,13 @@ import {
|
|||||||
TargetDateColumn,
|
TargetDateColumn,
|
||||||
TotalPriceColumn
|
TotalPriceColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
|
import {
|
||||||
|
AssignedToMeFilter,
|
||||||
|
OutstandingFilter,
|
||||||
|
OverdueFilter,
|
||||||
|
StatusFilterOptions,
|
||||||
|
TableFilter
|
||||||
|
} from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
export function SalesOrderTable({ params }: { params?: any }) {
|
export function SalesOrderTable({ params }: { params?: any }) {
|
||||||
@ -24,7 +31,21 @@ export function SalesOrderTable({ params }: { params?: any }) {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
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
|
// TODO: Row actions
|
||||||
|
|
||||||
@ -80,6 +101,7 @@ export function SalesOrderTable({ params }: { params?: any }) {
|
|||||||
...params,
|
...params,
|
||||||
customer_detail: true
|
customer_detail: true
|
||||||
},
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
onRowClick: (row: any) => {
|
onRowClick: (row: any) => {
|
||||||
if (row.pk) {
|
if (row.pk) {
|
||||||
navigate(`/sales/sales-order/${row.pk}/`);
|
navigate(`/sales/sales-order/${row.pk}/`);
|
||||||
|
@ -11,7 +11,7 @@ import { apiUrl } from '../../../states/ApiState';
|
|||||||
import { Thumbnail } from '../../images/Thumbnail';
|
import { Thumbnail } from '../../images/Thumbnail';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { StatusColumn } from '../ColumnRenderers';
|
import { StatusColumn } from '../ColumnRenderers';
|
||||||
import { TableFilter } from '../Filter';
|
import { StatusFilterOptions, TableFilter } from '../Filter';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
import { InvenTreeTable } from './../InvenTreeTable';
|
import { InvenTreeTable } from './../InvenTreeTable';
|
||||||
|
|
||||||
@ -243,15 +243,98 @@ function stockItemTableColumns(): TableColumn[] {
|
|||||||
function stockItemTableFilters(): TableFilter[] {
|
function stockItemTableFilters(): TableFilter[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'test_filter',
|
name: 'active',
|
||||||
label: t`Test Filter`,
|
label: t`Active`,
|
||||||
description: t`This is a test filter`,
|
description: t`Show stock for active parts`
|
||||||
type: 'choice',
|
},
|
||||||
choiceFunction: () => [
|
{
|
||||||
{ value: '1', label: 'One' },
|
name: 'status',
|
||||||
{ value: '2', label: 'Two' },
|
label: t`Status`,
|
||||||
{ value: '3', label: 'Three' }
|
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 { YesNoButton } from '../../items/YesNoButton';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { DescriptionColumn } from '../ColumnRenderers';
|
import { DescriptionColumn } from '../ColumnRenderers';
|
||||||
|
import { TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,6 +19,31 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
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(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -69,6 +95,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
|
|||||||
props={{
|
props={{
|
||||||
enableDownload: true,
|
enableDownload: true,
|
||||||
params: params,
|
params: params,
|
||||||
|
customFilters: tableFilters,
|
||||||
onRowClick: (record) => {
|
onRowClick: (record) => {
|
||||||
navigate(`/stock/location/${record.pk}`);
|
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 { 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 = {
|
export type TableState = {
|
||||||
tableKey: string;
|
tableKey: string;
|
||||||
|
activeFilters: TableFilter[];
|
||||||
|
setActiveFilters: (filters: TableFilter[]) => void;
|
||||||
|
clearActiveFilters: () => void;
|
||||||
refreshTable: () => void;
|
refreshTable: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom hook for managing the state of an <InvenTreeTable> component.
|
* 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.
|
* Refer to the TableState type definition for more information.
|
||||||
* refreshTable: A callback function to externally refresh the table.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function useTable(tableName: string): TableState {
|
export function useTable(tableName: string): TableState {
|
||||||
@ -27,8 +37,23 @@ export function useTable(tableName: string): TableState {
|
|||||||
setTableKey(generateTableName());
|
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 {
|
return {
|
||||||
tableKey,
|
tableKey,
|
||||||
|
activeFilters,
|
||||||
|
setActiveFilters,
|
||||||
|
clearActiveFilters,
|
||||||
refreshTable
|
refreshTable
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user