diff --git a/assets/js/products-block.js b/assets/js/products-block.js index c075eecba40..6e69542d6d2 100644 --- a/assets/js/products-block.js +++ b/assets/js/products-block.js @@ -99,7 +99,8 @@ var _wp$components = wp.components, withAPIData = _wp$components.withAPIData, Dropdown = _wp$components.Dropdown, Dashicon = _wp$components.Dashicon, - RangeControl = _wp$components.RangeControl; + RangeControl = _wp$components.RangeControl, + Tooltip = _wp$components.Tooltip; var ToggleControl = InspectorControls.ToggleControl, SelectControl = InspectorControls.SelectControl; @@ -452,6 +453,30 @@ var ProductsBlockSettingsEditor = function (_React$Component3) { ); } + var done_button = wp.element.createElement( + 'button', + { type: 'button', className: 'button wc-products-settings__footer-button', onClick: this.props.done_callback }, + __('Done') + ); + if (['', 'specific', 'category', 'attribute'].includes(this.state.display) && !this.props.selected_display_setting.length) { + var done_tooltips = { + '': __('Please select which products you\'d like to display'), + specific: __('Please search for and select products to display'), + category: __('Please select at least one category to display'), + attribute: __('Please select an attribute') + }; + + done_button = wp.element.createElement( + Tooltip, + { text: done_tooltips[this.state.display] }, + wp.element.createElement( + 'button', + { type: 'button', className: 'button wc-products-settings__footer-button disabled' }, + __('Done') + ) + ); + } + return wp.element.createElement( 'div', { className: 'wc-products-settings ' + (this.state.expanded_group ? 'expanded-group-' + this.state.expanded_group : '') }, @@ -468,11 +493,7 @@ var ProductsBlockSettingsEditor = function (_React$Component3) { wp.element.createElement( 'div', { className: 'wc-products-settings__footer' }, - wp.element.createElement( - 'button', - { type: 'button', className: 'button wc-products-settings__footer-button', onClick: this.props.done_callback }, - __('Done') - ) + done_button ) ); } @@ -798,12 +819,20 @@ registerBlockType('woocommerce/products', { * @return Component */ function getSettingsEditor() { + + var update_display_callback = function update_display_callback(value) { + if (display !== value) { + setAttributes({ + display: value, + display_setting: [] + }); + } + }; + return wp.element.createElement(ProductsBlockSettingsEditor, { selected_display: display, selected_display_setting: display_setting, - update_display_callback: function update_display_callback(value) { - return setAttributes({ display: value }); - }, + update_display_callback: update_display_callback, update_display_setting_callback: function update_display_setting_callback(value) { return setAttributes({ display_setting: value }); }, @@ -843,7 +872,7 @@ registerBlockType('woocommerce/products', { } if ('specific' === display) { - shortcode_atts.set('include', display_setting.join(',')); + shortcode_atts.set('ids', display_setting.join(',')); } else if ('category' === display) { shortcode_atts.set('category', display_setting.join(',')); } else if ('featured' === display) { @@ -935,6 +964,13 @@ var _ReactTransitionGroup = ReactTransitionGroup, TransitionGroup = _ReactTransitionGroup.TransitionGroup, CSSTransition = _ReactTransitionGroup.CSSTransition; +/** + * Product data cache. + * Reduces the number of API calls and makes UI smoother and faster. + */ + +var PRODUCT_DATA = {}; + /** * When the display mode is 'Specific products' search for and add products to the block. * @@ -1046,7 +1082,7 @@ var ProductsSpecificSelect = exports.ProductsSpecificSelect = function (_React$C selectedProducts: this.state.selectedProducts }), wp.element.createElement(ProductSpecificSelectedProducts, { - products: this.state.selectedProducts, + productIds: this.state.selectedProducts, removeProductCallback: this.removeProduct.bind(this) }) ); @@ -1200,6 +1236,32 @@ var ProductSpecificSearchResults = withAPIData(function (props) { return __('No products found'); } + // Populate the cache. + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = products.data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var product = _step2.value; + + PRODUCT_DATA[product.id] = product; + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + return wp.element.createElement(ProductSpecificSearchResultsDropdown, { products: products.data, addProductCallback: addProductCallback, @@ -1236,13 +1298,13 @@ var ProductSpecificSearchResultsDropdown = function (_React$Component3) { var productElements = []; - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; try { - for (var _iterator2 = products[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var product = _step2.value; + for (var _iterator3 = products[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var product = _step3.value; if (selectedProducts.includes(product.id)) { continue; @@ -1262,16 +1324,16 @@ var ProductSpecificSearchResultsDropdown = function (_React$Component3) { )); } } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; + _didIteratorError3 = true; + _iteratorError3 = err; } finally { try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); } } finally { - if (_didIteratorError2) { - throw _iteratorError2; + if (_didIteratorError3) { + throw _iteratorError3; } } } @@ -1367,27 +1429,146 @@ var ProductSpecificSearchResultsDropdownElement = function (_React$Component4) { var ProductSpecificSelectedProducts = withAPIData(function (props) { - if (!props.products.length) { + if (!props.productIds.length) { return { products: [] }; } + // Determine which products are not already in the cache and only fetch uncached products. + var uncachedProducts = []; + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = props.productIds[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var productId = _step4.value; + + if (!PRODUCT_DATA.hasOwnProperty(productId)) { + uncachedProducts.push(productId); + } + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + return { - products: '/wc/v2/products?include=' + props.products.join(',') + products: uncachedProducts.length ? '/wc/v2/products?include=' + uncachedProducts.join(',') : [] }; })(function (_ref2) { - var products = _ref2.products, + var productIds = _ref2.productIds, + products = _ref2.products, removeProductCallback = _ref2.removeProductCallback; - if (!products.data) { - return null; + + // Add new products to cache. + if (products.data) { + var _iteratorNormalCompletion5 = true; + var _didIteratorError5 = false; + var _iteratorError5 = undefined; + + try { + for (var _iterator5 = products.data[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { + var product = _step5.value; + + PRODUCT_DATA[product.id] = product; + } + } catch (err) { + _didIteratorError5 = true; + _iteratorError5 = err; + } finally { + try { + if (!_iteratorNormalCompletion5 && _iterator5.return) { + _iterator5.return(); + } + } finally { + if (_didIteratorError5) { + throw _iteratorError5; + } + } + } } - if (0 === products.data.length) { + if (0 === productIds.length) { return __('No products selected'); } + var productElements = []; + + var _loop = function _loop(productId) { + + // Skip products that aren't in the cache yet or failed to fetch. + if (!PRODUCT_DATA.hasOwnProperty(productId)) { + return 'continue'; + } + + var productData = PRODUCT_DATA[productId]; + + productElements.push(wp.element.createElement( + 'li', + { className: 'wc-products-list-card__item' }, + wp.element.createElement( + 'div', + { className: 'wc-products-list-card__content' }, + wp.element.createElement('img', { src: productData.images[0].src }), + wp.element.createElement( + 'span', + { className: 'wc-products-list-card__content-item-name' }, + productData.name + ), + wp.element.createElement( + 'button', + { + type: 'button', + id: 'product-' + productData.id, + onClick: function onClick() { + removeProductCallback(productData.id); + } }, + wp.element.createElement(Dashicon, { icon: 'no-alt' }) + ) + ) + )); + }; + + var _iteratorNormalCompletion6 = true; + var _didIteratorError6 = false; + var _iteratorError6 = undefined; + + try { + for (var _iterator6 = productIds[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { + var productId = _step6.value; + + var _ret = _loop(productId); + + if (_ret === 'continue') continue; + } + } catch (err) { + _didIteratorError6 = true; + _iteratorError6 = err; + } finally { + try { + if (!_iteratorNormalCompletion6 && _iterator6.return) { + _iterator6.return(); + } + } finally { + if (_didIteratorError6) { + throw _iteratorError6; + } + } + } + return wp.element.createElement( 'div', { className: 'wc-products-list-card__results-wrapper' }, @@ -1397,32 +1578,7 @@ var ProductSpecificSelectedProducts = withAPIData(function (props) { wp.element.createElement( 'ul', null, - products.data.map(function (product) { - return wp.element.createElement( - 'li', - { className: 'wc-products-list-card__item' }, - wp.element.createElement( - 'div', - { className: 'wc-products-list-card__content' }, - wp.element.createElement('img', { src: product.images[0].src }), - wp.element.createElement( - 'span', - { className: 'wc-products-list-card__content-item-name' }, - product.name - ), - wp.element.createElement( - 'button', - { - type: 'button', - id: 'product-' + product.id, - onClick: function onClick() { - removeProductCallback(product.id); - } }, - wp.element.createElement(Dashicon, { icon: 'no-alt' }) - ) - ) - ); - }) + productElements ) ) ); diff --git a/assets/js/products-block.jsx b/assets/js/products-block.jsx index a2a703621a5..3287513e9ad 100644 --- a/assets/js/products-block.jsx +++ b/assets/js/products-block.jsx @@ -1,7 +1,7 @@ const { __ } = wp.i18n; const { RawHTML } = wp.element; const { registerBlockType, InspectorControls, BlockControls } = wp.blocks; -const { Toolbar, withAPIData, Dropdown, Dashicon, RangeControl } = wp.components; +const { Toolbar, withAPIData, Dropdown, Dashicon, RangeControl, Tooltip } = wp.components; const { ToggleControl, SelectControl } = InspectorControls; import { ProductsSpecificSelect } from './views/specific-select.jsx'; @@ -267,6 +267,23 @@ class ProductsBlockSettingsEditor extends React.Component { ); } + let done_button = ; + if ( ['', 'specific', 'category', 'attribute'].includes( this.state.display ) && ! this.props.selected_display_setting.length ) { + const done_tooltips = { + '': __( 'Please select which products you\'d like to display' ), + specific: __( 'Please search for and select products to display' ), + category: __( 'Please select at least one category to display' ), + attribute: __( 'Please select an attribute' ), + } + + done_button = ( + + + + ); + } + + return (

{ __( 'Products' ) }

@@ -278,7 +295,7 @@ class ProductsBlockSettingsEditor extends React.Component { { extra_settings }
- + { done_button }
); @@ -526,11 +543,21 @@ registerBlockType( 'woocommerce/products', { * @return Component */ function getSettingsEditor() { + + const update_display_callback = ( value ) => { + if ( display !== value ) { + setAttributes( { + display: value, + display_setting: [], + } ); + } + }; + return ( setAttributes( { display: value } ) } + update_display_callback={ update_display_callback } update_display_setting_callback={ ( value ) => setAttributes( { display_setting: value } ) } done_callback={ () => setAttributes( { edit_mode: false } ) } /> @@ -564,7 +591,7 @@ registerBlockType( 'woocommerce/products', { } if ( 'specific' === display ) { - shortcode_atts.set( 'include', display_setting.join( ',' ) ); + shortcode_atts.set( 'ids', display_setting.join( ',' ) ); } else if ( 'category' === display ) { shortcode_atts.set( 'category', display_setting.join( ',' ) ); } else if ( 'featured' === display ) { diff --git a/assets/js/views/specific-select.jsx b/assets/js/views/specific-select.jsx index b457f7dc79b..8167e659d8c 100644 --- a/assets/js/views/specific-select.jsx +++ b/assets/js/views/specific-select.jsx @@ -2,6 +2,12 @@ const { __ } = wp.i18n; const { Toolbar, withAPIData, Dropdown, Dashicon } = wp.components; const { TransitionGroup, CSSTransition } = ReactTransitionGroup; +/** + * Product data cache. + * Reduces the number of API calls and makes UI smoother and faster. + */ +const PRODUCT_DATA = {}; + /** * When the display mode is 'Specific products' search for and add products to the block. * @@ -76,7 +82,7 @@ export class ProductsSpecificSelect extends React.Component { selectedProducts={ this.state.selectedProducts } /> @@ -194,6 +200,11 @@ const ProductSpecificSearchResults = withAPIData( ( props ) => { return __( 'No products found' ); } + // Populate the cache. + for ( let product of products.data ) { + PRODUCT_DATA[ product.id ] = product; + } + return { - if ( ! props.products.length ) { + if ( ! props.productIds.length ) { return { products: [] }; } + // Determine which products are not already in the cache and only fetch uncached products. + let uncachedProducts = []; + for( const productId of props.productIds ) { + if ( ! PRODUCT_DATA.hasOwnProperty( productId ) ) { + uncachedProducts.push( productId ); + } + } + return { - products: '/wc/v2/products?include=' + props.products.join( ',' ) + products: uncachedProducts.length ? '/wc/v2/products?include=' + uncachedProducts.join( ',' ) : [] }; - } )( ( { products, removeProductCallback } ) => { - if ( ! products.data ) { - return null; + } )( ( { productIds, products, removeProductCallback } ) => { + + // Add new products to cache. + if ( products.data ) { + for ( const product of products.data ) { + PRODUCT_DATA[ product.id ] = product; + } } - if ( 0 === products.data.length ) { + if ( 0 === productIds.length ) { return __( 'No products selected' ); } + const productElements = []; + + for ( const productId of productIds ) { + + // Skip products that aren't in the cache yet or failed to fetch. + if ( ! PRODUCT_DATA.hasOwnProperty( productId ) ) { + continue; + } + + const productData = PRODUCT_DATA[ productId ]; + + productElements.push( +
  • +
    + + { productData.name } + +
    +
  • + ); + } + return (
      - { products.data.map( ( product ) => ( -
    • -
      - - { product.name } - -
      -
    • - ) ) } + { productElements }