Search: store foundation for static-filter URL contract (SEARCH-219, 1/3)#49038
Conversation
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Code Coverage SummaryCoverage changed in 3 files.
Full summary · PHP report · JS report If appropriate, add one of these labels to override the failing coverage check:
Covered by non-unit tests
|
…StaticFilterSelections return shape (PR #49038) - Move the orphaned setFilter() JSDoc block back to sit immediately above the setFilter() declaration. The insertion of setStaticFilter() pushed the block out of place; JSDoc processors attach to the next decl, so setFilter() was effectively undocumented. - Add RESERVED_PARAMS guard to stateToUrlParams' static-filter serializer so a misconfigured filter whose filter_id matches a reserved param (s / orderby / min_price / max_price) can't silently overwrite the search query / sort / price params. Mirrors urlParamsToState which already filtered these. - Align gateStaticFilterSelections' return shape with gateActiveFilters: now returns { gated, droppedAny } so callers in handlePopState() and initialize() can skip the state write when the gate was a no-op. The prior length-comparison fallback in initialize() collapses to a single boolean check. Addresses claude[bot]'s review on PR #49038. <!-- jp-loop -->
This comment has been minimized.
This comment has been minimized.
Adds the JS store plumbing the upcoming `jetpack-search/filter-static` block depends on. Foundation-only — inert until a block contributes a `kind: 'static'` filterConfig entry. - `store/url-state.js`: `stateToUrlParams` accepts a `staticFilterSelections` slice and serializes each `kind === 'static'` entry as a scalar `?filter_id=value` param (no `[]` suffix). `urlParamsToState` mirrors the parse direction — scalar params land in `staticFilterSelections` only when their key is registered as kind=static, so plugin-emitted scalar params can't pollute state. - `store/index.js`: new `staticFilterSelections` state slice, `setStaticFilter` (replace, not toggle) and `onStaticFilterChange` actions, `gateStaticFilterSelections` helper. Slice threaded through `syncToUrl`, `handlePopState`, `clearFilters`, `initialize()` re-gating, the search-request builder, and the `hasActiveFilters` / `activeFilterCount` getters. - `store/api.js`: `buildStaticFilterClauses` emits ES `term` clauses keyed by the static filter id; folded into the existing `bool.must` pipeline alongside dynamic facet filters, static post-type clauses, and the price range. URL contract matches the legacy instant-search overlay's static-filter shape (scalar, single value per key) so deep links round-trip between the two surfaces. Precedent: `priceRange` is a sibling slice with the same shape (see `search-blocks/AGENTS.md` "URL format"). Test coverage: - url-state.test.js (+8 cases): serialize gated on kind=static; parse pulls only configured keys; ignores unrelated scalar params; empty values dropped; last-write-wins on duplicate params. - store.test.js (+7 cases): setStaticFilter replace/clear; clearFilters resets the slice; gateStaticFilterSelections rule set including a prototype-pollution defence. - api.test.js (+6 cases): buildStaticFilterClauses gating + empty-value drop; buildSearchUrl integration alone and combined with dynamic filters.
…StaticFilterSelections return shape (PR #49038) - Move the orphaned setFilter() JSDoc block back to sit immediately above the setFilter() declaration. The insertion of setStaticFilter() pushed the block out of place; JSDoc processors attach to the next decl, so setFilter() was effectively undocumented. - Add RESERVED_PARAMS guard to stateToUrlParams' static-filter serializer so a misconfigured filter whose filter_id matches a reserved param (s / orderby / min_price / max_price) can't silently overwrite the search query / sort / price params. Mirrors urlParamsToState which already filtered these. - Align gateStaticFilterSelections' return shape with gateActiveFilters: now returns { gated, droppedAny } so callers in handlePopState() and initialize() can skip the state write when the gate was a no-op. The prior length-comparison fallback in initialize() collapses to a single boolean check. Addresses claude[bot]'s review on PR #49038. <!-- jp-loop -->
3abcffa to
dbca1bf
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…g (PR #49038) Block-side static-filter radios need a reactive checked binding so they stay in sync with the store across paths that mutate state.staticFilterSelections outside the radio's own change event — the clearFilters() reset and the popstate-driven URL rewind. Without this, radios visually keep their last clicked state even after the store has cleared the slice. The callback reads the per-<li> context (filterKey + optionValue) that the filter-static block's render.php will emit, and compares against state.staticFilterSelections[filterKey]. Each radio in render.php then binds: data-wp-bind--checked="callbacks.isStaticFilterSelected" Server-rendered `checked` still seeds first paint; this takes over after hydration. Surfaced during the review cycle on PR #49040. <!-- jp-loop -->
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…nt (PR #49040) Two findings from claude[bot]'s re-review on PR #49040: - Bug: Radio state drifted from the store after clearFilters() or handlePopState() because the radios had no reactive checked binding — the change event only fires on user-initiated transitions, so any state mutation through other paths left the DOM stale. Fix: - Add per-<li> data-wp-context with the option's value so the new callbacks.isStaticFilterSelected (landed in the foundation PR #49038) can compare it against state.staticFilterSelections. - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to each radio input. Server-rendered `checked` still seeds first paint; the reactive binding takes over after IA hydration. - Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count was '19 + 8'; updated to '20 + 8' to reflect the new filter-static entry in BLOCK_ICONS. Also deferred: the 're-pick a checked radio to deselect' line in the prior testing instructions is unreachable (browser change event semantics) — removing it from the PR body in a follow-up edit instead of carrying a separate code change. <!-- jp-loop -->
…9038) Two non-blocker nits from claude[bot]'s re-review: - afterEach in the 'store callbacks' describe block now resets state.staticFilterSelections to {} so the assignment at the end of the isStaticFilterSelected test doesn't leak into later cases in the same block. Currently benign (no downstream reader), but defensive. - Add a test pinning the ?. guard on state.staticFilterSelections: when the slice is undefined (PHP seed hasn't populated it yet, e.g. on a page with no filter-static block), the callback returns false instead of crashing. <!-- jp-loop -->
…nt (PR #49040) Two findings from claude[bot]'s re-review on PR #49040: - Bug: Radio state drifted from the store after clearFilters() or handlePopState() because the radios had no reactive checked binding — the change event only fires on user-initiated transitions, so any state mutation through other paths left the DOM stale. Fix: - Add per-<li> data-wp-context with the option's value so the new callbacks.isStaticFilterSelected (landed in the foundation PR #49038) can compare it against state.staticFilterSelections. - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to each radio input. Server-rendered `checked` still seeds first paint; the reactive binding takes over after IA hydration. - Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count was '19 + 8'; updated to '20 + 8' to reflect the new filter-static entry in BLOCK_ICONS. Also deferred: the 're-pick a checked radio to deselect' line in the prior testing instructions is unreachable (browser change event semantics) — removing it from the PR body in a follow-up edit instead of carrying a separate code change. <!-- jp-loop -->
) Pre-existing 'Code coverage requirement' check flagged that the threading paths through hasActiveFilters / activeFilterCount / syncToUrl weren't exercised end-to-end. Added: - hasActiveFilters + activeFilterCount include staticFilterSelections — counts entries with non-empty values, treats empty-string ('cleared') entries as inactive. - syncToUrl writes static-filter selections as scalar params alongside other state — regression guard against a future change forgetting to thread the slice through pushStateToUrl. - beforeEach now resets state.staticFilterSelections so subsequent tests don't inherit leftover values from earlier ones (the new syncToUrl test would otherwise leak its assignment forward). <!-- jp-loop -->
…nt (PR #49040) Two findings from claude[bot]'s re-review on PR #49040: - Bug: Radio state drifted from the store after clearFilters() or handlePopState() because the radios had no reactive checked binding — the change event only fires on user-initiated transitions, so any state mutation through other paths left the DOM stale. Fix: - Add per-<li> data-wp-context with the option's value so the new callbacks.isStaticFilterSelected (landed in the foundation PR #49038) can compare it against state.staticFilterSelections. - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to each radio input. Server-rendered `checked` still seeds first paint; the reactive binding takes over after IA hydration. - Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count was '19 + 8'; updated to '20 + 8' to reflect the new filter-static entry in BLOCK_ICONS. Also deferred: the 're-pick a checked radio to deselect' line in the prior testing instructions is unreachable (browser change event semantics) — removing it from the PR body in a follow-up edit instead of carrying a separate code change. <!-- jp-loop -->
This comment has been minimized.
This comment has been minimized.
🤖 Review-cycle summary —
|
| Source | Item | Resolution |
|---|---|---|
claude[bot] |
Orphaned setFilter JSDoc | Moved back above setFilter (dbca1bf6c0). |
claude[bot] |
Missing RESERVED_PARAMS guard in stateToUrlParams | Added symmetric guard with urlParamsToState (dbca1bf6c0). |
claude[bot] |
gateStaticFilterSelections return shape asymmetry with gateActiveFilters | Now returns { gated, droppedAny } (dbca1bf6c0). |
claude[bot] (PR #49040 surfacing) |
Radio state drift after clearFilters() / handlePopState() — needed reactive binding | Added callbacks.isStaticFilterSelected (7b0731e9d9). |
claude[bot] |
Test afterEach didn't reset staticFilterSelections + missing undefined-slice test | Reset in afterEach + new test pinning the ?. guard (64f937627d). |
Test count: 540 → 554 (+14) JS tests passing.
CI: 59 functional checks pass. The one failing check — Code coverage requirement — is an informational threshold; integration tests added in 294993299b lifted coverage of the threading paths, but the line-coverage gate still flags. Per the review-cycle contract, non-security informational failures don't block the clean transition.
Reviewer outcome:
claude[bot](round 3 + 4 re-reviews): "Good to merge from a review standpoint."copilot-swe-agent: "I don't see new blocking correctness/security issues."
…nt (PR #49040) Two findings from claude[bot]'s re-review on PR #49040: - Bug: Radio state drifted from the store after clearFilters() or handlePopState() because the radios had no reactive checked binding — the change event only fires on user-initiated transitions, so any state mutation through other paths left the DOM stale. Fix: - Add per-<li> data-wp-context with the option's value so the new callbacks.isStaticFilterSelected (landed in the foundation PR #49038) can compare it against state.staticFilterSelections. - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to each radio input. Server-rendered `checked` still seeds first paint; the reactive binding takes over after IA hydration. - Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count was '19 + 8'; updated to '20 + 8' to reflect the new filter-static entry in BLOCK_ICONS. Also deferred: the 're-pick a checked radio to deselect' line in the prior testing instructions is unreachable (browser change event semantics) — removing it from the PR body in a follow-up edit instead of carrying a separate code change. <!-- jp-loop -->
First of three sequential PRs for SEARCH-219.
staticFilterSelectionsslice, scalar URL serialization, ES term clauses. Inert until a block contributes akind:'static'filterConfig entry.Filter_Statichelper +/wp-json/jetpack-search/v1/static-filtersREST endpoint + thestaticFilterSelectionsseed slot.jetpack-search/filter-staticblock + editor wiring + user-visible changelog entry.Proposed changes
store/url-state.js—stateToUrlParamsserializesstaticFilterSelectionsas scalar?filter_id=valueparams, gated onfilterConfigs[key].kind === 'static'.urlParamsToStatemirrors the parse direction; only keys whose filterConfigs entry is registered as kind=static land in the slice, so plugin-emitted scalar params can't pollute it.store/index.js— newstaticFilterSelectionsstate slice (sibling ofpriceRange),setStaticFilter(replace, not toggle) andonStaticFilterChangeactions,gateStaticFilterSelectionshelper. Slice is threaded throughsyncToUrl,handlePopState,clearFilters,initialize()re-gating, the search-request builder, and thehasActiveFilters/activeFilterCountgetters.store/api.js—buildStaticFilterClausesemits EStermclauses keyed by the static filter id; folded into the existingbool.mustpipeline alongside dynamic facet filters, static post-type clauses, and the price range.URL contract matches the legacy instant-search overlay's static-filter shape (scalar, single value per key) so deep links round-trip between the two surfaces. Precedent:
priceRangeis a sibling slice with the same shape (seesearch-blocks/AGENTS.md"URL format").Related product discussion/links
Does this pull request change what data or activity we track or use?
No.
Testing instructions
This PR is internal plumbing — nothing visible without parts 2 and 3. The new Jest cases exercise the surface:
pnpm exec jest tests/js/search-blocks/url-state.test.js— 63 cases passing (8 new for the static-filter branch).pnpm exec jest tests/js/search-blocks/store.test.js— coversgateStaticFilterSelections,setStaticFilterreplace/clear,clearFiltersreset.pnpm exec jest tests/js/search-blocks/api.test.js— coversbuildStaticFilterClausesplusbuildSearchUrlintegration alone and combined with dynamic filters.