From 84caee9f728234e89ed4d0b3b33eaded4b057645 Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 2 Oct 2026 12:00:54 +0100 Subject: [PATCH] fix(search): show applied type and faith by label, in any case 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 --- .../components/FilterBarTypeFaith.test.tsx | 24 ++++++++++++ .../components/ResultsToolbar.test.tsx | 11 ++++++ nextjs-app/components/FilterBar.tsx | 38 +++++++++++++++++-- nextjs-app/components/HomeView.tsx | 7 +++- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx b/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx index f03d1b9..0a82705 100644 --- a/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx +++ b/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx @@ -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(); + 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(); + 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'); + }); +}); + diff --git a/nextjs-app/__tests__/components/ResultsToolbar.test.tsx b/nextjs-app/__tests__/components/ResultsToolbar.test.tsx index bc16678..5a9b92e 100644 --- a/nextjs-app/__tests__/components/ResultsToolbar.test.tsx +++ b/nextjs-app/__tests__/components/ResultsToolbar.test.tsx @@ -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(); + const chips = container.querySelector('[class*="activeFilters"]')!; + expect(chips).toHaveTextContent('State school: council-run'); + expect(chips).not.toHaveTextContent(/\bcouncil\b(?!-run)/); + }); +}); + diff --git a/nextjs-app/components/FilterBar.tsx b/nextjs-app/components/FilterBar.tsx index 6265ac7..0d5a831 100644 --- a/nextjs-app/components/FilterBar.tsx +++ b/nextjs-app/components/FilterBar.tsx @@ -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 ( 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> = { @@ -490,16 +511,22 @@ export function FilterBar({ ); + const typeSelected = selectedOf(typeOptions, currentType); + const faithSelected = selectedOf(faithOptions, currentFaith); + const typeSelect = (look: Look) => (
{searchParams.get('search') && Search: {searchParams.get('search')} { e.preventDefault(); }}>×} {searchParams.get('local_authority') && {searchParams.get('local_authority')}} - {searchParams.get('school_type') && {searchParams.get('school_type')}} + {schoolTypeParam && {schoolTypeChip}}
)}