Search 3.0: backend support for experience field in settings API (RSM-2291)#48540
Search 3.0: backend support for experience field in settings API (RSM-2291)#48540adamwoodnz merged 11 commits intotrunkfrom
experience field in settings API (RSM-2291)#48540Conversation
experience field in settings API (RSM-2291)
|
@copilot follow the PR template please |
experience field in settings API (RSM-2291)experience field in settings API (RSM-2291)
Updated the PR description to follow the template. |
|
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! |
|
@claude review please |
This comment was marked as outdated.
This comment was marked as outdated.
Code Coverage SummaryCoverage changed in 4 files.
|
2826973 to
afbff56
Compare
`classic` collides with the older Jetpack Search "classic search"
terminology, which refers to a deprecated experience. Rename the new
non-instant non-embedded option to `inline` everywhere it appears:
the `EXPERIENCE` enum, `EXPERIENCE_ORDER`, the label/description/icon
switches, the legacy-fallback derivation in the selectors, and the
test fixtures that exercise the value.
User-facing labels are unchanged ("Theme search", "Your theme's search
layout, with faster results.") — only the wire/storage value changes.
Backend (PR #48540) needs a matching rename in `Module_Control`
constants (EXPERIENCE_CLASSIC → EXPERIENCE_INLINE) and validation list
so the wire format stays consistent end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…or (RSM-2116) The new feature selector can't actually persist changes until the back-end `experience` field lands (RSM-2291 / PR #48540), so until then the dashboard would be effectively read-only when `jetpack_search_blocks_enabled` is on. Render `ModuleControl` regardless of the feature flag for now so admins can keep managing Search settings. The `FeatureSelector` still appears above it when the flag is on, giving a preview of the new UI. Updated the dashboard-page branching test to assert both components render together when the flag is on, and that ModuleControl always renders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9a2d7aa to
fde14a1
Compare
|
@claude fresh review please |
|
Claude finished @adamwoodnz's task in 2m 27s —— View job Review
Overall, this is a well-designed implementation. The storage model (off in module state, inline as absence of option, only embedded/overlay written affirmatively) is clean and the legacy fallback path is handled correctly. A few things worth addressing: Bug / Logic Issues1. Partial failure leaves inconsistent state in
If This is a pre-existing pattern in this codebase (other callers also don't roll back), but the new Minor Issues2. Dead
}
return true; // <-- unreachable: all valid values have case branches, invalid rejected above
}The 3. Comment typo: "classic" should be "inline"
// which encapsulates the storage shape (off → module deactivate, classic → delete option,
4. Silent field-dropping when
Sending Test Coverage Notes5.
The filter 6. No test for mixed There's no REST test asserting that Nits
SummaryThe implementation logic is sound. The storage design is well-reasoned and the legacy fallback correctly handles pre-existing sites. Three things worth fixing before merge: the stale "classic" comment typo, the dead |
* Search: expose jetpack_search_blocks_enabled to dashboard initial state (RSM-2116)
Add searchBlocksEnabled field to the siteData array in Initial_State class
to expose the jetpack_search_blocks_enabled filter to the React dashboard
so it can gate the new feature-selection UI on the same flag used by the
backend to register the Interactivity API blocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search: add isSearchBlocksEnabled selector for dashboard gate (RSM-2116)
* Search: add experience selectors to jetpack-settings store (RSM-2116)
* Search: add saveExperience thunk + pending/last-saved actions (RSM-2116)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Search: add EXPERIENCE constants for feature selector (RSM-2116)
* Search: add Badge component for feature selector (RSM-2116)
* Search: add ExperienceOption component for feature selector (RSM-2116)
* Search: add FeatureSelector component (RSM-2116)
* Search: branch dashboard page on searchBlocksEnabled (RSM-2116)
Wire `dashboard-page.jsx` to render `<FeatureSelector>` when
`isSearchBlocksEnabled` is true, falling back to `<ModuleControl>` for
legacy sites. Adds branch tests that confirm both paths render correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Search: style the feature selector to match the design (RSM-2116)
* Search: changelog entry for feature-selection UI (RSM-2116)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search: refine feature-selector copy and update tests (RSM-2116)
Update the experience labels and descriptions to read more naturally —
sentence-case titles, references to "search-as-you-type" in the
Embedded and Overlay descriptions, and "Theme search" with the
description "Your theme's search layout, with faster results." for the
classic experience. Off becomes "WordPress default search.". Update
the affected component tests to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search: align feature-selector width with dashboard sections (RSM-2116)
Constrain the feature selector to the same 1040px max-width and
auto-centering used by `.jp-search-dashboard-row` above (MockedSearch
and RecordMeter), so the picker no longer goes full-bleed against
those sections.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search: wrap feature-selector in dashboard-row + add vertical padding (RSM-2116)
Reuse the existing `.jp-search-dashboard-wrap` + `.jp-search-dashboard-row`
grid pattern instead of bespoke max-width CSS, so the selector aligns
with the sections above (MockedSearch, RecordMeter) at every
breakpoint. A new `.jp-search-feature-selector-wrap` class provides
vertical padding around the card without inheriting the bottom border
of the meter wrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search: center-align feature-selector option content (RSM-2116)
Switch the radio row from `align-items: flex-start` to `center` so the
radio, icon, title, description, and ACTIVE badge all line up on a
shared baseline. Drop the now-redundant 2px top offset on the radio
that was needed for first-line alignment under flex-start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search: replace literal SCSS values with WPDS tokens (RSM-2116)
Use `@wordpress/theme` design tokens (`--wpds-dimension-*`,
`--wpds-color-*`, `--wpds-border-*`, `--wpds-typography-font-size-*`)
in the feature selector instead of hard-coded px / hex values. Tokens
are loaded by the surrounding admin shell, so no new dependency is
needed in the search package. The pill border-radius on the badge
stays literal because it's a shape, not a token-relevant value.
* Search: simplify save payload to { experience } only (RSM-2116)
The feature-selector UI is gated behind `jetpack_search_blocks_enabled`,
so the front end and back end ship together. There is no reason to
keep sending the legacy `module_active` and `instant_search_enabled`
booleans — the back end is responsible for translating `experience`
into whatever it persists, and for migrating any pre-existing booleans
on first read so the initial active row reflects the user's prior
state.
`experienceToPayload()` now returns `{ experience }` for all four IDs.
Updated the unit test and the EXPERIENCE constant doc comment to match.
* Search: drop experienceToPayload helper (RSM-2116)
Now that the payload is just `{ experience }`, the helper is a no-op
wrapper around its own argument. Inline at the single call site in
`saveExperience` and remove the dedicated unit test.
* Search: promote Badge to a shared dashboard component (RSM-2116)
Move `feature-selector/badge.jsx` to `components/badge/index.jsx` with
its own colocated `style.scss`, generalising the CSS class names from
`jp-search-feature-selector__badge*` to `jp-search-badge*`. The pill's
appearance (font, padding, color, pill shape) lives with the component
itself; the layout-only `margin-inline-start: auto` that right-aligns
the ACTIVE badge inside an option row stays in the feature selector
where that flex layout is defined.
* Search: align Badge styles with the WPDS Badge spec (RSM-2116)
Match the wp-ui-components Badge primitive: rounded rectangle
(`--wpds-border-radius-lg`) instead of pill, body-sm typography
(`--wpds-typography-font-size-sm` / `line-height-xs`), regular weight,
and natural-case labels (no uppercase / letter-spacing). Use
`--wpds-color-fg-content-success` for the foreground to mirror how the
informational variant pairs `bg-surface-info` with `fg-content-info`.
* Search: split Badge intent — recommended → info, active → success (RSM-2116)
Recommended (Embedded row) is a hint, not a state, so it now uses the
WPDS informational color pair (`bg-surface-info` / `fg-content-info`).
Active continues to use the success pair to mark the user's current
running experience.
* Search: use WPDS interactive-brand stroke for selected option (RSM-2116)
The `.is-selected` row was using `--wpds-color-fg-content-success-weak`
(a content/foreground token, semantically Jetpack green) for its border
and inner shadow. Switch to `--wpds-color-stroke-interactive-brand` so
the selection ring picks up the admin's standard interactive colour
rather than baking in a brand-specific green.
* Search: render selector heading as a visible h2 (RSM-2116)
Promote "Pick what visitors see when they search" from a visually
hidden `<legend>` to a visible `<h2>` rendered above the form, matching
the heading level used by sibling dashboard sections (record-meter,
plan-summary). The fieldset references the heading via aria-labelledby
so its group role still has an accessible name; the existing
`getByRole('group', { name: /pick what visitors.../i })` test confirms.
* Search: drop divider line between options and save row (RSM-2116)
Remove the top border (and its companion padding) on the footer so
the Save row sits directly below the option list, separated only by
the existing margin gap.
* Search: drop idle status copy from selector footer (RSM-2116)
The "Currently active. Edits apply when you save." line restated what
the form already conveys — the ACTIVE badge marks the running
experience and the Save button is the obvious commit. Leave the
status `<p>` empty when idle so the footer is a simple Save row, and
keep the `Saving…` message during the in-flight update for the
aria-live region.
* Search: use navigationOverlay icon for the Overlay row (RSM-2116)
The `desktop` icon (a generic monitor) didn't read as "overlay" — it
suggested device targeting. `navigationOverlay` is purpose-built for
this idea in `@wordpress/icons` and matches what the rest of the
WordPress admin uses for overlay-style UI.
* Search: use published icon for the Theme search row (RSM-2116)
The `search` icon was redundant on a tile already labelled "Theme
search" inside a feature called Search. `published` (a document with
a checkmark) reads as "your theme's published search page," matching
what the option actually points users at.
* Search: swap Theme search → trendingUp, Off → notAllowed (RSM-2116)
`published` (a checkmarked document) didn't communicate the value
prop of the Theme search row, which is "faster results from your
existing layout." `trendingUp` reads as acceleration / improvement.
`cancelCircleFilled` was a harsh red-error metaphor for what is just
a "search is off" state. `notAllowed` (the universal circle-with-slash)
is a calmer, more neutral "off" indicator.
* Search: use clsx for option row class names (RSM-2116)
Replace the manual array-filter-join with `clsx`, matching how the rest
of the dashboard composes class strings. Same output, less ceremony.
* Search: Badge prop rename `variant` → `intent` to match WPDS (RSM-2116)
The WPDS Badge primitive uses an `intent` prop with values like
`informational` and `success`. Mirror that vocabulary in the local
Badge so call sites express *what kind of badge* rather than picking
a colour: Recommended renders as `intent="informational"`, Active as
`intent="success"`. Class names follow (`--informational` /
`--success`), and the option-row right-alignment override now selects
the trailing badge by structure (`.option > .badge`) instead of by
intent.
* Search: use @wordpress/ui Badge instead of the local one (RSM-2116)
@wordpress/ui shipped its Badge primitive in v0.12.0. Drop our local
`components/badge/` (8 lines of JSX, 18 lines of SCSS) and use the
shipped component, so the search dashboard inherits future Badge
fixes / token tweaks without us copying them in.
Intent mapping changes from our temporary local component:
- Recommended: `intent="informational"` (unchanged)
- Active: `intent="stable"` (was `intent="success"` locally; @wordpress/ui
uses `stable` for the green "currently running well" semantic, mapped
to the same `--wpds-color-bg-surface-success` / `fg-content-success`
tokens we were already using)
The right-alignment override on the Active badge moves to a new layout
class `jp-search-feature-selector__option-trailing` passed via the
Badge's `className` prop, decoupling layout from intent and from the
shipped Badge's hashed CSS module class names.
Bumps the search package only; other consumers stay on 0.11.0.
* Search: only promote experience on confirmed save success (RSM-2116)
`updateJetpackSettings` catches its own errors and returns an
error-notice action instead of throwing, which meant `saveExperience`
was unconditionally calling `setLastSavedExperience` and clearing
`pending_experience` even when the API call failed — moving the
Active badge to the failed selection and removing the user's pending
choice so they couldn't retry without re-clicking.
Capture the inner generator's return value and gate the promotion
on `notice.status === 'is-success'`. On failure we exit early and
pending stays put, so the user can retry. Added a test for the
failure path; updated the existing success-path test to use the
real notice shape rather than an `{ ok: true }` placeholder.
Caught by Copilot review on PR #48500.
* Search: drop unused __label rule from selector styles (RSM-2116)
Leftover from when the heading was a visually-hidden legend with a
separate visible label. No JSX renders an element with this class
anymore, so the rule was dead CSS.
Caught by Copilot review on PR #48500.
* Search: use @wordpress/ui Stack for the option list (RSM-2116)
The custom `__options` rule was just `display: flex; flex-direction:
column; gap: var(--wpds-dimension-gap-sm)` — exactly what the shipped
`Stack` primitive does. Swap the wrapper `<div>` for `<Stack
direction="column" gap="sm">` and drop the rule.
* Search: use @wordpress/ui Stack inside the option label too (RSM-2116)
The option row's flex layout (radio · icon · body · optional Active
badge) is now a `<Stack direction="row" gap="md" align="center">`
rendered inside the `<label>`. The label keeps its semantics (radio
click target, upgrade-tooltip `title`) and its visual chrome (padding,
border, hover, is-selected, is-disabled); the Stack handles row
alignment and the design-token gap.
Mobile breakpoint's `flex-wrap: wrap` rule moves to the new Stack's
`__option-stack` class so it still wraps when the row is too tight.
* Search: convert option-body, option-title, and footer to Stacks (RSM-2116)
Three more flex containers in the selector were just `display: flex`
+ direction + gap — exactly what `<Stack>` does. Replace each with the
shipped primitive:
- `__option-body`: column gap=xs (description wraps below title)
- `__option-title`: row gap=sm align=center (label + Recommended badge)
- `__footer`: row gap=md align=center justify=space-between (status +
Save button)
The corresponding SCSS rules now keep only what Stack can't express:
`flex: 1 1 auto` on the body so it fills remaining row space, the
title's typography, and the footer's top margin. The footer keeps
`aria-live="polite"` via prop pass-through.
* Search: drop redundant `direction="row"` from row Stacks (RSM-2116)
`<Stack>` with no `direction` defaults to row (it sets
`flexDirection: undefined`, so the browser falls back to the CSS
default). Drop the explicit `direction="row"` from the option-stack,
title, and footer Stacks; the column Stacks (outer options list,
option-body) keep `direction="column"` since that's a real override.
* Search: use @wordpress/ui Button for the Save action (RSM-2116)
Now that `@wordpress/ui` is a direct dep (Stack, Badge), use its Button
for the Save action too. Maps the previous `@wordpress/components` API
across:
- `variant="primary"` → drop (defaults `variant="solid" tone="brand"`
give the same primary-brand button)
- `isBusy` → `loading`
- `aria-disabled={...}` → `disabled={...}`. `@wordpress/ui` Button has
`focusableWhenDisabled=true` by default, so it still renders
`aria-disabled="true"` rather than the native `disabled` attribute,
preserving focus order. Existing tests that assert
`aria-disabled="true"`/`"false"` continue to pass unchanged.
Update the docblock to reflect the new behaviour and drop the stale
"would pull a new runtime dep" justification.
* Search: roll back optimistic `experience` on failed save (RSM-2116)
`updateJetpackSettings` writes the optimistic settings into the store
before yielding the API control. On failure the catch block restored
only `module_active` / `instant_search_enabled` — the new `experience`
field was left at the unconfirmed value. No selector reads
`jetpackSettings.experience` today (we read `last_saved_experience`),
so this was latent — but cheap to fix and keeps the store consistent
for whenever consumers do start reading it.
Caught by claude[bot] review on PR #48500.
* Search: document the embedded gap in deriveExperienceFromBooleans (RSM-2116)
The booleans returned by the REST endpoint can't distinguish Embedded
from Theme search — both are `module_active=true,
instant_search_enabled=false`. Add an inline NOTE so future
contributors don't add an `'embedded'` branch and silently change the
fallback behaviour for sites with that boolean pair.
Caught by claude[bot] review on PR #48500.
* Search: drop redundant pendingExperience check in onSubmit (RSM-2116)
`isSaveDisabled = !isDirty || isUpdating`, and `isDirty` already
guarantees `pending_experience` is non-null when it's true. So when
`isSaveDisabled` is false, `pendingExperience` is guaranteed
non-null — the `! pendingExperience` branch is dead code that
implied the value could be falsy when the save is enabled.
Caught by claude[bot] review on PR #48500.
* Search: combine useSelect calls in ExperienceOption (RSM-2116)
Two `useSelect` subscriptions per row × four rows was eight store
listeners; collapse to one per row. Same observable values, half the
subscription churn on store updates.
Caught by claude[bot] review on PR #48500.
* Search: 2-column grid for option row, content wraps in column 2 (RSM-2116)
Improve mobile / narrow-viewport behavior (kangzj review on PR #48500).
Previously the option row was a single flex row that wrapped on phone-
width, which let the radio drop to its own line below the title. The
new layout pins the radio to its own grid column on the left, vertically
centered against the content; everything else (icon · title · description
· Recommended badge · Active badge) lives in a wrapping Stack in column
2 and wraps among itself when the viewport is narrow.
The mobile-specific `flex-wrap: wrap` override is no longer needed —
the column-2 Stack always wraps — so the phone-down rule shrinks back
to just the smaller card padding.
* Search: keep title beside icon, allow Recommended badge to wrap (RSM-2116)
Move the wrap point from the column-2 Stack to the title Stack:
- column-2 (icon · body · active badge) no longer wraps, so the icon
always shares a row with the body's title and the description sits
directly beneath it. The active badge stays vertically centered to
the body's right edge.
- the title Stack (label text + Recommended badge) now wraps, so on
narrow viewports the Recommended badge drops onto a second line
beneath the label text rather than pushing the rest of the row out
of shape.
* Search: reset WP Admin's negative margin on the option radio (RSM-2116)
WP Admin's `wp-admin/css/forms.css` ships `margin: -4px 4px 0 0` on
`input[type=radio]` to fudge alignment with adjacent label baselines
in classic forms. That same negative margin was leaking into the
feature-selector option row, pulling the radio about 2px above the
icon's vertical centre.
Reset the margin to 0 on `.jp-search-feature-selector__option-radio`
so the grid's `align-items: center` puts the radio on the same
baseline as the icon. Verified in Chrome: previous deltaY between
radio centre and icon centre was -2px, now 0.
* Search: bump option-radio reset specificity past WP forms.css (RSM-2116)
WP Admin's `input[type="checkbox"], input[type="radio"]` rule (with
the bad `margin: -4px 4px 0 0`) has specificity (0,1,1), the same as
our previous `.jp-search-feature-selector__option-radio` reset
(0,1,0) — except the WP rule wins because of the attribute selector.
Qualify ours with `input[type="radio"]` so the reset lands at (0,2,1)
and reliably overrides.
* Search: stack option title with icon, description below (RSM-2116)
Restructure each feature-selector row so the icon sits next to the
title, the description wraps full width below them, and the Active
badge anchors a third grid column. The Recommended badge stays on
the title line when there's room and wraps to its own line on narrow
viewports without dropping below the description.
Previously the row was a 2-column grid (radio | flex row of icon,
body, badge) with the description nested under the title. The new
shape is a 3-column grid (radio | content stack | active badge),
which keeps the radio and Active badge centred against the whole
content block and lets the headline Stack handle its own wrap.
* Search: tighten icon-title gap, loosen title-description gap (RSM-2116)
The headline row's `md` gap left the icon floating noticeably away
from its title; dropping to `sm` brings them visibly together as a
single label unit. The content stack's `xs` gap, by contrast, made
the description crowd the title — `sm` gives the description the
breathing room it needs to read as secondary copy.
* Search: give feature-selector heading a WPDS line-height (RSM-2116)
The h2 was inheriting WP Admin's tight default line-height, which
left the descenders crowding the card below. Apply
`--wpds-typography-line-height-md` so the heading breathes in line
with the rest of the WPDS-tokenised dashboard typography.
* Search: rephrase feature-selector heading (RSM-2116)
"Pick what visitors see when they search" framed the choice as a
content question, but the four options are really four different
search products. "Select a search experience for your visitors"
matches the language used in the option labels and badges (Embedded
search / Overlay search / Theme search) and reads as a configuration
choice, which is what this section actually is.
* Search: align feature-selector tests with new heading copy (RSM-2116)
37987ec reworded the feature-selector h2 from "Pick what visitors
see when they search" to "Select a search experience for your
visitors" but left two tests that look up the fieldset by its old
accessible name. Update both name patterns so the suite reflects the
shipping copy.
* Search: replace option grid with @wordpress/ui Stack (RSM-2116)
Each feature-selector row was a CSS grid (auto 1fr auto) that
hand-rolled the same flex layout @wordpress/ui's Stack already
provides. Swap to a row Stack that renders as the wrapping <label>
via Base UI's `render` prop, so the radio | content | active-badge
layout, gap, and vertical centering all come from design-system
tokens instead of bespoke grid CSS.
The middle column now grows via `flex: 1 1 auto` on the content
Stack (the row Stack's other two children — the radio and the
trailing Active badge — keep their intrinsic widths). A
`flex-shrink: 0` on the radio prevents the description's wrapping
content from squashing it on narrow viewports, where grid's `auto`
column had been doing that work for free.
* Search: align frontend with backend `experience` field name (RSM-2116)
Backend (RSM-2291) settled on `experience` as the field name in the REST
response and initial state, replacing the temporary `last_saved_experience`
forward-compat name. Update the dashboard store to match:
- Rename the store slot `last_saved_experience` → `experience`.
- Drop the redundant `getLastSavedExperience` selector — `getActiveExperience`
is the only consumer of that slot, and it now reads `experience` directly
with the legacy-boolean derivation as defence-in-depth.
- Rename `setLastSavedExperience` → `setActiveExperience` (paired with
`getActiveExperience`); `saveExperience` writes through it after a
successful save as a belt-and-braces complement to the post-save
`fetchJetpackSettings` round-trip in `updateJetpackSettings`.
- Update test fixtures (`baseSettings` / `settings` blocks) and the
jetpack-settings store test to the new field/action names.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search dashboard: rename `classic` experience to `inline` (RSM-2116)
`classic` collides with the older Jetpack Search "classic search"
terminology, which refers to a deprecated experience. Rename the new
non-instant non-embedded option to `inline` everywhere it appears:
the `EXPERIENCE` enum, `EXPERIENCE_ORDER`, the label/description/icon
switches, the legacy-fallback derivation in the selectors, and the
test fixtures that exercise the value.
User-facing labels are unchanged ("Theme search", "Your theme's search
layout, with faster results.") — only the wire/storage value changes.
Backend (PR #48540) needs a matching rename in `Module_Control`
constants (EXPERIENCE_CLASSIC → EXPERIENCE_INLINE) and validation list
so the wire format stays consistent end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Search dashboard: keep ModuleControl rendered alongside FeatureSelector (RSM-2116)
The new feature selector can't actually persist changes until the back-end
`experience` field lands (RSM-2291 / PR #48540), so until then the
dashboard would be effectively read-only when `jetpack_search_blocks_enabled`
is on. Render `ModuleControl` regardless of the feature flag for now so
admins can keep managing Search settings. The `FeatureSelector` still
appears above it when the flag is on, giving a preview of the new UI.
Updated the dashboard-page branching test to assert both components
render together when the flag is on, and that ModuleControl always
renders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add SEARCH_MODULE_EXPERIENCE_OPTION_KEY constant and experience value constants to Module_Control - Add get_experience() method that returns persisted value or derives from legacy booleans - Add update_experience() method that writes experience and legacy booleans in lockstep - Update REST update_settings() to accept experience-only requests - Update REST get_settings() to include last_saved_experience in response - Update validate_search_settings() to allow experience-only requests - Update Initial_State to include last_saved_experience - Update existing tests to include last_saved_experience in expected responses - Add new tests for experience read/write paths - Add changelog entry" Agent-Logs-Url: https://github.com/Automattic/jetpack/sessions/40f5e762-e4d0-4932-b4ac-5bd0ec01f26b Co-authored-by: adamwoodnz <1017872+adamwoodnz@users.noreply.github.com>
Off and classic are no longer stored in `jetpack_search_experience`. Off
is read from `Modules::is_active()` because that bit lives in the global
`jetpack_active_modules` option; storing `'off'` here would drift the
moment any non-Search path (Jetpack dashboard module toggle, WP-CLI,
`Jetpack::activate_module()`) flipped the global. Classic is the absence
of an opt-in — `update_experience('classic')` deletes the option so
existing classic sites stay at zero writes and don't need a migration.
The wire format still exposes all four values (`embedded | overlay |
classic | off`), so JS callers and external consumers see a clean enum;
`get_experience()` resolves them from the on/off bit + saved value +
legacy `instant_search_enabled` fallback.
Renames the REST/initial-state field from `last_saved_experience` to
`experience` to match the JS selector (`getActiveExperience`) and the
request-body field name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the frontend rename in PR #48500 — `classic` collided with the deprecated original Jetpack Search "classic search" terminology. Wire value is now `'inline'`; storage shape is unchanged (still deleted on write, since inline is the absence of an opt-in). - `Module_Control::EXPERIENCE_CLASSIC` → `EXPERIENCE_INLINE` (value `'inline'`) - `update_experience()` switch case + validation list - `get_experience()` legacy-fallback return value and docblock - Test names + values in `Module_Control_Test` and `REST_Controller_Test` - Changelog entry Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the bot review on #48540: - Drop the unreachable `return true` after the exhaustive switch in `update_experience()`. The four valid values each return inside their case and the in_array guard rejects everything else, so the trailing return was dead code. - Fix the stale "classic" mention in the REST controller's delegation comment — it should read "inline → delete option" after the rename. - Reject requests that mix `experience` with `module_active` or `instant_search_enabled` rather than silently dropping the legacy fields. `experience` writes the legacy booleans in lockstep, so there is no scenario where the caller needs both. Returns 400 `rest_invalid_arguments` with a clear message; new REST test covers the two mixing combinations. - Tighten `test_update_experience_off_preserves_other_state` so it proves deactivation actually ran. Previously the active-modules filter was removed before the assertion, so `is_active() === false` was trivially true regardless of whether `deactivate()` had been called. Now reads `jetpack_active_modules` directly while the filter is still active, matching the pattern in `test_deactivate_module`. Two items intentionally not changed: the partial-failure rollback in the overlay branch (acknowledged as a known package-wide pattern by the reviewer; "rolling back" by deactivating the module would itself be unrequested state mutation), and the `SEARCH_MODULE_EXPERIENCE_OPTION_KEY` prefix nit (the option name is what's stored in `wp_options` and prefixing it is the correct namespace for cross-plugin coexistence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cac4907 to
860d670
Compare
The four `if ( is_wp_error( $result ) ) { return $result; }` branches in
`update_experience()`'s INLINE / EMBEDDED / OVERLAY cases were the only
new lines in this PR not exercised by tests (per the PR's coverage
report). They're behavioral — a typo in any of them would silently
swallow a partial-failure error and write the experience option in an
inconsistent state. Adds:
- A data-provider-driven test asserting each of the three experiences
that calls activate() propagates the WP_Error from a plan-without-
search and leaves the experience option unwritten.
- A test asserting overlay propagates the WP_Error from
enable_instant_search() (plan supports search but not instant search)
and leaves the experience option unwritten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…is on (RSM-2291) The frontend PR (#48500) shipped a transitional state where ModuleControl rendered alongside FeatureSelector even with the feature flag on, so admins could still manage Search settings while the back-end `experience` field was unimplemented. With this PR adding that back end, the new selector can fully persist the user's choice on its own — the legacy toggles below it become redundant. Reverts the dashboard-page bottom-area to the either/or shape: when `jetpack_search_blocks_enabled` is on, render only `<FeatureSelector>`; otherwise render only `<ModuleControl>`. Updates the branching test to assert ModuleControl is *absent* when the flag is on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review feedback on PR #48540, add the new `jetpack_search_experience` option to the Search sync module's options whitelist so writes propagate to the WPcom shadow replicastore. Without this, the dashboard saves locally but the WPcom-side experience derivation can't see the user's choice. Note: requires a coordinated WPcom-side whitelist entry for the option to actually be retained on the cache site.
|
@kangzj — addressed in ed61648. Added Flagging for the next reviewer / for WPcom coordination: per Tracked as a follow-up: RSM-2535. |
| // experience option untouched so re-enabling later restores the user's | ||
| // prior preference (matches legacy ModuleControl behaviour). | ||
| ( new Modules() )->deactivate( self::JETPACK_SEARCH_MODULE_SLUG ); | ||
| return true; |
There was a problem hiding this comment.
should we return what the previous step's result rather than always successful return?
Similar applies the the following statements as well.
There was a problem hiding this comment.
Partially addressed in caa3678 — the OFF branch now propagates Modules::deactivate()'s bool instead of hard-returning true. Added test_update_experience_off_when_module_already_inactive_returns_false to cover the no-op path (3a5e726 stabilizes its setup against state leakage from earlier tests in the class).
For the INLINE / EMBEDDED / OVERLAY branches I left the explicit return true in place. The remaining inner steps are update_option() / delete_option() / disable_instant_search() calls — all of which return false for benign no-change writes (the option already holds the target value). Propagating those would falsely report failure when a caller re-saves the same experience. The actual failure paths in those branches (activate() and enable_instant_search() returning WP_Error) are already short-circuited above. Happy to revisit if I'm reading the intent wrong.
kangzj
left a comment
There was a problem hiding this comment.
Two suggestions on the new update_settings flow — both quality nits, neither blocking. See inline.
| $module_active = isset( $request_body['module_active'] ) ? (bool) $request_body['module_active'] : null; | ||
| $instant_search_enabled = isset( $request_body['instant_search_enabled'] ) ? (bool) $request_body['instant_search_enabled'] : null; | ||
| $swap_classic_to_inline_search = isset( $request_body['swap_classic_to_inline_search'] ) ? (bool) $request_body['swap_classic_to_inline_search'] : null; | ||
| $experience = isset( $request_body['experience'] ) ? sanitize_text_field( $request_body['experience'] ) : null; |
There was a problem hiding this comment.
sanitize_text_field() is applied without a type check. If a client sends "experience": [] or "experience": {}, this triggers an array-to-string conversion warning (and a deprecation notice on PHP 8.1+). The downstream in_array check in update_experience() catches the bad value, but the warning is unnecessary noise.
Guard with is_string() (or cast (string)) before sanitizing, e.g.:
$experience = isset( $request_body['experience'] ) && is_string( $request_body['experience'] )
? sanitize_text_field( $request_body['experience'] )
: null;There was a problem hiding this comment.
Fixed in 24b58b4 — added an is_string() guard up front so non-string experience payloads resolve to null and skip the experience branch. The downstream in_array() guard in update_experience() is left as defence-in-depth.
| if ( $experience !== null ) { | ||
| $result = $this->search_module->update_experience( $experience ); | ||
| if ( is_wp_error( $result ) ) { | ||
| return $result; | ||
| } | ||
| return rest_ensure_response( $this->get_settings() ); | ||
| } |
There was a problem hiding this comment.
swap_classic_to_inline_search is silently dropped when combined with experience.
validate_search_settings() rejects mixing experience with module_active / instant_search_enabled (good), but swap_classic_to_inline_search is allowed through — and then this early return ignores it without applying the value or surfacing an error. The validation error message also doesn't mention this field, so a caller debugging a missing swap update would have no signal.
Today's only caller is the new front-end (which sends { experience } alone), so impact is theoretical. But it's a footgun for any external API consumer that bundles fields, and the asymmetry contradicts the validation's stated intent ("don't silently lose those fields").
Fix options:
- Extend the validation rejection to also block
swap_classic_to_inline_searchwhenexperienceis set, or - Process
swap_classic_to_inline_searchbefore the early return (the existing branch below already handles a swap-only update path).
There was a problem hiding this comment.
Fixed in 4af42ed — validate_search_settings() now also rejects mixing experience with swap_classic_to_inline_search (returning 400 rest_invalid_arguments). Updated the error message and extended test_update_settings_experience_rejects_mixed_legacy_fields to cover the new combination.
Per PR #48540 review: passing an array or object payload through sanitize_text_field() triggers a PHP 8.1+ array-to-string deprecation notice. The downstream in_array() check in update_experience() already rejects non-string values, but the warning is unnecessary noise. Add an is_string() guard so non-string values resolve to null up front and skip the experience branch entirely.
…(RSM-2291) Per PR #48540 review: validate_search_settings() rejected mixing `experience` with `module_active` / `instant_search_enabled`, but let `swap_classic_to_inline_search` through. The early return in the `experience` branch of update_settings() then dropped that field silently — same footgun the existing rejection was meant to prevent. Extend the rejection to cover all three legacy fields and update the error message accordingly. Today's only caller (the new front end) sends `{ experience }` alone, so this is a defensive fix for external API consumers. Add the new mixing combination to the existing rejection test.
…('off') (RSM-2291)
Per PR #48540 review: the OFF branch was discarding Modules::deactivate()'s
bool and hard-returning true. Propagate the bool instead — false signals
the module was already inactive (a benign no-op), true signals it was
removed from jetpack_active_modules.
The REST controller only branches on is_wp_error(), so a propagated false
still falls through to the success response — but direct callers of
update_experience() can now distinguish the two cases.
INLINE / EMBEDDED / OVERLAY are unchanged: their inner update_option()
and delete_option() calls return false on no-change writes (option already
at the target value), which can't be safely conflated with failure.
WP_Error paths in those branches are already propagated.
Update test_update_experience_off_preserves_other_state to assert the
new return value, and add coverage for the no-op deactivate path.
The new test added in caa3678 assumed an empty jetpack_active_modules option, but earlier tests in the same class activate the module via update_experience() and leak 'search' into the persisted option. Seed the option with an empty array up front so deactivate() truly is a no-op (update_option returns false when the stored value already matches).
|
All feedback addressed @kangzj, will merge after tests complete. Let me know if you have follow ups. |
Fixes RSM-2291: Search dashboard feature selection: backend support for
experiencefieldWhy
Site admins using the new feature-selector UI on the Search dashboard (shipped behind
jetpack_search_blocks_enabledin #48500) currently can't actually save their choice — the existing settings API only models two booleans (module_active,instant_search_enabled) which can't distinguish the new "Embedded" experience from "Theme search," so picking Embedded silently degrades to Theme search after a save. This PR adds the back-end persistence and read path the new UI needs, so the dashboard reflects and persists the admin's actual choice.Proposed changes
This branch is rebased on #48500, so the diff includes both the frontend feature-selector and the back-end work below.
experienceas one ofembedded | overlay | inline | off. Storage is narrower than the wire format — each fact lives where it's already owned:'off'is read fromjetpack_active_modules(the global Jetpack module-active state) and not stored in this package's option, so flipping Search on/off via the global Jetpack dashboard,wp jetpack module …, orJetpack::activate_module()doesn't drift.'inline'is the absence of an opt-in; saving inline deletesjetpack_search_experiencerather than writing the value, so existing sites that have never engaged with the new UI are already correctly classified.'embedded'and'overlay'are the only values actually written to the new option.module_active/instant_search_enabledare kept in lockstep so unmigrated readers (Initializer, Options, sidebar registration) keep working until they're migrated to consultget_experience()in a follow-up.Module_Control::get_experience()resolves the wire value: off if the module isn't active, the savedembedded/overlayvalue if present, otherwise a legacy-boolean fallback (instant_search_enabled=true→'overlay', else'inline') for sites that have never saved via the new UI.Module_Control::update_experience()translates each wire value onto the storage shape. Adds theEXPERIENCE_OVERLAY/EXPERIENCE_EMBEDDED/EXPERIENCE_INLINE/EXPERIENCE_OFFconstants and theSEARCH_MODULE_EXPERIENCE_OPTION_KEY(jetpack_search_experience) option key.'off'deactivates the module and leaves bothinstant_search_enabledand the experience option untouched, so re-enabling later restores the prior preference (matches legacyModuleControlbehaviour).POST /jetpack/v4/search/settingsaccepts anexperience-only request body and delegates toupdate_experience().validate_search_settings()allows experience-only requests without requiring the legacy booleans.GET /jetpack/v4/search/settingsreturns the resolvedexperiencevalue alongside the existing fields.experienceso the React app can highlight the active row immediately at boot.Module_Control_Testcases covering off-when-inactive (with stale option present), saved-value reads, legacy fallback, inline-deletes-option (seeded with'embedded'to prove the switch clears it), off-preserves-other-state, and invalid-value rejection. Adds matching REST-controller integration tests for each wire value plus an invalid case.Related product discussion/links
Does this pull request change what data or activity we track or use?
No.
Testing instructions
The new UI is behind
add_filter( 'jetpack_search_blocks_enabled', '__return_true' );— set that on a test site so the new dashboard renders alongside the legacyModuleControl./jetpack/v4/search/settingswith each wire value:{ "experience": "overlay" }→ response:module_active=true,instant_search_enabled=true,experience: "overlay". Confirmjetpack_search_experienceoption is'overlay'.{ "experience": "embedded" }→ response:module_active=true,instant_search_enabled=false,experience: "embedded". Confirmjetpack_search_experienceoption is'embedded'.{ "experience": "inline" }→ same booleans as embedded butexperience: "inline". Confirmjetpack_search_experienceoption is deleted (not present inwp_options).{ "experience": "off" }→module_active=false,experience: "off". Confirminstant_search_enabledis unchanged from before the call, and thejetpack_search_experienceoption is preserved (so re-enabling later restores the prior overlay-vs-embedded choice)./jetpack/v4/search/settingson a fresh site (no experience option saved):experience: "off".instant_search_enabled=true→experience: "overlay"(legacy fallback).instant_search_enabled=false→experience: "inline"(legacy fallback).experience(e.g."foo") → confirm 400 response.module_active/instant_search_enabled) still work as before.wp jetpack module deactivate search, then GET/search/settings→experience: "off"even if thejetpack_search_experienceoption still has a value (the global on/off bit wins).