From b3c84b22b66a14c731b78412eef2a30f83304759 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 6 May 2026 10:46:58 -0300 Subject: [PATCH] Search: migrate dashboard notices to @wordpress/ui Notice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the local SimpleNotice + NoticeAction wrappers with Notice.* from @wordpress/ui across the search dashboard: - record-meter/notice-box: tier notices use Notice.Root/Title/Description + Notice.Actions/ActionLink (openInNewTab); the legacy jp-search-notice-box__important class becomes intent="error". - pages/sections/first-run-section: indexing notice uses Notice.Root/Title/ Description. - global-notices: snackbar notices use Notice.Root + Notice.Description + Notice.CloseIcon, with auto-dismiss preserved via a useEffect timer. Status strings remain in the redux store; mapped to wp-ui intents at render. Drops vestigial fields the redux notice creators never set anyway (isCompact, isPersistent, displayOnNextPage, notice.button/href). Deletes the SimpleNotice/NoticeAction sources, their stylesheets, and notice-box.scss; strips the dead .dops-notice* overrides from global-notices/style.scss while keeping the snackbar positioning rules. Removes one site of @automattic/jetpack-components Gridicon usage (SimpleNotice's icon-wrapper) — helps the broader modernization sweep. Follow-up to simison review feedback on #48537. --- pnpm-lock.yaml | 3 + .../chore-search-notice-action-wp-ui-link | 4 + projects/packages/search/package.json | 1 + .../components/global-notices/index.jsx | 69 +++-- .../global-notices/store/actions.js | 2 - .../components/global-notices/style.scss | 43 --- .../src/dashboard/components/notice/index.jsx | 130 -------- .../components/notice/notice-action.jsx | 40 --- .../dashboard/components/notice/style.scss | 287 ------------------ .../components/notice/test/index.test.jsx | 23 -- .../pages/sections/first-run-section.jsx | 41 +-- .../components/record-meter/notice-box.jsx | 31 +- .../components/record-meter/notice-box.scss | 79 ----- 13 files changed, 72 insertions(+), 681 deletions(-) create mode 100644 projects/packages/search/changelog/chore-search-notice-action-wp-ui-link delete mode 100644 projects/packages/search/src/dashboard/components/notice/index.jsx delete mode 100644 projects/packages/search/src/dashboard/components/notice/notice-action.jsx delete mode 100644 projects/packages/search/src/dashboard/components/notice/style.scss delete mode 100644 projects/packages/search/src/dashboard/components/notice/test/index.test.jsx delete mode 100644 projects/packages/search/src/dashboard/components/record-meter/notice-box.scss diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ee061220b24..3c111105450a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4077,6 +4077,9 @@ importers: '@wordpress/server-side-render': specifier: 6.20.0 version: 6.20.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/ui': + specifier: 0.11.0 + version: 0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/url': specifier: 4.44.0 version: 4.44.0 diff --git a/projects/packages/search/changelog/chore-search-notice-action-wp-ui-link b/projects/packages/search/changelog/chore-search-notice-action-wp-ui-link new file mode 100644 index 000000000000..1b5ba85ba8c8 --- /dev/null +++ b/projects/packages/search/changelog/chore-search-notice-action-wp-ui-link @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Search: migrate dashboard notices to the @wordpress/ui Notice component. Removes the local SimpleNotice and NoticeAction wrappers along with their stylesheets. diff --git a/projects/packages/search/package.json b/projects/packages/search/package.json index be4cb14e03f3..7e98456623f1 100644 --- a/projects/packages/search/package.json +++ b/projects/packages/search/package.json @@ -59,6 +59,7 @@ "@wordpress/icons": "12.2.0", "@wordpress/interactivity": "6.44.0", "@wordpress/server-side-render": "6.20.0", + "@wordpress/ui": "0.11.0", "@wordpress/url": "4.44.0", "@wordpress/viewport": "6.44.0", "clsx": "2.1.1", diff --git a/projects/packages/search/src/dashboard/components/global-notices/index.jsx b/projects/packages/search/src/dashboard/components/global-notices/index.jsx index a3cdb9560feb..fdef8a4d2bb4 100644 --- a/projects/packages/search/src/dashboard/components/global-notices/index.jsx +++ b/projects/packages/search/src/dashboard/components/global-notices/index.jsx @@ -1,8 +1,35 @@ -import SimpleNotice from 'components/notice/index.jsx'; -import NoticeAction from 'components/notice/notice-action'; +import { Notice } from '@wordpress/ui'; +import { useCallback, useEffect } from 'react'; import './style.scss'; +const STATUS_TO_INTENT = { + 'is-success': 'success', + 'is-error': 'error', + 'is-warning': 'warning', + 'is-info': 'info', +}; + +const NoticeItem = ( { notice, onDismissNotice } ) => { + const { id, duration, showDismiss = true, status, text } = notice; + + const handleDismiss = useCallback( () => onDismissNotice( id ), [ onDismissNotice, id ] ); + + useEffect( () => { + if ( duration > 0 ) { + const timer = setTimeout( handleDismiss, duration ); + return () => clearTimeout( timer ); + } + }, [ duration, handleDismiss ] ); + + return ( + + { text && { text } } + { showDismiss && } + + ); +}; + /** * NoticesList component * @@ -12,36 +39,24 @@ import './style.scss'; export default function NoticesList( props = { handleLocalNoticeDismissClick: null, notices: Object.freeze( [] ) } ) { - const noticesList = props.notices.map( function ( notice ) { - const onDismissClick = theNotice => () => { - theNotice && props.handleLocalNoticeDismissClick( theNotice.id ); - }; - return ( - - { notice.button && ( - - { notice.button } - - ) } - - ); - } ); - - if ( ! noticesList.length ) { + const onDismissNotice = useCallback( + noticeId => props.handleLocalNoticeDismissClick?.( noticeId ), + [ props ] + ); + + if ( ! props.notices.length ) { return null; } return (
- { noticesList } + { props.notices.map( notice => ( + + ) ) }
); } diff --git a/projects/packages/search/src/dashboard/components/global-notices/store/actions.js b/projects/packages/search/src/dashboard/components/global-notices/store/actions.js index 612dbf8aec9f..2445f643c88f 100644 --- a/projects/packages/search/src/dashboard/components/global-notices/store/actions.js +++ b/projects/packages/search/src/dashboard/components/global-notices/store/actions.js @@ -18,8 +18,6 @@ export function createNotice( status, text, options = {} ) { id: options.id || ++createNoticeCounter, duration: options.duration ?? 2000, showDismiss: typeof options.showDismiss === 'boolean' ? options.showDismiss : true, - isPersistent: options.isPersistent || false, - displayOnNextPage: options.displayOnNextPage || false, status: status, text: text, }; diff --git a/projects/packages/search/src/dashboard/components/global-notices/style.scss b/projects/packages/search/src/dashboard/components/global-notices/style.scss index 374e7f87583c..67023bf83dbb 100644 --- a/projects/packages/search/src/dashboard/components/global-notices/style.scss +++ b/projects/packages/search/src/dashboard/components/global-notices/style.scss @@ -1,5 +1,4 @@ @use "../../scss/z-index"; -@use "scss/calypso-colors"; @use "scss/calypso-mixins"; .global-notices { @@ -38,45 +37,3 @@ max-width: calc(100% - 64px - 160px); } } - -.global-notices .dops-notice { - flex-wrap: nowrap; - margin-bottom: 0; - text-align: left; - pointer-events: auto; - border-radius: 0; - box-shadow: - 0 2px 5px rgba(0, 0, 0, 0.2), - 0 0 56px rgba(0, 0, 0, 0.15); - - .dops-notice__icon-wrapper { - border-radius: 0; - } - - @include calypso-mixins.breakpoint( ">660px" ) { - display: flex; - overflow: hidden; - margin-bottom: 24px; - border-radius: 3px; - - .dops-notice__icon-wrapper { - border-radius: 3px 0 0 3px; - } - } -} - -.global-notices .dops-notice a.dops-notice__action { - - @include calypso-mixins.breakpoint( ">660px" ) { - font-size: 14px; - padding: 13px 16px; - } -} - -.global-notices .dops-notice__dismiss { - flex-shrink: 0; - - @include calypso-mixins.breakpoint( ">660px" ) { - padding: 13px 16px 0; - } -} diff --git a/projects/packages/search/src/dashboard/components/notice/index.jsx b/projects/packages/search/src/dashboard/components/notice/index.jsx deleted file mode 100644 index 93f3696c679d..000000000000 --- a/projects/packages/search/src/dashboard/components/notice/index.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Gridicon } from '@automattic/jetpack-components'; -import clsx from 'clsx'; -import PropTypes from 'prop-types'; -import { Component } from 'react'; - -import './style.scss'; - -const noop = () => {}; - -export default class SimpleNotice extends Component { - static displayName = 'SimpleNotice'; - - static defaultProps = { - duration: 0, - status: null, - showDismiss: true, - className: '', - onDismissClick: noop, - }; - - static propTypes = { - // we should validate the allowed statuses - status: PropTypes.string, - showDismiss: PropTypes.bool, - isCompact: PropTypes.bool, - duration: PropTypes.number, - text: PropTypes.oneOfType( [ - PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ), - PropTypes.arrayOf( PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ) ), - ] ), - icon: PropTypes.string, - onDismissClick: PropTypes.func, - className: PropTypes.string, - }; - - dismissTimeout = null; - - componentDidMount() { - if ( this.props.duration > 0 ) { - this.dismissTimeout = setTimeout( this.props.onDismissClick, this.props.duration ); - } - } - - componentWillUnmount() { - if ( this.dismissTimeout ) { - clearTimeout( this.dismissTimeout ); - } - } - - getIcon = () => { - let icon; - - switch ( this.props.status ) { - case 'is-info': - icon = 'info'; - break; - case 'is-success': - icon = 'checkmark'; - break; - case 'is-error': - icon = 'notice'; - break; - case 'is-warning': - icon = 'notice'; - break; - default: - icon = 'info'; - break; - } - - return icon; - }; - - clearText = text => { - if ( 'string' === typeof text ) { - return text.replace( /(<([^>]+)>)/gi, '' ); - } - return text; - }; - - onKeyDownCallback = callback => event => { - if ( event.which === 13 || event.which === 32 ) { - callback && callback( event ); - } - }; - - render() { - const { - children, - className, - icon, - isCompact, - onDismissClick, - showDismiss = ! isCompact, // by default, show on normal notices, don't show on compact ones - status, - text, - dismissText, - } = this.props; - const classes = clsx( 'dops-notice', status, className, { - 'is-compact': isCompact, - 'is-dismissable': showDismiss, - } ); - - return ( -
- - - - - { text ? this.clearText( text ) : children } - - { text ? children : null } - { showDismiss && ( - - - - { dismissText } - - - ) } -
- ); - } -} diff --git a/projects/packages/search/src/dashboard/components/notice/notice-action.jsx b/projects/packages/search/src/dashboard/components/notice/notice-action.jsx deleted file mode 100644 index b50e4d5d23a5..000000000000 --- a/projects/packages/search/src/dashboard/components/notice/notice-action.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Gridicon } from '@automattic/jetpack-components'; -import PropTypes from 'prop-types'; -import { Component } from 'react'; - -import './style.scss'; - -export default class NoticeAction extends Component { - static displayName = 'NoticeAction'; - - static propTypes = { - href: PropTypes.string, - onClick: PropTypes.func, - external: PropTypes.bool, - icon: PropTypes.string, - }; - - static defaultProps = { - external: false, - }; - - render() { - const attributes = { - className: 'dops-notice__action', - href: this.props.href, - onClick: this.props.onClick, - }; - - if ( this.props.external ) { - attributes.target = '_blank'; - } - - return ( - - { this.props.children } - { this.props.icon && } - { this.props.external && } - - ); - } -} diff --git a/projects/packages/search/src/dashboard/components/notice/style.scss b/projects/packages/search/src/dashboard/components/notice/style.scss deleted file mode 100644 index 9e5960e8fb30..000000000000 --- a/projects/packages/search/src/dashboard/components/notice/style.scss +++ /dev/null @@ -1,287 +0,0 @@ -@use "scss/calypso-colors"; -@use "scss/layout"; -@use "scss/calypso-mixins"; - -.dops-notice { - display: flex; - position: relative; - width: 100%; - margin-bottom: 24px; - box-sizing: border-box; - animation: appear 0.3s ease-in-out; - background: calypso-colors.$gray-dark; - color: calypso-colors.$white; - border-radius: 3px; - line-height: 1.5; - - // Success! - &.is-success { - - .dops-notice__icon-wrapper { - background: calypso-colors.$alert-green; - } - } - - // Warning - &.is-warning { - - .dops-notice__icon-wrapper { - background: calypso-colors.$alert-yellow; - } - } - - // Error! OHNO! - &.is-error { - - .dops-notice__icon-wrapper { - background: calypso-colors.$alert-red; - } - } - - // General notice - &.is-info { - - .dops-notice__icon-wrapper { - background: calypso-colors.$blue-medium; - } - } - - &.is-success, - &.is-error, - &.is-warning, - &.is-info { - - .dops-notice__dismiss { - overflow: hidden; - } - } -} - -.dops-notice__icon-wrapper { - background: calypso-colors.$gray-text-min; - color: calypso-colors.$white; - display: flex; - align-items: baseline; - width: 47px; - justify-content: center; - border-radius: 3px 0 0 3px; - flex-shrink: 0; - align-self: stretch; - - .gridicon { - margin-top: 10px; - - @include calypso-mixins.breakpoint( ">480px" ) { - margin-top: 12px; - } - } -} - -.dops-notice__content.dops-notice__content { - padding: 13px; - font-size: 12px; - flex-grow: 1; - - @include calypso-mixins.breakpoint( ">480px" ) { - font-size: 14px; - } - - a { - text-decoration: underline; - color: calypso-colors.$white; - } - - a:hover { - text-decoration: none; - } -} - -.dops-notice__text { - - a.dops-notice__text-no-underline { - text-decoration: none; - } - - a, - a:visited { - text-decoration: underline; - color: calypso-colors.$white; - - &:hover { - color: calypso-colors.$white; - text-decoration: none; - } - } - - ul { - margin-bottom: 0; - margin-left: 0; - } - - li { - margin-left: 2em; - margin-top: 0.5em; - } - - p { - margin-bottom: 0; - margin-top: 0.5em; - - &:first-child { - margin-top: 0; - } - } -} - -.dops-notice__button { - cursor: pointer; - margin-left: 0.428em; -} - -// "X" for dismissing a notice -.dops-notice__dismiss { - flex-shrink: 0; - padding: 12px; - cursor: pointer; - padding-bottom: 0; - - .gridicon { - width: 18px; - height: 18px; - } - - @include calypso-mixins.breakpoint( ">480px" ) { - padding: 11px; - padding-bottom: 0; - - .gridicon { - width: 24px; - height: 24px; - } - } - - .dops-notice & { - overflow: hidden; - color: calypso-colors.$gray-lighten-10; - - &:hover, - &:focus { - color: calypso-colors.$white; - } - } -} - -// specificity for general `a` elements within notice is too great -a.dops-notice__action { - cursor: pointer; - font-size: 12px; - font-weight: 400; - text-decoration: none; - white-space: nowrap; - color: calypso-colors.$gray-lighten-10; - padding: 13px; - display: flex; - align-items: center; - - @include calypso-mixins.breakpoint( ">480px" ) { - flex-shrink: 1; - flex-grow: 0; - align-items: center; - border-radius: 0; - font-size: 14px; - margin: 0 0 0 auto; // forces the element to the right; - padding: 13px 16px; - - .gridicon { - width: 24px; - height: 24px; - } - } - - &:visited { - color: calypso-colors.$gray-lighten-10; - } - - &:hover { - color: calypso-colors.$white; - } - - .gridicon { - margin-left: 8px; - opacity: 0.7; - width: 18px; - height: 18px; - } -} - -// Compact notices -.dops-notice.is-compact { - display: inline-flex; - flex-wrap: nowrap; - flex-direction: row; - width: auto; - border-radius: 3px; - min-height: 20px; - margin: 0; - padding: 0; - text-decoration: none; - text-transform: none; - vertical-align: middle; - line-height: 1.5; - - .dops-notice__content { - font-size: 12px; - padding: 6px 10px; - } - - .dops-notice__icon-wrapper { - width: 28px; - - .dops-notice__icon { - width: 18px; - height: 18px; - margin: 0; - } - - .gridicon { - margin-top: 6px; - } - } - - .dops-notice__dismiss { - position: relative; - align-self: center; - flex: none; - margin: 0 8px 0 0; - padding: 0; - - .gridicon { - width: 18px; - height: 18px; - } - } - - a.dops-notice__action { - background: transparent; - display: inline-block; - margin: 0; - font-size: 12px; - align-self: center; - margin-left: 16px; - padding: 0 10px; - - &:hover, - &:active, - &:focus { - background: transparent; - } - - .gridicon { - margin-left: 8px; - width: 14px; - height: 14px; - vertical-align: sub; - opacity: 1; - } - } -} diff --git a/projects/packages/search/src/dashboard/components/notice/test/index.test.jsx b/projects/packages/search/src/dashboard/components/notice/test/index.test.jsx deleted file mode 100644 index 9b1c592a2e45..000000000000 --- a/projects/packages/search/src/dashboard/components/notice/test/index.test.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { render } from '@testing-library/react'; -import SimpleNotice from 'components/notice'; - -describe( 'SimpleNotice', function () { - const testProps = { - className: 'test-class', - }; - describe( 'rendering', function () { - it( 'can render', () => { - const { container } = render( ); - expect( - // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access - container.getElementsByClassName( 'dops-notice__icon-wrapper' ).length - ).toBeGreaterThan( 0 ); - } ); - - it( 'can render with class name passed in', () => { - const { container } = render( Toggle Label ); - // eslint-disable-next-line testing-library/no-node-access - expect( container.firstChild ).toHaveClass( 'test-class' ); - } ); - } ); -} ); diff --git a/projects/packages/search/src/dashboard/components/pages/sections/first-run-section.jsx b/projects/packages/search/src/dashboard/components/pages/sections/first-run-section.jsx index 7c4a99fa7b7b..246e9173fb98 100644 --- a/projects/packages/search/src/dashboard/components/pages/sections/first-run-section.jsx +++ b/projects/packages/search/src/dashboard/components/pages/sections/first-run-section.jsx @@ -1,10 +1,8 @@ import { IndeterminateProgressBar, ThemeProvider } from '@automattic/jetpack-components'; import { __, sprintf } from '@wordpress/i18n'; -import SimpleNotice from 'components/notice'; +import { Notice } from '@wordpress/ui'; import PlanSummary from './plan-summary'; -// import './first-run-section.scss'; - const FirstRunSection = ( { planInfo, siteTitle } ) => { return (
@@ -21,9 +19,6 @@ const FirstRunSection = ( { planInfo, siteTitle } ) => { ); }; -// TODO: Move this back inline. -// Per Jason's feedback, doesn't think we should break this out. -// https://github.com/Automattic/jetpack/pull/26639#discussion_r989592860 const ProgressWrapper = ( { siteTitle } ) => { return (
@@ -41,28 +36,16 @@ const ProgressWrapper = ( { siteTitle } ) => { ); }; -// TODO: Remove const variables. -// Per Jason's feedback, thinks we should put these inline. -// https://github.com/Automattic/jetpack/pull/26639#discussion_r989593312 -const NoticeWrapper = () => { - const noticeBoxClassName = 'jp-search-notice-box'; - const header = __( "We're gathering your usage data.", 'jetpack-search-pkg' ); - const message = __( - 'If you have recently set up Search, please allow a little time for indexing to complete.', - 'jetpack-search-pkg' - ); - return ( - -

