Skip to content

Commit aada4b7

Browse files
committed
Persist advanced-column selection in the URL
1 parent 016bd07 commit aada4b7

7 files changed

Lines changed: 143 additions & 21 deletions

File tree

src/__tests__/CompareResults/ResultsTable.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,4 +1330,42 @@ describe('Advanced-columns toggle for mann-whitney-u testVersion', () => {
13301330
expect(header().querySelector('.delta-header')).toBeFalsy();
13311331
expect(header().querySelector('.effects-header')).toBeTruthy();
13321332
});
1333+
1334+
it('shows the advanced columns named in the advanced_columns URL param', async () => {
1335+
const { testCompareMannWhitneyData } = getTestData();
1336+
setupAndRender(
1337+
testCompareMannWhitneyData,
1338+
'test_version=mann-whitney-u&advanced_columns=cliffs_delta',
1339+
);
1340+
await screen.findByText('a11yr');
1341+
1342+
const header = screen.getByTestId('table-header');
1343+
expect(header.querySelector('.delta-header')).toBeTruthy();
1344+
expect(header.querySelector('.effects-header')).toBeFalsy();
1345+
});
1346+
1347+
it('persists the advanced-column selection to the advanced_columns URL param', async () => {
1348+
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
1349+
const { testCompareMannWhitneyData } = getTestData();
1350+
setupAndRender(testCompareMannWhitneyData, 'test_version=mann-whitney-u');
1351+
await screen.findByText('a11yr');
1352+
1353+
const advancedParam = () =>
1354+
new URLSearchParams(window.location.search).get('advanced_columns');
1355+
expect(advancedParam()).toBeNull();
1356+
1357+
await user.click(screen.getByRole('button', { name: /Advanced columns/ }));
1358+
await user.click(screen.getByRole('checkbox', { name: "Cliff's Delta" }));
1359+
expect(advancedParam()).toBe('cliffs_delta');
1360+
1361+
await user.click(screen.getByRole('checkbox', { name: 'CLES' }));
1362+
expect(advancedParam()).toBe('cliffs_delta,cles');
1363+
1364+
// Turning a column off updates the param; turning the last one off removes it.
1365+
await user.click(screen.getByRole('checkbox', { name: "Cliff's Delta" }));
1366+
expect(advancedParam()).toBe('cles');
1367+
1368+
await user.click(screen.getByRole('checkbox', { name: 'CLES' }));
1369+
expect(advancedParam()).toBeNull();
1370+
});
13331371
});

