WooCommerce Gutenberg Products Block - Version 1.3.0

Version Description

  • 2019-01-15 =

  • Feature: Added new blocks: "Featured Product", "Hand-picked Products", "Best Selling Products", "Newest Products", "On Sale Products", "Top Rated Products"

  • Enhancement: Create new "WooCommerce" block category, all blocks are found there now

  • Enhancement: Added a control to "Products by Category" block to control whether products need to match any selected categories or all selected categories

  • Fix: A "Products by Category" block with no category selected will no longer show all products

  • Legacy block: Remove legacy "Products" block from being shown in the block inserter (still loading the block for an existing uses)

  • Legacy block: Fix an issue with imageless products in the legacy "Products" block.

  • Components: Add new Control components ProductControl, ProductsControl, ProductOrderbyControl

  • Components: Update SearchListControl to allow selecting a single item

  • API: Add cat_operator support to products endpoint

  • API: Add product description & short_description to each product

  • API: Add attribute group names to each attribute

  • Build: Update packages

  • Build: Add cssnano to minify CSS

  • Build: Split out node_modules code into separate vendors files

=

Download this release

Release Info

Developer ryelle
Plugin Icon 128x128 WooCommerce Gutenberg Products Block
Version 1.3.0
Comparing to
See all releases

Code changes from version 1.2.0 to 1.3.0

Files changed (75) hide show
  1. assets/css/abstracts/_breakpoints.scss +2 -1
  2. assets/css/abstracts/_mixins.scss +12 -0
  3. assets/css/abstracts/_variables.scss +12 -0
  4. assets/css/product-category-block.scss +0 -69
  5. assets/css/products-grid.scss +53 -0
  6. assets/js/blocks/featured-product/block.js +354 -0
  7. assets/js/blocks/featured-product/index.js +128 -0
  8. assets/js/blocks/featured-product/style.scss +124 -0
  9. assets/js/blocks/handpicked-products/block.js +240 -0
  10. assets/js/blocks/handpicked-products/index.js +91 -0
  11. assets/js/blocks/handpicked-products/style.scss +3 -0
  12. assets/js/blocks/product-best-sellers/block.js +181 -0
  13. assets/js/blocks/product-best-sellers/index.js +54 -0
  14. assets/js/{product-category-block.js → blocks/product-category/block.js} +66 -152
  15. assets/js/blocks/product-category/index.js +73 -0
  16. assets/js/blocks/product-category/style.scss +3 -0
  17. assets/js/blocks/product-new/block.js +174 -0
  18. assets/js/blocks/product-new/index.js +54 -0
  19. assets/js/blocks/product-on-sale/block.js +188 -0
  20. assets/js/blocks/product-on-sale/index.js +62 -0
  21. assets/js/blocks/product-top-rated/block.js +174 -0
  22. assets/js/blocks/product-top-rated/index.js +54 -0
  23. assets/js/components/icons/checkbox-checked.js +22 -0
  24. assets/js/components/icons/checkbox-unchecked.js +22 -0
  25. assets/js/components/icons/index.js +7 -0
  26. assets/js/components/icons/new-releases.js +20 -0
  27. assets/js/components/icons/radio-selected.js +19 -0
  28. assets/js/components/icons/radio-unselected.js +22 -0
  29. assets/js/components/icons/widgets.js +20 -0
  30. assets/js/components/product-attribute-control/index.js +147 -0
  31. assets/js/components/product-attribute-control/style.scss +14 -0
  32. assets/js/components/product-category-control/index.js +64 -58
  33. assets/js/components/product-category-control/style.scss +9 -68
  34. assets/js/components/product-control/index.js +87 -0
  35. assets/js/components/product-orderby-control/index.js +62 -0
  36. assets/js/components/product-preview/index.js +4 -2
  37. assets/js/components/product-preview/style.scss +26 -69
  38. assets/js/components/products-control/index.js +94 -0
  39. assets/js/components/search-list-control/icons.js +0 -48
  40. assets/js/components/search-list-control/index.js +52 -72
  41. assets/js/components/search-list-control/item.js +132 -0
  42. assets/js/components/search-list-control/style.scss +90 -7
  43. assets/js/legacy/products-block.jsx +5 -2
  44. assets/{css → js/legacy}/products-block.scss +0 -0
  45. assets/js/legacy/views/specific-select.jsx +7 -2
  46. assets/js/utils/get-query.js +42 -17
  47. assets/js/utils/get-shortcode.js +51 -14
  48. assets/js/utils/products.js +25 -0
  49. assets/js/utils/shared-attributes.js +19 -10
  50. build/featured-product.css +2 -0
  51. build/featured-product.js +1 -0
  52. build/handpicked-products.css +3 -0
  53. build/handpicked-products.js +1 -0
  54. build/product-best-sellers.css +3 -0
  55. build/product-best-sellers.js +1 -0
  56. build/product-category-block.css +0 -2472
  57. build/product-category.css +4 -0
  58. build/product-category.js +1 -0
  59. build/product-new.css +3 -0
  60. build/product-new.js +1 -0
  61. build/product-on-sale.css +3 -0
  62. build/product-on-sale.js +1 -0
  63. build/product-top-rated.css +3 -0
  64. build/product-top-rated.js +1 -0
  65. build/products-block.css +1 -565
  66. build/products-block.js +1 -1
  67. build/products-grid.css +1 -0
  68. build/products-grid.js +1 -0
  69. build/vendors.css +1 -0
  70. build/{product-category-block.js → vendors.js} +10 -10
  71. includes/blocks/class-wc-block-featured-product.php +173 -0
  72. includes/class-wgpb-product-attribute-terms-controller.php +29 -10
  73. includes/class-wgpb-products-controller.php +89 -15
  74. readme.txt +48 -7
  75. woocommerce-gutenberg-products-block.php +293 -66
