Make the flag filters switches, and drop the half nothing asks for - #233
Merged
Conversation
They shipped as three-state chip rows -- All / Breaking / Not breaking -- on the reasoning that an editor clearing a false alarm wants the stories that are not flagged. In use that third state earns nothing: the reason to open this screen is to find what IS flagged, and two rows of three chips cost more room in the filter bar than the one question they answer. Each is now a switch: on narrows to the flagged articles, off does not filter. Hand-rolled, because there is no switch in components/ui and no @radix-ui/react-switch in the tree, and pulling one in for two toggles would be more code than the twenty lines here. role="switch" with aria-checked is what makes it announce correctly; the track and knob are aria-hidden spans inside the button rather than focusable elements of their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4qBhBdQto1yNp1zP7VLYc
?breaking=false and ?featured=false answered with the articles that do NOT carry the flag. Nothing asks for that: the screen's switches cannot, and a switch is the only client there is. It was carried on the theory that someone would want it later, which is how a second, subtler code path stays in the listing query earning nothing. What goes with it is the COALESCE(col, 0) = 0 form that made it correct. Both columns are nullable and the WordPress archive holds NULL, so the unflagged half had to be spelled carefully or it would have answered with only the few hundred rows the CMS itself wrote. The remaining `= 1` needs no such care -- NULL simply is not 1 -- so removing the half removes the trap with it. The parameter still parses the same set of true spellings, including bare ?breaking. Anything else, false included, now means "do not filter", and the test pins that so it cannot quietly start inverting instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4qBhBdQto1yNp1zP7VLYc
ssavutu
force-pushed
the
fix/flag-filter-toggles
branch
from
September 4, 2026 03:38
edb217b to
1dfc2a5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #231, which shipped Breaking and Featured as three-state chip rows: All / Breaking / Not breaking, and the same for Featured.
Switches
That third state does not earn its place. The reason to open the articles screen is to find what is flagged — which stories are still breaking, what is currently pinned. "Not breaking" is a question nobody asks, and two rows of three chips cost more room in the filter bar than the one question they answer.
Hand-rolled rather than a dependency: there is no switch in
components/uiand no@radix-ui/react-switchin the tree, and pulling one in for two toggles would be more code than the twenty lines here.role="switch"witharia-checkedis what makes it announce correctly; the track and knob arearia-hiddenspans inside the button rather than focusable elements of their own. Knob travel is 3px of padding on both ends — checked against the box model and rendered headless, because a hand-rolled switch is easy to get a pixel wrong.Dropping the unflagged half
?breaking=falseand?featured=falseanswered with the articles that do not carry the flag. Nothing asks for that — the screen's switches cannot, and a switch is the only client there is. It was kept on the theory someone would want it later, which is how a second, subtler branch stays in the listing query earning nothing.What goes with it is the
COALESCE(col, 0) = 0form that made it correct. Both columns are nullable and the WordPress archive holds NULL, so the unflagged half had to be spelled carefully or it would have answered with only the few hundred rows the CMS itself wrote. The remaining= 1needs no such care — NULL simply is not 1 — so removing the half removes the trap with it.The parameter still parses the same true spellings, including bare
?breaking. Anything else,falseincluded, now means "do not filter", and a test pins that so it cannot quietly start inverting instead. The NULL-flagged seed row stays too: it is what proves= 1keeps nine thousand imported rows out of a list of breaking stories.Note on persisted filter state
The screen remembers its filters in
sessionStorage. The old key (breakingFilter: "all" | "on" | "off") is no longer read; the new one defaults to off. An editor with the tab already open and "Not breaking" selected sees the filter clear itself once, which is the correct end state rather than a stale one.Testing
Run against a real MariaDB 11.8 (
CMS_TEST_DSN), not just compiled: the full server suite passes, includingTestArticleFlagFiltersHTTP. Worth stating plainly because CI has no database service, so every integration test in this repo skips there — a greenbackendcheck does not mean these ran.go build,go vet,tsc --noEmit, eslint and vitest (20 tests) all pass.swag init --parseDependency --parseInternalrerun for the docs gate; that diff is only the two reworded descriptions.🤖 Generated with Claude Code
https://claude.ai/code/session_01L4qBhBdQto1yNp1zP7VLYc