{ header }

- { message } -
- ); -}; +const NoticeWrapper = () => ( + + { __( "We're gathering your usage data.", 'jetpack-search-pkg' ) } + + { __( + 'If you have recently set up Search, please allow a little time for indexing to complete.', + 'jetpack-search-pkg' + ) } + + +); export default FirstRunSection; diff --git a/projects/packages/search/src/dashboard/components/record-meter/notice-box.jsx b/projects/packages/search/src/dashboard/components/record-meter/notice-box.jsx index fd4a833f5ddb..e5c70dacfad9 100644 --- a/projects/packages/search/src/dashboard/components/record-meter/notice-box.jsx +++ b/projects/packages/search/src/dashboard/components/record-meter/notice-box.jsx @@ -1,9 +1,6 @@ import { formatNumber } from '@automattic/number-formatters'; import { __, sprintf } from '@wordpress/i18n'; -import SimpleNotice from 'components/notice'; -import NoticeAction from 'components/notice/notice-action'; - -import './notice-box.scss'; +import { Notice } from '@wordpress/ui'; const CLOSE_TO_LIMIT_PERCENT = 0.8; @@ -96,26 +93,18 @@ export function NoticeBox( props ) { const NOTICES = getNotices( props.tierMaximumRecords ); const notice = NOTICES[ activeNoticeIds[ 0 ] ]; - const noticeBoxClassName = notice.isImportant - ? 'jp-search-notice-box jp-search-notice-box__important' - : 'jp-search-notice-box'; - return ( - - { notice.header &&

{ notice.header }

} - { notice.message && { notice.message } } + + { notice.header && { notice.header } } + { notice.message && { notice.message } } { notice.link && ( - - { notice.link.text } - + + + { notice.link.text } + + ) } -
+ ); } diff --git a/projects/packages/search/src/dashboard/components/record-meter/notice-box.scss b/projects/packages/search/src/dashboard/components/record-meter/notice-box.scss deleted file mode 100644 index 345efe6c1550..000000000000 --- a/projects/packages/search/src/dashboard/components/record-meter/notice-box.scss +++ /dev/null @@ -1,79 +0,0 @@ -// Variables -@use "@automattic/color-studio/dist/color-variables" as studio; - -.jp-search-notice-box { - background-color: #fff; - color: #000; - border: studio.$studio-gray-5 0.5px solid; - border-radius: 5px; - padding: 24px; -} - -.jp-search-notice-box .dops-notice__content { - padding: 0 16px; -} - -.jp-search-notice-box .dops-notice__body { - display: block; - padding-bottom: 4px; - padding-top: 16px; -} - -// Use dops classname for increased specificity. -.jp-search-notice-box.dops-notice > span.dops-notice__icon-wrapper { - background-color: rgba(255, 255, 255, 0); - padding: 0; -} - -.jp-search-notice-box .dops-notice__text, -.jp-search-notice-box .dops-notice__text .dops-notice__header { - margin: 0; - display: block; - font-size: 1em; -} - -.jp-search-notice-box__important { - border: studio.$studio-red-50 0.5px solid; -} - -.jp-search-notice-box__important .dops-notice__content .dops-notice__header { - font-weight: 700; -} - -.jp-search-notice-box .dops-notice__action { - color: #000; - display: block; - font-weight: 700; - padding: 0 0 6px 0; -} - -// Need to specify 'a.' to override component styles -.jp-search-notice-box a.dops-notice__action { - - &:link, - &:visited, - &:hover, - &:active { - color: #000; - text-decoration: underline; - } -} - -.jp-search-notice-box .dops-notice__icon-wrapper { - color: #000; - flex-grow: 0; - justify-content: left; - padding: 0; - width: 24px; -} - -.jp-search-notice-box__important .dops-notice__action, -.jp-search-notice-box__important .dops-notice__icon-wrapper, -.jp-search-notice-box__important, -.jp-search-notice-box__important .dops-notice__header { - color: studio.$studio-red-50; -} - -.jp-search-notice-box > .dops-notice__icon-wrapper > svg { - margin: 0; -}