Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit fa126e8

Browse files
authored
feat: keep page info in url (#89)
* fix(URL): add pageInfo in URL * feat(filters): use searchParams in order to persist filters * fix(test): fix JSON.parse * fix(test): remove warning for empty string title * fix(test): add tests for next page * fix(test): add URL related test * fix(searchParam): edge cases of transition * fix(router): add all handleNavigate cases
1 parent 4435ec4 commit fa126e8

File tree

12 files changed

+206
-42
lines changed

12 files changed

+206
-42
lines changed

aqueductcore/frontend/src/__mocks__/AppContextAQDMock.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Experimental_CssVarsProvider as CssVarsProvider } from "@mui/material/styles";
2+
import { BrowserRouter, MemoryRouter, MemoryRouterProps } from "react-router-dom";
23
import { experimental_extendTheme as extendTheme } from "@mui/material/styles";
3-
import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
44
import { MockedProvider } from "@apollo/client/testing";
55
import CssBaseline from "@mui/material/CssBaseline";
66
import { PropsWithChildren } from "react";
@@ -32,6 +32,7 @@ interface AppContextAQDMockProps {
3232
getExperimentFiles_mockMockMode?: keyof typeof getExperimentFiles_mock;
3333
getUserInformation_mockMockMode?: keyof typeof getUserInformation_mock;
3434
getExperiment_mockMockMode?: keyof typeof getExperiment_mock;
35+
browserRouter?: boolean;
3536
memoryRouterProps?: MemoryRouterProps
3637
}
3738

@@ -48,6 +49,7 @@ function AppContextAQDMock({
4849
getUserInformation_mockMockMode = "success",
4950
getExperiment_mockMockMode = "success",
5051
memoryRouterProps,
52+
browserRouter,
5153
children,
5254
}: AppContextAQDMockProps) {
5355
Object.defineProperty(window, "matchMedia", {
@@ -92,16 +94,24 @@ function AppContextAQDMock({
9294
...getUserInformation_mock[getUserInformation_mockMockMode],
9395
getExperiment_mock[getExperiment_mockMockMode],
9496
];
97+
98+
const App =
99+
<>
100+
<CssBaseline />
101+
{children}
102+
<Toaster />
103+
</>
95104
return (
96105
<MockedProvider mocks={mocks} addTypename={false}>
97106
<CssVarsProvider theme={themeConfig}>
98-
{/* <BrowserRouter> */}
99-
<MemoryRouter {...memoryRouterProps}>
100-
<CssBaseline />
101-
{children}
102-
<Toaster />
103-
</MemoryRouter>
104-
{/* </BrowserRouter> */}
107+
{browserRouter ?
108+
<BrowserRouter>
109+
{App}
110+
</BrowserRouter> :
111+
<MemoryRouter {...memoryRouterProps}>
112+
{App}
113+
</MemoryRouter>
114+
}
105115
</CssVarsProvider>
106116
</MockedProvider>
107117
);

aqueductcore/frontend/src/__mocks__/queries/getAllExperimentsMock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const getAllExperiments_mock = {
2121
title: "",
2222
tags: null,
2323
shouldIncludeTags: null,
24-
},
24+
}
2525
},
2626
},
2727
result: {

aqueductcore/frontend/src/__mocks__/queries/getAllExperimentsWithNameFilterMock.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const getAllExperimentsWithNameFilter_mock = {
2121
endDate: null,
2222
title: filterByThisTitle,
2323
tags: null,
24+
shouldIncludeTags: null
2425
},
2526
},
2627
},
@@ -46,6 +47,7 @@ export const getAllExperimentsWithNameFilter_mock = {
4647
endDate: null,
4748
title: filterByThisTitle,
4849
tags: null,
50+
shouldIncludeTags: null
4951
},
5052
},
5153
},

