Skip to content

Commit

Permalink
Block Directory: Throw error if we have an issue registering blocks. (#…
Browse files Browse the repository at this point in the history
…23439)

* Throw error if we have an issue registering blocks.

* Remove empty newline.

* Try out more error handling.

* Add typerror.

* Slight refactor.

* Add `isInstallable` property to disable the Add button on blocks with fatal errors

* Update the copy of error messages

* Remove variable abbreviation.

* Move the state shape from action to reducer.

* Fix broken tests.

* Fix typo in test.

* Fix margin bottom for content.

Co-authored-by: Kelly Dwan <ryelle@users.noreply.github.com>
  • Loading branch information
StevenDufresne and ryelle authored Jul 3, 2020
1 parent 5fd9619 commit f14005a
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ function DownloadableBlockHeader( {
title,
rating,
ratingCount,
isLoading,
isLoading = false,
isInstallable = true,
onClick,
} ) {
return (
Expand All @@ -31,10 +32,10 @@ function DownloadableBlockHeader( {
<Button
isSecondary
isBusy={ isLoading }
disabled={ isLoading }
disabled={ isLoading || ! isInstallable }
onClick={ ( event ) => {
event.preventDefault();
if ( ! isLoading ) {
if ( ! isLoading && isInstallable ) {
onClick();
}
} }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import { pluginWithIcon } from './fixtures';
const getContainer = (
{ icon, title, rating, ratingCount },
onClick = jest.fn(),
isLoading = false
isLoading = false,
isInstallable = true
) => {
return shallow(
<DownloadableBlockHeader
Expand All @@ -27,6 +28,7 @@ const getContainer = (
rating={ rating }
ratingCount={ ratingCount }
isLoading={ isLoading }
isInstallable={ isInstallable }
/>
);
};
Expand Down Expand Up @@ -54,5 +56,21 @@ describe( 'DownloadableBlockHeader', () => {
expect( event.preventDefault ).toHaveBeenCalled();
expect( onClickMock ).toHaveBeenCalledTimes( 0 );
} );

test( 'should not trigger the onClick function if not installable', () => {
const onClickMock = jest.fn();
const wrapper = getContainer(
pluginWithIcon,
onClickMock,
false,
false
);
const event = {
preventDefault: jest.fn(),
};
wrapper.find( Button ).simulate( 'click', event );
expect( onClickMock ).toHaveBeenCalledTimes( 0 );
expect( event.preventDefault ).toHaveBeenCalled();
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,18 @@ import DownloadableBlockInfo from '../downloadable-block-info';
import DownloadableBlockNotice from '../downloadable-block-notice';

export default function DownloadableBlockListItem( { item, onClick } ) {
const isLoading = useSelect(
( select ) => select( 'core/block-directory' ).isInstalling( item.id ),
const { isLoading, isInstallable } = useSelect(
( select ) => {
const { isInstalling, getErrorNoticeForBlock } = select(
'core/block-directory'
);
const notice = getErrorNoticeForBlock( item.id );
const hasFatal = notice && notice.isFatal;
return {
isLoading: isInstalling( item.id ),
isInstallable: ! hasFatal,
};
},
[ item ]
);

Expand Down Expand Up @@ -41,6 +51,7 @@ export default function DownloadableBlockListItem( { item, onClick } ) {
rating={ rating }
ratingCount={ ratingCount }
isLoading={ isLoading }
isInstallable={ isInstallable }
/>
</header>
<section className="block-directory-downloadable-block-list-item__body">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ exports[`DownloadableBlockListItem should render a block item 1`] = `
>
<DownloadableBlockHeader
icon="block-default"
isInstallable={true}
isLoading={false}
onClick={[MockFunction]}
rating={5}
title="Boxer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import { shallow } from 'enzyme';

/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
Expand All @@ -18,6 +23,11 @@ jest.mock( '@wordpress/data/src/components/use-select', () => {

describe( 'DownloadableBlockListItem', () => {
it( 'should render a block item', () => {
useSelect.mockImplementation( () => ( {
isLoading: false,
isInstallable: true,
} ) );

const wrapper = shallow(
<DownloadableBlockListItem onClick={ jest.fn() } item={ item } />
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@ export const DownloadableBlockNotice = ( { block, onClick } ) => {
className="block-directory-downloadable-block-notice"
>
<div className="block-directory-downloadable-block-notice__content">
{ errorNotice }
{ errorNotice.message }
</div>
<Button
isSmall
isPrimary
onClick={ () => {
if ( errorNotice.isFatal ) {
window.location.reload();
return false;
}

onClick( block );
} }
>
{ __( 'Retry' ) }
{ errorNotice.isFatal ? __( 'Reload' ) : __( 'Retry' ) }
</Button>
</Notice>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ describe( 'DownloadableBlockNotice', () => {
} );

it( 'should return something when there are error notices', () => {
useSelect.mockImplementation( () => 'Plugin not found.' );
useSelect.mockImplementation( () => {
return { message: 'Plugin not found.', isFatal: false };
} );
const wrapper = shallow(
<DownloadableBlockNotice
block={ plugin }
Expand Down
43 changes: 35 additions & 8 deletions packages/block-directory/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,38 @@ export function* installBlockType( block ) {

yield loadAssets( assets );
const registeredBlocks = yield select( 'core/blocks', 'getBlockTypes' );
if ( ! registeredBlocks.length ) {
throw new Error( __( 'Unable to get block types.' ) );
if (
! registeredBlocks.length ||
! registeredBlocks.filter( ( i ) => i.name === block.name ).length
) {
throw new Error(
__( 'Error registering block. Try reloading the page.' )
);
}

success = true;
} catch ( error ) {
yield setErrorNotice( id, error.message || __( 'An error occurred.' ) );
let message = error.message || __( 'An error occurred.' );

// Errors we throw are fatal
let isFatal = error instanceof Error;

// Specific API errors that are fatal
const fatalAPIErrors = {
folder_exists: __(
'This block is already installed. Try reloading the page.'
),
unable_to_connect_to_filesystem: __(
'Error installing block. You can reload the page and try again.'
),
};

if ( fatalAPIErrors[ error.code ] ) {
isFatal = true;
message = fatalAPIErrors[ error.code ];
}

yield setErrorNotice( id, message, isFatal );
}
yield setIsInstalling( block.id, false );
return success;
Expand Down Expand Up @@ -154,15 +180,17 @@ export function setIsInstalling( blockId, isInstalling ) {
* Sets an error notice string to be displayed to the user
*
* @param {string} blockId The ID of the block plugin. eg: my-block
* @param {string} notice The message shown in the notice.
* @param {string} message The message shown in the notice.
* @param {boolean} isFatal Whether the user can recover from the error
*
* @return {Object} Action object.
*/
export function setErrorNotice( blockId, notice ) {
export function setErrorNotice( blockId, message, isFatal = false ) {
return {
type: 'SET_ERROR_NOTICE',
blockId,
notice,
message,
isFatal,
};
}

Expand All @@ -175,8 +203,7 @@ export function setErrorNotice( blockId, notice ) {
*/
export function clearErrorNotice( blockId ) {
return {
type: 'SET_ERROR_NOTICE',
type: 'CLEAR_ERROR_NOTICE',
blockId,
notice: false,
};
}
13 changes: 12 additions & 1 deletion packages/block-directory/src/store/reducer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* External dependencies
*/

import { omit } from 'lodash';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -88,8 +94,13 @@ export const errorNotices = ( state = {}, action ) => {
case 'SET_ERROR_NOTICE':
return {
...state,
[ action.blockId ]: action.notice,
[ action.blockId ]: {
message: action.message,
isFatal: action.isFatal,
},
};
case 'CLEAR_ERROR_NOTICE':
return omit( state, action.blockId );
}
return state;
};
Expand Down
2 changes: 1 addition & 1 deletion packages/block-directory/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,5 @@ export function getErrorNotices( state ) {
* @return {string|boolean} The error text, or false if no error.
*/
export function getErrorNoticeForBlock( state, blockId ) {
return state.errorNotices[ blockId ] || false;
return state.errorNotices[ blockId ];
}
9 changes: 3 additions & 6 deletions packages/block-directory/src/store/test/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ describe( 'actions', () => {
const generator = installBlockType( item );

expect( generator.next().value ).toEqual( {
type: 'SET_ERROR_NOTICE',
type: 'CLEAR_ERROR_NOTICE',
blockId: item.id,
notice: false,
} );

expect( generator.next().value ).toEqual( {
Expand Down Expand Up @@ -82,9 +81,8 @@ describe( 'actions', () => {
const generator = installBlockType( { ...item, assets: [] } );

expect( generator.next().value ).toEqual( {
type: 'SET_ERROR_NOTICE',
type: 'CLEAR_ERROR_NOTICE',
blockId: item.id,
notice: false,
} );

expect( generator.next().value ).toMatchObject( {
Expand All @@ -108,9 +106,8 @@ describe( 'actions', () => {
const generator = installBlockType( item );

expect( generator.next().value ).toEqual( {
type: 'SET_ERROR_NOTICE',
type: 'CLEAR_ERROR_NOTICE',
blockId: item.id,
notice: false,
} );

expect( generator.next().value ).toEqual( {
Expand Down
53 changes: 34 additions & 19 deletions packages/block-directory/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,62 +87,77 @@ describe( 'state', () => {
describe( 'errorNotices()', () => {
it( 'should set an error notice', () => {
const initialState = deepFreeze( {} );
const error = {
message: 'error',
isFatal: false,
};

const state = errorNotices( initialState, {
type: 'SET_ERROR_NOTICE',
blockId: 'block/has-error',
notice: 'Error',
...error,
} );

expect( state ).toEqual( {
'block/has-error': 'Error',
'block/has-error': error,
} );
} );

it( 'should set a second error notice', () => {
const initialState = deepFreeze( {
'block/has-error': 'Error',
'block/has-error': {
message: 'error',
isFatal: false,
},
} );
const state = errorNotices( initialState, {
type: 'SET_ERROR_NOTICE',
blockId: 'block/another-error',
notice: 'Error',
message: 'error',
isFatal: true,
} );

expect( state ).toEqual( {
'block/has-error': 'Error',
'block/another-error': 'Error',
'block/has-error': {
message: 'error',
isFatal: false,
},
'block/another-error': {
message: 'error',
isFatal: true,
},
} );
} );

it( 'should clear an existing error notice', () => {
const initialState = deepFreeze( {
'block/has-error': 'Error',
'block/has-error': {
message: 'existing error',
isFatal: false,
},
} );

const state = errorNotices( initialState, {
type: 'SET_ERROR_NOTICE',
type: 'CLEAR_ERROR_NOTICE',
blockId: 'block/has-error',
notice: false,
} );

expect( state ).toEqual( {
'block/has-error': false,
} );
expect( state ).toEqual( {} );
} );

it( 'should clear a nonexistent error notice', () => {
const initialState = deepFreeze( {
'block/has-error': 'Error',
'block/has-error': {
message: 'new error',
isFatal: true,
},
} );
const state = errorNotices( initialState, {
type: 'SET_ERROR_NOTICE',
type: 'CLEAR_ERROR_NOTICE',
blockId: 'block/no-error',
notice: false,
} );

expect( state ).toEqual( {
'block/has-error': 'Error',
'block/no-error': false,
} );
expect( state ).toEqual( initialState );
} );
} );
} );
Loading

0 comments on commit f14005a

Please sign in to comment.