Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update editor sidebar validation notifications #5929

Merged
merged 34 commits into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e7b63c8
Introduce editor sidebar notification component
delawski Feb 25, 2021
6ea0cb6
Introduce AMP status icon to be used in sidebar notifications
delawski Feb 25, 2021
bba583a
Set `isFetchingErrors` flag when editor loads
delawski Feb 25, 2021
6baa417
Extract validation status messages to notification components
delawski Feb 25, 2021
dd50f6c
Update the reviewed errors toggle appearance
delawski Feb 25, 2021
fdd63e2
Use uniform CSS naming convention for Icon and add unit tests
delawski Feb 25, 2021
99344da
Do not show revalidate notification when fetching validation errors
delawski Feb 25, 2021
d729b5a
Add unit test to cover validation errors fetching state
delawski Feb 25, 2021
6916764
Merge branch 'develop' into feature/5304-validation-notifications
delawski Mar 8, 2021
924f812
Fix build issue related to SVG markup
delawski Mar 8, 2021
543ab48
Add loading state message and reorder notifications in sidebar
delawski Mar 8, 2021
c37125e
Add API request error handling
delawski Mar 8, 2021
ab4f1f3
Merge branch 'develop' of github.com:ampproject/amp-wp into feature/5…
westonruter Mar 16, 2021
e3ebdca
Add review link back to validation status notification
delawski Mar 16, 2021
6bb8883
Use consistent border-left treatment for error messages
delawski Mar 16, 2021
74868b9
Use refreshed design for AMP sidebar
delawski Mar 16, 2021
e442893
Revamp sidebar AMP errors list CSS
delawski Mar 17, 2021
3d04b45
Clarify what outside content means
westonruter Mar 18, 2021
9d35150
Add index back to errors list `key` in case there is no `clientId`
delawski Mar 18, 2021
6b78c8b
Remove redundant error state from status notification
delawski Mar 18, 2021
14f2d1d
Update assets/src/block-editor/components/sidebar-notification/index.js
delawski Mar 18, 2021
27c7019
Fix broken unit tests and update snapshots
delawski Mar 18, 2021
e365eaf
Merge remote-tracking branch 'origin/feature/5304-validation-notifica…
delawski Mar 18, 2021
74c6e37
Show revalidate notification when editor mode has switched
westonruter Mar 18, 2021
9bfb41b
Fix test after message change in 74c6e3798
westonruter Mar 18, 2021
8b42ad9
Add error counts to notification messages; make clear when no issues
westonruter Mar 19, 2021
6f678f2
Remove reference to 'post' in pending-save message
westonruter Mar 19, 2021
d1c05da
Change block editor plugin title from 'AMP' to 'AMP Validation'
westonruter Mar 19, 2021
cb830a0
Remove unnecessary optional chaining operator
delawski Mar 19, 2021
efeba44
Use more exact types in JSDoc and propTypes definition
delawski Mar 19, 2021
aa668d0
Get `blockName` directly without other properties
delawski Mar 19, 2021
56ceb8c
Check if post is new early
delawski Mar 19, 2021
4794812
Fix unit tests and update snapshots
delawski Mar 19, 2021
17ef326
Use array find method instead of loop
delawski Mar 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions assets/images/amp-logo-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/images/bell-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions assets/src/block-editor/components/sidebar-notification/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { ReactNode } from 'react';

/**
* Internal dependencies
*/
import './style.css';
import { Loading } from '../../../components/loading';

/**
* Notification component used in the block editor sidebar.
*
* @param {Object} props
* @param {ReactNode} props.action Call to action element.
* @param {ReactNode} props.icon Status icon element.
* @param {boolean} props.isLoading Flag indicating if it's a loading message.
* @param {string} props.message Message text.
*/
export function SidebarNotification( {
action,
icon,
isLoading = false,
message,
} ) {
const iconElement = isLoading ? <Loading /> : icon;

return (
<div className={ classnames( 'sidebar-notification', { 'is-loading': isLoading } ) }>
{ iconElement && (
<div className="sidebar-notification__icon">
{ iconElement }
</div>
) }
<div className="sidebar-notification__content">
<p>
{ message }
</p>
{ action && (
<p>
{ action }
</p>
) }
</div>
</div>
);
}
SidebarNotification.propTypes = {
action: PropTypes.node,
icon: PropTypes.node,
isLoading: PropTypes.bool,
message: PropTypes.string.isRequired,
};

