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 }) => [ + onSelectFilterMode('arrIncludesSome')} + > + {MRT_Functions_Localisation.filterArrIncludesSome} + , + onSelectFilterMode('arrIncludesAll')} + > + {MRT_Functions_Localisation.filterArrIncludesAll} + , + onSelectFilterMode('arrExcludesSome')} + > + {MRT_Functions_Localisation.filterArrExcludesSome} + , + + onSelectFilterMode('arrExcludesAll')} + > + {MRT_Functions_Localisation.filterArrExcludesAll} + , + ], 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 }) => [ onSelectFilterMode('arrIncludesSome')} > - Includes + {MRT_Functions_Localisation.filterArrIncludesSome} , onSelectFilterMode('arrIncludesNone')} + key="arrExcludesSome" + onClick={() => onSelectFilterMode('arrExcludesSome')} > - Excludes + {MRT_Functions_Localisation.filterArrExcludesSome} , ], 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 }) => [ onSelectFilterMode('arrIncludesSome')} > - Includes + {MRT_Functions_Localisation.filterArrIncludesSome} , onSelectFilterMode('arrIncludesNone')} + key="arrExcludesSome" + onClick={() => onSelectFilterMode('arrExcludesSome')} > - Excludes + {MRT_Functions_Localisation.filterArrExcludesSome} , ], 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';