Add block error boundary to JS-rendered blocks (https://github.com/woocommerce/woocommerce-blocks/pull/1166)
* Add block error boundary to All Products and Reviews blocks * Add block error boundary to Price Filter and Attributes Filter blocks * Add image * Make block error component use props instead of hardcoded values * Add props to BlockErrorBoundary * Change 'text' prop to 'content' * Add docs to proptypes * Replace 'content' prop with 'text'
This commit is contained in:
parent
2fde5d6890
commit
0739e4c536
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 296 KiB |
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { WC_BLOCKS_ASSET_URL } from '@woocommerce/block-settings';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const BlockError = ( {
|
||||||
|
imageUrl = `${ WC_BLOCKS_ASSET_URL }img/block-error.svg`,
|
||||||
|
header = __( 'Oops!', 'woo-gutenberg-products-block' ),
|
||||||
|
text = __(
|
||||||
|
'There was an error with loading this content.',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
),
|
||||||
|
errorMessage,
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<div className="wc-block-error">
|
||||||
|
{ imageUrl && (
|
||||||
|
<img
|
||||||
|
className="wc-block-error__image"
|
||||||
|
src={ imageUrl }
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<div className="wc-block-error__content">
|
||||||
|
{ header && (
|
||||||
|
<p className="wc-block-error__header">{ header }</p>
|
||||||
|
) }
|
||||||
|
{ text && <p className="wc-block-error__text">{ text }</p> }
|
||||||
|
{ errorMessage && (
|
||||||
|
<p className="wc-block-error__message">{ errorMessage }</p>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockError.propTypes = {
|
||||||
|
/**
|
||||||
|
* Error message to display below the content.
|
||||||
|
*/
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Text to display as the heading of the error block.
|
||||||
|
* If it's `null` or an empty string, no header will be displayed.
|
||||||
|
* If it's not defined, the default header will be used.
|
||||||
|
*/
|
||||||
|
header: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* URL of the image to display.
|
||||||
|
* If it's `null` or an empty string, no image will be displayed.
|
||||||
|
* If it's not defined, the default image will be used.
|
||||||
|
*/
|
||||||
|
imageUrl: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Text to display in the error block below the header.
|
||||||
|
* If it's `null` or an empty string, nothing will be displayed.
|
||||||
|
* If it's not defined, the default text will be used.
|
||||||
|
*/
|
||||||
|
text: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockError;
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import BlockError from './block-error';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
class BlockErrorBoundary extends Component {
|
||||||
|
state = { hasError: false };
|
||||||
|
|
||||||
|
static getDerivedStateFromError( error ) {
|
||||||
|
return { errorMessage: error.message, hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { header, imageUrl, showErrorMessage, text } = this.props;
|
||||||
|
const { errorMessage, hasError } = this.state;
|
||||||
|
|
||||||
|
if ( hasError ) {
|
||||||
|
return (
|
||||||
|
<BlockError
|
||||||
|
errorMessage={ showErrorMessage ? errorMessage : null }
|
||||||
|
header={ header }
|
||||||
|
imageUrl={ imageUrl }
|
||||||
|
text={ text }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockErrorBoundary.propTypes = {
|
||||||
|
/**
|
||||||
|
* Text to display as the heading of the error block.
|
||||||
|
* If it's `null` or an empty string, no header will be displayed.
|
||||||
|
* If it's not defined, the default header will be used.
|
||||||
|
*/
|
||||||
|
header: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* URL of the image to display.
|
||||||
|
* If it's `null` or an empty string, no image will be displayed.
|
||||||
|
* If it's not defined, the default image will be used.
|
||||||
|
*/
|
||||||
|
imageUrl: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Whether to display the JS error message.
|
||||||
|
*/
|
||||||
|
showErrorMessage: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Text to display in the error block below the header.
|
||||||
|
* If it's `null` or an empty string, nothing will be displayed.
|
||||||
|
* If it's not defined, the default text will be used.
|
||||||
|
*/
|
||||||
|
text: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockErrorBoundary.defaultProps = {
|
||||||
|
showErrorMessage: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockErrorBoundary;
|
|
@ -0,0 +1,30 @@
|
||||||
|
.wc-block-error {
|
||||||
|
display: flex;
|
||||||
|
background-color: #f3f3f4;
|
||||||
|
border-left: 4px solid #6d6d6d;
|
||||||
|
padding: $gap-larger $gap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-error__header {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-error__text,
|
||||||
|
.wc-block-error__message {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint( ">480px" ) {
|
||||||
|
.wc-block-error {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-error__image + .wc-block-error__content {
|
||||||
|
margin-left: $gap;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import CheckboxList from '@woocommerce/base-components/checkbox-list';
|
import CheckboxList from '@woocommerce/base-components/checkbox-list';
|
||||||
|
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -173,7 +174,7 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
||||||
const TagName = `h${ attributes.headingLevel }`;
|
const TagName = `h${ attributes.headingLevel }`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<BlockErrorBoundary>
|
||||||
{ ! isPreview && attributes.heading && (
|
{ ! isPreview && attributes.heading && (
|
||||||
<TagName>{ attributes.heading }</TagName>
|
<TagName>{ attributes.heading }</TagName>
|
||||||
) }
|
) }
|
||||||
|
@ -185,7 +186,7 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
||||||
isLoading={ attributeTermsLoading }
|
isLoading={ attributeTermsLoading }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</BlockErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,10 @@ import {
|
||||||
useQueryStateByKey,
|
useQueryStateByKey,
|
||||||
useQueryStateByContext,
|
useQueryStateByContext,
|
||||||
} from '@woocommerce/base-hooks';
|
} from '@woocommerce/base-hooks';
|
||||||
import { useCallback, Fragment } from '@wordpress/element';
|
import { useCallback } from '@wordpress/element';
|
||||||
import PriceSlider from '@woocommerce/base-components/price-slider';
|
import PriceSlider from '@woocommerce/base-components/price-slider';
|
||||||
import { CURRENCY } from '@woocommerce/settings';
|
import { CURRENCY } from '@woocommerce/settings';
|
||||||
|
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component displaying a price filter.
|
* Component displaying a price filter.
|
||||||
|
@ -62,7 +63,7 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
|
||||||
const TagName = `h${ attributes.headingLevel }`;
|
const TagName = `h${ attributes.headingLevel }`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<BlockErrorBoundary>
|
||||||
{ ! isPreview && attributes.heading && (
|
{ ! isPreview && attributes.heading && (
|
||||||
<TagName>{ attributes.heading }</TagName>
|
<TagName>{ attributes.heading }</TagName>
|
||||||
) }
|
) }
|
||||||
|
@ -81,7 +82,7 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
|
||||||
isLoading={ isLoading }
|
isLoading={ isLoading }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</BlockErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||||
import ProductListContainer from '@woocommerce/base-components/product-list/container';
|
import ProductListContainer from '@woocommerce/base-components/product-list/container';
|
||||||
import { InnerBlockConfigurationProvider } from '@woocommerce/base-context/inner-block-configuration-context';
|
import { InnerBlockConfigurationProvider } from '@woocommerce/base-context/inner-block-configuration-context';
|
||||||
import { ProductLayoutContextProvider } from '@woocommerce/base-context/product-layout-context';
|
import { ProductLayoutContextProvider } from '@woocommerce/base-context/product-layout-context';
|
||||||
|
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||||
|
|
||||||
const layoutContextConfig = {
|
const layoutContextConfig = {
|
||||||
layoutStyleClassPrefix: 'wc-block-grid',
|
layoutStyleClassPrefix: 'wc-block-grid',
|
||||||
|
@ -33,14 +34,16 @@ class Block extends Component {
|
||||||
* wc-block-{$this->block_name},
|
* wc-block-{$this->block_name},
|
||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<InnerBlockConfigurationProvider value={ parentBlockConfig }>
|
<BlockErrorBoundary>
|
||||||
<ProductLayoutContextProvider value={ layoutContextConfig }>
|
<InnerBlockConfigurationProvider value={ parentBlockConfig }>
|
||||||
<ProductListContainer
|
<ProductLayoutContextProvider value={ layoutContextConfig }>
|
||||||
attributes={ attributes }
|
<ProductListContainer
|
||||||
urlParameterSuffix={ urlParameterSuffix }
|
attributes={ attributes }
|
||||||
/>
|
urlParameterSuffix={ urlParameterSuffix }
|
||||||
</ProductLayoutContextProvider>
|
/>
|
||||||
</InnerBlockConfigurationProvider>
|
</ProductLayoutContextProvider>
|
||||||
|
</InnerBlockConfigurationProvider>
|
||||||
|
</BlockErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import LoadMoreButton from '@woocommerce/base-components/load-more-button';
|
||||||
import ReviewList from '@woocommerce/base-components/review-list';
|
import ReviewList from '@woocommerce/base-components/review-list';
|
||||||
import ReviewSortSelect from '@woocommerce/base-components/review-sort-select';
|
import ReviewSortSelect from '@woocommerce/base-components/review-sort-select';
|
||||||
import withReviews from '@woocommerce/base-hocs/with-reviews';
|
import withReviews from '@woocommerce/base-hocs/with-reviews';
|
||||||
|
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block rendered in the editor.
|
* Block rendered in the editor.
|
||||||
|
@ -51,20 +52,26 @@ class EditorBlock extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disabled>
|
<BlockErrorBoundary>
|
||||||
{ attributes.showOrderby && ENABLE_REVIEW_RATING && (
|
<Disabled>
|
||||||
<ReviewSortSelect readOnly value={ attributes.orderby } />
|
{ attributes.showOrderby && ENABLE_REVIEW_RATING && (
|
||||||
) }
|
<ReviewSortSelect
|
||||||
<ReviewList attributes={ attributes } reviews={ reviews } />
|
readOnly
|
||||||
{ attributes.showLoadMore && totalReviews > reviews.length && (
|
value={ attributes.orderby }
|
||||||
<LoadMoreButton
|
/>
|
||||||
screenReaderLabel={ __(
|
) }
|
||||||
'Load more reviews',
|
<ReviewList attributes={ attributes } reviews={ reviews } />
|
||||||
'woo-gutenberg-products-block'
|
{ attributes.showLoadMore &&
|
||||||
|
totalReviews > reviews.length && (
|
||||||
|
<LoadMoreButton
|
||||||
|
screenReaderLabel={ __(
|
||||||
|
'Load more reviews',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
) }
|
) }
|
||||||
/>
|
</Disabled>
|
||||||
) }
|
</BlockErrorBoundary>
|
||||||
</Disabled>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ENABLE_REVIEW_RATING } from '@woocommerce/block-settings';
|
import { ENABLE_REVIEW_RATING } from '@woocommerce/block-settings';
|
||||||
import LoadMoreButton from '@woocommerce/base-components/load-more-button';
|
import LoadMoreButton from '@woocommerce/base-components/load-more-button';
|
||||||
import ReviewSortSelect from '@woocommerce/base-components/review-sort-select';
|
import ReviewSortSelect from '@woocommerce/base-components/review-sort-select';
|
||||||
import ReviewList from '@woocommerce/base-components/review-list';
|
import ReviewList from '@woocommerce/base-components/review-list';
|
||||||
import withReviews from '@woocommerce/base-hocs/with-reviews';
|
import withReviews from '@woocommerce/base-hocs/with-reviews';
|
||||||
|
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block rendered in the frontend.
|
* Block rendered in the frontend.
|
||||||
|
@ -27,7 +27,7 @@ const FrontendBlock = ( {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<BlockErrorBoundary>
|
||||||
{ attributes.showOrderby !== 'false' && ENABLE_REVIEW_RATING && (
|
{ attributes.showOrderby !== 'false' && ENABLE_REVIEW_RATING && (
|
||||||
<ReviewSortSelect
|
<ReviewSortSelect
|
||||||
defaultValue={ orderby }
|
defaultValue={ orderby }
|
||||||
|
@ -45,7 +45,7 @@ const FrontendBlock = ( {
|
||||||
) }
|
) }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</Fragment>
|
</BlockErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,3 +22,4 @@ export const HAS_TAGS = getSetting( 'hasTags', true );
|
||||||
export const HOME_URL = getSetting( 'homeUrl', '' );
|
export const HOME_URL = getSetting( 'homeUrl', '' );
|
||||||
export const PRODUCT_COUNT = getSetting( 'productCount', 0 );
|
export const PRODUCT_COUNT = getSetting( 'productCount', 0 );
|
||||||
export const ATTRIBUTES = getSetting( 'attributes', [] );
|
export const ATTRIBUTES = getSetting( 'attributes', [] );
|
||||||
|
export const WC_BLOCKS_ASSET_URL = getSetting( 'wcBlocksAssetUrl', '' );
|
||||||
|
|
|
@ -117,6 +117,7 @@ class Assets {
|
||||||
'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ),
|
'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ),
|
||||||
'productCount' => array_sum( (array) $product_counts ),
|
'productCount' => array_sum( (array) $product_counts ),
|
||||||
'attributes' => wc_get_attribute_taxonomies(),
|
'attributes' => wc_get_attribute_taxonomies(),
|
||||||
|
'wcBlocksAssetUrl' => plugins_url( 'assets/', __DIR__ ),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue