fix(web): align filter panel sections across every view (#802)#810
Merged
Conversation
The filter panel's section list was hardcoded per surface and had drifted: Photos and Spaces offered 10 sections, Map offered 9 (no "Text") and album views offered 8 (no "Text", no "Albums"). Users had to remember which filters worked where. Introduce `ALL_FILTER_SECTIONS` as the single source of truth and derive every surface from it, so a section added in one place appears everywhere. A surface may only drop a section when the server physically cannot honour it there, and each exclusion is now justified in a parity spec: - Map, Spaces, Photos, album asset picker: the full set. - Album detail: everything except "Albums" — asset.repository.ts guards isInAlbum/isNotInAlbum with `&& !options.albumId`, so inside one album they are a tautology and an empty set respectively. Rendering them would give the user two dead controls. The album asset picker gains "Albums" as well as "Text": it is not album-scoped server-side (`timelineAlbumId` is stripped before the request leaves the timeline manager), so "not in any album" works there and is the natural way to find un-filed photos to add. Wiring the text fields through: - `applyTextFilters` centralises trimming and blank-omission for description / originalFileName / ocr; map, album, photos and space option builders all use it instead of four near-identical copies. - The map timeline already hit TimeBucketDto, which supports all three, so that half was a pure client gap. Map *markers* went through FilteredMapMarkerDto, which had no text fields — so without a server change the pins would have kept showing assets the timeline had filtered out. searchAssetBuilder already implements all three predicates, so this is three DTO fields plus three lines of pass-through. Also fixes a rating-parity bug found while tracing: getFilteredMapMarkers passed `rating` but never `ratingIsMinimum`, so map markers matched exact ratings while every other surface matched `>=`, despite the DTO documenting the field as a minimum. Docs: map-filtering.md listed neither Albums nor Text, and its closing section claimed the map has no location filter — the map config has included 'location' for some time.
The Recently Added view (#805) landed on main after this branch was cut and shipped its own hardcoded copy of the ten filter sections. The lists happen to agree today, but a second source of truth is exactly the drift #802 is about, so point it at ALL_FILTER_SECTIONS and extend the parity guard to cover the surface.
Deeds67
force-pushed
the
fix/802-filter-panel-parity
branch
from
July 21, 2026 20:28
6eca1ca to
d7b589a
Compare
Deeds67
added a commit
that referenced
this pull request
Jul 23, 2026
* fix(web): align filter panel sections across every view (#802) The filter panel's section list was hardcoded per surface and had drifted: Photos and Spaces offered 10 sections, Map offered 9 (no "Text") and album views offered 8 (no "Text", no "Albums"). Users had to remember which filters worked where. Introduce `ALL_FILTER_SECTIONS` as the single source of truth and derive every surface from it, so a section added in one place appears everywhere. A surface may only drop a section when the server physically cannot honour it there, and each exclusion is now justified in a parity spec: - Map, Spaces, Photos, album asset picker: the full set. - Album detail: everything except "Albums" — asset.repository.ts guards isInAlbum/isNotInAlbum with `&& !options.albumId`, so inside one album they are a tautology and an empty set respectively. Rendering them would give the user two dead controls. The album asset picker gains "Albums" as well as "Text": it is not album-scoped server-side (`timelineAlbumId` is stripped before the request leaves the timeline manager), so "not in any album" works there and is the natural way to find un-filed photos to add. Wiring the text fields through: - `applyTextFilters` centralises trimming and blank-omission for description / originalFileName / ocr; map, album, photos and space option builders all use it instead of four near-identical copies. - The map timeline already hit TimeBucketDto, which supports all three, so that half was a pure client gap. Map *markers* went through FilteredMapMarkerDto, which had no text fields — so without a server change the pins would have kept showing assets the timeline had filtered out. searchAssetBuilder already implements all three predicates, so this is three DTO fields plus three lines of pass-through. Also fixes a rating-parity bug found while tracing: getFilteredMapMarkers passed `rating` but never `ratingIsMinimum`, so map markers matched exact ratings while every other surface matched `>=`, despite the DTO documenting the field as a minimum. Docs: map-filtering.md listed neither Albums nor Text, and its closing section claimed the map has no location filter — the map config has included 'location' for some time. * fix(web): derive Recently Added sections from the canonical list (#802) The Recently Added view (#805) landed on main after this branch was cut and shipped its own hardcoded copy of the ten filter sections. The lists happen to agree today, but a second source of truth is exactly the drift #802 is about, so point it at ALL_FILTER_SECTIONS and extend the parity guard to cover the surface.
Deeds67
added a commit
that referenced
this pull request
Jul 23, 2026
eslint-plugin-unicorn v72 on the rolling branch flags the three-space literals in the #810 filter-option specs via unicorn/prefer-string-repeat. Those specs were authored against main's older unicorn and were green there. Semantically identical rewrite; specs still pass 44/44.
Deeds67
added a commit
that referenced
this pull request
Jul 24, 2026
* fix(web): align filter panel sections across every view (#802) The filter panel's section list was hardcoded per surface and had drifted: Photos and Spaces offered 10 sections, Map offered 9 (no "Text") and album views offered 8 (no "Text", no "Albums"). Users had to remember which filters worked where. Introduce `ALL_FILTER_SECTIONS` as the single source of truth and derive every surface from it, so a section added in one place appears everywhere. A surface may only drop a section when the server physically cannot honour it there, and each exclusion is now justified in a parity spec: - Map, Spaces, Photos, album asset picker: the full set. - Album detail: everything except "Albums" — asset.repository.ts guards isInAlbum/isNotInAlbum with `&& !options.albumId`, so inside one album they are a tautology and an empty set respectively. Rendering them would give the user two dead controls. The album asset picker gains "Albums" as well as "Text": it is not album-scoped server-side (`timelineAlbumId` is stripped before the request leaves the timeline manager), so "not in any album" works there and is the natural way to find un-filed photos to add. Wiring the text fields through: - `applyTextFilters` centralises trimming and blank-omission for description / originalFileName / ocr; map, album, photos and space option builders all use it instead of four near-identical copies. - The map timeline already hit TimeBucketDto, which supports all three, so that half was a pure client gap. Map *markers* went through FilteredMapMarkerDto, which had no text fields — so without a server change the pins would have kept showing assets the timeline had filtered out. searchAssetBuilder already implements all three predicates, so this is three DTO fields plus three lines of pass-through. Also fixes a rating-parity bug found while tracing: getFilteredMapMarkers passed `rating` but never `ratingIsMinimum`, so map markers matched exact ratings while every other surface matched `>=`, despite the DTO documenting the field as a minimum. Docs: map-filtering.md listed neither Albums nor Text, and its closing section claimed the map has no location filter — the map config has included 'location' for some time. * fix(web): derive Recently Added sections from the canonical list (#802) The Recently Added view (#805) landed on main after this branch was cut and shipped its own hardcoded copy of the ten filter sections. The lists happen to agree today, but a second source of truth is exactly the drift #802 is about, so point it at ALL_FILTER_SECTIONS and extend the parity guard to cover the surface.
Deeds67
added a commit
that referenced
this pull request
Jul 24, 2026
eslint-plugin-unicorn v72 on the rolling branch flags the three-space literals in the #810 filter-option specs via unicorn/prefer-string-repeat. Those specs were authored against main's older unicorn and were green there. Semantically identical rewrite; specs still pass 44/44.
Deeds67
added a commit
that referenced
this pull request
Jul 25, 2026
* fix(web): align filter panel sections across every view (#802) The filter panel's section list was hardcoded per surface and had drifted: Photos and Spaces offered 10 sections, Map offered 9 (no "Text") and album views offered 8 (no "Text", no "Albums"). Users had to remember which filters worked where. Introduce `ALL_FILTER_SECTIONS` as the single source of truth and derive every surface from it, so a section added in one place appears everywhere. A surface may only drop a section when the server physically cannot honour it there, and each exclusion is now justified in a parity spec: - Map, Spaces, Photos, album asset picker: the full set. - Album detail: everything except "Albums" — asset.repository.ts guards isInAlbum/isNotInAlbum with `&& !options.albumId`, so inside one album they are a tautology and an empty set respectively. Rendering them would give the user two dead controls. The album asset picker gains "Albums" as well as "Text": it is not album-scoped server-side (`timelineAlbumId` is stripped before the request leaves the timeline manager), so "not in any album" works there and is the natural way to find un-filed photos to add. Wiring the text fields through: - `applyTextFilters` centralises trimming and blank-omission for description / originalFileName / ocr; map, album, photos and space option builders all use it instead of four near-identical copies. - The map timeline already hit TimeBucketDto, which supports all three, so that half was a pure client gap. Map *markers* went through FilteredMapMarkerDto, which had no text fields — so without a server change the pins would have kept showing assets the timeline had filtered out. searchAssetBuilder already implements all three predicates, so this is three DTO fields plus three lines of pass-through. Also fixes a rating-parity bug found while tracing: getFilteredMapMarkers passed `rating` but never `ratingIsMinimum`, so map markers matched exact ratings while every other surface matched `>=`, despite the DTO documenting the field as a minimum. Docs: map-filtering.md listed neither Albums nor Text, and its closing section claimed the map has no location filter — the map config has included 'location' for some time. * fix(web): derive Recently Added sections from the canonical list (#802) The Recently Added view (#805) landed on main after this branch was cut and shipped its own hardcoded copy of the ten filter sections. The lists happen to agree today, but a second source of truth is exactly the drift #802 is about, so point it at ALL_FILTER_SECTIONS and extend the parity guard to cover the surface.
Deeds67
added a commit
that referenced
this pull request
Jul 25, 2026
eslint-plugin-unicorn v72 on the rolling branch flags the three-space literals in the #810 filter-option specs via unicorn/prefer-string-repeat. Those specs were authored against main's older unicorn and were green there. Semantically identical rewrite; specs still pass 44/44.
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.
Fixes #802.
The report
The filter panel showed a different set of sections depending on which view you were in. Photos offered 10 sections; Map offered 9, missing Text entirely.
Root cause
Each surface hardcoded its own
sectionsarray, and they had drifted apart:map-filter-config.ts)textalbum-filter-config.ts)text, noalbumsalbumsintentionally excluded)ALL_FILTER_SECTIONSis now the single source of truth every surface derives from, so a section added in one place appears everywhere. Exclusions must be justified infilter-section-parity.spec.ts.Deliberate exclusion:
albumson album detailThe reporter noted some divergence may be intentional, and this is the one case.
asset.repository.ts:369,374guards bothisInAlbumandisNotInAlbumwith&& !options.albumId— scoped to a single album, the former is a tautology and the latter an always-empty set, and the server drops them. Rendering the section would give two controls that do nothing.The asset picker is the opposite case and gains both
albumsandtext: it isn't album-scoped server-side (timelineAlbumIdis stripped intimeline-manager/internal/request-options.ts), so "not in any album" works there and is the natural way to find un-filed photos to add.Server change: map markers
Adding
textto the Map panel client-side alone would have been worse than the bug. The map timeline hitsTimeBucketDto, which already supportsdescription/originalFileName/ocr— but map markers go throughFilteredMapMarkerDto, which had none of them. The pins would have kept showing assets the timeline had already filtered out.searchAssetBuilderalready implements all three predicates (database.ts:862,869,874), so this is three DTO fields plus three lines of pass-through — no join or index work.Drive-by: rating parity
Found while tracing.
getFilteredMapMarkerspassedratingbut neverratingIsMinimum, sosearchAssetBuilderfell to its=branch — map markers matched exact ratings while every other surface matched>=, despite the DTO documenting the field as "Minimum star rating". Fixed here at Pierre's request, with its own tests.Tests
TDD throughout — every test was watched failing first, including a temporary revert of
ALL_FILTER_SECTIONSto confirm the localStorage upgrade-path tests were meaningful.filter-section-parity.spec.ts(new) — the regression guard: canonical set, per-surface exclusions with documented reasons, consistent ordering, no invented sections.map-filter-options/album-filter-options— text forwarding across all five builders, with edge cases: trimming,undefined, empty string, whitespace-only, each field independently, space-scoped variants, andbuildMapTimelineOptions(undefined, ...).filter-panel.spec.ts— the upgrade path, which decides whether existing users actually see the fix: a returning Map user with a pre-fix stored section set getstextrevealed, without un-hiding sections they deliberately hid, and without re-introducing it once they hide it post-fix.map-page.spec.ts— end-to-end: setting a text filter reachesgetFilteredMapMarkerswith all three fields.gallery-map.dto.spec.ts— the three fields viadescribe.each: plain strings, regex/wildcard characters verbatim, empty string, absent, non-string rejection.shared-space.service.spec.ts— pass-through (global + space-scoped + absent) and bothratingIsMinimumcases.Verification
serverunitwebunitservertsc / eslint / prettierwebtsc / prettierwebeslintOpenAPI + SDK + Dart client regenerated; the diff is confined to the map-markers endpoint.
Not included
FilterSuggestionsRequestDtostill has no text fields, so suggestion lists are computed over a wider set than the grid shows. That is pre-existing and already true on Photos, so it's consistent rather than a regression — worth a separate issue if we want suggestions to narrow by text.Docs
map-filtering.mdlisted neither Albums nor Text, and its closing section claimed the map has no location filter — the map config has includedlocationfor some time.