* 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
This commit is contained in:
Kelly Dwan 2019-06-17 10:23:59 -04:00 committed by GitHub
parent 5f76139679
commit 27345f93a2
12 changed files with 386 additions and 20 deletions

View File

@ -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',

View File

@ -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 ) => (
<ul key={ parentKey }>
{ items.map( ( cat ) => {
const count = hasCount ? <span>({ cat.count })</span> : null;
return [
<li key={ cat.term_id }><a>{ cat.name }</a> { count }</li>, // eslint-disable-line
!! cat.children &&
!! cat.children.length &&
renderList( cat.children ),
];
} ) }
</ul>
);
return (
<div className="wc-block-product-categories">
{ isDropdown ? (
<SelectControl
label={ __( 'Select a category', 'woo-gutenberg-products-block' ) }
options={ categories.map( ( cat ) => ( {
label: hasCount ? `${ cat.name } (${ cat.count })` : cat.name,
value: cat.term_id,
} ) ) }
onChange={ noop }
/>
) : (
renderList( categories )
) }
</div>
);
};
ProductCategoriesBlock.propTypes = {
/**
* The attributes for this block
*/
attributes: PropTypes.object.isRequired,
};
export default ProductCategoriesBlock;

View File

@ -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 (
<Fragment>
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Content', 'woo-gutenberg-products-block' ) }
initialOpen
>
<ToggleControl
label={ __( 'Show as dropdown', 'woo-gutenberg-products-block' ) }
help={
isDropdown ?
__( 'Categories are shown in a dropdown.', 'woo-gutenberg-products-block' ) :
__( 'Categories are shown in a list.', 'woo-gutenberg-products-block' )
}
checked={ isDropdown }
onChange={ () => setAttributes( { isDropdown: ! isDropdown } ) }
/>
<ToggleControl
label={ __( 'Show product count', 'woo-gutenberg-products-block' ) }
help={
hasCount ?
__( 'Product count is visible.', 'woo-gutenberg-products-block' ) :
__( 'Product count is hidden.', 'woo-gutenberg-products-block' )
}
checked={ hasCount }
onChange={ () => setAttributes( { hasCount: ! hasCount } ) }
/>
{ ! isDropdown && (
<ToggleControl
label={ __( 'Show hierarchy', 'woo-gutenberg-products-block' ) }
help={
isHierarchical ?
__( 'Hierarchy is visible.', 'woo-gutenberg-products-block' ) :
__( 'Hierarchy is hidden.', 'woo-gutenberg-products-block' )
}
checked={ isHierarchical }
onChange={ () => setAttributes( { isHierarchical: ! isHierarchical } ) }
/>
) }
<ToggleControl
label={ __( 'Show empty categories', 'woo-gutenberg-products-block' ) }
help={
hasEmpty ?
__( 'Empty categories are visible.', 'woo-gutenberg-products-block' ) :
__( 'Empty categories are hidden.', 'woo-gutenberg-products-block' )
}
checked={ hasEmpty }
onChange={ () => setAttributes( { hasEmpty: ! hasEmpty } ) }
/>
</PanelBody>
</InspectorControls>
<Block attributes={ attributes } />
</Fragment>
);
}

View File

@ -0,0 +1,3 @@
.wc-block-product-categories.wc-block-product-categories ul {
margin-left: 20px;
}

View File

@ -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( <Block attributes={ attributes } />, el );
} );
}

View File

@ -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;
}

View File

@ -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: <IconFolder />,
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 <div { ...props }>LOADING</div>;
},
} );

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { Icon } from '@wordpress/components';
export default () => (
<Icon
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fillRule="nonzero" d="M21.913 7.0946H2.0254c-1.1708 0-2.0984.9908-2.0205 2.16l.741 11.0724c.0714 1.0638.9552 1.8892 2.0206 1.8892h18.4054c1.0654 0 1.9492-.8254 2.0205-1.8892l.7411-11.0724c.0779-1.1692-.8497-2.16-2.0205-2.16zm-8.8006-4.6573h5.987c1.119 0 2.0255.9065 2.0255 2.0254v.934H2.8103V2.0255C2.8103.9064 3.7168 0 4.8357 0h6.2513c1.119 0 2.0254.9065 2.0254 2.0254v.4119zm-7.0589 11.619a.926.926 0 1 1 0-1.852h11.8297a.926.926 0 1 1 0 1.852H6.0535z" />
</svg>
}
/>
);

View File

@ -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';

View File

@ -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,

View File

@ -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 );
?>
<script type="text/javascript">
var wcSettings = wcSettings || JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( $settings ) ); ?>' ) );
</script>
<?php
}
/**
* Output block-related data on a global object.
*
* This is used to map site settings & data into JS-accessible variables.
*
* @since 2.0.0
*/
public static function print_script_block_data() {
$product_counts = wp_count_posts( 'product' );
$product_categories = get_terms(
'product_cat',
array(
'hide_empty' => 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,
);
?>
<script type="text/javascript">
var wcSettings = wcSettings || JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( $settings ) ); ?>' ) );
var wc_product_block_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( $block_settings ) ); ?>' ) );
</script>
<?php
@ -329,6 +351,15 @@ class WGPB_Block_Library {
'style' => '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',
)
);
}
/**

View File

@ -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',