From 27345f93a2c35256111c5f8c347bc2b2167273f5 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Mon, 17 Jun 2019 10:23:59 -0400 Subject: [PATCH] Add/block product categories (https://github.com/woocommerce/woocommerce-blocks/pull/613) * Add initial work for Product Categories List block * Add empty category toggle * Add option to show list as a dropdown * Fix console warnings * Only show proptypes linter warning if the component declares proptypes * Add frontend script to render the product categories * Split wcSettings & wc_product_block_data globals so that the later can be used from the frontend * Remove wp dependencies, these are added by the webpack script now * Capture all "woo packages" into a separate script chunk These require `wcSettings`, which can't be available on the frontend - but we do need vendors on the frontend. * Rewrite component into a function * Fix validation error on editor reload --- plugins/woocommerce-blocks/.eslintrc.js | 2 +- .../js/blocks/product-categories/block.js | 70 +++++++++++++ .../js/blocks/product-categories/edit.js | 72 ++++++++++++++ .../js/blocks/product-categories/editor.scss | 3 + .../js/blocks/product-categories/frontend.js | 27 +++++ .../js/blocks/product-categories/hierarchy.js | 36 +++++++ .../js/blocks/product-categories/index.js | 98 +++++++++++++++++++ .../assets/js/components/icons/folder.js | 19 ++++ .../assets/js/components/icons/index.js | 1 + .../assets/js/utils/deprecations.js | 2 +- .../assets/php/class-wgpb-block-library.php | 67 +++++++++---- plugins/woocommerce-blocks/webpack.config.js | 9 ++ 12 files changed, 386 insertions(+), 20 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-categories/edit.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-categories/editor.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-categories/frontend.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-categories/hierarchy.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-categories/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/components/icons/folder.js diff --git a/plugins/woocommerce-blocks/.eslintrc.js b/plugins/woocommerce-blocks/.eslintrc.js index 86ac0738fb3..f3d48bd0b40 100644 --- a/plugins/woocommerce-blocks/.eslintrc.js +++ b/plugins/woocommerce-blocks/.eslintrc.js @@ -101,7 +101,7 @@ module.exports = { 'react/jsx-key': 'error', 'react/jsx-tag-spacing': 'error', 'react/no-children-prop': 'off', - 'react/prop-types': 'warn', + 'react/prop-types': [ 'warn', { 'skipUndeclared': true } ], 'react/react-in-jsx-scope': 'off', semi: 'error', 'semi-spacing': 'error', diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js new file mode 100644 index 00000000000..c997feedeab --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { noop } from 'lodash'; +import { SelectControl } from '@wordpress/components'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import { buildTermsTree } from './hierarchy'; + +function getCategories( { hasEmpty, isDropdown, isHierarchical } ) { + const categories = wc_product_block_data.productCategories.filter( + ( cat ) => hasEmpty || !! cat.count + ); + return ! isDropdown && isHierarchical ? + buildTermsTree( categories ) : + categories; +} + +/** + * Component displaying the categories as dropdown or list. + */ +const ProductCategoriesBlock = ( { attributes } ) => { + const { hasCount, isDropdown } = attributes; + const categories = getCategories( attributes ); + const parentKey = 'parent-' + categories[ 0 ].term_id; + + const renderList = ( items ) => ( + + ); + + return ( +
+ { isDropdown ? ( + ( { + label: hasCount ? `${ cat.name } (${ cat.count })` : cat.name, + value: cat.term_id, + } ) ) } + onChange={ noop } + /> + ) : ( + renderList( categories ) + ) } +
+ ); +}; + +ProductCategoriesBlock.propTypes = { + /** + * The attributes for this block + */ + attributes: PropTypes.object.isRequired, +}; + +export default ProductCategoriesBlock; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/edit.js b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/edit.js new file mode 100644 index 00000000000..cb3519a4878 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/edit.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { InspectorControls } from '@wordpress/editor'; +import { PanelBody, ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import Block from './block.js'; + +export default function( { attributes, setAttributes } ) { + const { hasCount, hasEmpty, isDropdown, isHierarchical } = attributes; + + return ( + + + + setAttributes( { isDropdown: ! isDropdown } ) } + /> + setAttributes( { hasCount: ! hasCount } ) } + /> + { ! isDropdown && ( + setAttributes( { isHierarchical: ! isHierarchical } ) } + /> + ) } + setAttributes( { hasEmpty: ! hasEmpty } ) } + /> + + + + + ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/editor.scss new file mode 100644 index 00000000000..e933d3f9481 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/editor.scss @@ -0,0 +1,3 @@ +.wc-block-product-categories.wc-block-product-categories ul { + margin-left: 20px; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/frontend.js b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/frontend.js new file mode 100644 index 00000000000..1d9a313dee7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/frontend.js @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Block from './block.js'; + +const containers = document.querySelectorAll( + '.wp-block-woocommerce-product-categories' +); + +if ( containers.length ) { + containers.forEach( ( el ) => { + const data = JSON.parse( JSON.stringify( el.dataset ) ); + const attributes = { + hasCount: data.hasCount === 'true', + hasEmpty: data.hasEmpty === 'true', + isDropdown: data.isDropdown === 'true', + isHierarchical: data.isHierarchical === 'true', + }; + + render( , el ); + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/hierarchy.js b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/hierarchy.js new file mode 100644 index 00000000000..c7d029c4ab8 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/hierarchy.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { forEach, groupBy } from 'lodash'; + +/** + * Returns terms in a tree form. + * + * @param {Array} list Array of terms in flat format. + * + * @return {Array} Array of terms in tree format. + */ +export function buildTermsTree( list = [] ) { + const termsByParent = groupBy( list, 'parent' ); + + const fillWithChildren = ( terms ) => { + return terms.map( ( term ) => { + const children = termsByParent[ term.term_id ]; + delete termsByParent[ term.term_id ]; + return { + ...term, + children: children && children.length ? fillWithChildren( children ) : [], + }; + } ); + }; + + const tree = fillWithChildren( termsByParent[ '0' ] || [] ); + delete termsByParent[ '0' ]; + + // anything left in termsByParent has no visible parent + forEach( termsByParent, ( terms ) => { + tree.push( ...fillWithChildren( terms || [] ) ); + } ); + + return tree; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/index.js b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/index.js new file mode 100644 index 00000000000..83b4b392d24 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/index.js @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import edit from './edit.js'; +import { IconFolder } from '../../components/icons'; + +registerBlockType( 'woocommerce/product-categories', { + title: __( 'Product Categories List', 'woo-gutenberg-products-block' ), + icon: { + src: , + foreground: '#96588a', + }, + category: 'woocommerce', + keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ], + description: __( + 'Show your product categories as a list or dropdown.', + 'woo-gutenberg-products-block' + ), + supports: { + align: [ 'wide', 'full' ], + }, + + attributes: { + /** + * Whether to show the product count in each category. + */ + hasCount: { + type: 'boolean', + default: true, + source: 'attribute', + selector: 'div', + attribute: 'data-has-count', + }, + + /** + * Whether to show empty categories in the list. + */ + hasEmpty: { + type: 'boolean', + default: false, + source: 'attribute', + selector: 'div', + attribute: 'data-has-empty', + }, + + /** + * Whether to display product categories as a dropdown (true) or list (false). + */ + isDropdown: { + type: 'boolean', + default: false, + source: 'attribute', + selector: 'div', + attribute: 'data-is-dropdown', + }, + + /** + * Whether the product categories should display with hierarchy. + */ + isHierarchical: { + type: 'boolean', + default: true, + source: 'attribute', + selector: 'div', + attribute: 'data-is-hierarchical', + }, + }, + + edit, + + /** + * Save the props to post content. + */ + save( { attributes } ) { + const { hasCount, hasEmpty, isDropdown, isHierarchical } = attributes; + const props = {}; + if ( hasCount ) { + props[ 'data-has-count' ] = true; + } + if ( hasEmpty ) { + props[ 'data-has-empty' ] = true; + } + if ( isDropdown ) { + props[ 'data-is-dropdown' ] = true; + } + if ( isHierarchical ) { + props[ 'data-is-hierarchical' ] = true; + } + return
LOADING
; + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/components/icons/folder.js b/plugins/woocommerce-blocks/assets/js/components/icons/folder.js new file mode 100644 index 00000000000..361d1dcdfba --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/components/icons/folder.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { Icon } from '@wordpress/components'; + +export default () => ( + + + + } + /> +); diff --git a/plugins/woocommerce-blocks/assets/js/components/icons/index.js b/plugins/woocommerce-blocks/assets/js/components/icons/index.js index cbb730830f5..1a8d162bae9 100644 --- a/plugins/woocommerce-blocks/assets/js/components/icons/index.js +++ b/plugins/woocommerce-blocks/assets/js/components/icons/index.js @@ -1,6 +1,7 @@ // Export each icon as a named component. export { default as IconCheckChecked } from './checkbox-checked'; export { default as IconCheckUnchecked } from './checkbox-unchecked'; +export { default as IconFolder } from './folder'; export { default as IconNewReleases } from './new-releases'; export { default as IconRadioSelected } from './radio-selected'; export { default as IconRadioUnselected } from './radio-unselected'; diff --git a/plugins/woocommerce-blocks/assets/js/utils/deprecations.js b/plugins/woocommerce-blocks/assets/js/utils/deprecations.js index cedbc6c9c20..0d1e0ea3140 100644 --- a/plugins/woocommerce-blocks/assets/js/utils/deprecations.js +++ b/plugins/woocommerce-blocks/assets/js/utils/deprecations.js @@ -17,7 +17,7 @@ export const deprecatedConvertToShortcode = ( blockType ) => { const { align, contentVisibility, - } = props.attributes; /* eslint-disable-line react/prop-types */ + } = props.attributes; const classes = classnames( align ? `align${ align }` : '', { 'is-hidden-title': ! contentVisibility.title, 'is-hidden-price': ! contentVisibility.price, diff --git a/plugins/woocommerce-blocks/assets/php/class-wgpb-block-library.php b/plugins/woocommerce-blocks/assets/php/class-wgpb-block-library.php index 72b0e21455d..8da0de736cb 100644 --- a/plugins/woocommerce-blocks/assets/php/class-wgpb-block-library.php +++ b/plugins/woocommerce-blocks/assets/php/class-wgpb-block-library.php @@ -52,7 +52,9 @@ class WGPB_Block_Library { } self::register_blocks(); self::register_assets(); - add_action( 'admin_print_footer_scripts', array( 'WGPB_Block_Library', 'print_script_settings' ), 1 ); + add_action( 'admin_print_footer_scripts', array( 'WGPB_Block_Library', 'print_script_wc_settings' ), 1 ); + add_action( 'admin_print_footer_scripts', array( 'WGPB_Block_Library', 'print_script_block_data' ), 1 ); + add_action( 'wp_print_footer_scripts', array( 'WGPB_Block_Library', 'print_script_block_data' ), 1 ); add_action( 'body_class', array( 'WGPB_Block_Library', 'add_theme_body_class' ), 1 ); } @@ -122,30 +124,28 @@ class WGPB_Block_Library { // Shared libraries and components across all blocks. self::register_script( 'wc-blocks', plugins_url( 'build/blocks.js', WGPB_PLUGIN_FILE ), array(), false ); self::register_script( 'wc-vendors', plugins_url( 'build/vendors.js', WGPB_PLUGIN_FILE ), array(), false ); + self::register_script( 'wc-packages', plugins_url( 'build/packages.js', WGPB_PLUGIN_FILE ), array(), false ); + self::register_script( 'wc-frontend', plugins_url( 'build/frontend.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors' ) ); // Individual blocks. - self::register_script( 'wc-handpicked-products', plugins_url( 'build/handpicked-products.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-product-best-sellers', plugins_url( 'build/product-best-sellers.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-product-category', plugins_url( 'build/product-category.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-product-new', plugins_url( 'build/product-new.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-product-on-sale', plugins_url( 'build/product-on-sale.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-product-top-rated', plugins_url( 'build/product-top-rated.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-products-attribute', plugins_url( 'build/products-attribute.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); - self::register_script( 'wc-featured-product', plugins_url( 'build/featured-product.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-blocks' ) ); + self::register_script( 'wc-handpicked-products', plugins_url( 'build/handpicked-products.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-product-best-sellers', plugins_url( 'build/product-best-sellers.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-product-category', plugins_url( 'build/product-category.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-product-new', plugins_url( 'build/product-new.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-product-on-sale', plugins_url( 'build/product-on-sale.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-product-top-rated', plugins_url( 'build/product-top-rated.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-products-attribute', plugins_url( 'build/products-attribute.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-featured-product', plugins_url( 'build/featured-product.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); + self::register_script( 'wc-product-categories', plugins_url( 'build/product-categories.js', WGPB_PLUGIN_FILE ), array( 'wc-vendors', 'wc-packages', 'wc-blocks' ) ); } /** - * Output useful globals before printing any script tags. - * * These are used by @woocommerce/components & the block library to set up defaults - * based on user-controlled settings from WordPress. - * - * @since 2.0.0 + * based on user-controlled settings from WordPress. Only use this in wp-admin. */ - public static function print_script_settings() { + public static function print_script_wc_settings() { global $wp_locale; - $code = get_woocommerce_currency(); - $product_counts = wp_count_posts( 'product' ); + $code = get_woocommerce_currency(); // NOTE: wcSettings is not used directly, it's only for @woocommerce/components // @@ -174,7 +174,29 @@ class WGPB_Block_Library { ); // NOTE: wcSettings is not used directly, it's only for @woocommerce/components. $settings = apply_filters( 'woocommerce_components_settings', $settings ); + ?> + + false, + 'pad_counts' => true, + ) + ); // Global settings used in each block. $block_settings = array( 'min_columns' => wc_get_theme_support( 'product_blocks::min_columns', 1 ), @@ -188,10 +210,10 @@ class WGPB_Block_Library { 'min_height' => wc_get_theme_support( 'featured_block::min_height', 500 ), 'default_height' => wc_get_theme_support( 'featured_block::default_height', 500 ), 'isLargeCatalog' => $product_counts->publish > 200, + 'productCategories' => $product_categories, ); ?> 'wc-block-style', ) ); + register_block_type( + 'woocommerce/product-categories', + array( + 'editor_script' => 'wc-product-categories', + 'editor_style' => 'wc-block-editor', + 'style' => 'wc-block-style', + 'script' => 'wc-frontend', + ) + ); } /** diff --git a/plugins/woocommerce-blocks/webpack.config.js b/plugins/woocommerce-blocks/webpack.config.js index 2f10dec1226..70b6abef726 100644 --- a/plugins/woocommerce-blocks/webpack.config.js +++ b/plugins/woocommerce-blocks/webpack.config.js @@ -27,10 +27,12 @@ const GutenbergBlocksConfig = { entry: { // Shared blocks code blocks: './assets/js/index.js', + frontend: [ './assets/js/blocks/product-categories/frontend.js' ], // Blocks 'handpicked-products': './assets/js/blocks/handpicked-products/index.js', 'product-best-sellers': './assets/js/blocks/product-best-sellers/index.js', 'product-category': './assets/js/blocks/product-category/index.js', + 'product-categories': './assets/js/blocks/product-categories/index.js', 'product-new': './assets/js/blocks/product-new/index.js', 'product-on-sale': './assets/js/blocks/product-on-sale/index.js', 'product-top-rated': './assets/js/blocks/product-top-rated/index.js', @@ -50,6 +52,13 @@ const GutenbergBlocksConfig = { optimization: { splitChunks: { cacheGroups: { + packages: { + test: /[\\/]node_modules[\\/]@woocommerce/, + name: 'packages', + chunks: 'all', + enforce: true, + priority: 10, // Higher priority to ensure @woocommerce/* packages are caught here. + }, commons: { test: /[\\/]node_modules[\\/]/, name: 'vendors',