src/components/CompareResults/AdvancedColumnsMenu.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Checkbox from '@mui/material/Checkbox';
44
import FormControlLabel from '@mui/material/FormControlLabel';
55
import Menu from '@mui/material/Menu';
66
import MenuItem from '@mui/material/MenuItem';
7-
import type { PayloadAction } from '@reduxjs/toolkit';
87
import {
98
usePopupState,
109
bindTrigger,
@@ -13,42 +12,57 @@ import {
1312

1413
import { useAppDispatch } from '../../hooks/app';
1514
import useAdvancedColumns from '../../hooks/useAdvancedColumns';
15+
import useRawSearchParams from '../../hooks/useRawSearchParams';
1616
import {
1717
updateShowCliffsDelta,
1818
updateShowCles,
1919
} from '../../reducers/ColumnPrefsSlice';
20+
import type { AdvancedColumns } from '../../types/types';
21+
import {
22+
ADVANCED_COLUMNS_PARAM,
23+
serializeAdvancedColumns,
24+
} from '../../utils/advancedColumnsUrl';
2025

2126
// Dropdown that reveals a checkbox for each advanced statistics column
2227
// (Cliff's Delta, CLES). Each is toggled independently — either, both, or
23-
// neither can be shown. Self-contained: reads/writes the columnPrefs Redux
24-
// slice and mirrors each choice to localStorage so it persists across reloads.
25-
// Shared by the main results controls and the subtests controls.
28+
// neither can be shown. The selection is stored in the URL so a shared link
29+
// reproduces it (via history.replaceState — no data refetch) and mirrored to
30+
// Redux for reactive rendering. Shared by the main and subtests controls.
2631
function AdvancedColumnsMenu() {
2732
const popupState = usePopupState({
2833
variant: 'popover',
2934
popupId: 'advanced-columns-menu',
3035
});
3136
const dispatch = useAppDispatch();
3237
const { cliffsDelta, cles } = useAdvancedColumns();
38+
const [, updateRawSearchParams] = useRawSearchParams();
39+
40+
const applyAdvancedColumns = (next: AdvancedColumns) => {
41+
dispatch(updateShowCliffsDelta(next.cliffsDelta));
42+
dispatch(updateShowCles(next.cles));
3343

34-
// Dispatch the toggle and mirror the choice to localStorage under its key.
35-
const toggle =
36-
(action: (value: boolean) => PayloadAction<boolean>, storageKey: string) =>
37-
(checked: boolean) => {
38-
dispatch(action(checked));
39-
localStorage.setItem(storageKey, String(checked));
40-
};
44+
const params = new URLSearchParams(window.location.search);
45+
const value = serializeAdvancedColumns(next);
46+
if (value) {
47+
params.set(ADVANCED_COLUMNS_PARAM, value);
48+
} else {
49+
params.delete(ADVANCED_COLUMNS_PARAM);
50+
}
51+
updateRawSearchParams(params);
52+
};
4153

4254
const columns = [
4355
{
4456
label: "Cliff's Delta",
4557
checked: cliffsDelta,
46-
onChange: toggle(updateShowCliffsDelta, 'showCliffsDelta'),
58+
onChange: (checked: boolean) =>
59+
applyAdvancedColumns({ cliffsDelta: checked, cles }),
4760
},
4861
{
4962
label: 'CLES',
5063
checked: cles,
51-
onChange: toggle(updateShowCles, 'showCles'),
64+
onChange: (checked: boolean) =>
65+
applyAdvancedColumns({ cliffsDelta, cles: checked }),
5266
},
5367
];
5468

src/components/CompareResults/ResultsTable.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import TableHeader from './TableHeader';
1212
import { MANN_WHITNEY_U } from '../../common/constants';
1313
import useAdvancedColumns from '../../hooks/useAdvancedColumns';
1414
import useRawSearchParams from '../../hooks/useRawSearchParams';
15+
import useSeedAdvancedColumnsFromUrl from '../../hooks/useSeedAdvancedColumnsFromUrl';
1516
import useTableFilters from '../../hooks/useTableFilters';
1617
import useTableSort from '../../hooks/useTableSort';
1718
import { Framework, TestVersion } from '../../types/types';
@@ -36,6 +37,7 @@ export default function ResultsTable() {
3637
// This is our custom hook that updates the search params without a rerender.
3738
const [rawSearchParams, updateRawSearchParams] = useRawSearchParams();
3839

40+
useSeedAdvancedColumnsFromUrl();
3941
const advancedColumns = useAdvancedColumns();
4042

4143
const columnsConfig = useMemo(

src/components/CompareResults/SubtestsResults/SubtestsResultsTable.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import NoResultsFound from '.././NoResultsFound';
99
import TableHeader from '.././TableHeader';
1010
import { STUDENT_T } from '../../../common/constants';
1111
import useAdvancedColumns from '../../../hooks/useAdvancedColumns';
12+
import useSeedAdvancedColumnsFromUrl from '../../../hooks/useSeedAdvancedColumnsFromUrl';
1213
import useTableFilters, { filterResults } from '../../../hooks/useTableFilters';
1314
import useTableSort, { sortResults } from '../../../hooks/useTableSort';
1415
import type { CombinedResultsItemType } from '../../../types/state';
@@ -80,6 +81,7 @@ function SubtestsResultsTable({
8081
replicates,
8182
testVersion,
8283
}: ResultsTableProps) {
84+
useSeedAdvancedColumnsFromUrl();
8385
const advancedColumns = useAdvancedColumns();
8486
const columnsConfiguration = useMemo(
8587
() =>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useEffect } from 'react';
2+
3+
import { useAppDispatch } from './app';
4+
import {
5+
updateShowCliffsDelta,
6+
updateShowCles,
7+
} from '../reducers/ColumnPrefsSlice';
8+
import {
9+
ADVANCED_COLUMNS_PARAM,
10+
parseAdvancedColumns,
11+
} from '../utils/advancedColumnsUrl';
12+
13+
// On mount, seed the advanced-column visibility from the URL so a shared link
14+
// reproduces the selected columns. Toggling updates both the URL (for sharing)
15+
// and Redux (for reactive rendering); this only handles the initial
16+
// URL → Redux direction. Call once per results view.
17+
function useSeedAdvancedColumnsFromUrl() {
18+
const dispatch = useAppDispatch();
19+
useEffect(() => {
20+
const params = new URLSearchParams(window.location.search);
21+
if (!params.has(ADVANCED_COLUMNS_PARAM)) {
22+
return;
23+
}
24+
const { cliffsDelta, cles } = parseAdvancedColumns(params);
25+
dispatch(updateShowCliffsDelta(cliffsDelta));
26+
dispatch(updateShowCles(cles));
27+
}, []);
28+
}
29+
30+
export default useSeedAdvancedColumnsFromUrl;

src/reducers/ColumnPrefsSlice.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
22

3-
// Results-view display preferences, persisted to localStorage so choices
4-
// survive reloads:
3+
// Results-view display preferences:
54
// - showCliffsDelta / showCles: the two advanced statistics columns, toggled
6-
// independently from the "Advanced columns" dropdown. Either, both, or
7-
// neither can be shown.
5+
// independently from the "Advanced columns" dropdown. Persisted in the URL
6+
// (see utils/advancedColumnsUrl) so shared links reproduce the selection;
7+
// seeded into this slice on mount. Default off (the simplified view).
88
// - showHowToRead: when true the "How to read the results" helper panel is
9-
// shown above the table. Defaults on so new contributors get the guidance;
10-
// dismissing it (once) is remembered.
9+
// shown above the table. Persisted to localStorage.
1110
const initialState: {
1211
showCliffsDelta: boolean;
1312
showCles: boolean;
1413
showHowToRead: boolean;
1514
} = {
16-
showCliffsDelta: localStorage.getItem('showCliffsDelta') === 'true',
17-
showCles: localStorage.getItem('showCles') === 'true',
15+
showCliffsDelta: false,
16+
showCles: false,
1817
showHowToRead: localStorage.getItem('showHowToRead') !== 'false',
1918
};
2019

src/utils/advancedColumnsUrl.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { AdvancedColumns } from '../types/types';
2+
3+
// The advanced (power-user) columns are persisted in the URL so a shared link
4+
// reproduces the selected columns. Encoded as a comma-separated list of the
5+
// enabled column keys, e.g. `?advanced_columns=cliffs_delta,cles`. An absent
6+
// param means neither is shown (the simplified view).
7+
export const ADVANCED_COLUMNS_PARAM = 'advanced_columns';
8+
9+
const CLIFFS_DELTA = 'cliffs_delta';
10+
const CLES = 'cles';
11+
12+
// Parse advanced-column visibility from a URL search string or params.
13+
export function parseAdvancedColumns(
14+
search: string | URLSearchParams,
15+
): AdvancedColumns {
16+
const params =
17+
typeof search === 'string' ? new URLSearchParams(search) : search;
18+
const enabled = (params.get(ADVANCED_COLUMNS_PARAM) ?? '')
19+
.split(',')
20+
.filter(Boolean);
21+
return {
22+
cliffsDelta: enabled.includes(CLIFFS_DELTA),
23+
cles: enabled.includes(CLES),
24+
};
25+
}
26+
27+
// Serialize to the comma-list value, or null when no advanced column is on so
28+
// the caller can delete the param and keep shared URLs clean.
29+
export function serializeAdvancedColumns(
30+
advanced: AdvancedColumns,
31+
): string | null {
32+
const enabled = [
33+
advanced.cliffsDelta ? CLIFFS_DELTA : null,
34+
advanced.cles ? CLES : null,
35+
].filter(Boolean);
36+
return enabled.length ? enabled.join(',') : null;
37+
}

0 commit comments

Comments
 (0)