/**
* Sidebar notifications container component.
*
* @param {Object} props
* @param {Object} props.children Component children.
* @param {boolean} props.isShady Flag indicating if the component should have a background.
*/
export function SidebarNotificationsContainer( { children, isShady } ) {
return (
<div className={ classnames( 'sidebar-notifications-container', { 'is-shady': isShady } ) }>
{ children }
</div>
);
}
SidebarNotificationsContainer.propTypes = {
children: PropTypes.any,
isShady: PropTypes.bool,
};
60 changes: 60 additions & 0 deletions assets/src/block-editor/components/sidebar-notification/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.sidebar-notification {
display: flex;
min-height: 67px;
overflow: hidden;
padding: 14px 18px;
position: relative;
}

.sidebar-notification.is-loading {
align-items: center;
}

.sidebar-notification__icon {
align-items: center;
border-radius: 50%;
display: flex;
flex: 0 0 auto;
height: 24px;
justify-content: center;
margin: 0 12px 0 0;
position: relative;
width: 24px;
}

.sidebar-notification__icon svg {
height: 100%;
width: 100%;
}

.sidebar-notification__content {
display: flex;
flex-direction: column;
justify-content: center;
}

.sidebar-notification__content > p {
margin: 0;
}

.sidebar-notification__content > p + p {
margin-top: 4px;
}

/*
* Sidebar notifications container with light background.
*/
.sidebar-notifications-container.is-shady {
background: #f4f4f4;
border-bottom: 1px solid #e3e4e7;
padding: 18px 10px;
}

.sidebar-notifications-container.is-shady > .sidebar-notification {
background: #fff;
border-radius: 15px;
}

.sidebar-notifications-container.is-shady > .sidebar-notification + .sidebar-notification {
margin-top: 15px;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* External dependencies
*/
import { act } from 'react-dom/test-utils';

/**
* WordPress dependencies
*/
import { render, unmountComponentAtNode } from '@wordpress/element';

/**
* Internal dependencies
*/
import { SidebarNotification } from '../index';

describe( 'SidebarNotification', () => {
let container;

beforeEach( () => {
container = document.createElement( 'div' );
document.body.appendChild( container );
} );

afterEach( () => {
unmountComponentAtNode( container );
container.remove();
container = null;
} );

it( 'renders notification without icon and call to action', () => {
act( () => {
render( <SidebarNotification message="Foobar" />, container );
} );

expect( container.innerHTML ).toMatchSnapshot();
expect( container.children ).toHaveLength( 1 );
expect( container.querySelector( '.sidebar-notification' ) ).not.toBeNull();
expect( container.querySelector( '.sidebar-notification__icon' ) ).toBeNull();
expect( container.querySelector( '.sidebar-notification__content' ).textContent ).toBe( 'Foobar' );
} );

it( 'renders status message with icon and call to action', () => {
act( () => {
render(
<SidebarNotification
message="Foobar"
icon={ <svg /> }
action={ <button /> }
/>,
container,
);
} );

expect( container.innerHTML ).toMatchSnapshot();
expect( container.querySelector( 'svg' ) ).not.toBeNull();
expect( container.querySelector( 'button' ) ).not.toBeNull();
} );
} );

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { act } from 'react-dom/test-utils';

/**
* WordPress dependencies
*/
import { render, unmountComponentAtNode } from '@wordpress/element';

/**
* Internal dependencies
*/
import { SidebarNotificationsContainer } from '../index';

describe( 'SidebarNotificationsContainer', () => {
let container;

beforeEach( () => {
container = document.createElement( 'div' );
document.body.appendChild( container );
} );

afterEach( () => {
unmountComponentAtNode( container );
container.remove();
container = null;
} );

it( 'renders sidebar notifications container along with children', () => {
act( () => {
render(
<SidebarNotificationsContainer>
{ 'Foo' }
</SidebarNotificationsContainer>,
container,
);
} );

expect( container.querySelector( '.sidebar-notifications-container' ) ).not.toBeNull();
expect( container.querySelector( '.sidebar-notifications-container' ).textContent ).toBe( 'Foo' );
} );
} );

2 changes: 2 additions & 0 deletions assets/src/block-validation/amp-validation-status/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as AMPValidationStatusNotification } from './status-notification';
export { default as AMPRevalidateNotification } from './revalidate-notification';
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { usePrevious } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { BLOCK_VALIDATION_STORE_KEY } from '../store';
import BellIcon from '../../../images/bell-icon.svg';
import { SidebarNotification } from '../../block-editor/components/sidebar-notification';