aqueductcore/frontend/src/__mocks__/queries/getAllExperimentsWithTagFilterMock.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const getAllExperimentsWithTagFilter_mock = {
2121
endDate: null,
2222
title: "",
2323
tags: [filterByThisTag],
24+
shouldIncludeTags: null
2425
},
2526
},
2627
},
@@ -47,6 +48,7 @@ export const getAllExperimentsWithTagFilter_mock = {
4748
endDate: null,
4849
title: "",
4950
tags: [filterByThisTag],
51+
shouldIncludeTags: null
5052
},
5153
},
5254
},

aqueductcore/frontend/src/__tests__/ExperimentRecordsPage.test.tsx

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { render } from "@testing-library/react";
1+
import { act, render, waitFor } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
33

4-
import { ExperimentRecordsColumns } from "pages/ExperimentRecordsPage";
5-
import ExperimentRecordsPage from "pages/ExperimentRecordsPage";
64
import { filterByThisTitle } from "__mocks__/queries/getAllExperimentsWithNameFilterMock";
75
import { filterByThisTag } from "__mocks__/queries/getAllExperimentsWithTagFilterMock";
6+
import { ARCHIVED, experimentRecordsRowsPerPageOptions } from "constants/constants";
7+
import { ExperimentRecordsColumns } from "pages/ExperimentRecordsPage";
88
import { ExperimentDataMock } from "__mocks__/ExperimentDataMock";
9+
import ExperimentRecordsPage from "pages/ExperimentRecordsPage";
910
import AppContextAQDMock from "__mocks__/AppContextAQDMock";
10-
import { ARCHIVED, experimentRecordsRowsPerPageOptions } from "constants/constants";
1111

