diff --git a/src/catalogue/category/catalogueCardView.component.test.tsx b/src/catalogue/category/catalogueCardView.component.test.tsx
index a907729a2..c10e53cf1 100644
--- a/src/catalogue/category/catalogueCardView.component.test.tsx
+++ b/src/catalogue/category/catalogueCardView.component.test.tsx
@@ -169,4 +169,35 @@ describe('CardView', () => {
expect(clearFiltersButton).toBeDisabled();
});
+
+ it('renders the multi-select filter mode dropdown correctly', async () => {
+ createView();
+
+ await user.click(screen.getByText('Show Filters'));
+
+ const dropdownButtons = await screen.findAllByTestId('FilterListIcon');
+
+ expect(dropdownButtons[3]).toBeInTheDocument();
+
+ await user.click(dropdownButtons[3]);
+
+ const includeAnyText = await screen.findByRole('menuitem', {
+ name: 'Includes any',
+ });
+ const excludeAnyText = await screen.findByRole('menuitem', {
+ name: 'Excludes any',
+ });
+
+ const includeAllText = await screen.findByRole('menuitem', {
+ name: 'Includes all',
+ });
+ const excludeAllText = await screen.findByRole('menuitem', {
+ name: 'Excludes all',
+ });
+
+ expect(includeAnyText).toBeInTheDocument();
+ expect(excludeAnyText).toBeInTheDocument();
+ expect(includeAllText).toBeInTheDocument();
+ expect(excludeAllText).toBeInTheDocument();
+ });
});
diff --git a/src/catalogue/category/catalogueCardView.component.tsx b/src/catalogue/category/catalogueCardView.component.tsx
index 03faf884f..fca0edbfa 100644
--- a/src/catalogue/category/catalogueCardView.component.tsx
+++ b/src/catalogue/category/catalogueCardView.component.tsx
@@ -3,6 +3,7 @@ import {
Button,
Collapse,
Grid,
+ MenuItem,
Typography,
useMediaQuery,
useTheme,
@@ -17,7 +18,16 @@ import React from 'react';
import { CatalogueCategory } from '../../api/api.types';
import CardViewFilters from '../../common/cardView/cardViewFilters.component';
import { usePreservedTableState } from '../../common/preservedTableState.component';
-import { displayTableRowCountText, getPageHeightCalc } from '../../utils';
+import {
+ COLUMN_FILTER_FUNCTIONS,
+ COLUMN_FILTER_MODE_OPTIONS,
+ COLUMN_FILTER_VARIANTS,
+ customFilterFunctions,
+ displayTableRowCountText,
+ getInitialColumnFilterFnState,
+ getPageHeightCalc,
+ MRT_Functions_Localisation,
+} from '../../utils';
import CatalogueCard from './catalogueCard.component';
export interface CatalogueCardViewProps {
catalogueCategoryData: CatalogueCategory[];
@@ -42,14 +52,6 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
selectedCategories,
} = props;
- const { preservedState, onPreservedStatesChange } = usePreservedTableState({
- initialState: {
- pagination: { pageSize: 30, pageIndex: 0 },
- },
- storeInUrl: true,
- paginationOnly: true,
- });
-
// Display total and pagination on separate lines if on a small screen
const theme = useTheme();
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
@@ -71,14 +73,18 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
header: 'Name',
accessorFn: (row) => row.name,
id: 'name',
+ filterVariant: COLUMN_FILTER_VARIANTS.string,
+ filterFn: COLUMN_FILTER_FUNCTIONS.string,
+ columnFilterModeOptions: COLUMN_FILTER_MODE_OPTIONS.string,
size: 300,
},
{
header: 'Last modified',
accessorFn: (row) => new Date(row.modified_time),
id: 'modified_time',
- filterVariant: 'datetime-range',
- filterFn: 'betweenInclusive',
+ filterVariant: COLUMN_FILTER_VARIANTS.datetime,
+ filterFn: COLUMN_FILTER_FUNCTIONS.datetime,
+ columnFilterModeOptions: COLUMN_FILTER_MODE_OPTIONS.datetime,
size: 500,
enableGrouping: false,
},
@@ -87,8 +93,9 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
header: 'Created',
accessorFn: (row) => new Date(row.modified_time),
id: 'created',
- filterVariant: 'datetime-range',
- filterFn: 'betweenInclusive',
+ filterVariant: COLUMN_FILTER_VARIANTS.datetime,
+ filterFn: COLUMN_FILTER_FUNCTIONS.datetime,
+ columnFilterModeOptions: COLUMN_FILTER_MODE_OPTIONS.datetime,
size: 500,
enableGrouping: false,
},
@@ -96,9 +103,43 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
header: 'Property names',
accessorFn: (row) =>
row.properties.map((value) => value['name']).join(', '),
- id: 'property-names',
+ id: 'properties',
size: 350,
- filterVariant: 'autocomplete',
+ filterVariant: 'multi-select',
+ filterFn: 'arrIncludesSome',
+ columnFilterModeOptions: [
+ 'arrIncludesSome',
+ 'arrIncludesAll',
+ 'arrExcludesSome',
+ 'arrExcludesAll',
+ ],
+ renderColumnFilterModeMenuItems: ({ onSelectFilterMode }) => [
+ ,
+ ,
+ ,
+
+ ,
+ ],
filterSelectOptions: propertyNames,
enableGrouping: false,
},
@@ -106,17 +147,33 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
header: 'Is Leaf',
accessorFn: (row) => (row.is_leaf === true ? 'Yes' : 'No'),
id: 'is-leaf',
+ filterVariant: COLUMN_FILTER_VARIANTS.boolean,
+ enableColumnFilterModes: false,
size: 200,
- filterVariant: 'autocomplete',
},
];
}, [propertyNames]);
+
+ const initialColumnFilterFnState = React.useMemo(() => {
+ return getInitialColumnFilterFnState(columns);
+ }, [columns]);
+
+ const { preservedState, onPreservedStatesChange } = usePreservedTableState({
+ initialState: {
+ pagination: { pageSize: 30, pageIndex: 0 },
+ columnFilterFns: initialColumnFilterFnState,
+ },
+ storeInUrl: true,
+ paginationOnly: true,
+ });
+
const table = useMaterialReactTable({
// Data
columns: columns,
data: catalogueCategoryData ?? [],
// Features
enableColumnOrdering: false,
+ enableColumnFilterModes: true,
enableColumnPinning: false,
enableTopToolbar: true,
enableFacetedValues: true,
@@ -130,6 +187,7 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
enableHiding: false,
enableFullScreenToggle: false,
enablePagination: true,
+ filterFns: customFilterFunctions,
// Other settings
paginationDisplayMode: 'pages',
positionToolbarAlertBanner: 'bottom',
@@ -137,6 +195,7 @@ function CatalogueCardView(props: CatalogueCardViewProps) {
// Localisation
localization: {
...MRT_Localization_EN,
+ ...MRT_Functions_Localisation,
rowsPerPage: 'Categories per page',
},
// State
diff --git a/src/items/itemsTable.component.test.tsx b/src/items/itemsTable.component.test.tsx
index b0745f42f..17b8ba9d6 100644
--- a/src/items/itemsTable.component.test.tsx
+++ b/src/items/itemsTable.component.test.tsx
@@ -446,10 +446,10 @@ describe('Items Table', () => {
await user.click(dropdownButton);
const includeText = await screen.findByRole('menuitem', {
- name: 'Includes',
+ name: 'Includes any',
});
const excludeText = await screen.findByRole('menuitem', {
- name: 'Excludes',
+ name: 'Excludes any',
});
expect(includeText).toBeInTheDocument();
diff --git a/src/items/itemsTable.component.tsx b/src/items/itemsTable.component.tsx
index c7238319a..1e6955156 100644
--- a/src/items/itemsTable.component.tsx
+++ b/src/items/itemsTable.component.tsx
@@ -248,19 +248,19 @@ export function ItemsTable(props: ItemTableProps) {
id: 'item.usage_status',
filterVariant: 'multi-select',
filterFn: 'arrIncludesSome',
- columnFilterModeOptions: ['arrIncludesSome', 'arrIncludesNone'],
+ columnFilterModeOptions: ['arrIncludesSome', 'arrExcludesSome'],
renderColumnFilterModeMenuItems: ({ onSelectFilterMode }) => [
,
,
],
size: 350,
@@ -273,19 +273,19 @@ export function ItemsTable(props: ItemTableProps) {
id: 'system.name',
filterVariant: 'multi-select',
filterFn: 'arrIncludesSome',
- columnFilterModeOptions: ['arrIncludesSome', 'arrIncludesNone'],
+ columnFilterModeOptions: ['arrIncludesSome', 'arrExcludesSome'],
renderColumnFilterModeMenuItems: ({ onSelectFilterMode }) => [
,
,
],
size: 350,
diff --git a/src/utils.test.tsx b/src/utils.test.tsx
index 45aaff9aa..697dd0315 100644
--- a/src/utils.test.tsx
+++ b/src/utils.test.tsx
@@ -1,13 +1,12 @@
import { Link } from '@mui/material';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { MRT_ColumnDef, MRT_RowData } from 'material-react-table';
+import { MRT_ColumnDef } from 'material-react-table';
import { UsageStatus } from './api/api.types';
import { renderComponentWithRouterProvider } from './testUtils';
import {
OverflowTip,
checkForDuplicates,
- customFilterFunctions,
generateUniqueId,
generateUniqueName,
generateUniqueNameUsingCode,
@@ -320,41 +319,6 @@ describe('Utility functions', () => {
});
});
-describe('customFilterFunctions', () => {
- describe('arrIncludesNone', () => {
- const person: MRT_RowData = {
- name: 'Dan',
- age: 4,
- status: 'unemployed',
- getValue: (id: string) => {
- return person[id as keyof MRT_RowData]; // Return the corresponding field from the object
- },
- };
- const filterExclude: (
- row: MRT_RowData,
- id: string,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- filterValue: any
- ) => boolean = customFilterFunctions['arrIncludesNone'];
- it('should correctly exclude record', () => {
- const result = filterExclude(person, 'status', ['unemployed']);
- expect(result).toBe(false);
- });
- it('should correctly include record', () => {
- const result = filterExclude(person, 'age', [8, 29]);
- expect(result).toBe(true);
- });
- it('should correctly exclude record, when filter value is not a list', () => {
- const result = filterExclude(person, 'status', 'unemployed');
- expect(result).toBe(false);
- });
- it('should correctly include record, when filter value is not a list', () => {
- const result = filterExclude(person, 'age', 3);
- expect(result).toBe(true);
- });
- });
-});
-
describe('checkForDuplicates', () => {
it('should return an empty array when there are no duplicates', () => {
const data = [
diff --git a/src/utils.tsx b/src/utils.tsx
index d85385912..ab499716a 100644
--- a/src/utils.tsx
+++ b/src/utils.tsx
@@ -7,12 +7,14 @@ import {
Typography,
type TableCellProps,
} from '@mui/material';
+import { FilterFn, FilterMeta, Row } from '@tanstack/table-core';
import { format, parseISO } from 'date-fns';
import {
MRT_Cell,
MRT_Column,
MRT_ColumnDef,
MRT_ColumnFilterFnsState,
+ MRT_FilterFns,
MRT_FilterOption,
MRT_Header,
MRT_Row,
@@ -413,23 +415,35 @@ export const getInitialColumnFilterFnState = (
return initialState;
};
-interface FilterFn {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (row: MRT_RowData, id: string, filterValue: any): boolean;
-}
-
-export const customFilterFunctions: Record = {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- arrIncludesNone: (row: MRT_RowData, id: string, filterValue: any) => {
- if (Array.isArray(filterValue)) {
- return !filterValue.includes(row.getValue(id));
- }
- return row.getValue(id) !== filterValue;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const customFilterFunctions: Record> = {
+ arrExcludesSome: (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ row: Row,
+ id: string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ filterValue: any,
+ addMeta: (meta: FilterMeta) => void
+ ) => {
+ return !MRT_FilterFns.arrIncludesSome(row, id, filterValue, addMeta);
+ },
+ arrExcludesAll: (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ row: Row,
+ id: string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ filterValue: any,
+ addMeta: (meta: FilterMeta) => void
+ ) => {
+ return !MRT_FilterFns.arrIncludesAll(row, id, filterValue, addMeta);
},
};
export const MRT_Functions_Localisation: Record = {
- filterArrIncludesNone: 'Excludes',
+ filterArrIncludesSome: 'Includes any',
+ filterArrExcludesSome: 'Excludes any',
+ filterArrIncludesAll: 'Includes all',
+ filterArrExcludesAll: 'Excludes all',
};
type DataTypes = 'boolean' | 'string' | 'number' | 'null' | 'datetime' | 'date';