assets/css/abstracts/_breakpoints.scss CHANGED
@@ -5,7 +5,8 @@
5
6
// Think very carefully before adding a new breakpoint.
7
// The list below is based on wp-admin's main breakpoints
8
- $breakpoints: 320px, 400px, 600px, 782px, 960px, 1280px, 1440px;
9
10
@mixin breakpoint( $sizes... ) {
11
@each $size in $sizes {
5
6
// Think very carefully before adding a new breakpoint.
7
// The list below is based on wp-admin's main breakpoints
8
+ // See https://github.com/WordPress/gutenberg/tree/master/packages/viewport#breakpoints
9
+ $breakpoints: 480px, 600px, 782px, 960px, 1280px, 1440px;
10
11
@mixin breakpoint( $sizes... ) {
12
@each $size in $sizes {
assets/css/abstracts/_mixins.scss CHANGED
@@ -57,3 +57,15 @@
57
margin: unset;
58
overflow: hidden;
59
}
57
margin: unset;
58
overflow: hidden;
59
}
60
+
61
+ // Create a string-repeat function
62
+ @function repeat($character, $n) {
63
+ @if $n == 0 {
64
+ @return "";
65
+ }
66
+ $c: "";
67
+ @for $i from 1 through $n {
68
+ $c: $c + $character;
69
+ }
70
+ @return $c;
71
+ }
assets/css/abstracts/_variables.scss CHANGED
@@ -5,3 +5,15 @@ $gap: 16px;
5
$gap-small: 12px;
6
$gap-smaller: 8px;
7
$gap-smallest: 4px;
5
$gap-small: 12px;
6
$gap-smaller: 8px;
7
$gap-smallest: 4px;
8
+
9
+ // Variables pulled from Gutenberg.
10
+ // Editor Widths
11
+ $sidebar-width: 280px;
12
+ $content-width: 610px; // For the visual width, subtract 30px (2 * $block-padding + 2px borders). This comes to 580px, which is optimized for 70 characters.
13
+
14
+ // Blocks
15
+ $block-padding: 14px; // Space between block footprint and focus boundaries. These are drawn outside the block footprint, and do not affect the size.
16
+ $block-spacing: 4px; // Vertical space between blocks.
17
+ $block-side-ui-width: 28px; // Width of the movers/drag handle UI.
18
+ $block-side-ui-clearance: 2px; // Space between movers/drag handle UI, and block.
19
+ $block-container-side-padding: $block-side-ui-width + $block-padding + 2 * $block-side-ui-clearance; // Total space left and right of the block footprint.
assets/css/product-category-block.scss DELETED
@@ -1,69 +0,0 @@
1
- // Import the woocommerce components stylesheet
2
- // @todo Move this to a separate file so we can build a cacheable single stylesheet for all blocks.
3
- @import "../../node_modules/@woocommerce/components/build-style/style.css";
4
-
5
- // Hack to hide preview overflow.
6
- .editor-block-preview__content {
7
- overflow: hidden;
8
- }
9
-
10
- .wc-block-products-category {
11
- overflow: hidden;
12
-
13
- &.components-placeholder {
14
- padding: 2em 1em;
15
- }
16
-
17
- .editor-block-preview & {
18
- min-width: 5em;
19
-
20
- .wc-product-preview__title,
21
- .wc-product-preview__price,
22
- .wc-product-preview__add-to-cart {
23
- font-size: 0.6em;
24
- }
25
-
26
- &.cols-2 {
27
- min-width: 2 * 5em;
28
- }
29
- &.cols-3 {
30
- min-width: 3 * 5em;
31
- }
32
- &.cols-4 {
33
- min-width: 4 * 5em;
34
- }
35
- &.cols-5 {
36
- min-width: 5 * 5em;
37
- }
38
- &.cols-6 {
39
- min-width: 6 * 5em;
40
- }
41
-
42
- &.is-loading,
43
- &.is-not-found {
44
- min-width: auto;
45
- }
46
- }
47
- }
48
-
49
- .wc-block-products-category__selection {
50
- width: 100%;
51
- }
52
-
53
- .components-panel {
54
- .woocommerce-search-list {
55
- padding: 0;
56
- }
57
- .woocommerce-search-list__selected {
58
- margin: 0 0 $gap;
59
- padding: 0;
60
- border-top: none;
61
- // 54px is the height of 1 row of tags in the sidebar.
62
- min-height: 54px;
63
- }
64
- .woocommerce-search-list__search {
65
- margin: 0 0 $gap;
66
- padding: 0;
67
- border-top: none;
68
- }
69
- }
assets/css/products-grid.scss ADDED
@@ -0,0 +1,53 @@
1
+ // Import the woocommerce components stylesheet
2
+ @import "~@woocommerce/components/build-style/style.css";
3
+
4
+ // Hack to hide preview overflow.
5
+ .editor-block-preview__content {
6
+ overflow: hidden;
7
+ }
8
+
9
+ .wc-block-products-grid {
10
+ overflow: hidden;
11
+ display: flex;
12
+ flex-wrap: wrap;
13
+ justify-content: flex-start;
14
+
15
+ &.is-loading,
16
+ &.is-not-found,
17
+ &.cols-1 {
18
+ display: block;
19
+ }
20
+
21
+ .wc-product-preview {
22
+ flex: 1;
23
+ padding: $gap/2;
24
+ }
25
+
26
+ @for $i from 2 to 7 {
27
+ &.cols-#{$i} .wc-product-preview {
28
+ max-width: calc(#{ 100% / $i });
29
+ min-width: calc(#{ 100% / $i });
30
+ flex: 1;
31
+ }
32
+ }
33
+
34
+ &.components-placeholder {
35
+ padding: 2em 1em;
36
+ }
37
+
38
+ // Styles for "resuable block" preview.
39
+ .editor-block-preview & {
40
+ min-width: 5em;
41
+
42
+ @for $i from 1 to 7 {
43
+ &.cols-#{$i} {
44
+ min-width: $i * 5em;
45
+ }
46
+ }
47
+
48
+ &.is-loading,
49
+ &.is-not-found {
50
+ min-width: auto;
51
+ }
52
+ }
53
+ }
assets/js/blocks/featured-product/block.js ADDED
@@ -0,0 +1,354 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import apiFetch from '@wordpress/api-fetch';
6
+ import {
7
+ AlignmentToolbar,
8
+ BlockControls,
9
+ InspectorControls,
10
+ MediaUpload,
11
+ MediaUploadCheck,
12
+ PanelColorSettings,
13
+ RichText,
14
+ withColors,
15
+ } from '@wordpress/editor';
16
+ import {
17
+ Button,
18
+ IconButton,
19
+ PanelBody,
20
+ Placeholder,
21
+ RangeControl,
22
+ Spinner,
23
+ ToggleControl,
24
+ Toolbar,
25
+ withSpokenMessages,
26
+ } from '@wordpress/components';
27
+ import classnames from 'classnames';
28
+ import { Component, Fragment } from '@wordpress/element';
29
+ import { compose } from '@wordpress/compose';
30
+ import { debounce, isObject } from 'lodash';
31
+ import PropTypes from 'prop-types';
32
+
33
+ /**
34
+ * Internal dependencies
35
+ */
36
+ import ProductControl from '../../components/product-control';
37
+ import {
38
+ getImageSrcFromProduct,
39
+ getImageIdFromProduct,
40
+ } from '../../utils/products';
41
+
42
+ /**
43
+ * Generate a style object given either a product object or URL to an image.
44
+ *
45
+ * @param {object|string} url A product object as returned from the API, or an image URL.
46
+ * @return {object} A style object with a backgroundImage set (if a valid image is provided).
47
+ */
48
+ function backgroundImageStyles( url ) {
49
+ // If `url` is an object, it's actually a product.
50
+ if ( isObject( url ) ) {
51
+ url = getImageSrcFromProduct( url );
52
+ }
53
+ if ( url ) {
54
+ return { backgroundImage: `url(${ url })` };
55
+ }
56
+ return {};
57
+ }
58
+
59
+ /**
60
+ * Convert the selected ratio to the correct background class.
61
+ *
62
+ * @param {number} ratio Selected opacity from 0 to 100.
63
+ * @return {string} The class name, if applicable (not used for ratio 0 or 50).
64
+ */
65
+ function dimRatioToClass( ratio ) {
66
+ return ratio === 0 || ratio === 50 ?
67
+ null :
68
+ `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
69
+ }
70
+
71
+ /**
72
+ * Component to handle edit mode of "Featured Product".
73
+ */
74
+ class FeaturedProduct extends Component {
75
+ constructor() {
76
+ super( ...arguments );
77
+ this.state = {
78
+ product: false,
79
+ loaded: false,
80
+ };
81
+
82
+ this.debouncedGetProduct = debounce( this.getProduct.bind( this ), 200 );
83
+ }
84
+
85
+ componentDidMount() {
86
+ this.getProduct();
87
+ }
88
+
89
+ componentDidUpdate( prevProps ) {
90
+ if ( prevProps.attributes.productId !== this.props.attributes.productId ) {
91
+ this.debouncedGetProduct();
92
+ }
93
+ }
94
+
95
+ getProduct() {
96
+ const { productId } = this.props.attributes;
97
+ if ( ! productId ) {
98
+ // We've removed the selected product, or no product is selected yet.
99
+ this.setState( { product: false, loaded: true } );
100
+ return;
101
+ }
102
+ apiFetch( {
103
+ path: `/wc-pb/v3/products/${ productId }`,
104
+ } )
105
+ .then( ( product ) => {
106
+ this.setState( { product, loaded: true } );
107
+ } )
108
+ .catch( () => {
109
+ this.setState( { product: false, loaded: true } );
110
+ } );
111
+ }
112
+
113
+ getInspectorControls() {
114
+ const {
115
+ attributes,
116
+ setAttributes,
117
+ overlayColor,
118
+ setOverlayColor,
119
+ } = this.props;
120
+
121
+ return (
122
+ <InspectorControls key="inspector">
123
+ <PanelBody
124
+ title={ __( 'Product', 'woo-gutenberg-products-block' ) }
125
+ initialOpen={ false }
126
+ >
127
+ <ProductControl
128
+ selected={ attributes.productId || 0 }
129
+ onChange={ ( value = [] ) => {
130
+ const id = value[ 0 ] ? value[ 0 ].id : 0;
131
+ setAttributes( { productId: id, mediaId: 0, mediaSrc: '' } );
132
+ } }
133
+ />
134
+ </PanelBody>
135
+ <PanelBody title={ __( 'Content', 'woo-gutenberg-products-block' ) }>
136
+ <ToggleControl
137
+ label="Show description"
138
+ checked={ attributes.showDesc }
139
+ onChange={ () => setAttributes( { showDesc: ! attributes.showDesc } ) }
140
+ />
141
+ <ToggleControl
142
+ label="Show price"
143
+ checked={ attributes.showPrice }
144
+ onChange={ () => setAttributes( { showPrice: ! attributes.showPrice } ) }
145
+ />
146
+ </PanelBody>
147
+ <PanelColorSettings
148
+ title={ __( 'Overlay', 'woo-gutenberg-products-block' ) }
149
+ colorSettings={ [
150
+ {
151
+ value: overlayColor.color,
152
+ onChange: setOverlayColor,
153
+ label: __( 'Overlay Color', 'woo-gutenberg-products-block' ),
154
+ },
155
+ ] }
156
+ >
157
+ <RangeControl
158
+ label={ __( 'Background Opacity', 'woo-gutenberg-products-block' ) }
159
+ value={ attributes.dimRatio }
160
+ onChange={ ( ratio ) => setAttributes( { dimRatio: ratio } ) }
161
+ min={ 0 }
162
+ max={ 100 }
163
+ step={ 10 }
164
+ />
165
+ </PanelColorSettings>
166
+ </InspectorControls>
167
+ );
168
+ }
169
+
170
+ renderEditMode() {
171
+ const { attributes, debouncedSpeak, setAttributes } = this.props;
172
+ const onDone = () => {
173
+ setAttributes( { editMode: false } );
174
+ debouncedSpeak(
175
+ __(
176
+ 'Showing Featured Product block preview.',
177
+ 'woo-gutenberg-products-block'
178
+ )
179
+ );
180
+ };
181
+
182
+ return (
183
+ <Placeholder
184
+ icon="star-filled"
185
+ label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
186
+ className="wc-block-featured-product"
187
+ >
188
+ { __(
189
+ 'Visually highlight a product and encourage prompt action',
190
+ 'woo-gutenberg-products-block'
191
+ ) }
192
+ <div className="wc-block-handpicked-products__selection">
193
+ <ProductControl
194
+ selected={ attributes.productId || 0 }
195
+ onChange={ ( value = [] ) => {
196
+ const id = value[ 0 ] ? value[ 0 ].id : 0;
197
+ setAttributes( { productId: id, mediaId: 0, mediaSrc: '' } );
198
+ } }
199
+ />
200
+ <Button isDefault onClick={ onDone }>
201
+ { __( 'Done', 'woo-gutenberg-products-block' ) }
202
+ </Button>
203
+ </div>
204
+ </Placeholder>
205
+ );
206
+ }
207
+
208
+ render() {
209
+ const { attributes, setAttributes, overlayColor } = this.props;
210
+ const {
211
+ contentAlign,
212
+ dimRatio,
213
+ editMode,
214
+ linkText,
215
+ showDesc,
216
+ showPrice,
217
+ } = attributes;
218
+ const { loaded, product } = this.state;
219
+ const classes = classnames(
220
+ 'wc-block-featured-product',
221
+ {
222
+ 'is-loading': ! product && ! loaded,
223
+ 'is-not-found': ! product && loaded,
224
+ 'has-background-dim': dimRatio !== 0,
225
+ },
226
+ dimRatioToClass( dimRatio ),
227
+ contentAlign !== 'center' && `has-${ contentAlign }-content`
228
+ );
229
+ const mediaId = attributes.mediaId || getImageIdFromProduct( product );
230
+
231
+ const style = !! product ?
232
+ backgroundImageStyles( attributes.mediaSrc || product ) :
233
+ {};
234
+ if ( overlayColor.color ) {
235
+ style.backgroundColor = overlayColor.color;
236
+ }
237
+
238
+ return (
239
+ <Fragment>
240
+ <BlockControls>
241
+ <AlignmentToolbar
242
+ value={ contentAlign }
243
+ onChange={ ( nextAlign ) => {
244
+ setAttributes( { contentAlign: nextAlign } );
245
+ } }
246
+ />
247
+ <Toolbar
248
+ controls={ [
249
+ {
250
+ icon: 'edit',
251
+ title: __( 'Edit' ),
252
+ onClick: () => setAttributes( { editMode: ! editMode } ),
253
+ isActive: editMode,
254
+ },
255
+ ] }
256
+ />
257
+ <MediaUploadCheck>
258
+ <Toolbar>
259
+ <MediaUpload
260
+ onSelect={ ( media ) => {
261
+ setAttributes( { mediaId: media.id, mediaSrc: media.url } );
262
+ } }
263
+ allowedTypes={ [ 'image' ] }
264
+ value={ mediaId }
265
+ render={ ( { open } ) => (
266
+ <IconButton
267
+ className="components-toolbar__control"
268
+ label={ __( 'Edit media' ) }
269
+ icon="format-image"
270
+ onClick={ open }
271
+ />
272
+ ) }
273
+ />
274
+ </Toolbar>
275
+ </MediaUploadCheck>
276
+ </BlockControls>
277
+ { ! attributes.editMode && this.getInspectorControls() }
278
+ { editMode ? (
279
+ this.renderEditMode()
280
+ ) : (
281
+ <Fragment>
282
+ { !! product ? (
283
+ <div className={ classes } style={ style }>
284
+ <h2 className="wc-block-featured-product__title">
285
+ { product.name }
286
+ </h2>
287
+ { showDesc && (
288
+ <div
289
+ className="wc-block-featured-product__description"
290
+ dangerouslySetInnerHTML={ {
291
+ __html: product.short_description,
292
+ } }
293
+ />
294
+ ) }
295
+ { showPrice && (
296
+ <div
297
+ className="wc-block-featured-product__price"
298
+ dangerouslySetInnerHTML={ { __html: product.price_html } }
299
+ />
300
+ ) }
301
+ <div className="wc-block-featured-product__link wp-block-button">
302
+ <RichText
303
+ value={ linkText }
304
+ onChange={ ( value ) => setAttributes( { linkText: value } ) }
305
+ formattingControls={ [ 'bold', 'italic', 'strikethrough' ] }
306
+ className="wp-block-button__link"
307
+ keepPlaceholderOnFocus
308
+ />
309
+ </div>
310
+ </div>
311
+ ) : (
312
+ <Placeholder
313
+ className="wc-block-featured-product"
314
+ icon="star-filled"
315
+ label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
316
+ >
317
+ { ! loaded ? (
318
+ <Spinner />
319
+ ) : (
320
+ __( 'No product is selected.', 'woo-gutenberg-products-block' )
321
+ ) }
322
+ </Placeholder>
323
+ ) }
324
+ </Fragment>
325
+ ) }
326
+ </Fragment>
327
+ );
328
+ }
329
+ }
330
+
331
+ FeaturedProduct.propTypes = {
332
+ /**
333
+ * The attributes for this block
334
+ */
335
+ attributes: PropTypes.object.isRequired,
336
+ /**
337
+ * The register block name.
338
+ */
339
+ name: PropTypes.string.isRequired,
340
+ /**
341
+ * A callback to update attributes
342
+ */
343
+ setAttributes: PropTypes.func.isRequired,
344
+ // from withColors
345
+ overlayColor: PropTypes.object,
346
+ setOverlayColor: PropTypes.func.isRequired,
347
+ // from withSpokenMessages
348
+ debouncedSpeak: PropTypes.func.isRequired,
349
+ };
350
+
351
+ export default compose( [
352
+ withColors( { overlayColor: 'background-color' } ),
353
+ withSpokenMessages,
354
+ ] )( FeaturedProduct );
assets/js/blocks/featured-product/index.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { registerBlockType } from '@wordpress/blocks';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import './style.scss';
11
+ import Block from './block';
12
+
13
+ /**
14
+ * Register and run the "Featured Product" block.
15
+ */
16
+ registerBlockType( 'woocommerce/featured-product', {
17
+ title: __( 'Featured Product', 'woo-gutenberg-products-block' ),
18
+ icon: 'star-filled',
19
+ category: 'woocommerce',
20
+ keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
21
+ description: __(
22
+ 'Visually highlight a product and encourage prompt action.',
23
+ 'woo-gutenberg-products-block'
24
+ ),
25
+ supports: {
26
+ align: [ 'wide', 'full' ],
27
+ },
28
+ attributes: {
29
+ /**
30
+ * Alignment of content inside block.
31
+ */
32
+ contentAlign: {
33
+ type: 'string',
34
+ default: 'center',
35
+ },
36
+
37
+ /**
38
+ * Percentage opacity of overlay.
39
+ */
40
+ dimRatio: {
41
+ type: 'number',
42
+ default: 50,
43
+ },
44
+
45
+ /**
46
+ * Toggle for edit mode in the block preview.
47
+ */
48
+ editMode: {
49
+ type: 'boolean',
50
+ default: true,
51
+ },
52
+
53
+ /**
54
+ * ID for a custom image, overriding the product's featured image.
55
+ */
56
+ mediaId: {
57
+ type: 'number',
58
+ default: 0,
59
+ },
60
+
61
+ /**
62
+ * URL for a custom image, overriding the product's featured image.
63
+ */
64
+ mediaSrc: {
65
+ type: 'string',
66
+ default: '',
67
+ },
68
+
69
+ /**
70
+ * The overlay color, from the color list.
71
+ */
72
+ overlayColor: {
73
+ type: 'string',
74
+ },
75
+
76
+ /**
77
+ * The overlay color, if a custom color value.
78
+ */
79
+ customOverlayColor: {
80
+ type: 'string',
81
+ },
82
+
83
+ /**
84
+ * Text for the product link.
85
+ */
86
+ linkText: {
87
+ type: 'string',
88
+ default: __( 'Shop now', 'woo-gutenberg-products-block' ),
89
+ },
90
+
91
+ /**
92
+ * The product ID to display.
93
+ */
94
+ productId: {
95
+ type: 'number',
96
+ },
97
+
98
+ /**
99
+ * Show the product description.
100
+ */
101
+ showDesc: {
102
+ type: 'boolean',
103
+ default: true,
104
+ },
105
+
106
+ /**
107
+ * Show the product price.
108
+ */
109
+ showPrice: {
110
+ type: 'boolean',
111
+ default: true,
112
+ },
113
+ },
114
+
115
+ /**
116
+ * Renders and manages the block.
117
+ */
118
+ edit( props ) {
119
+ return <Block { ...props } />;
120
+ },
121
+
122
+ /**
123
+ * Block content is rendered in PHP, not via save function.
124
+ */
125
+ save() {
126
+ return null;
127
+ },
128
+ } );
assets/js/blocks/featured-product/style.scss ADDED
@@ -0,0 +1,124 @@
1
+ .wc-block-featured-product {
2
+ position: relative;
3
+ background-color: $black;
4
+ background-size: cover;
5
+ background-position: center center;
6
+ min-height: 500px;
7
+ width: 100%;
8
+ margin: 0 0 1.5em 0;
9
+ display: flex;
10
+ justify-content: center;
11
+ align-items: center;
12
+ overflow: hidden;
13
+ flex-wrap: wrap;
14
+ align-content: center;
15
+
16
+ &.components-placeholder {
17
+ // Reset the background for the placeholders.
18
+ background-color: rgba( 139, 139, 150, .1 );
19
+ }
20
+
21
+ &.has-left-content {
22
+ justify-content: flex-start;
23
+
24
+ .wc-block-featured-product__title,
25
+ .wc-block-featured-product__description,
26
+ .wc-block-featured-product__price,
27
+ .wc-block-featured-product__link {
28
+ margin-left: 0;
29
+ text-align: left;
30
+ }
31
+ }
32
+
33
+ &.has-right-content {
34
+ justify-content: flex-end;
35
+
36
+ .wc-block-featured-product__title,
37
+ .wc-block-featured-product__description,
38
+ .wc-block-featured-product__price,
39
+ .wc-block-featured-product__link {
40
+ margin-right: 0;
41
+ text-align: right;
42
+ }
43
+ }
44
+
45
+ .wc-block-featured-product__title,
46
+ .wc-block-featured-product__description,
47
+ .wc-block-featured-product__price,
48
+ .wc-block-featured-product__link {
49
+ color: $white;
50
+ line-height: 1.25;
51
+ z-index: 1;
52
+ margin-bottom: 0;
53
+ width: 100%;
54
+ padding: 0 48px 16px 48px;
55
+ text-align: center;
56
+
57
+ a,
58
+ a:hover,
59
+ a:focus,
60
+ a:active {
61
+ color: $white;
62
+ }
63
+ }
64
+
65
+ .wc-block-featured-product__title {
66
+ margin-top: 0;
67
+
68
+ &:before {
69
+ display: none;
70
+ }
71
+ }
72
+
73
+ .wc-block-featured-product__description {
74
+ p {
75
+ margin: 0;
76
+ }
77
+ }
78
+
79
+ &.has-background-dim::before {
80
+ content: "";
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ bottom: 0;
85
+ right: 0;
86
+ background-color: inherit;
87
+ opacity: 0.5;
88
+ z-index: 1;
89
+ }
90
+
91
+ @for $i from 1 through 10 {
92
+ &.has-background-dim.has-background-dim-#{ $i * 10 }::before {
93
+ opacity: $i * 0.1;
94
+ }
95
+ }
96
+
97
+ // Apply max-width to floated items that have no intrinsic width
98
+ &.alignleft,
99
+ &.alignright {
100
+ max-width: $content-width / 2;
101
+ width: 100%;
102
+ }
103
+
104
+ // Using flexbox without an assigned height property breaks vertical center alignment in IE11.
105
+ // Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue.
106
+ &::after {
107
+ display: block;
108
+ content: "";
109
+ font-size: 0;
110
+ min-height: inherit;
111
+
112
+ // IE doesn't support flex so omit that.
113
+ @supports (position: sticky) {
114
+ content: none;
115
+ }
116
+ }
117
+
118
+ // Aligned cover blocks should not use our global alignment rules
119
+ &.aligncenter,
120
+ &.alignleft,
121
+ &.alignright {
122
+ display: flex;
123
+ }
124
+ }
assets/js/blocks/handpicked-products/block.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { addQueryArgs } from '@wordpress/url';
6
+ import apiFetch from '@wordpress/api-fetch';
7
+ import { BlockControls, InspectorControls } from '@wordpress/editor';
8
+ import {
9
+ Button,
10
+ PanelBody,
11
+ Placeholder,
12
+ RangeControl,
13
+ Spinner,
14
+ Toolbar,
15
+ withSpokenMessages,
16
+ } from '@wordpress/components';
17
+ import { Component, Fragment } from '@wordpress/element';
18
+ import { debounce } from 'lodash';
19
+ import PropTypes from 'prop-types';
20
+
21
+ /**
22
+ * Internal dependencies
23
+ */
24
+ import getQuery from '../../utils/get-query';
25
+ import { IconWidgets } from '../../components/icons';
26
+ import ProductsControl from '../../components/products-control';
27
+ import ProductOrderbyControl from '../../components/product-orderby-control';
28
+ import ProductPreview from '../../components/product-preview';
29
+
30
+ /**
31
+ * Component to handle edit mode of "Hand-picked Products".
32
+ */
33
+ class ProductsBlock extends Component {
34
+ constructor() {
35
+ super( ...arguments );
36
+ this.state = {
37
+ products: [],
38
+ loaded: false,
39
+ };
40
+
41
+ this.debouncedGetProducts = debounce( this.getProducts.bind( this ), 200 );
42
+ }
43
+
44
+ componentDidMount() {
45
+ this.getProducts();
46
+ }
47
+
48
+ componentDidUpdate( prevProps ) {
49
+ const hasChange = [ 'products', 'columns', 'orderby' ].reduce( ( acc, key ) => {
50
+ return acc || prevProps.attributes[ key ] !== this.props.attributes[ key ];
51
+ }, false );
52
+ if ( hasChange ) {
53
+ this.debouncedGetProducts();
54
+ }
55
+ }
56
+
57
+ getProducts() {
58
+ if ( ! this.props.attributes.products.length ) {
59
+ // We've removed all selected products, or products haven't been selected yet.
60
+ this.setState( { products: [], loaded: true } );
61
+ return;
62
+ }
63
+ apiFetch( {
64
+ path: addQueryArgs(
65
+ '/wc-pb/v3/products',
66
+ getQuery( this.props.attributes, this.props.name )
67
+ ),
68
+ } )
69
+ .then( ( products ) => {
70
+ this.setState( { products, loaded: true } );
71
+ } )
72
+ .catch( () => {
73
+ this.setState( { products: [], loaded: true } );
74
+ } );
75
+ }
76
+
77
+ getInspectorControls() {
78
+ const { attributes, setAttributes } = this.props;
79
+ const { columns, orderby } = attributes;
80
+
81
+ return (
82
+ <InspectorControls key="inspector">
83
+ <PanelBody
84
+ title={ __( 'Layout', 'woo-gutenberg-products-block' ) }
85
+ initialOpen
86
+ >
87
+ <RangeControl
88
+ label={ __( 'Columns', 'woo-gutenberg-products-block' ) }
89
+ value={ columns }
90
+ onChange={ ( value ) => setAttributes( { columns: value } ) }
91
+ min={ wc_product_block_data.min_columns }
92
+ max={ wc_product_block_data.max_columns }
93
+ />
94
+ </PanelBody>
95
+ <PanelBody
96
+ title={ __( 'Order By', 'woo-gutenberg-products-block' ) }
97
+ initialOpen={ false }
98
+ >
99
+ <ProductOrderbyControl
100
+ setAttributes={ setAttributes }
101
+ value={ orderby }
102
+ />
103
+ </PanelBody>
104
+ <PanelBody
105
+ title={ __( 'Products', 'woo-gutenberg-products-block' ) }
106
+ initialOpen={ false }
107
+ >
108
+ <ProductsControl
109
+ selected={ attributes.products }
110
+ onChange={ ( value = [] ) => {
111
+ const ids = value.map( ( { id } ) => id );
112
+ setAttributes( { products: ids } );
113
+ } }
114
+ />
115
+ </PanelBody>
116
+ </InspectorControls>
117
+ );
118
+ }
119
+
120
+ renderEditMode() {
121
+ const { attributes, debouncedSpeak, setAttributes } = this.props;
122
+ const onDone = () => {
123
+ setAttributes( { editMode: false } );
124
+ debouncedSpeak(
125
+ __(
126
+ 'Showing Hand-picked Products block preview.',
127
+ 'woo-gutenberg-products-block'
128
+ )
129
+ );
130
+ };
131
+
132
+ return (
133
+ <Placeholder
134
+ icon={ <IconWidgets /> }
135
+ label={ __( 'Hand-picked Products', 'woo-gutenberg-products-block' ) }
136
+ className="wc-block-products-grid wc-block-handpicked-products"
137
+ >
138
+ { __(
139
+ 'Display a selection of hand-picked products in a grid',
140
+ 'woo-gutenberg-products-block'
141
+ ) }
142
+ <div className="wc-block-handpicked-products__selection">
143
+ <ProductsControl
144
+ selected={ attributes.products }
145
+ onChange={ ( value = [] ) => {
146
+ const ids = value.map( ( { id } ) => id );
147
+ setAttributes( { products: ids } );
148
+ } }
149
+ />
150
+ <Button isDefault onClick={ onDone }>
151
+ { __( 'Done', 'woo-gutenberg-products-block' ) }
152
+ </Button>
153
+ </div>
154
+ </Placeholder>
155
+ );
156
+ }
157
+
158
+ render() {
159
+ const { setAttributes } = this.props;
160
+ const { columns, editMode } = this.props.attributes;
161
+ const { loaded, products } = this.state;
162
+ const hasSelectedProducts = products && products.length;
163
+ const classes = [ 'wc-block-products-grid', 'wc-block-handpicked-products' ];
164
+ if ( columns ) {
165
+ classes.push( `cols-${ columns }` );
166
+ }
167
+ if ( ! hasSelectedProducts ) {
168
+ if ( ! loaded ) {
169
+ classes.push( 'is-loading' );
170
+ } else {
171
+ classes.push( 'is-not-found' );
172
+ }
173
+ }
174
+
175
+ return (
176
+ <Fragment>
177
+ <BlockControls>
178
+ <Toolbar
179
+ controls={ [
180
+ {
181
+ icon: 'edit',
182
+ title: __( 'Edit' ),
183
+ onClick: () => setAttributes( { editMode: ! editMode } ),
184
+ isActive: editMode,
185
+ },
186
+ ] }
187
+ />
188
+ </BlockControls>
189
+ { this.getInspectorControls() }
190
+ { editMode ? (
191
+ this.renderEditMode()
192
+ ) : (
193
+ <div className={ classes.join( ' ' ) }>
194
+ { hasSelectedProducts ? (
195
+ products.map( ( product ) => (
196
+ <ProductPreview product={ product } key={ product.id } />
197
+ ) )
198
+ ) : (
199
+ <Placeholder
200
+ icon={ <IconWidgets /> }
201
+ label={ __(
202
+ 'Hand-picked Products',
203
+ 'woo-gutenberg-products-block'
204
+ ) }
205
+ >
206
+ { ! loaded ? (
207
+ <Spinner />
208
+ ) : (
209
+ __(
210
+ 'No products are selected.',
211
+ 'woo-gutenberg-products-block'
212
+ )
213
+ ) }
214
+ </Placeholder>
215
+ ) }
216
+ </div>
217
+ ) }
218
+ </Fragment>
219
+ );
220
+ }
221
+ }
222
+
223
+ ProductsBlock.propTypes = {
224
+ /**
225
+ * The attributes for this block
226
+ */
227
+ attributes: PropTypes.object.isRequired,
228
+ /**
229
+ * The register block name.
230
+ */
231
+ name: PropTypes.string.isRequired,
232
+ /**
233
+ * A callback to update attributes
234
+ */
235
+ setAttributes: PropTypes.func.isRequired,
236
+ // from withSpokenMessages
237
+ debouncedSpeak: PropTypes.func.isRequired,
238
+ };
239
+
240
+ export default withSpokenMessages( ProductsBlock );
assets/js/blocks/handpicked-products/index.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { registerBlockType } from '@wordpress/blocks';
6
+ import { RawHTML } from '@wordpress/element';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import './style.scss';
12
+ import Block from './block';
13
+ import getShortcode from '../../utils/get-shortcode';
14
+ import { IconWidgets } from '../../components/icons';
15
+
16
+ registerBlockType( 'woocommerce/handpicked-products', {
17
+ title: __( 'Hand-picked Products', 'woo-gutenberg-products-block' ),
18
+ icon: <IconWidgets />,
19
+ category: 'woocommerce',
20
+ keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
21
+ description: __(
22
+ 'Display a selection of hand-picked products in a grid.',
23
+ 'woo-gutenberg-products-block'
24
+ ),
25
+ supports: {
26
+ align: [ 'wide', 'full' ],
27
+ },
28
+ attributes: {
29
+ /**
30
+ * Alignment of product grid
31
+ */
32
+ align: {
33
+ type: 'string',
34
+ },
35
+
36
+ /**
37
+ * Number of columns.
38
+ */
39
+ columns: {
40
+ type: 'number',
41
+ default: wc_product_block_data.default_columns,
42
+ },
43
+
44
+ /**
45
+ * Toggle for edit mode in the block preview.
46
+ */
47
+ editMode: {
48
+ type: 'boolean',
49
+ default: true,
50
+ },
51
+
52
+ /**
53
+ * How to order the products: 'date', 'popularity', 'price_asc', 'price_desc' 'rating', 'title'.
54
+ */
55
+ orderby: {
56
+ type: 'string',
57
+ default: 'date',
58
+ },
59
+
60
+ /**
61
+ * The list of product IDs to display
62
+ */
63
+ products: {
64
+ type: 'array',
65
+ default: [],
66
+ },
67
+ },
68
+
69
+ /**
70
+ * Renders and manages the block.
71
+ */
72
+ edit( props ) {
73
+ return <Block { ...props } />;
74
+ },
75
+
76
+ /**
77
+ * Save the block content in the post content. Block content is saved as a products shortcode.
78
+ *
79
+ * @return string
80
+ */
81
+ save( props ) {
82
+ const {
83
+ align,
84
+ } = props.attributes; /* eslint-disable-line react/prop-types */
85
+ return (
86
+ <RawHTML className={ align ? `align${ align }` : '' }>
87
+ { getShortcode( props, 'woocommerce/handpicked-products' ) }
88
+ </RawHTML>
89
+ );
90
+ },
91
+ } );
assets/js/blocks/handpicked-products/style.scss ADDED
@@ -0,0 +1,3 @@
1
+ .wc-block-handpicked-products__selection {
2
+ width: 100%;
3
+ }
assets/js/blocks/product-best-sellers/block.js ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { addQueryArgs } from '@wordpress/url';
6
+ import apiFetch from '@wordpress/api-fetch';
7
+ import { InspectorControls } from '@wordpress/editor';
8
+ import { Component, Fragment } from '@wordpress/element';
9
+ import { debounce } from 'lodash';
10
+ import Gridicon from 'gridicons';
11
+ import {
12
+ PanelBody,
13
+ Placeholder,
14
+ RangeControl,
15
+ Spinner,
16
+ } from '@wordpress/components';
17
+ import PropTypes from 'prop-types';
18
+
19
+ /**
20
+ * Internal dependencies
21
+ */
22
+ import getQuery from '../../utils/get-query';
23
+ import ProductCategoryControl from '../../components/product-category-control';
24
+ import ProductPreview from '../../components/product-preview';
25
+
26
+ /**
27
+ * Component to handle edit mode of "Best Selling Products".
28
+ */
29
+ class ProductBestSellersBlock extends Component {
30
+ constructor() {
31
+ super( ...arguments );
32
+ this.state = {
33
+ products: [],
34
+ loaded: false,
35
+ };
36
+
37
+ this.debouncedGetProducts = debounce( this.getProducts.bind( this ), 200 );
38
+ }
39
+
40
+ componentDidMount() {
41
+ this.getProducts();
42
+ }
43
+
44
+ componentDidUpdate( prevProps ) {
45
+ const hasChange = [ 'categories', 'catOperator', 'columns', 'rows' ].reduce(
46
+ ( acc, key ) => {
47
+ return acc || prevProps.attributes[ key ] !== this.props.attributes[ key ];
48
+ },
49
+ false
50
+ );
51
+ if ( hasChange ) {
52
+ this.debouncedGetProducts();
53
+ }
54
+ }
55
+
56
+ getProducts() {
57
+ apiFetch( {
58
+ path: addQueryArgs(
59
+ '/wc-pb/v3/products',
60
+ getQuery( this.props.attributes, this.props.name )
61
+ ),
62
+ } )
63
+ .then( ( products ) => {
64
+ this.setState( { products, loaded: true } );
65
+ } )
66
+ .catch( () => {
67
+ this.setState( { products: [], loaded: true } );
68
+ } );
69
+ }
70
+
71
+ getInspectorControls() {
72
+ const { attributes, setAttributes } = this.props;
73
+ const { categories, catOperator, columns, rows } = attributes;
74
+
75
+ return (
76
+ <InspectorControls key="inspector">
77
+ <PanelBody
78
+ title={ __( 'Layout', 'woo-gutenberg-products-block' ) }
79
+ initialOpen
80
+ >
81
+ <RangeControl
82
+ label={ __( 'Columns', 'woo-gutenberg-products-block' ) }
83
+ value={ columns }
84
+ onChange={ ( value ) => setAttributes( { columns: value } ) }
85
+ min={ wc_product_block_data.min_columns }
86
+ max={ wc_product_block_data.max_columns }
87
+ />
88
+ <RangeControl
89
+ label={ __( 'Rows', 'woo-gutenberg-products-block' ) }
90
+ value={ rows }
91
+ onChange={ ( value ) => setAttributes( { rows: value } ) }
92
+ min={ wc_product_block_data.min_rows }
93
+ max={ wc_product_block_data.max_rows }
94
+ />
95
+ </PanelBody>
96
+ <PanelBody
97
+ title={ __(
98
+ 'Filter by Product Category',
99
+ 'woo-gutenberg-products-block'
100
+ ) }
101
+ initialOpen={ false }
102
+ >
103
+ <ProductCategoryControl
104
+ selected={ categories }
105
+ onChange={ ( value = [] ) => {
106
+ const ids = value.map( ( { id } ) => id );
107
+ setAttributes( { categories: ids } );
108
+ } }
109
+ operator={ catOperator }
110
+ onOperatorChange={ ( value = 'any' ) =>
111
+ setAttributes( { catOperator: value } )
112
+ }
113
+ />
114
+ </PanelBody>
115
+ </InspectorControls>
116
+ );
117
+ }
118
+
119
+ render() {
120
+ const { columns } = this.props.attributes;
121
+ const { loaded, products } = this.state;
122
+ const classes = [
123
+ 'wc-block-products-grid',
124
+ 'wc-block-best-selling-products',
125
+ ];
126
+ if ( columns ) {
127
+ classes.push( `cols-${ columns }` );
128
+ }
129
+ if ( products && ! products.length ) {
130
+ if ( ! loaded ) {
131
+ classes.push( 'is-loading' );
132
+ } else {
133
+ classes.push( 'is-not-found' );
134
+ }
135
+ }
136
+
137
+ return (
138
+ <Fragment>
139
+ { this.getInspectorControls() }
140
+ <div className={ classes.join( ' ' ) }>
141
+ { products.length ? (
142
+ products.map( ( product ) => (
143
+ <ProductPreview product={ product } key={ product.id } />
144
+ ) )
145
+ ) : (
146
+ <Placeholder
147
+ icon={ <Gridicon icon="stats-up-alt" /> }
148
+ label={ __(
149
+ 'Best Selling Products',
150
+ 'woo-gutenberg-products-block'
151
+ ) }
152
+ >
153
+ { ! loaded ? (
154
+ <Spinner />
155
+ ) : (
156
+ __( 'No products found.', 'woo-gutenberg-products-block' )
157
+ ) }
158
+ </Placeholder>
159
+ ) }
160
+ </div>
161
+ </Fragment>
162
+ );
163
+ }
164
+ }
165
+
166
+ ProductBestSellersBlock.propTypes = {
167
+ /**
168
+ * The attributes for this block
169
+ */
170
+ attributes: PropTypes.object.isRequired,
171
+ /**
172
+ * The register block name.
173
+ */
174
+ name: PropTypes.string.isRequired,
175
+ /**
176
+ * A callback to update attributes
177
+ */
178
+ setAttributes: PropTypes.func.isRequired,
179
+ };
180
+
181
+ export default ProductBestSellersBlock;
assets/js/blocks/product-best-sellers/index.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import Gridicon from 'gridicons';
6
+ import { registerBlockType } from '@wordpress/blocks';
7
+ import { RawHTML } from '@wordpress/element';
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import Block from './block';
13
+ import getShortcode from '../../utils/get-shortcode';
14
+ import sharedAttributes from '../../utils/shared-attributes';
15
+
16
+ registerBlockType( 'woocommerce/product-best-sellers', {
17
+ title: __( 'Best Selling Products', 'woo-gutenberg-products-block' ),
18
+ icon: <Gridicon icon="stats-up-alt" />,
19
+ category: 'woocommerce',
20
+ keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
21
+ description: __(
22
+ 'Display a grid of your all-time best selling products.',
23
+ 'woo-gutenberg-products-block'
24
+ ),
25
+ supports: {
26
+ align: [ 'wide', 'full' ],
27
+ },
28
+ attributes: {
29
+ ...sharedAttributes,
30
+ },
31
+
32
+ /**
33
+ * Renders and manages the block.
34
+ */
35
+ edit( props ) {
36
+ return <Block { ...props } />;
37
+ },
38
+
39
+ /**
40
+ * Save the block content in the post content. Block content is saved as a products shortcode.
41
+ *
42
+ * @return string
43
+ */
44
+ save( props ) {
45
+ const {
46
+ align,
47
+ } = props.attributes; /* eslint-disable-line react/prop-types */
48
+ return (
49
+ <RawHTML className={ align ? `align${ align }` : '' }>
50
+ { getShortcode( props, 'woocommerce/product-best-sellers' ) }
51
+ </RawHTML>
52
+ );
53
+ },
54
+ } );
assets/js/{product-category-block.js → blocks/product-category/block.js} RENAMED
@@ -1,51 +1,43 @@
1
/**
2
* External dependencies
3
*/
4
- import { __ } from '@wordpress/i18n';
5
import { addQueryArgs } from '@wordpress/url';
6
import apiFetch from '@wordpress/api-fetch';
7
- import { Component, Fragment, RawHTML } from '@wordpress/element';
8
- import {
9
- BlockAlignmentToolbar,
10
- BlockControls,
11
- InspectorControls,
12
- } from '@wordpress/editor';
13
import {
14
Button,
15
PanelBody,
16
Placeholder,
17
RangeControl,
18
- SelectControl,
19
Spinner,
20
Toolbar,
21
withSpokenMessages,
22
} from '@wordpress/components';
23
import PropTypes from 'prop-types';
24
- import { registerBlockType } from '@wordpress/blocks';
25
26
/**
27
* Internal dependencies
28
*/
29
- import '../css/product-category-block.scss';
30
- import getQuery from './utils/get-query';
31
- import getShortcode from './utils/get-shortcode';
32
- import ProductCategoryControl from './components/product-category-control';
33
- import ProductPreview from './components/product-preview';
34
- import sharedAttributes from './utils/shared-attributes';
35
-
36
- // Only enable center, wide, and full alignments
37
- const validAlignments = [ 'center', 'wide', 'full' ];
38
39
/**
40
* Component to handle edit mode of "Products by Category".
41
*/
42
- export default class ProductByCategoryBlock extends Component {
43
constructor() {
44
super( ...arguments );
45
this.state = {
46
products: [],
47
loaded: false,
48
};
49
}
50
51
componentDidMount() {
@@ -55,21 +47,31 @@ export default class ProductByCategoryBlock extends Component {
55
}
56
57
componentDidUpdate( prevProps ) {
58
- const hasChange = [ 'rows', 'columns', 'orderby', 'categories' ].reduce(
59
- ( acc, key ) => {
60
- return acc || prevProps.attributes[ key ] !== this.props.attributes[ key ];
61
- },
62
- false
63
- );
64
if ( hasChange ) {
65
- this.getProducts();
66
}
67
}
68
69
getProducts() {
70
- this.setState( { products: [], loaded: false } );
71
apiFetch( {
72
- path: addQueryArgs( '/wc-pb/v3/products', getQuery( this.props.attributes ) ),
73
} )
74
.then( ( products ) => {
75
this.setState( { products, loaded: true } );
@@ -81,7 +83,7 @@ export default class ProductByCategoryBlock extends Component {
81
82
getInspectorControls() {
83
const { attributes, setAttributes } = this.props;
84
- const { columns, orderby, rows } = attributes;
85
86
return (
87
<InspectorControls key="inspector">
@@ -95,6 +97,10 @@ export default class ProductByCategoryBlock extends Component {
95
const ids = value.map( ( { id } ) => id );
96
setAttributes( { categories: ids } );
97
} }
98
/>
99
</PanelBody>
100
<PanelBody
@@ -120,55 +126,9 @@ export default class ProductByCategoryBlock extends Component {
120
title={ __( 'Order By', 'woo-gutenberg-products-block' ) }
121
initialOpen={ false }
122
>
123
- <SelectControl
124
- label={ __( 'Order products by', 'woo-gutenberg-products-block' ) }
125
value={ orderby }
126
- options={ [
127
- {
128
- label: __(
129
- 'Newness - newest first',
130
- 'woo-gutenberg-products-block'
131
- ),
132
- value: 'date',
133
- },
134
- {
135
- label: __(
136
- 'Price - low to high',
137
- 'woo-gutenberg-products-block'
138
- ),
139
- value: 'price_asc',
140
- },
141
- {
142
- label: __(
143
- 'Price - high to low',
144
- 'woo-gutenberg-products-block'
145
- ),
146
- value: 'price_desc',
147
- },
148
- {
149
- label: __(
150
- 'Rating - highest first',
151
- 'woo-gutenberg-products-block'
152
- ),
153
- value: 'rating',
154
- },
155
- {
156
- label: __( 'Sales - most first', 'woo-gutenberg-products-block' ),
157
- value: 'popularity',
158
- },
159
- {
160
- label: __(
161
- 'Title - alphabetical',
162
- 'woo-gutenberg-products-block'
163
- ),
164
- value: 'title',
165
- },
166
- {
167
- label: __( 'Menu Order', 'woo-gutenberg-products-block' ),
168
- value: 'menu_order',
169
- },
170
- ] }
171
- onChange={ ( value ) => setAttributes( { orderby: value } ) }
172
/>
173
</PanelBody>
174
</InspectorControls>
@@ -180,7 +140,10 @@ export default class ProductByCategoryBlock extends Component {
180
const onDone = () => {
181
setAttributes( { editMode: false } );
182
debouncedSpeak(
183
- __( 'Showing product block preview.', 'woo-gutenberg-products-block' )
184
);
185
};
186
@@ -188,7 +151,7 @@ export default class ProductByCategoryBlock extends Component {
188
<Placeholder
189
icon="category"
190
label={ __( 'Products by Category', 'woo-gutenberg-products-block' ) }
191
- className="wc-block-products-category"
192
>
193
{ __(
194
'Display a grid of products from your selected categories',
@@ -201,6 +164,10 @@ export default class ProductByCategoryBlock extends Component {
201
const ids = value.map( ( { id } ) => id );
202
setAttributes( { categories: ids } );
203
} }
204
/>
205
<Button isDefault onClick={ onDone }>
206
{ __( 'Done', 'woo-gutenberg-products-block' ) }
@@ -212,9 +179,9 @@ export default class ProductByCategoryBlock extends Component {
212
213
render() {
214
const { setAttributes } = this.props;
215
- const { columns, align, editMode } = this.props.attributes;
216
const { loaded, products } = this.state;
217
- const classes = [ 'wc-block-products-category' ];
218
if ( columns ) {
219
classes.push( `cols-${ columns }` );
220
}
@@ -226,14 +193,21 @@ export default class ProductByCategoryBlock extends Component {
226
}
227
}
228
229
return (
230
<Fragment>
231
<BlockControls>
232
- <BlockAlignmentToolbar
233
- controls={ validAlignments }
234
- value={ align }
235
- onChange={ ( nextAlign ) => setAttributes( { align: nextAlign } ) }
236
- />
237
<Toolbar
238
controls={ [
239
{
@@ -262,14 +236,7 @@ export default class ProductByCategoryBlock extends Component {
262
'woo-gutenberg-products-block'
263
) }
264
>
265
- { ! loaded ? (
266
- <Spinner />
267
- ) : (
268
- __(
269
- 'No products in this category.',
270
- 'woo-gutenberg-products-block'
271
- )
272
- ) }
273
</Placeholder>
274
) }
275
</div>
@@ -284,6 +251,10 @@ ProductByCategoryBlock.propTypes = {
284
* The attributes for this block
285
*/
286
attributes: PropTypes.object.isRequired,
287
/**
288
* A callback to update attributes
289
*/
@@ -292,61 +263,4 @@ ProductByCategoryBlock.propTypes = {
292
debouncedSpeak: PropTypes.func.isRequired,
293
};
294
295
- const WrappedProductByCategoryBlock = withSpokenMessages(
296
- ProductByCategoryBlock
297
- );
298
-
299
- /**
300
- * Register and run the "Products by Category" block.
301
- */
302
- registerBlockType( 'woocommerce/product-category', {
303
- title: __( 'Products by Category', 'woo-gutenberg-products-block' ),
304
- icon: 'category',
305
- category: 'widgets',
306
- keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
307
- description: __(
308
- 'Display a grid of products from your selected categories.',
309
- 'woo-gutenberg-products-block'
310
- ),
311
- attributes: {
312
- ...sharedAttributes,
313
- editMode: {
314
- type: 'boolean',
315
- default: true,
316
- },
317
- categories: {
318
- type: 'array',
319
- default: [],
320
- },
321
- },
322
-
323
- getEditWrapperProps( attributes ) {
324
- const { align } = attributes;
325
- if ( -1 !== validAlignments.indexOf( align ) ) {
326
- return { 'data-align': align };
327
- }
328
- },
329
-
330
- /**
331
- * Renders and manages the block.
332
- */
333
- edit( props ) {
334
- return <WrappedProductByCategoryBlock { ...props } />;
335
- },
336
-
337
- /**
338
- * Save the block content in the post content. Block content is saved as a products shortcode.
339
- *
340
- * @return string
341
- */
342
- save( props ) {
343
- const {
344
- align,
345
- } = props.attributes; /* eslint-disable-line react/prop-types */
346
- return (
347
- <RawHTML className={ align ? `align${ align }` : '' }>
348
- { getShortcode( props ) }
349
- </RawHTML>
350
- );
351
- },
352
- } );
1
/**
2
* External dependencies
3
*/
4
+ import { __, _n } from '@wordpress/i18n';
5
import { addQueryArgs } from '@wordpress/url';
6
import apiFetch from '@wordpress/api-fetch';
7
+ import { BlockControls, InspectorControls } from '@wordpress/editor';
8
import {
9
Button,
10
PanelBody,
11
Placeholder,
12
RangeControl,
13
Spinner,
14
Toolbar,
15
withSpokenMessages,
16
} from '@wordpress/components';
17
+ import { Component, Fragment } from '@wordpress/element';
18
+ import { debounce } from 'lodash';
19
import PropTypes from 'prop-types';
20
21
/**
22
* Internal dependencies
23
*/
24
+ import getQuery from '../../utils/get-query';
25
+ import ProductCategoryControl from '../../components/product-category-control';
26
+ import ProductOrderbyControl from '../../components/product-orderby-control';
27
+ import ProductPreview from '../../components/product-preview';
28
29
/**
30
* Component to handle edit mode of "Products by Category".
31
*/
32
+ class ProductByCategoryBlock extends Component {
33
constructor() {
34
super( ...arguments );
35
this.state = {
36
products: [],
37
loaded: false,
38
};
39
+
40
+ this.debouncedGetProducts = debounce( this.getProducts.bind( this ), 200 );
41
}
42
43
componentDidMount() {
47
}
48
49
componentDidUpdate( prevProps ) {
50
+ const hasChange = [
51
+ 'categories',
52
+ 'catOperator',
53
+ 'columns',
54
+ 'orderby',
55
+ 'rows',
56
+ ].reduce( ( acc, key ) => {
57
+ return acc || prevProps.attributes[ key ] !== this.props.attributes[ key ];
58
+ }, false );
59
if ( hasChange ) {
60
+ this.debouncedGetProducts();
61
}
62
}
63
64
getProducts() {
65
+ if ( ! this.props.attributes.categories.length ) {
66
+ // We've removed all selected categories, or no categories have been selected yet.
67
+ this.setState( { products: [], loaded: true } );
68
+ return;
69
+ }
70
apiFetch( {
71
+ path: addQueryArgs(
72
+ '/wc-pb/v3/products',
73
+ getQuery( this.props.attributes, this.props.name )
74
+ ),
75
} )
76
.then( ( products ) => {
77
this.setState( { products, loaded: true } );
83
84
getInspectorControls() {
85
const { attributes, setAttributes } = this.props;
86
+ const { columns, catOperator, orderby, rows } = attributes;
87
88
return (
89
<InspectorControls key="inspector">
97
const ids = value.map( ( { id } ) => id );
98
setAttributes( { categories: ids } );
99
} }
100
+ operator={ catOperator }
101
+ onOperatorChange={ ( value = 'any' ) =>
102
+ setAttributes( { catOperator: value } )
103
+ }
104
/>
105
</PanelBody>
106
<PanelBody
126
title={ __( 'Order By', 'woo-gutenberg-products-block' ) }
127
initialOpen={ false }
128
>
129
+ <ProductOrderbyControl
130
+ setAttributes={ setAttributes }
131
value={ orderby }
132
/>
133
</PanelBody>
134
</InspectorControls>
140
const onDone = () => {
141
setAttributes( { editMode: false } );
142
debouncedSpeak(
143
+ __(
144
+ 'Showing Products by Category block preview.',
145
+ 'woo-gutenberg-products-block'
146
+ )
147
);
148
};
149
151
<Placeholder
152
icon="category"
153
label={ __( 'Products by Category', 'woo-gutenberg-products-block' ) }
154
+ className="wc-block-products-grid wc-block-products-category"
155
>
156
{ __(
157
'Display a grid of products from your selected categories',
164
const ids = value.map( ( { id } ) => id );
165
setAttributes( { categories: ids } );
166
} }
167
+ operator={ attributes.catOperator }
168
+ onOperatorChange={ ( value = 'any' ) =>
169
+ setAttributes( { catOperator: value } )
170
+ }
171
/>
172
<Button isDefault onClick={ onDone }>
173
{ __( 'Done', 'woo-gutenberg-products-block' ) }
179
180
render() {
181
const { setAttributes } = this.props;
182
+ const { categories, columns, editMode } = this.props.attributes;
183
const { loaded, products } = this.state;
184
+ const classes = [ 'wc-block-products-grid', 'wc-block-products-category' ];
185
if ( columns ) {
186
classes.push( `cols-${ columns }` );
187
}
193
}
194
}
195
196
+ const nothingFound = ! categories.length ?
197
+ __(
198
+ 'Select at least one category to display its products.',
199
+ 'woo-gutenberg-products-block'
200
+ ) :
201
+ _n(
202
+ 'No products in this category.',
203
+ 'No products in these categories.',
204
+ categories.length,
205
+ 'woo-gutenberg-products-block'
206
+ );
207
+
208
return (
209
<Fragment>
210
<BlockControls>
211
<Toolbar
212
controls={ [
213
{
236
'woo-gutenberg-products-block'
237
) }
238
>
239
+ { ! loaded ? <Spinner /> : nothingFound }
240
</Placeholder>
241
) }
242
</div>
251
* The attributes for this block
252
*/
253
attributes: PropTypes.object.isRequired,
254
+ /**
255
+ * The register block name.
256
+ */
257
+ name: PropTypes.string.isRequired,
258
/**
259
* A callback to update attributes
260
*/
263
debouncedSpeak: PropTypes.func.isRequired,
264
};
265
266
+ export default withSpokenMessages( ProductByCategoryBlock );
assets/js/blocks/product-category/index.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { registerBlockType } from '@wordpress/blocks';
6
+ import { RawHTML } from '@wordpress/element';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import './style.scss';
12
+ import Block from './block';
13
+ import getShortcode from '../../utils/get-shortcode';
14
+ import sharedAttributes from '../../utils/shared-attributes';
15
+
16
+ /**
17
+ * Register and run the "Products by Category" block.
18
+ */
19
+ registerBlockType( 'woocommerce/product-category', {
20
+ title: __( 'Products by Category', 'woo-gutenberg-products-block' ),
21
+ icon: 'category',
22
+ category: 'woocommerce',
23
+ keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
24
+ description: __(
25
+ 'Display a grid of products from your selected categories.',
26
+ 'woo-gutenberg-products-block'
27
+ ),
28
+ supports: {
29
+ align: [ 'wide', 'full' ],
30
+ },
31
+ attributes: {
32
+ ...sharedAttributes,
33
+
34
+ /**
35
+ * Toggle for edit mode in the block preview.
36
+ */
37
+ editMode: {
38
+ type: 'boolean',
39
+ default: true,
40
+ },
41
+
42
+ /**
43
+ * How to order the products: 'date', 'popularity', 'price_asc', 'price_desc' 'rating', 'title'.
44
+ */
45
+ orderby: {
46
+ type: 'string',
47
+ default: 'date',
48
+ },
49
+ },
50
+
51
+ /**
52
+ * Renders and manages the block.
53
+ */
54
+ edit( props ) {
55
+ return <Block { ...props } />;
56
+ },
57
+
58
+ /**
59
+ * Save the block content in the post content. Block content is saved as a products shortcode.
60
+ *
61
+ * @return string
62
+ */
63
+ save( props ) {
64
+ const {
65
+ align,
66
+ } = props.attributes; /* eslint-disable-line react/prop-types */
67
+ return (
68
+ <RawHTML className={ align ? `align${ align }` : '' }>
69
+ { getShortcode( props, 'woocommerce/product-category' ) }
70
+ </RawHTML>
71
+ );
72
+ },
73
+ } );
assets/js/blocks/product-category/style.scss ADDED
@@ -0,0 +1,3 @@
1
+ .wc-block-products-category__selection {
2
+ width: 100%;
3
+ }
assets/js/blocks/product-new/block.js ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { addQueryArgs } from '@wordpress/url';
6
+ import apiFetch from '@wordpress/api-fetch';
7
+ import { InspectorControls } from '@wordpress/editor';
8
+ import { Component, Fragment } from '@wordpress/element';
9
+ import { debounce } from 'lodash';
10
+ import {
11
+ PanelBody,
12
+ Placeholder,
13
+ RangeControl,
14
+ Spinner,
15
+ } from '@wordpress/components';
16
+ import PropTypes from 'prop-types';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import getQuery from '../../utils/get-query';
22
+ import { IconNewReleases } from '../../components/icons';
23
+ import ProductCategoryControl from '../../components/product-category-control';
24
+ import ProductPreview from '../../components/product-preview';
25
+
26
+ /**
27
+ * Component to handle edit mode of "Newest Products".
28
+ */
29
+ class ProductNewestBlock extends Component {
30
+ constructor() {
31
+ super( ...arguments );
32
+ this.state = {
33
+ products: [],
34
+ loaded: false,
35
+ };
36
+
37
+ this.debouncedGetProducts = debounce( this.getProducts.bind( this ), 200 );
38
+ }
39
+
40
+ componentDidMount() {
41
+ if ( this.props.attributes.categories ) {
42
+ this.getProducts();
43
+ }
44
+ }
45
+
46
+ componentDidUpdate( prevProps ) {
47
+ const hasChange = [ 'rows', 'columns', 'categories', 'catOperator' ].reduce( ( acc, key ) => {
48
+ return acc || prevProps.attributes[ key ] !== this.props.attributes[ key ];
49
+ }, false );
50
+ if ( hasChange ) {
51
+ this.debouncedGetProducts();
52
+ }
53
+ }
54
+
55
+ getProducts() {
56
+ apiFetch( {
57
+ path: addQueryArgs(
58
+ '/wc-pb/v3/products',
59
+ getQuery( this.props.attributes, this.props.name )
60
+ ),
61
+ } )
62
+ .then( ( products ) => {
63
+ this.setState( { products, loaded: true } );
64
+ } )
65
+