Skip to content

Commit 3afc4b9

Browse files
Merge pull request #1589 from ral-facilities/create-banner-when-spares-filter-is-applied-#1587
Create a banner for when the spares filter is applied on items table #1587
2 parents eab817c + 6652971 commit 3afc4b9

File tree

7 files changed

+295
-153
lines changed

7 files changed

+295
-153
lines changed

cypress/e2e/with_mock_data/items.cy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,12 @@ describe('Items', () => {
505505
cy.findByText('eUc80U7FqJum').should('not.exist');
506506
cy.findByText('WrgqAVk3qUQK').should('not.exist');
507507
cy.findByRole('button', { name: 'Show Spare Items' }).should('be.disabled');
508+
509+
cy.findByText('Spares Definition Filter Applied').should('exist');
510+
cy.findByLabelText(
511+
'Items that are contained within the system type Storage are classified as spares'
512+
).should('exist');
513+
508514
cy.findByRole('button', { name: 'Clear Filters' }).click();
509515
cy.findByText('dfzqkOJbqifO').should('exist');
510516
cy.findByText('tenrMn1KOmIg').should('exist');

src/catalogue/items/__snapshots__/catalogueItemsPage.component.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ exports[`CatalogueItemsPage > renders a catalogue items page correctly 1`] = `
212212
</div>
213213
</div>
214214
<div
215-
class="MuiTableContainer-root css-13mblvd-MuiTableContainer-root"
215+
class="MuiTableContainer-root css-1h4z99x-MuiTableContainer-root"
216216
data-testid="catalogue-items-table-container"
217217
>
218218
<table

src/catalogue/items/catalogueItemsTable.component.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,6 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => {
169169
isItemSelectable,
170170
requestOrigin,
171171
} = props;
172-
// Breadcrumbs + Mui table V2 + extra
173-
const tableHeight = getPageHeightCalc('50px + 110px + 48px');
174172
const contentHeight = getPageHeightCalc('80px');
175173

176174
const { data: catalogueItemsData, isLoading: isLoadingCatalogueItems } =
@@ -819,10 +817,23 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => {
819817
'aria-label': `${row.original.catalogueItem.name} row`,
820818
};
821819
},
822-
muiTableContainerProps: {
823-
sx: { height: dense ? '360.4px' : tableHeight, flexShrink: 1 },
824-
// @ts-expect-error: MRT Table Container props does not have data-testid
825-
'data-testid': 'catalogue-items-table-container',
820+
muiTableContainerProps: ({ table }) => {
821+
const showAlert =
822+
table.getState().showAlertBanner ||
823+
table.getFilteredSelectedRowModel().rows.length > 0 ||
824+
table.getState().grouping.length > 0;
825+
return {
826+
sx: {
827+
height: dense
828+
? '360.4px'
829+
: getPageHeightCalc(
830+
// Breadcrumbs + Mui table V2 + extra
831+
`50px + 110px + 48px ${showAlert ? '+ 58.75px' : ''}`
832+
),
833+
flexShrink: 1,
834+
},
835+
'data-testid': 'catalogue-items-table-container',
836+
};
826837
},
827838
muiTableBodyCellProps: ({ column, row }) => {
828839
const disabledGroupedHeaderColumnIDs = [

src/items/__snapshots__/itemsTable.component.test.tsx.snap

Lines changed: 92 additions & 124 deletions
Large diffs are not rendered by default.

src/items/itemsTable.component.test.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { fireEvent, screen, waitFor } from '@testing-library/react';
22
import userEvent, { UserEvent } from '@testing-library/user-event';
3-
import { CatalogueCategory, CatalogueItem } from '../api/api.types';
3+
import { DefaultBodyType, http, HttpResponse, PathParams } from 'msw';
4+
import {
5+
CatalogueCategory,
6+
CatalogueItem,
7+
SparesDefinition,
8+
} from '../api/api.types';
9+
import { server } from '../mocks/server';
10+
import SystemTypesJSON from '../mocks/SystemTypes.json';
411
import {
512
getCatalogueCategoryById,
613
getCatalogueItemById,
@@ -227,6 +234,16 @@ describe('Items Table', () => {
227234
expect(screen.queryByText('tenrMn1KOmIg')).not.toBeInTheDocument();
228235
});
229236

237+
expect(
238+
await screen.findByText('Spares Definition Filter Applied')
239+
).toBeInTheDocument();
240+
241+
expect(
242+
screen.getByLabelText(
243+
'Items that are contained within the system type Storage are classified as spares'
244+
)
245+
).toBeInTheDocument();
246+
230247
const clearFiltersButton = screen.getByRole('button', {
231248
name: 'Clear Filters',
232249
});
@@ -238,6 +255,59 @@ describe('Items Table', () => {
238255
});
239256
});
240257

258+
it('sets the spares definition filter and clears the spares filters (multiple systems types in spares definition)', async () => {
259+
server.use(
260+
http.get<PathParams, DefaultBodyType, SparesDefinition>(
261+
'/v1/settings/spares-definition',
262+
() => {
263+
return HttpResponse.json(
264+
{ system_types: [SystemTypesJSON[0], SystemTypesJSON[2]] },
265+
{ status: 200 }
266+
);
267+
}
268+
)
269+
);
270+
props.catalogueCategory = getCatalogueCategoryById(
271+
'9'
272+
) as CatalogueCategory;
273+
props.catalogueItem = getCatalogueItemById('11') as CatalogueItem;
274+
createView();
275+
276+
await waitFor(() => {
277+
expect(screen.getByText('Serial Number')).toBeInTheDocument();
278+
});
279+
const showSparesButton = screen.getByRole('button', {
280+
name: 'Show Spare Items',
281+
});
282+
expect(showSparesButton).not.toBeDisabled();
283+
284+
await user.click(showSparesButton);
285+
286+
await waitFor(() => {
287+
expect(screen.queryByText('tenrMn1KOmIg')).not.toBeInTheDocument();
288+
});
289+
290+
expect(
291+
await screen.findByText('Spares Definition Filter Applied')
292+
).toBeInTheDocument();
293+
294+
expect(
295+
screen.getByLabelText(
296+
'Items that are contained within a system type of one of Storage or Scrapped are classified as spares'
297+
)
298+
).toBeInTheDocument();
299+
300+
const clearSparesFiltersButton = screen.getByRole('button', {
301+
name: 'Clear Spares Definition Filter',
302+
});
303+
304+
await user.click(clearSparesFiltersButton);
305+
306+
await waitFor(() => {
307+
expect(screen.getByText('tenrMn1KOmIg')).toBeInTheDocument();
308+
});
309+
});
310+
241311
it('navigates to catalogue item landing page', async () => {
242312
createView();
243313
const serialNumber = '5YUQDDjKpz2z';

src/items/itemsTable.component.tsx

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { InfoOutlined } from '@mui/icons-material';
12
import AddIcon from '@mui/icons-material/Add';
23
import ClearIcon from '@mui/icons-material/Clear';
34
import DeleteIcon from '@mui/icons-material/Delete';
@@ -6,12 +7,16 @@ import SaveAsIcon from '@mui/icons-material/SaveAs';
67
import {
78
Box,
89
Button,
10+
IconButton,
911
ListItemIcon,
1012
ListItemText,
1113
MenuItem,
1214
Link as MuiLink,
1315
TableCellBaseProps,
16+
Tooltip,
17+
Typography,
1418
} from '@mui/material';
19+
import Grid from '@mui/material/Grid2';
1520
import {
1621
MaterialReactTable,
1722
useMaterialReactTable,
@@ -93,8 +98,14 @@ export function ItemsTable(props: ItemTableProps) {
9398
const { data: usageStatusData, isLoading: isLoadingUsageStatus } =
9499
useGetUsageStatuses();
95100

96-
const { encodedSparesFilter, isLoading: isLoadingSparesDefinition } =
97-
useSparesFilterState();
101+
const {
102+
encodedSparesFilter,
103+
isLoading: isLoadingSparesDefinition,
104+
sparesDefinition,
105+
} = useSparesFilterState();
106+
107+
const isSparesDefinitionDefined =
108+
sparesDefinition !== '' && sparesDefinition.system_types.length !== 0;
98109

99110
const systemIdSet = new Set<string>(
100111
itemsData?.map((item) => item.system_id) ?? []
@@ -142,8 +153,6 @@ export function ItemsTable(props: ItemTableProps) {
142153
'create' | 'duplicate' | 'edit'
143154
>('create');
144155

145-
// Breadcrumbs + Mui table V2 + extra
146-
const tableHeight = getPageHeightCalc('50px + 110px + 48px');
147156
const columns = React.useMemo<MRT_ColumnDef<TableRowData>[]>(() => {
148157
const viewCatalogueItemProperties = catalogueCategory?.properties ?? [];
149158
const systemTypeValues = systemTypesData?.map((type) => type.value);
@@ -479,7 +488,7 @@ export function ItemsTable(props: ItemTableProps) {
479488
},
480489
manualFiltering: false,
481490
paginationDisplayMode: 'pages',
482-
positionToolbarAlertBanner: 'bottom',
491+
positionToolbarAlertBanner: 'top',
483492
autoResetPageIndex: false,
484493
displayColumnDefOptions: dense
485494
? undefined
@@ -507,10 +516,31 @@ export function ItemsTable(props: ItemTableProps) {
507516
//MRT
508517
mrtTheme,
509518
//MUI
510-
muiTableContainerProps: {
511-
sx: { height: dense ? '360.4px' : tableHeight },
512-
// @ts-expect-error: MRT Table Container props does not have data-testid
513-
'data-testid': 'items-table-container',
519+
muiToolbarAlertBannerProps: {
520+
sx: {
521+
'& .MuiAlert-message': {
522+
maxWidth: undefined,
523+
width: '100%',
524+
},
525+
},
526+
},
527+
muiTableContainerProps: ({ table }) => {
528+
const showAlert =
529+
table.getState().showAlertBanner ||
530+
table.getFilteredSelectedRowModel().rows.length > 0 ||
531+
table.getState().grouping.length > 0;
532+
return {
533+
sx: {
534+
height: dense
535+
? '360.4px'
536+
: getPageHeightCalc(
537+
// Breadcrumbs + Mui table V2 + extra
538+
`50px + 110px + 48px ${showAlert ? '+ 54px' : ''}`
539+
),
540+
flexShrink: 1,
541+
},
542+
'data-testid': 'items-table-container',
543+
};
514544
},
515545
muiTableBodyCellProps: ({ column }) => {
516546
const disabledGroupedHeaderColumnIDs = [
@@ -606,18 +636,20 @@ export function ItemsTable(props: ItemTableProps) {
606636
>
607637
Clear Filters
608638
</Button>
609-
<Button
610-
sx={{ mx: 0.5 }}
611-
variant="outlined"
612-
disabled={searchParams.get('state') === encodedSparesFilter}
613-
onClick={() => {
614-
const newParams = new URLSearchParams(searchParams);
615-
newParams.set('state', encodedSparesFilter);
616-
setSearchParams(newParams, { replace: false });
617-
}}
618-
>
619-
Show Spare Items
620-
</Button>
639+
{isSparesDefinitionDefined && (
640+
<Button
641+
sx={{ mx: 0.5 }}
642+
variant="outlined"
643+
disabled={searchParams.get('state') === encodedSparesFilter}
644+
onClick={() => {
645+
const newParams = new URLSearchParams(searchParams);
646+
newParams.set('state', encodedSparesFilter);
647+
setSearchParams(newParams, { replace: false });
648+
}}
649+
>
650+
Show Spare Items
651+
</Button>
652+
)}
621653
</Box>
622654
),
623655
renderRowActionMenuItems: ({ closeMenu, row, table }) => {
@@ -669,6 +701,51 @@ export function ItemsTable(props: ItemTableProps) {
669701
</MenuItem>,
670702
];
671703
},
704+
renderToolbarAlertBannerContent:
705+
isSparesDefinitionDefined &&
706+
searchParams.get('state') === encodedSparesFilter
707+
? ({ table }) => (
708+
<Grid container alignItems="center" sx={{ px: 1, py: 0.5 }}>
709+
<Grid size={2} />
710+
<Grid size={8}>
711+
<Box display="flex" alignItems="center" justifyContent="center">
712+
<Typography variant="inherit" sx={{ pr: 1 }}>
713+
Spares Definition Filter Applied
714+
</Typography>
715+
<Tooltip
716+
title={
717+
sparesDefinition.system_types.length === 1
718+
? `Items that are contained within the system type ${sparesDefinition.system_types[0].value} are classified as spares`
719+
: `Items that are contained within a system type of one of ${sparesDefinition.system_types
720+
.map((sys) => sys.value)
721+
.join(', ')
722+
.replace(
723+
/, ([^,]*)$/,
724+
' or $1'
725+
)} are classified as spares`
726+
}
727+
>
728+
<InfoOutlined fontSize="small" />
729+
</Tooltip>
730+
</Box>
731+
</Grid>
732+
<Grid size={2} display="flex" justifyContent="flex-end">
733+
<Tooltip title="Clear Spares Definition Filter">
734+
<span>
735+
<IconButton
736+
size="small"
737+
aria-label="Clear Spares Definition Filter"
738+
onClick={() => table.resetColumnFilters()}
739+
sx={{ color: 'inherit' }}
740+
>
741+
<ClearIcon fontSize="small" />
742+
</IconButton>
743+
</span>
744+
</Tooltip>
745+
</Grid>
746+
</Grid>
747+
)
748+
: undefined,
672749
renderBottomToolbarCustomActions: ({ table }) =>
673750
displayTableRowCountText(table, itemsData, 'Items', {
674751
paddingLeft: '8px',
@@ -683,6 +760,13 @@ export function ItemsTable(props: ItemTableProps) {
683760
: undefined,
684761
});
685762

763+
React.useEffect(() => {
764+
if (isSparesDefinitionDefined)
765+
table.setShowAlertBanner(
766+
searchParams.get('state') === encodedSparesFilter
767+
);
768+
}, [encodedSparesFilter, isSparesDefinitionDefined, searchParams, table]);
769+
686770
return (
687771
<div style={{ width: '100%' }}>
688772
<MaterialReactTable table={table} />

src/utils.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from 'material-react-table';
2727
import React from 'react';
2828
import { useGetSparesDefinition } from './api/settings';
29+
import { SparesDefinition } from './api/api.types';
2930

3031
/* Returns a name avoiding duplicates by appending _copy_n for nth copy */
3132
export const generateUniqueName = (
@@ -597,6 +598,7 @@ export const COLUMN_FILTER_BOOLEAN_OPTIONS = ['Yes', 'No'];
597598
export const useSparesFilterState = (
598599
urlParamName?: string
599600
): {
601+
sparesDefinition: '' | SparesDefinition;
600602
sparesFilterState: string;
601603
encodedSparesFilter: string;
602604
isLoading: boolean;
@@ -629,5 +631,6 @@ export const useSparesFilterState = (
629631
sparesFilterState,
630632
encodedSparesFilter,
631633
isLoading: isLoadingSparesDefinition,
634+
sparesDefinition,
632635
};
633636
};

0 commit comments

Comments
 (0)