From b7ad8a885faf47fee5c8dd678f84db4a19f0c7fb Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 15 Apr 2026 11:18:38 +0100 Subject: [PATCH 1/4] Settings: Add hasAnyMatchingModule selector for search state Aggregate selector that returns true when searchTerm is set and at least one module matches via isModuleFound. This will drive empty-state rendering on the settings screen. --- .../jetpack/_inc/client/state/search/reducer.js | 15 +++++++++++++++ ...05-show-empty-state-message-on-settings-screen | 4 ++++ 2 files changed, 19 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/jpprod-105-show-empty-state-message-on-settings-screen diff --git a/projects/plugins/jetpack/_inc/client/state/search/reducer.js b/projects/plugins/jetpack/_inc/client/state/search/reducer.js index 243d84b26bf6..cef58a89cfe9 100644 --- a/projects/plugins/jetpack/_inc/client/state/search/reducer.js +++ b/projects/plugins/jetpack/_inc/client/state/search/reducer.js @@ -64,3 +64,18 @@ export function isModuleFound( state, module ) { .indexOf( currentSearchTerm.toLowerCase() ) > -1 ); } + +/** + * Returns whether any module matches the current search term. + * + * @param {object} state - Global state tree + * @return {boolean} True only when there is an active search term and at least one module matches it. + */ +export function hasAnyMatchingModule( state ) { + if ( ! getSearchTerm( state ) ) { + return false; + } + + const items = state.jetpack?.modules?.items ?? {}; + return Object.values( items ).some( item => item?.module && isModuleFound( state, item.module ) ); +} diff --git a/projects/plugins/jetpack/changelog/jpprod-105-show-empty-state-message-on-settings-screen b/projects/plugins/jetpack/changelog/jpprod-105-show-empty-state-message-on-settings-screen new file mode 100644 index 000000000000..40aa3b904ab6 --- /dev/null +++ b/projects/plugins/jetpack/changelog/jpprod-105-show-empty-state-message-on-settings-screen @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Settings: Show an empty state when search returns no matching settings. From b1e5f11caef172995bc9b0b232fd461a33095feb Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 15 Apr 2026 11:21:51 +0100 Subject: [PATCH 2/4] Settings: Show centered heading when search returns no matches Drive empty-state visibility from Redux state (via hasAnyMatchingModule) instead of the previous display: none + :last-of-type CSS hack, which did not reliably show or hide the message. Render the empty state as an h2 using the existing jp-settings__section-title treatment plus a new --centered BEM modifier, so it visually matches the main settings heading. Removes the obsolete .jp-no-results block. --- .../jetpack/_inc/client/settings/index.jsx | 22 +++++++++++-------- .../jetpack/_inc/client/settings/style.scss | 10 --------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/settings/index.jsx b/projects/plugins/jetpack/_inc/client/settings/index.jsx index d1d1a5f1e6ea..e09fb2aaf6af 100644 --- a/projects/plugins/jetpack/_inc/client/settings/index.jsx +++ b/projects/plugins/jetpack/_inc/client/settings/index.jsx @@ -13,6 +13,7 @@ import SearchableModules from 'searchable-modules'; import Security from 'security'; import Sharing from 'sharing'; import { isModuleActivated as isModuleActivatedSelector } from 'state/modules'; +import { hasAnyMatchingModule as hasAnyMatchingModuleSelector } from 'state/search'; import Traffic from 'traffic'; import Writing from 'writing'; import { FEATURE_JETPACK_EARN } from '../lib/plans/constants'; @@ -29,6 +30,7 @@ class Settings extends Component { siteRawUrl, blogID, userCanManageModules, + hasAnyMatchingModule, } = this.props; const { pathname } = location; const commonProps = { @@ -36,19 +38,20 @@ class Settings extends Component { rewindStatus, userCanManageModules, }; + const showEmptySearchState = !! searchTerm && ! hasAnyMatchingModule; return (
-
- { searchTerm - ? sprintf( - /* translators: %s: a search term entered in search form. */ - __( 'No search results found for %s', 'jetpack' ), - searchTerm - ) - : __( 'Enter a search term to find settings or close search.', 'jetpack' ) } -
+ { showEmptySearchState && ( +

+ { sprintf( + /* translators: %s: a search term entered in search form. */ + __( 'No search results found for %s', 'jetpack' ), + searchTerm + ) } +

+ ) } { return { isModuleActivated: module => isModuleActivatedSelector( state, module ), + hasAnyMatchingModule: hasAnyMatchingModuleSelector( state ), }; } )( props => ); diff --git a/projects/plugins/jetpack/_inc/client/settings/style.scss b/projects/plugins/jetpack/_inc/client/settings/style.scss index 098bf5ecad31..30d81678536e 100644 --- a/projects/plugins/jetpack/_inc/client/settings/style.scss +++ b/projects/plugins/jetpack/_inc/client/settings/style.scss @@ -110,16 +110,6 @@ width: 100%; } - .jp-no-results { - display: none; - font-size: rem.convert(14px); - line-height: 1.5; - - &:last-of-type { - display: inherit; - } - } - @include breakpoints.breakpoint( "<480px" ) { .dops-search.is-expanded-to-container { From cdd90b0b3daca275e555b7e7547511d429825ae9 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 15 Apr 2026 14:46:19 +0100 Subject: [PATCH 3/4] Settings: Render search empty state with @wordpress/ui EmptyState Replace the ad-hoc h2 with the Gutenberg EmptyState compound component (icon + title + description) wrapped in a Stack for horizontal centering, matching the Jetpack settings treatment and the Gutenberg storybook default. Reset h2/p margins inside the wrapper because the component's built-in margin: 0 lives in @layer wp-ui-components and loses to unlayered admin styles. --- .../jetpack/_inc/client/settings/index.jsx | 24 +++++++++++++------ .../jetpack/_inc/client/settings/style.scss | 13 ++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/settings/index.jsx b/projects/plugins/jetpack/_inc/client/settings/index.jsx index e09fb2aaf6af..5f694e383ff2 100644 --- a/projects/plugins/jetpack/_inc/client/settings/index.jsx +++ b/projects/plugins/jetpack/_inc/client/settings/index.jsx @@ -1,5 +1,7 @@ import { GlobalNotices, ThemeProvider } from '@automattic/jetpack-components'; import { __, sprintf } from '@wordpress/i18n'; +import { search } from '@wordpress/icons'; +import { EmptyState, Stack } from '@wordpress/ui'; import { Component } from 'react'; import { connect } from 'react-redux'; import { useLocation } from 'react-router'; @@ -44,13 +46,21 @@ class Settings extends Component {
{ showEmptySearchState && ( -

- { sprintf( - /* translators: %s: a search term entered in search form. */ - __( 'No search results found for %s', 'jetpack' ), - searchTerm - ) } -

+ + + + + + { __( 'No matching settings', 'jetpack' ) } + + { sprintf( + /* translators: %s: a search term entered in search form. */ + __( 'No search results found for %s', 'jetpack' ), + searchTerm + ) } + + + ) } Date: Thu, 16 Apr 2026 08:35:44 +0100 Subject: [PATCH 4/4] Settings: Add tests for hasAnyMatchingModule selector Cover the four key cases: empty search term, unloaded modules state, at least one matching module, and no matching modules. --- .../client/state/search/test/selectors.js | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/_inc/client/state/search/test/selectors.js b/projects/plugins/jetpack/_inc/client/state/search/test/selectors.js index 176ed1da2eff..370bdc04f8bc 100644 --- a/projects/plugins/jetpack/_inc/client/state/search/test/selectors.js +++ b/projects/plugins/jetpack/_inc/client/state/search/test/selectors.js @@ -1,4 +1,4 @@ -import { isModuleFound } from '../index'; +import { hasAnyMatchingModule, isModuleFound } from '../index'; describe( 'Module found selector', () => { let state = {}; @@ -69,3 +69,41 @@ describe( 'Module found selector', () => { ); } ); } ); + +describe( 'hasAnyMatchingModule selector', () => { + const buildState = ( searchTerm, items ) => ( { + jetpack: { + modules: { items }, + search: { searchTerm }, + }, + } ); + + const items = { + photon: { + module: 'photon', + name: 'Photon', + description: 'Serve images from the WordPress.com CDN.', + }, + protect: { + module: 'protect', + name: 'Protect', + description: 'Prevent brute-force login attacks.', + }, + }; + + test( 'returns false when the search term is empty', () => { + expect( hasAnyMatchingModule( buildState( '', items ) ) ).toBe( false ); + } ); + + test( 'returns false when the modules state has not loaded yet', () => { + expect( hasAnyMatchingModule( buildState( 'photon', undefined ) ) ).toBe( false ); + } ); + + test( 'returns true when at least one module matches the search term', () => { + expect( hasAnyMatchingModule( buildState( 'brute-force', items ) ) ).toBe( true ); + } ); + + test( 'returns false when no module matches the search term', () => { + expect( hasAnyMatchingModule( buildState( 'asdfqwerty', items ) ) ).toBe( false ); + } ); +} );