diff --git a/cypress/e2e/filtering.cy.ts b/cypress/e2e/filtering.cy.ts index 3f6f9c54..f9e3ab9e 100644 --- a/cypress/e2e/filtering.cy.ts +++ b/cypress/e2e/filtering.cy.ts @@ -539,6 +539,12 @@ describe('Filtering Component', () => { }); }); + it('display favourite filters', () => { + cy.findByDisplayValue('test 1').should('exist'); + cy.findByDisplayValue('test 2').should('exist'); + cy.findByDisplayValue('test 3').should('exist'); + }); + it('renders the save button as disabled if name field is empty', () => { cy.findByRole('button', { name: 'Add new favourite filter' }).click(); cy.findByText('Add Favourite filter').should('exist'); diff --git a/e2e/real/filtering.spec.ts b/e2e/real/filtering.spec.ts index ce8379d8..c293bfbf 100644 --- a/e2e/real/filtering.spec.ts +++ b/e2e/real/filtering.spec.ts @@ -96,3 +96,33 @@ test('should be able to add a multiple filters', async ({ page }) => { await expect(page.getByText('1–7 of 7')).toBeVisible(); }); + +test('should be able to add a favourite filter', async ({ page }) => { + await page.getByRole('button', { name: 'Filters' }).click(); + await page.getByText('Favourite filters').click(); + + await page.getByRole('button', { name: 'Add new favourite filter' }).click(); + + const firstFilterInput = page + .getByRole('combobox', { + name: 'Filter', + exact: true, + }) + .first(); + + const nameFields = await page.locator('label:has-text("Name")'); + + await nameFields.last().fill('test'); + + await firstFilterInput.pressSequentially('Relative humidity 209 < 42 '); + + // unfocus so combobox menu is not blocking add new filter button + await firstFilterInput.blur(); + + await page.getByRole('button', { name: 'Save' }).click(); + + const element = page.locator('input[value="test 1"]'); // Adjust the selector as necessary + + // Assert that the element exists + await expect(element).toBeVisible(); +}); diff --git a/src/api/favouriteFilters.test.tsx b/src/api/favouriteFilters.test.tsx index 345fdef2..069ea846 100644 --- a/src/api/favouriteFilters.test.tsx +++ b/src/api/favouriteFilters.test.tsx @@ -1,10 +1,10 @@ import { renderHook, waitFor } from '@testing-library/react'; -import { FavouriteFilter } from '../app.types'; +import { FavouriteFilterPost } from '../app.types'; import { hooksWrapperWithProviders } from '../testUtils'; import { useAddFavouriteFilter } from './favouriteFilters'; describe('session api functions', () => { - let mockData: FavouriteFilter; + let mockData: FavouriteFilterPost; beforeEach(() => { mockData = { name: 'test', diff --git a/src/api/favouriteFilters.tsx b/src/api/favouriteFilters.tsx index 1731b417..f36cef65 100644 --- a/src/api/favouriteFilters.tsx +++ b/src/api/favouriteFilters.tsx @@ -1,17 +1,19 @@ import { useMutation, UseMutationResult, + useQuery, useQueryClient, + UseQueryResult, } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; -import { FavouriteFilter } from '../app.types'; +import { FavouriteFilter, FavouriteFilterPost } from '../app.types'; import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; import { selectUrls } from '../state/slices/configSlice'; const addFavouriteFilter = ( apiUrl: string, - favouriteFilter: FavouriteFilter + favouriteFilter: FavouriteFilterPost ): Promise => { const queryParams = new URLSearchParams(); @@ -35,12 +37,12 @@ const addFavouriteFilter = ( export const useAddFavouriteFilter = (): UseMutationResult< string, AxiosError, - FavouriteFilter + FavouriteFilterPost > => { const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ - mutationFn: (favouriteFilter: FavouriteFilter) => + mutationFn: (favouriteFilter: FavouriteFilterPost) => addFavouriteFilter(apiUrl, favouriteFilter), onError: (error) => { console.log('Got error ' + error.message); @@ -50,3 +52,30 @@ export const useAddFavouriteFilter = (): UseMutationResult< }, }); }; + +const fetchFavouriteFilters = (apiUrl: string): Promise => { + return axios + .get(`${apiUrl}/users/filters`, { + headers: { + Authorization: `Bearer ${readSciGatewayToken()}`, + }, + }) + .then((response) => { + return response.data; + }); +}; + +export const useFavouriteFilters = (): UseQueryResult< + FavouriteFilter[], + AxiosError +> => { + const { apiUrl } = useAppSelector(selectUrls); + + return useQuery({ + queryKey: ['favouriteFilters'], + + queryFn: () => { + return fetchFavouriteFilters(apiUrl); + }, + }); +}; diff --git a/src/app.types.tsx b/src/app.types.tsx index 33041dc2..31a26c75 100644 --- a/src/app.types.tsx +++ b/src/app.types.tsx @@ -312,7 +312,11 @@ export interface APIError { detail: string | APIErrorResponse[]; } -export interface FavouriteFilter { +export interface FavouriteFilterPost { name: string; filter: string; } + +export interface FavouriteFilter extends FavouriteFilterPost { + _id: string; +} diff --git a/src/filtering/__snapshots__/filterDialogue.component.test.tsx.snap b/src/filtering/__snapshots__/filterDialogue.component.test.tsx.snap index d6d58081..b8b2e35c 100644 --- a/src/filtering/__snapshots__/filterDialogue.component.test.tsx.snap +++ b/src/filtering/__snapshots__/filterDialogue.component.test.tsx.snap @@ -149,6 +149,553 @@ exports[`Filter dialogue component > renders filter dialogue when dialogue is op /> +
+
+
+
+ +
+ + +
+
+
+
+
+
+ +
+
+ + Active Experiment + +
+
+ + > + +
+
+ + 3 + +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ +
+
+ + Shot Number + +
+
+ + = + +
+
+ + 1 + +
+ + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ +
+
+ + Active Experiment + +
+
+ + = + +
+
+ + 1 + +
+ + +
+
+
+
+
+ +
+
+ +
+
+
@@ -167,16 +714,6 @@ exports[`Filter dialogue component > renders filter dialogue when dialogue is op class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" /> - diff --git a/src/filtering/favouriteFiltersDialogue.component.tsx b/src/filtering/favouriteFiltersDialogue.component.tsx index ff665236..ad152086 100644 --- a/src/filtering/favouriteFiltersDialogue.component.tsx +++ b/src/filtering/favouriteFiltersDialogue.component.tsx @@ -10,7 +10,7 @@ import { } from '@mui/material'; import React from 'react'; import { useAddFavouriteFilter } from '../api/favouriteFilters'; -import { FavouriteFilter } from '../app.types'; +import { FavouriteFilterPost } from '../app.types'; import { FilterPageHelp } from './filterDialogue.component'; import FilterInput from './filterInput.component'; import { Token } from './filterParser'; @@ -60,7 +60,7 @@ const FavouriteFiltersDialogue = (props: FavouriteFiltersDialogueProps) => { const { mutateAsync: addFavouriteFilter } = useAddFavouriteFilter(); const handleSubmit = React.useCallback(() => { - const data: FavouriteFilter = { + const data: FavouriteFilterPost = { name: favouriteFilter.name, filter: JSON.stringify( favouriteFilter.filter.length === 0 ? '' : favouriteFilter.filter diff --git a/src/filtering/filterDialogue.component.tsx b/src/filtering/filterDialogue.component.tsx index 6dc8854d..5cc8b55a 100644 --- a/src/filtering/filterDialogue.component.tsx +++ b/src/filtering/filterDialogue.component.tsx @@ -1,4 +1,6 @@ import { AddCircle, Delete, Warning } from '@mui/icons-material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; import { Box, Button, @@ -10,12 +12,14 @@ import { Grid, IconButton, Tabs, + TextField, Tooltip, Typography, } from '@mui/material'; import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; import { useChannels } from '../api/channels'; +import { useFavouriteFilters } from '../api/favouriteFilters'; import { useIncomingRecordCount } from '../api/records'; import { timeChannelName } from '../app.types'; import { useAppDispatch, useAppSelector } from '../state/hooks'; @@ -345,6 +349,8 @@ const FilterDialogue = (props: FilterDialogueProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [incomingCount, incomingFilters]); + const { data: favouriteFilterData } = useFavouriteFilters(); + return ( @@ -433,6 +439,61 @@ const FilterDialogue = (props: FilterDialogueProps) => { Add new favourite filter + + {favouriteFilterData?.map((data) => { + return ( + + + + + + {}} + setError={() => {}} + readOnly + /> + + + + + + + + + + + + + + + + + ); + })} + { - {displayingWarningMessage ? ( - - -
- - {`This search will return over ${recordLimitWarning} + }} + arrow + placement="bottom" + title={ + + +
+ + {`This search will return over ${recordLimitWarning} results.`} - -
- - Click Apply again to continue - -
-
- } - > +
+
+ + Click Apply again to continue + +
+ + } + > + +
+ ) : ( - - ) : ( - - )} + ))}
); diff --git a/src/filtering/filterInput.component.tsx b/src/filtering/filterInput.component.tsx index dadd7d88..0bb93862 100644 --- a/src/filtering/filterInput.component.tsx +++ b/src/filtering/filterInput.component.tsx @@ -19,6 +19,7 @@ interface FilterInputProps { error?: string; setError: (error?: string) => void; flashingFilterValue?: string; + readOnly?: boolean; } export const useClickHandler = (props: { @@ -302,8 +303,15 @@ const filterOptions = createFilterOptions({ }); const FilterInput = (props: FilterInputProps) => { - const { channels, value, setValue, error, setError, flashingFilterValue } = - props; + const { + channels, + value, + setValue, + error, + setError, + flashingFilterValue, + readOnly, + } = props; const options = React.useMemo(() => { return [...operators, ...channels]; }, [channels]); @@ -362,6 +370,7 @@ const FilterInput = (props: FilterInputProps) => { autoHighlight filterOptions={filterOptions} multiple + readOnly={readOnly} options={options} freeSolo size="small" @@ -409,6 +418,8 @@ const FilterInput = (props: FilterInputProps) => { 'data-id': 'Input', startAdornment: tags.slice(0, inputIndex), endAdornment: tags.slice(inputIndex), + readOnly: readOnly, + disabled: readOnly, }} /> )} diff --git a/src/mocks/favouriteFilters.json b/src/mocks/favouriteFilters.json new file mode 100644 index 00000000..80efad55 --- /dev/null +++ b/src/mocks/favouriteFilters.json @@ -0,0 +1,17 @@ +[ + { + "_id": "66f18358e754d901424d6d22", + "name": "test 1", + "filter": "[{\"type\":\"channel\",\"value\":\"activeExperiment\",\"label\":\"Active Experiment\"},{\"type\":\"compop\",\"value\":\">\",\"label\":\">\"},{\"type\":\"number\",\"value\":\"3\",\"label\":\"3\"}]" + }, + { + "_id": "66f2b289641250b5f364361a", + "name": "test 2", + "filter": "[{\"type\":\"channel\",\"value\":\"shotnum\",\"label\":\"Shot Number\"},{\"type\":\"compop\",\"value\":\"=\",\"label\":\"=\"},{\"type\":\"number\",\"value\":\"1\",\"label\":\"1\"}]" + }, + { + "_id": "66f2e06a41a3fe93d6c81395", + "name": "test 3", + "filter": "[{\"type\":\"channel\",\"value\":\"activeExperiment\",\"label\":\"Active Experiment\"},{\"type\":\"compop\",\"value\":\"=\",\"label\":\"=\"},{\"type\":\"number\",\"value\":\"1\",\"label\":\"1\"}]" + } +] diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index e2a2976e..64df4361 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -12,6 +12,7 @@ import { PREFERRED_COLOUR_MAP_PREFERENCE_NAME } from '../settingsMenuItems.compo import channelsJson from './channels.json'; import colourMapsJson from './colourMaps.json'; import experimentsJson from './experiments.json'; +import favouriteFiltersJson from './favouriteFilters.json'; import functionsTokensJson from './functionTokens.json'; import functionsJson from './functions.json'; import recordsJson from './records.json'; @@ -374,4 +375,8 @@ export const handlers = [ http.post('/users/filters', async () => { return HttpResponse.json('1', { status: 201 }); }), + + http.get('/users/filters', async () => { + return HttpResponse.json(favouriteFiltersJson, { status: 201 }); + }), ]; diff --git a/src/search/components/dateTime.component.tsx b/src/search/components/dateTime.component.tsx index 7824f311..405ec285 100644 --- a/src/search/components/dateTime.component.tsx +++ b/src/search/components/dateTime.component.tsx @@ -9,8 +9,8 @@ import { } from '@mui/material'; import { styled } from '@mui/material/styles'; import { - DateTimePicker, DateTimeValidationError, + DesktopDateTimePicker, LocalizationProvider, } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3'; @@ -291,7 +291,7 @@ const DateTimeSearch = (props: DateTimeSearchProps): React.ReactElement => { - { -