feat(search): parametrise search facets + My Objects in the URL — ST-1b of #1825#1834
Merged
Merged
Conversation
…s[]=…&my=…) — ST-1b of #1825 Extend ST-1a's URL-as-source-of-truth from the query to the 8 facets + My Objects, so a FACETED search is shareable, bookmarkable, and back/forward-correct. - Reader: the Search page CREATEs a fresh session per distinct URL state (server search() = removeUnselected = REPLACE), because updateFacets MERGEs a delta and could never REMOVE a facet the URL dropped (deselect / Clear-All / class-switch) — the create-per-URL-state design. - Mirror: a slice→URL writer (reacting to redux facetState, gated by isFacetsStateSynced, debounced, pushed) serialises every facet change to the URL from ALL toggle sites — the Filters sidebar, chip-✕ deselect, Clear-All, AND the Results class / My-Objects tabs — without touching them. - MainSearchInput merges q into the current params (function updater) so committing a query preserves the active filters instead of clobbering them. - Race-fix: create-per-URL-state REPLACE could drop an option toggled while a create is in flight (a rapid 2nd selection); updateSearchState now carries pending-unsynced locals across the new-session REPLACE so no selection is lost, and stays !synced so the mirror re-fires the newer state. Facet ids only (catalog metadata, no PII); recipient-scoped re-eval inherited from /api/search; fail-closed parse. Legacy /search/{sessionId} deep-links + the /api/search contract unchanged (D9). Unit (node 24): searchUrlState facets/myObjects/removal/fail-closed round-trips + the slice race-fix (pending-local carry) + useQueryParams (ST-1a, unchanged) — 18/18 green. Part of #1825 — ST-1b (no closing keyword; the epic closes at ST-4 + ST-7..ST-13). Consumer-read: Search.tsx, MainSearchInput.tsx, searchRoutes.ts, useQueryParams.ts, dataEntitySearch.slice.ts, dataentitySearch.selectors.ts, dataentitiesSearch.thunks.ts, Results.tsx, SearchResultsTabs.tsx, Filters.tsx, SingleFilterItem.tsx, SelectedFilterOption.tsx, MultipleFilterItemAutocomplete.tsx, generated-sources/{FacetState,SearchFormData,SearchFormDataFilters, SearchFilterState}; odd-platform-api SearchServiceImpl.java + FacetStateDto.java (merge-vs-replace). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Backend Coverage
|
…resolve deselects, keep chip labels (ST-1b B1) The /review of ST-1b found BLOCKER B1: after a sidebar-facet deselect / Clear-All (or any statuses select), isFacetsStateSynced stranded false — the deselected id was carried as a phantom pending-local (the create echo omits unselected filters; statuses was never echoed at all), which froze the results re-fetch (Results.tsx fetches only when synced) and kept the facet→URL mirror armed to revert a later back/forward or query commit. Root-caused on the running system; three changes: - (A) FacetStateMapperImpl.mapDto echoes `statuses` like the other 7 facets (FORM_MAPPINGS already FILTERS on it; FacetState.statuses is an existing NOT_REQUIRED contract field — additive, no client regen). Without the echo a selected status is structurally indistinguishable from a pending local. - (B′) updateSearchState's new-session branch reconciles the optimistic slice against what THIS create actually requested (meta.arg.searchFormData.filters) instead of the `!(id in serverFacet)` heuristic: an option whose `selected` differs from the request is a genuine pending change (kept; synced stays false so the mirror re-fires); everything the create covered takes the authoritative server value. Fixes the stranded-synced class AND the symmetric rapid double-DESELECT lost-update (a facet deselected mid-flight is still `selected` in that create's response — the old heuristic resurrected it). - Label-preserve: a URL-derived create request carries facet IDS only, and the echo returns the request's names — name:null on the wire (captured live) — so the reducer keeps an already-known entityName when the echo has none. Without this every sidebar chip rendered a bare ✕ ~1s after selection (TextFormatted renders nothing for undefined). A FRESH deep-link still renders unlabelled chips (nothing known to preserve) — recipient-side label resolution is a tracked follow-up (the echo should then also honour SearchFilter.required:[id,name]). Tests (all RED on the pre-fix base, GREEN on the fix; run, not reasoned): - slice: a resolved deselect is dropped AND re-syncs (#3); a pending mid-flight deselect is preserved, not resurrected (#4); a name-less echo keeps the known chip label (#5); the in-flight SELECT carry (#1) + clean REPLACE (#2) stay green under the new predicate. - FacetStateMapperImplTest: mapDto echoes selected statuses (getStatuses() null on the base). - e2e IT-151 (odd-team): sidebar deselect/Clear-All reloads+broadens the results, Back reproduces the tagged state, no stale-mirror revert; a status select reaches the URL, refilters, and keeps its chip label; a statuses[]= deep-link reproduces the filtered results. 4/4 on the fix; the 2 new cases fail on the pre-fix build; all fail on the pre-ST-1b base. Part of #1825 — ST-1b (no closing keyword; the epic closes at ST-4 + ST-7..ST-13). Consumer-read: SearchServiceImpl.java, FacetStateDto.java, FacetStateMapperImpl.java, SearchMapperImpl.java, SearchController.java, FTSConstants.java (STATUSES filter semantics), DataEntityStatusDto.java, dataEntitySearch.slice.ts, dataentitySearch.selectors.ts, Search.tsx, searchUrlState.ts, Results.tsx (synced fetch-gate), SelectedFilterOption.tsx, MultipleFilterItem.tsx, MultipleFilterItemAutocomplete.tsx, TextFormatted.tsx, Filters.tsx, generated-sources/{FacetState,SearchFilter,SearchFormData,SearchFilterState}. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RamanDamayeu
approved these changes
Jul 2, 2026
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.
Part of #1825 — ST-1b (the facet half of ST-1). Builds on ST-1a (#1833, merged).
What
Extend ST-1a's URL is the source of truth from the query to the 8 facets + My Objects, so a faceted search is shareable, bookmarkable, and back/forward-correct.
search()=removeUnselected= REPLACE), becauseupdateFacetsMERGEs a delta and could never remove a facet the URL dropped (deselect / Clear-All / class-switch).facetState, gated byisFacetsStateSynced, debounced, pushed) serialises every facet change to the URL from all toggle sites — the Filters sidebar, chip-✕ deselect, Clear-All, and the Results class / My-Objects tabs — without touching them.qinto the current params (function updater) so committing a query preserves the active filters.updateSearchStatecarries pending-unsynced selections across the create REPLACE, so a rapid 2nd toggle is not lost.Facet ids only (catalog metadata, no PII); recipient-scoped re-eval inherited from
/api/search; fail-closed parse. Legacy/search/{sessionId}deep-links + the/api/searchcontract unchanged (D9).Tests
searchUrlStatefacets/removal/fail-closed round-trips + the slice race-fix (pending-local carry) +useQueryParams(ST-1a, unchanged).entityClasses[]=URL + refilter · All-tab removal · faceted-deep-link share · back/forward — GREEN on this branch, RED onmain(f63d391).Milestone: 1.0.0
Docs: documentation@
release/1.0.0(search.md— facets now ride the shareable URL) — publishes with the 1.0.0 release.🤖 Generated with Claude Code
Rework — review round 1 blocker fixed (
02f0ee60)The odd-team review rejected the first cut with a facet-sync blocker: after a sidebar-facet deselect / Clear-All (or any Statuses select),
isFacetsStateSyncedstrandedfalse— the deselected id was carried as a phantom pending-local (the create echo omits unselected filters;statuseswas never echoed at all), which froze the results re-fetch (Results.tsxfetches only when synced) and kept the facet→URL mirror armed to revert a later back/forward or query commit.Three changes (root causes fixed at the source;
Search.tsx/searchUrlState/MainSearchInputuntouched):FacetStateMapperImpl.mapDtoechoesstatuseslike the other 7 facets — it is already filtered on (FORM_MAPPINGS), andFacetState.statusesis an existing optional contract field (additive; no client regen). Also fixes a status deep-link never rendering its chip.updateSearchStatereconciles optimistic-vs-requested (meta.arg.searchFormData.filters) instead of the!(id in serverFacet)heuristic: a resolved deselect re-syncs (results reload), a mid-flight deselect is preserved rather than resurrected (the symmetric twin of the mid-flight-selection race), and the selection carry is unchanged.name:null— which blanked every selected-filter chip ~1 s after selection (a regression vs the pre-ST-1b PUT flow, found by driving the running UI; wire + pixel captured). The reducer now keeps a label it already knows. A fresh deep-link still renders unlabelled chips (nothing client-side to preserve) — server-side name resolution in the echo is a tracked follow-up (which should then also make the echo honourSearchFilter.required: [id, name]).Verification (each RED on the pre-fix build, GREEN on the fix — run, not reasoned): slice unit tests (resolved-deselect re-sync · pending-deselect preserved · label kept) +
FacetStateMapperImplTest(statuses echo) + IT-151 extended with 2 e2e cases (sidebar deselect/Clear-All reloads + broadens the results, Back reproduces, no stale-mirror revert · a status select reaches the URL, refilters, and keeps its chip label; astatuses[]=deep-link reproduces the filtered results). Full:odd-platform-api:buildgreen. Full regression re-run at this SHA: feature-complete 336 passed / 1 failed (the 1 = the known unmerged-#1815-Group-B favorites spec, contributor-independent), known-bugs 3-RED-expected, multi-stack + ingestion-e2e green.