fix(search): show applied type and faith by label, in any case
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 22s

The results list's chip printed the school type group's key ("post16")
rather than its label. A URL value that no option spelled the same way
(an old link's raw GIAS type, or a key in another case) left the select
reading "Any school type" while the list stayed filtered, and choosing
"Any" changed nothing. Values now match options ignoring case, as the
API does, and an unlisted one is shown as an option of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TudorandClaude Opus 5.5 committed 2026-10-02 12:00:54 +01:00
1 parent c992d7f3b9
commit 84caee9f72
4 files changed
+75 -5

No files matched your search

@@ -112,3 +112,27 @@ describe('Faith', () => {
expect(within(openSheet()).queryByRole('combobox', { name: 'Faith' })).not.toBeInTheDocument();
});
});
describe('a URL value the options do not spell the same way', () => {
const row = () => screen.getByRole('group', { name: 'Filters' });
it('shows an old raw-label type in the select, and lets "Any" clear it', () => {
params = new URLSearchParams('search=school&school_type=Community+school');
render(<FilterBar filters={filters} />);
const type = within(row()).getByRole('combobox', { name: 'School type' });
expect(type).toHaveValue('Community school');
expect(within(type).getByRole('option', { name: 'Community school' })).toBeInTheDocument();
fireEvent.change(type, { target: { value: '' } });
expect(pushedParams().get('school_type')).toBeNull();
});
it('matches a key in another case, in the select and the chip', () => {
params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=Special&faith=Roman_Catholic');
render(<FilterBar filters={filters} />);
expect(within(row()).getByRole('combobox', { name: 'School type' })).toHaveValue('special');
expect(screen.getByRole('button', { name: 'Remove filter: Special school (SEND)' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Remove filter: Roman Catholic' })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: 'Faith' })).toHaveValue('roman_catholic');
});
});
@@ -166,3 +166,14 @@ describe('the folded search', () => {
expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument();
});
});
describe('the results list filter chips', () => {
it('name a school type group by its label, not its key', () => {
params = new URLSearchParams('search=southmead&school_type=council');
const { container } = render(<HomeView initialSchools={results()} filters={filters} />);
const chips = container.querySelector('[class*="activeFilters"]')!;
expect(chips).toHaveTextContent('State school: council-run');
expect(chips).not.toHaveTextContent(/\bcouncil\b(?!-run)/);
});
});
+34 -4
View File
@@ -9,7 +9,7 @@ import { useSchoolSuggest } from "@/hooks/useSchoolSuggest";
import { SuggestList, suggestOptionId } from "./SuggestList";
import { FilterSheet, SheetField, RADIUS_OPTIONS, radiusLabel as milesLabel } from "./FilterSheet";
import type { Suggestion } from "@/lib/suggest";
import type { Filters, ResultFilters } from "@/lib/types";
import type { FilterOption, Filters, ResultFilters } from "@/lib/types";
import styles from "./FilterBar.module.css";
interface FilterBarProps {
@@ -84,6 +84,27 @@ function hasSecondaryFilters(phase: string): boolean {
return !(p === "nursery" || (p.includes("primary") && !p.includes("secondary")));
}
/**
* The option a URL value names, ignoring case as the API does. An old link's
* raw GIAS type names none, and is shown as itself.
*/
function optionFor(options: FilterOption[], value: string) {
const v = value.toLowerCase();
return options.find((o) => o.value.toLowerCase() === v);
}
/**
* A select's value and, when the URL holds something no option spells, an
* extra option for it, so the select shows what is applied and "Any" can be
* chosen to clear it. Without one, React shows the first option, "Any",
* while the list is still filtered, and choosing it changes nothing.
*/
function selectedOf(options: FilterOption[], value: string) {
if (!value) return { value, unlisted: null };
const match = optionFor(options, value);
return match ? { value: match.value, unlisted: null } : { value, unlisted: value };
}
function SlidersIcon() {
return (
<svg
@@ -444,7 +465,7 @@ export function FilterBar({
const labelled = { school_type: typeOptions, faith: faithOptions }[
key as "school_type" | "faith"
];
if (labelled) return labelled.find((o) => o.value === value)?.label ?? value;
if (labelled) return optionFor(labelled, value)?.label ?? value;
// Phase, gender and admissions values are lowercased option names; the
// rest are the names themselves.
const named: Partial<Record<FilterKey, string[]>> = {
@@ -490,16 +511,22 @@ export function FilterBar({
</SelectShell>
);
const typeSelected = selectedOf(typeOptions, currentType);
const faithSelected = selectedOf(faithOptions, currentFaith);
const typeSelect = (look: Look) => (
<SelectShell wide={look !== "pill"}>
<select
value={currentType}
value={typeSelected.value}
onChange={(e) => handleFilterChange("school_type", e.target.value)}
className={selectClass(look, currentType)}
aria-label="School type"
disabled={isPending && look !== "sheet"}
>
<option value="">Any school type</option>
{typeSelected.unlisted && (
<option value={typeSelected.unlisted}>{typeSelected.unlisted}</option>
)}
{typeOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
@@ -531,13 +558,16 @@ export function FilterBar({
const faithSelect = (look: Look) => (
<SelectShell wide>
<select
value={currentFaith}
value={faithSelected.value}
onChange={(e) => handleFilterChange("faith", e.target.value)}
className={selectClass(look, currentFaith)}
aria-label="Faith"
disabled={isPending && look !== "sheet"}
>
<option value="">Any faith or none</option>
{faithSelected.unlisted && (
<option value={faithSelected.unlisted}>{faithSelected.unlisted}</option>
)}
{faithOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
+6 -1
View File
@@ -292,6 +292,11 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
const isLocationSearch = !!searchParams.get('postcode');
const isSearchActive = !!(hasSearch || searchParams.get('local_authority') || searchParams.get('school_type'));
// The URL carries a type group's key; the chip reads its label. An old link's
// raw GIAS type reads as itself.
const schoolTypeParam = searchParams.get('school_type');
const schoolTypeChip = (schoolTypeParam && filters.school_type_groups?.find(
(o) => o.value.toLowerCase() === schoolTypeParam.toLowerCase())?.label) || schoolTypeParam;
const currentPhase = searchParams.get('phase') || '';
const secondaryCount = allSchools.filter(s => s.attainment_8_score != null).length;
const primaryCount = allSchools.filter(s => s.rwm_expected_pct != null).length;
@@ -811,7 +816,7 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
<div className={styles.activeFilters}>
{searchParams.get('search') && <span className={styles.filterChip}>Search: {searchParams.get('search')}<a href="/" className={styles.chipRemove} onClick={e => { e.preventDefault(); }}>×</a></span>}
{searchParams.get('local_authority') && <span className={styles.filterChip}>{searchParams.get('local_authority')}</span>}
{searchParams.get('school_type') && <span className={styles.filterChip}>{searchParams.get('school_type')}</span>}
{schoolTypeParam && <span className={styles.filterChip}>{schoolTypeChip}</span>}
</div>
)}