Skip to content

fix(web): align filter panel sections across every view (#802)#810

Merged
Deeds67 merged 2 commits into
mainfrom
fix/802-filter-panel-parity
Jul 23, 2026
Merged

fix(web): align filter panel sections across every view (#802)#810
Deeds67 merged 2 commits into
mainfrom
fix/802-filter-panel-parity

Conversation

@Deeds67

@Deeds67 Deeds67 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

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 sections array, and they had drifted apart:

Surface Before After
Photos, Spaces 10 (all) 10 (all)
Map (map-filter-config.ts) 9 — no text 10
Album detail (album-filter-config.ts) 8 — no text, no albums 9 (albums intentionally excluded)
Album asset picker 8 — shared the album-detail list 10

ALL_FILTER_SECTIONS is now the single source of truth every surface derives from, so a section added in one place appears everywhere. Exclusions must be justified in filter-section-parity.spec.ts.

Deliberate exclusion: albums on album detail

The reporter noted some divergence may be intentional, and this is the one case. asset.repository.ts:369,374 guards both isInAlbum and isNotInAlbum with && !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 albums and text: it isn't album-scoped server-side (timelineAlbumId is stripped in timeline-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 text to the Map panel client-side alone would have been worse than the bug. The map timeline hits TimeBucketDto, which already supports description/originalFileName/ocr — but map markers go through FilteredMapMarkerDto, which had none of them. The pins would have kept showing assets the timeline had already filtered out.

searchAssetBuilder already 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. getFilteredMapMarkers passed rating but never ratingIsMinimum, so searchAssetBuilder fell 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_SECTIONS to 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, and buildMapTimelineOptions(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 gets text revealed, 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 reaches getFilteredMapMarkers with all three fields.
  • gallery-map.dto.spec.ts — the three fields via describe.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 both ratingIsMinimum cases.

Verification

Gate Result
server unit 5114 passed, 9 skipped
web unit 3689 passed, 2 skipped, 8 todo
server tsc / eslint / prettier clean
web tsc / prettier clean
web eslint 0 errors (613 pre-existing tailwind warnings)

OpenAPI + SDK + Dart client regenerated; the diff is confined to the map-markers endpoint.

Not included

FilterSuggestionsRequestDto still 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.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.

@Deeds67 Deeds67 added the changelog:fix Bug fix for changelog label Jul 19, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation 🗄️server 🖥️web labels Jul 19, 2026
Deeds67 added 2 commits July 21, 2026 22:23
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
Deeds67 force-pushed the fix/802-filter-panel-parity branch from 6eca1ca to d7b589a Compare July 21, 2026 20:28
@Deeds67
Deeds67 merged commit 0d1bd4b into main Jul 23, 2026
44 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:fix Bug fix for changelog documentation Improvements or additions to documentation 🗄️server 🖥️web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Filter panel options differ between views (e.g. Photos vs Map) for the same user

1 participant