diff --git a/projects/plugins/jetpack/_inc/client/settings/index.jsx b/projects/plugins/jetpack/_inc/client/settings/index.jsx index d1d1a5f1e6ea..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'; @@ -13,6 +15,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 +32,7 @@ class Settings extends Component { siteRawUrl, blogID, userCanManageModules, + hasAnyMatchingModule, } = this.props; const { pathname } = location; const commonProps = { @@ -36,19 +40,28 @@ 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 && ( + + + + + + { __( 'No matching settings', 'jetpack' ) } + + { 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..ebf35410258c 100644 --- a/projects/plugins/jetpack/_inc/client/settings/style.scss +++ b/projects/plugins/jetpack/_inc/client/settings/style.scss @@ -110,13 +110,16 @@ width: 100%; } - .jp-no-results { - display: none; - font-size: rem.convert(14px); - line-height: 1.5; - - &:last-of-type { - display: inherit; + .jp-settings__empty-search-results { + margin-block: rem.convert(64px); + + // @wordpress/ui zeros margins on EmptyState.Title/Description inside + // @layer wp-ui-components, but unlayered admin styles (core + Jetpack) + // take precedence and reintroduce h2/p margins. Reset them here so the + // component renders with its intended tight spacing. + h2, + p { + margin: 0; } } 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/_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 ); + } ); +} ); 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.