1212
// Experiment table
1313
test("render page with no error", () => {
@@ -99,3 +99,80 @@ test("render filtered experiments based on Tags", async () => {
9999
});
100100

101101
//todo: start and end date
102+
103+
test("experiment table next page click", async () => {
104+
const { findByTitle, findByText } = render(
105+
<AppContextAQDMock>
106+
<ExperimentRecordsPage category="all" />
107+
</AppContextAQDMock>
108+
);
109+
110+
// 0- Just page loaded. ex: 1–10 of 14
111+
const length_of_all_active_experiments = ExperimentDataMock.filter((item) => !item.tags.includes(ARCHIVED)).length
112+
const firstNumberInPaginationRange = 1
113+
const secondNumberInPaginationRange = Math.min(length_of_all_active_experiments, experimentRecordsRowsPerPageOptions[0])
114+
const paginationString = `${firstNumberInPaginationRange}${secondNumberInPaginationRange} of ${length_of_all_active_experiments}`
115+
const pageInfo = await findByText(paginationString);
116+
expect(pageInfo).toBeInTheDocument()
117+
118+
// 1- Click next page >
119+
const nextPageIconButton = await findByTitle("Go to next page");
120+
await act(async () => {
121+
await userEvent.click(nextPageIconButton);
122+
});
123+
124+
// 2- What we expect in the next page. ex: 11-14 of 14
125+
const firstNumberInPaginationRangeAfterClick = experimentRecordsRowsPerPageOptions[0] + 1
126+
const secondNumberInPaginationRangeAfterClick = Math.min(length_of_all_active_experiments, 2 * experimentRecordsRowsPerPageOptions[0])
127+
const paginationStringAfterClick = `${firstNumberInPaginationRangeAfterClick}${secondNumberInPaginationRangeAfterClick} of ${length_of_all_active_experiments}`
128+
const pageInfoAfterClick = await findByText(paginationStringAfterClick);
129+
130+
expect(pageInfoAfterClick).toBeInTheDocument()
131+
132+
});
133+
134+
test("experiment table to keep the URL updated with pagination", async () => {
135+
const { findByTitle } = render(
136+
<AppContextAQDMock browserRouter>
137+
<ExperimentRecordsPage category="all" />
138+
</AppContextAQDMock>
139+
);
140+
141+
// 0- URL should be there by the first load
142+
await waitFor(async () => {
143+
expect(window.location.href).toMatch(/rowsPerPage=10&page=0/)
144+
});
145+
146+
// 1- Click next page >
147+
const nextPageIconButton = await findByTitle("Go to next page");
148+
await act(async () => {
149+
await userEvent.click(nextPageIconButton);
150+
});
151+
152+
// 2- next page should be reflected in the new page URL
153+
await waitFor(async () => {
154+
expect(window.location.href).toMatch(/rowsPerPage=10&page=1/)
155+
});
156+
157+
});
158+
159+
test("experiment table to keep the URL updated with filters - Search title", async () => {
160+
const { getByTitle } = render(
161+
<AppContextAQDMock browserRouter>
162+
<ExperimentRecordsPage category="all" />
163+
</AppContextAQDMock>
164+
);
165+
const searchBar = getByTitle("Search Experiments input");
166+
167+
await userEvent.click(searchBar);
168+
await userEvent.type(searchBar, filterByThisTitle);
169+
170+
await waitFor(async () => {
171+
expect(window.location.href).toMatch(/title=EXP_rabi/)
172+
});
173+
174+
// Clean up the input
175+
await userEvent.clear(searchBar);
176+
});
177+
178+
//todo: start and end date with the URL

aqueductcore/frontend/src/components/molecules/SearchBar/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const SearchBar = ({ searchString, handleSearchStringUpdate }: SearchBarP
4949
placeholder="Search by EID and Name"
5050
value={text}
5151
onChange={handleChange}
52+
inputProps={{ title: 'Search Experiments input' }}
5253
InputProps={{
5354
startAdornment: (
5455
<InputAdornment position="start">

aqueductcore/frontend/src/components/organisms/ExperimentDetails/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,13 @@ function ExperimentDetails({ experimentDetails }: ExperimentDetailsProps) {
308308

309309
function handleNavigateBack() {
310310
switch (location.state?.from) {
311+
//if user have chosen the experiment from the experiment records
311312
case "/aqd/experiments/favourites":
312313
case "/aqd/experiments/archived":
314+
case "/aqd/experiments":
313315
navigate(-1);
314316
break;
317+
//if the link is copied or other states
315318
default:
316319
navigate("..", { relative: "path" });
317320
}

aqueductcore/frontend/src/components/organisms/ExperimentsListTable/index.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import StarBorderOutlinedIcon from "@mui/icons-material/StarBorderOutlined";
2+
import { useNavigate, useSearchParams } from "react-router-dom";
23
import TablePagination from "@mui/material/TablePagination";
34
import TableContainer from "@mui/material/TableContainer";
45
import TableBody from "@mui/material/TableBody";
56
import TableCell from "@mui/material/TableCell";
67
import TableHead from "@mui/material/TableHead";
78
import StarIcon from "@mui/icons-material/Star";
8-
import { useNavigate } from "react-router-dom";
99
import TableRow from "@mui/material/TableRow";
1010
import { grey } from "@mui/material/colors";
11+
import { useEffect, useState } from "react";
1112
import Table from "@mui/material/Table";
1213
import Paper from "@mui/material/Paper";
13-
import { useState } from "react";
1414

15+
import { useRemoveTagFromExperiment } from "API/graphql/mutations/Experiment/removeTagFromExperiment";
16+
import { useAddTagToExperiment } from "API/graphql/mutations/Experiment/addTagToExperiment";
17+
import { FAVOURITE, experimentRecordsRowsPerPageOptions } from "constants/constants";
1518
import {
16-
ExperimentDataType,
1719
ExperimentRecordsColumnsType,
1820
ExperimentsListTableProps,
21+
ExperimentDataType,
1922
} from "types/globalTypes";
20-
import { useRemoveTagFromExperiment } from "API/graphql/mutations/Experiment/removeTagFromExperiment";
21-
import { useAddTagToExperiment } from "API/graphql/mutations/Experiment/addTagToExperiment";
22-
import { FAVOURITE, experimentRecordsRowsPerPageOptions } from "constants/constants";
2323

2424
function ExperimentsListTable({
2525
ExperimentRecordsColumns,
@@ -41,6 +41,7 @@ function ExperimentsListTable({
4141
const [showActionId, setShowActionId] = useState("-1");
4242
const { page, setPage, rowsPerPage, setRowsPerPage, count } = pageInfo;
4343
const navigate = useNavigate();
44+
const [searchParams, setSearchParams] = useSearchParams();
4445
const { mutate: mutateAddTag } = useAddTagToExperiment();
4546
const { mutate: mutateRemoveTag } = useRemoveTagFromExperiment();
4647

@@ -53,6 +54,13 @@ function ExperimentsListTable({
5354
setPage(0);
5455
};
5556

57+
useEffect(() => {
58+
const newQueryParameters: URLSearchParams = new URLSearchParams(searchParams);
59+
newQueryParameters.set('rowsPerPage', String(rowsPerPage))
60+
newQueryParameters.set('page', String(page))
61+
setSearchParams(newQueryParameters)
62+
}, [page, rowsPerPage])
63+
5664
function handleToggleFavorite(
5765
e: React.MouseEvent,
5866
experimentId: ExperimentDataType["id"],

aqueductcore/frontend/src/components/organisms/FilterExperiments/index.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useSearchParams } from "react-router-dom";
12
import { Grid, styled } from "@mui/material";
23
import { useEffect, useState } from "react";
34
import dayjs, { Dayjs } from "dayjs";
@@ -20,7 +21,8 @@ interface FilterExperimentsProps {
2021
}
2122

2223
function FilterExperiments({ filters, setFilters }: FilterExperimentsProps) {
23-
const [selectedTags, setSelectedTags] = useState<TagType[]>([]);
24+
const [searchParams, setSearchParams] = useSearchParams();
25+
const [selectedTags, setSelectedTags] = useState<TagType[]>(filters?.tags ?? []);
2426
const [searchString, setSearchString] = useState<string>(filters?.title ?? "");
2527
const [startDate, setStartDate] = useState<Dayjs | null>(
2628
filters?.startDate ? dayjs(filters.startDate) : null
@@ -44,21 +46,49 @@ function FilterExperiments({ filters, setFilters }: FilterExperimentsProps) {
4446
}, [selectedTags, searchString, startDate, endDate]);
4547

4648
const handleTagUpdate = (value: TagType[]) => {
49+
const newQueryParameters: URLSearchParams = new URLSearchParams(searchParams);
50+
if (value.length === 0) {
51+
newQueryParameters.delete('tags')
52+
} else {
53+
newQueryParameters.set('tags', JSON.stringify(value))
54+
}
55+
setSearchParams(newQueryParameters)
4756
setSelectedTags(value);
4857
};
4958

5059
const handleSearchStringUpdate = (value: string) => {
60+
const newQueryParameters: URLSearchParams = new URLSearchParams(searchParams);
61+
if (!value) {
62+
newQueryParameters.delete('title')
63+
} else {
64+
newQueryParameters.set('title', value)
65+
}
66+
setSearchParams(newQueryParameters)
5167
setSearchString(value);
5268
};
5369

5470
const handleStartDateUpdate = (value: Dayjs | null) => {
5571
if (value?.isValid() || value === null) {
72+
const newQueryParameters: URLSearchParams = new URLSearchParams(searchParams);
73+
if (!value) {
74+
newQueryParameters.delete('startDate')
75+
} else {
76+
newQueryParameters.set('startDate', (value?.toISOString()) ?? '')
77+
}
78+
setSearchParams(newQueryParameters)
5679
setStartDate(value);
5780
}
5881
};
5982

6083
const handleEndDateUpdate = (value: Dayjs | null) => {
6184
if (value?.isValid() || value === null) {
85+
const newQueryParameters: URLSearchParams = new URLSearchParams(searchParams);
86+
if (!value) {
87+
newQueryParameters.delete('endDate')
88+
} else {
89+
newQueryParameters.set('endDate', (value?.toISOString()) ?? '')
90+
}
91+
setSearchParams(newQueryParameters)
6292
setEndDate(value);
6393
}
6494
};

0 commit comments

Comments
 (0)