* error handling

* Update assets/js/blocks/featured-product/block.js

Co-Authored-By: Albert Juhé Lluveras <contact@albertjuhe.com>

* Update assets/js/blocks/featured-product/block.js

Co-Authored-By: Albert Juhé Lluveras <contact@albertjuhe.com>

* Move to dedicated component

* escape messages and remove debug code

* merge conflict

* move renderApiError

* Revert "move renderApiError"

This reverts commit 2d5ffdecf2fae66434ac16b71d9b349fa1b61783.

* Revert "merge conflict"

This reverts commit bdc8eb4bd0f8dd5e4525c758fe3e79ffeefff3f5.

* Update assets/js/components/api-error-placeholder/index.js

Co-Authored-By: Albert Juhé Lluveras <contact@albertjuhe.com>

* Update error notice
This commit is contained in:
Mike Jolley 2019-07-11 11:12:44 +01:00 committed by GitHub
parent 76f93866b3
commit ac74504400
5 changed files with 328 additions and 196 deletions

View File

@ -35,3 +35,17 @@
}
}
}
.wc-block-api-error {
.components-placeholder__fieldset {
display: block;
margin: 0;
padding: 0;
}
.wc-block-error__message {
margin-bottom: 16px;
}
.components-spinner {
float: none;
}
}

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { escapeHTML } from '@wordpress/escape-html';
import apiFetch from '@wordpress/api-fetch';
import {
AlignmentToolbar,
@ -36,6 +37,7 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import ProductControl from '../../components/product-control';
import ApiErrorPlaceholder from '../../components/api-error-placeholder';
import {
getImageSrcFromProduct,
getImageIdFromProduct,
@ -84,6 +86,7 @@ class FeaturedProduct extends Component {
this.state = {
product: false,
loaded: false,
error: false,
};
this.debouncedGetProduct = debounce( this.getProduct.bind( this ), 200 );
@ -103,17 +106,34 @@ class FeaturedProduct extends Component {
const { productId } = this.props.attributes;
if ( ! productId ) {
// We've removed the selected product, or no product is selected yet.
this.setState( { product: false, loaded: true } );
this.setState( { product: false, loaded: true, error: false } );
return;
}
apiFetch( {
path: `/wc/blocks/products/${ productId }`,
} )
.then( ( product ) => {
this.setState( { product, loaded: true } );
this.setState( { product, loaded: true, error: false } );
} )
.catch( () => {
this.setState( { product: false, loaded: true } );
.catch( ( apiError ) => {
const error = {
retry: this.debouncedGetProduct,
};
if ( isObject( apiError ) ) {
error.message = (
<span>
{ __( 'The following error was returned from the API', 'woo-gutenberg-products-block' ) }
<br />
<code>{ escapeHTML( apiError.message ) }</code>
</span>
);
} else {
error.message = __( 'An unknown error occurred which prevented the block from being updated.', 'woo-gutenberg-products-block' );
}
this.setState( { error: false } ); // Force update if error stays same.
this.setState( { product: false, loaded: true, error: error } );
} );
}
@ -187,44 +207,108 @@ class FeaturedProduct extends Component {
};
return (
<Placeholder
icon="star-filled"
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
className="wc-block-featured-product"
>
{ __(
'Visually highlight a product or variation and encourage prompt action',
'woo-gutenberg-products-block'
) }
<div className="wc-block-featured-product__selection">
<ProductControl
selected={ attributes.productId || 0 }
onChange={ ( value = [] ) => {
const id = value[ 0 ] ? value[ 0 ].id : 0;
setAttributes( { productId: id, mediaId: 0, mediaSrc: '' } );
} }
/>
<Button isDefault onClick={ onDone }>
{ __( 'Done', 'woo-gutenberg-products-block' ) }
</Button>
</div>
</Placeholder>
<Fragment>
{ this.getBlockControls() }
<Placeholder
icon="star-filled"
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
className="wc-block-featured-product"
>
{ __(
'Visually highlight a product or variation and encourage prompt action',
'woo-gutenberg-products-block'
) }
<div className="wc-block-featured-product__selection">
<ProductControl
selected={ attributes.productId || 0 }
onChange={ ( value = [] ) => {
const id = value[ 0 ] ? value[ 0 ].id : 0;
setAttributes( { productId: id, mediaId: 0, mediaSrc: '' } );
} }
/>
<Button isDefault onClick={ onDone }>
{ __( 'Done', 'woo-gutenberg-products-block' ) }
</Button>
</div>
</Placeholder>
</Fragment>
);
}
render() {
renderApiError() {
const { error } = this.state;
const onRetryCallback = () => {
error.retry();
};
return (
<ApiErrorPlaceholder
onRetry={ onRetryCallback }
errorMessage={ error.message }
className="wc-block-featured-product-error"
/>
);
}
getBlockControls() {
const { attributes, setAttributes } = this.props;
const { product } = this.state;
const { contentAlign, editMode } = attributes;
const mediaId = attributes.mediaId || getImageIdFromProduct( product );
return (
<BlockControls>
<AlignmentToolbar
value={ contentAlign }
onChange={ ( nextAlign ) => {
setAttributes( { contentAlign: nextAlign } );
} }
/>
<MediaUploadCheck>
<Toolbar>
<MediaUpload
onSelect={ ( media ) => {
setAttributes( { mediaId: media.id, mediaSrc: media.url } );
} }
allowedTypes={ [ 'image' ] }
value={ mediaId }
render={ ( { open } ) => (
<IconButton
className="components-toolbar__control"
label={ __( 'Edit media' ) }
icon="format-image"
onClick={ open }
disabled={ ! this.state.product }
/>
) }
/>
</Toolbar>
</MediaUploadCheck>
<Toolbar
controls={ [
{
icon: 'edit',
title: __( 'Edit' ),
onClick: () => setAttributes( { editMode: ! editMode } ),
isActive: editMode,
},
] }
/>
</BlockControls>
);
}
renderProduct() {
const { attributes, isSelected, overlayColor, setAttributes } = this.props;
const { loaded, product } = this.state;
const {
className,
contentAlign,
dimRatio,
editMode,
focalPoint,
height,
showDesc,
showPrice,
} = attributes;
const { loaded, product } = this.state;
const classes = classnames(
'wc-block-featured-product',
{
@ -237,11 +321,9 @@ class FeaturedProduct extends Component {
contentAlign !== 'center' && `has-${ contentAlign }-content`,
className,
);
const mediaId = attributes.mediaId || getImageIdFromProduct( product );
const style = !! product ?
backgroundImageStyles( attributes.mediaSrc || product ) :
{};
const style = backgroundImageStyles( attributes.mediaSrc || product );
if ( overlayColor.color ) {
style.backgroundColor = overlayColor.color;
}
@ -255,112 +337,107 @@ class FeaturedProduct extends Component {
};
return (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ contentAlign }
onChange={ ( nextAlign ) => {
setAttributes( { contentAlign: nextAlign } );
<ResizableBox
className={ classes }
size={ { height } }
minHeight={ MIN_HEIGHT }
enable={ { bottom: true } }
onResizeStop={ onResizeStop }
style={ style }
>
<div className="wc-block-featured-product__wrapper">
<h2
className="wc-block-featured-product__title"
dangerouslySetInnerHTML={ {
__html: product.name,
} }
/>
<MediaUploadCheck>
<Toolbar>
<MediaUpload
onSelect={ ( media ) => {
setAttributes( { mediaId: media.id, mediaSrc: media.url } );
} }
allowedTypes={ [ 'image' ] }
value={ mediaId }
render={ ( { open } ) => (
<IconButton
className="components-toolbar__control"
label={ __( 'Edit media' ) }
icon="format-image"
onClick={ open }
disabled={ ! this.state.product }
/>
) }
/>
</Toolbar>
</MediaUploadCheck>
</BlockControls>
{ ! attributes.editMode && this.getInspectorControls() }
{ editMode ? (
this.renderEditMode()
{ ! isEmpty( product.variation ) && (
<h3
className="wc-block-featured-product__variation"
dangerouslySetInnerHTML={ {
__html: product.variation,
} }
/>
) }
{ showDesc && (
<div
className="wc-block-featured-product__description"
dangerouslySetInnerHTML={ {
__html: product.description,
} }
/>
) }
{ showPrice && (
<div
className="wc-block-featured-product__price"
dangerouslySetInnerHTML={ { __html: product.price_html } }
/>
) }
<div className="wc-block-featured-product__link">
<InnerBlocks
template={ [
[
'core/button',
{
text: __(
'Shop now',
'woo-gutenberg-products-block'
),
url: product.permalink,
align: 'center',
},
],
] }
templateLock="all"
/>
</div>
</div>
</ResizableBox>
);
}
renderNoProduct() {
const { loaded } = this.state;
return (
<Placeholder
className="wc-block-featured-product"
icon="star-filled"
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
>
{ ! loaded ? (
<Spinner />
) : (
<Fragment>
{ !! product ? (
<ResizableBox
className={ classes }
size={ { height } }
minHeight={ MIN_HEIGHT }
enable={ { bottom: true } }
onResizeStop={ onResizeStop }
style={ style }
>
<div className="wc-block-featured-product__wrapper">
<h2
className="wc-block-featured-product__title"
dangerouslySetInnerHTML={ {
__html: product.name,
} }
/>
{ ! isEmpty( product.variation ) && (
<h3
className="wc-block-featured-product__variation"
dangerouslySetInnerHTML={ {
__html: product.variation,
} }
/>
) }
{ showDesc && (
<div
className="wc-block-featured-product__description"
dangerouslySetInnerHTML={ {
__html: product.description,
} }
/>
) }
{ showPrice && (
<div
className="wc-block-featured-product__price"
dangerouslySetInnerHTML={ { __html: product.price_html } }
/>
) }
<div className="wc-block-featured-product__link">
<InnerBlocks
template={ [
[
'core/button',
{
text: __(
'Shop now',
'woo-gutenberg-products-block'
),
url: product.permalink,
align: 'center',
},
],
] }
templateLock="all"
/>
</div>
</div>
</ResizableBox>
) : (
<Placeholder
className="wc-block-featured-product"
icon="star-filled"
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
>
{ ! loaded ? (
<Spinner />
) : (
__( 'No product is selected.', 'woo-gutenberg-products-block' )
) }
</Placeholder>
) }
</Fragment>
__( 'No product is selected.', 'woo-gutenberg-products-block' )
) }
</Placeholder>
);
}
render() {
const { product, error } = this.state;
const { attributes } = this.props;
const { editMode } = attributes;
// If there was an API error, render it.
if ( error ) {
return this.renderApiError();
}
// If editing, show edit controls.
if ( editMode ) {
return this.renderEditMode();
}
// Otherwise render the selected product!
return (
<Fragment>
{ this.getBlockControls() }
{ this.getInspectorControls() }
{ !! product ? (
this.renderProduct()
) : (
this.renderNoProduct()
) }
</Fragment>
);

View File

@ -8,6 +8,9 @@
z-index: 10;
}
}
.wc-block-featured-product__message {
margin-bottom: 16px;
}
.wc-block-featured-product__selection {
width: 100%;

View File

@ -0,0 +1,76 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import Gridicon from 'gridicons';
import classNames from 'classnames';
import {
Button,
Placeholder,
Spinner,
} from '@wordpress/components';
/**
* Internal dependencies
*/
class ApiErrorPlaceholder extends Component {
constructor() {
super( ...arguments );
this.state = {
retrying: false,
};
this.onRetry = this.onRetry.bind( this );
}
onRetry() {
const { onRetry } = this.props;
this.setState( { retrying: true } );
onRetry();
}
render() {
const { onRetry, errorMessage, className } = this.props;
const { retrying } = this.state;
return (
<Placeholder
icon={ <Gridicon icon="notice" /> }
label={ __( 'Sorry, an error occurred', 'woo-gutenberg-products-block' ) }
className={ classNames( 'wc-block-api-error', className ) }
>
<div className="wc-block-error__message">{ errorMessage }</div>
{ onRetry && (
<Fragment>
{ !! retrying ? (
<Spinner />
) : (
<Button isDefault onClick={ this.onRetry }>
{ __( 'Retry', 'woo-gutenberg-products-block' ) }
</Button>
) }
</Fragment>
) }
</Placeholder>
);
}
}
ApiErrorPlaceholder.propTypes = {
/**
* Callback to retry an action.
*/
onRetry: PropTypes.func.isRequired,
/**
* The error message to display from the API.
*/
errorMessage: PropTypes.node,
/**
* Classname to add to placeholder in addition to the defaults.
*/
className: PropTypes.string,
};
export default ApiErrorPlaceholder;

View File

@ -6528,8 +6528,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -6550,14 +6549,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -6572,20 +6569,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -6702,8 +6696,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -6715,7 +6708,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -6730,7 +6722,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -6738,14 +6729,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -6764,7 +6753,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -6845,8 +6833,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -6858,7 +6845,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -6944,8 +6930,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -6981,7 +6966,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -7001,7 +6985,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -7045,14 +7028,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@ -10171,8 +10152,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -10193,14 +10173,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -10215,20 +10193,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -10345,8 +10320,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -10358,7 +10332,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -10373,7 +10346,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -10381,14 +10353,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -10407,7 +10377,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -10495,8 +10464,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -10508,7 +10476,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -10594,8 +10561,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -10631,7 +10597,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -10651,7 +10616,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -10695,14 +10659,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},