/**
* AMP re-validate status message.
*/
export default function AMPRevalidateNotification() {
const [ didFetchErrors, setDidFetchErrors ] = useState( false );
const [ loadingMessage, setLoadingMessage ] = useState( '' );
const { autosave, savePost } = useDispatch( 'core/editor' );

const {
hasErrorsFromRemovedBlocks,
hasActiveMetaboxes,
isDraft,
isFetchingErrors,
isEditedPostNew,
isPostDirty,
} = useSelect( ( select ) => {
let _hasErrorsFromRemovedBlocks = false;
for ( const validationError of select( BLOCK_VALIDATION_STORE_KEY ).getValidationErrors() ) {
const { clientId } = validationError;
const blockDetails = clientId ? select( 'core/block-editor' ).getBlock( clientId ) : null;
if ( clientId && ! blockDetails ) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in aa668d0, we could get here just the block name instead of the entire block object, i.e.

Suggested change
const blockDetails = clientId ? select( 'core/block-editor' ).getBlock( clientId ) : null;
if ( clientId && ! blockDetails ) {
if ( clientId && ! select( 'core/block-editor' ).getBlockName( clientId ) ) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I think about it, we could actually replace the entire loop with find method like

-		let _hasErrorsFromRemovedBlocks = false;
-		for ( const validationError of select( BLOCK_VALIDATION_STORE_KEY ).getValidationErrors() ) {
-			const { clientId } = validationError;
-			const blockDetails = clientId ? select( 'core/block-editor' ).getBlock( clientId ) : null;
-			if ( clientId && ! blockDetails ) {
-				_hasErrorsFromRemovedBlocks = true;
-				break;
-			}
-		}
-
-		return {
-			hasErrorsFromRemovedBlocks: _hasErrorsFromRemovedBlocks,
+		const errors = select( BLOCK_VALIDATION_STORE_KEY ).getValidationErrors();
+
+		return {
+			hasErrorsFromRemovedBlocks: Boolean( errors.find( ( { clientId } ) => clientId && ! select( 'core/block-editor' ).getBlockName( clientId ) ) ),

But I guess it might not be as readable as the loop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that latter suggestion. Go ahead with it!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, it's done (in an even shorter form) in 17ef326.

_hasErrorsFromRemovedBlocks = true;
break;
}
}

return {
hasErrorsFromRemovedBlocks: _hasErrorsFromRemovedBlocks,
hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(),
isDraft: [ 'draft', 'auto-draft' ].indexOf( select( 'core/editor' ).getEditedPostAttribute( 'status' ) ) !== -1,
isEditedPostNew: select( 'core/editor' ).isEditedPostNew(),
isFetchingErrors: select( BLOCK_VALIDATION_STORE_KEY ).getIsFetchingErrors(),
isPostDirty: select( BLOCK_VALIDATION_STORE_KEY ).getIsPostDirty(),
};
}, [] );

const wasEditedPostNew = usePrevious( isEditedPostNew );
const wasFetchingErrors = usePrevious( isFetchingErrors );

useEffect( () => {
if ( didFetchErrors ) {
return;
}

// Set up the state right after errors fetching has finished.
if ( ! isFetchingErrors && wasFetchingErrors ) {
setDidFetchErrors( true );
}
}, [ didFetchErrors, isFetchingErrors, wasFetchingErrors ] );

/**
* Display best-suited loading message depending if the post has been
* already validated or not, or the editor has just been opened.
*/
useEffect( () => {
if ( didFetchErrors ) {
setLoadingMessage( __( 'Re-validating page content.', 'amp' ) );
} else if ( isEditedPostNew || wasEditedPostNew ) {
setLoadingMessage( __( 'Validating page content.', 'amp' ) );
} else {
setLoadingMessage( __( 'Loading…', 'amp' ) );
}
}, [ didFetchErrors, isEditedPostNew, wasEditedPostNew ] );

if ( isFetchingErrors ) {
return (
<SidebarNotification message={ loadingMessage } isLoading={ true } />
);
}

/**
* For posts where meta boxes are present, it's impossible to tell
* if a meta box content has changed or not. Because of that, a dirty
* state is ignored and it's always possible to save a post.
* Likewise, we always display the re-validate message if there are
* active meta boxes. Also show a re-validate message if there are validation
* errors which used to be in the content but are no longer found, potentially
* due to switching from the visual editor to the code editor.
*/
if ( ! isPostDirty && ! hasActiveMetaboxes && ! hasErrorsFromRemovedBlocks ) {
return null;
}

return (
<SidebarNotification
icon={ <BellIcon /> }
message={ hasActiveMetaboxes || ! isPostDirty
? __( 'Page content may have changed.', 'amp' )
: __( 'Page content has changed.', 'amp' ) }
action={ isDraft ? (
<Button
isLink
onClick={ () => savePost( { isPreview: true } ) }>
{ __( 'Save draft and validate', 'amp' ) }
</Button>
) : (
<Button
isLink
onClick={ () => autosave( { isPreview: true } ) }>
{ __( 'Re-validate', 'amp' ) }
</Button>
) }
/>
);
}
Loading