Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit dac6d9b

Browse files
authored
Add ProductControl, update SearchListControl to pick a "single item" (#304)
* Add a prop to turn on “single choice” mode * Create new ProductControl to select a single product * Remove align from shared attributes This is given to us by default for using supports.align * Add Featured Product block * Fix spelling mistake & copy-paste issue * Fix lint warning * Add featured product script to build process, register block in PHP
1 parent ac52ef6 commit dac6d9b

File tree

9 files changed

+439
-52
lines changed

9 files changed

+439
-52
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { __ } from '@wordpress/i18n';
5+
import apiFetch from '@wordpress/api-fetch';
6+
import { BlockControls, InspectorControls } from '@wordpress/editor';
7+
import {
8+
Button,
9+
PanelBody,
10+
Placeholder,
11+
Spinner,
12+
Toolbar,
13+
withSpokenMessages,
14+
} from '@wordpress/components';
15+
import { Component, Fragment } from '@wordpress/element';
16+
import { debounce } from 'lodash';
17+
import PropTypes from 'prop-types';
18+
19+
/**
20+
* Internal dependencies
21+
*/
22+
import { IconStar } from '../../components/icons';
23+
import ProductControl from '../../components/product-control';
24+
import ProductPreview from '../../components/product-preview';
25+
26+
/**
27+
* Component to handle edit mode of "Featured Product".
28+
*/
29+
class FeaturedProduct extends Component {
30+
constructor() {
31+
super( ...arguments );
32+
this.state = {
33+
product: false,
34+
loaded: false,
35+
};
36+
37+
this.debouncedGetProduct = debounce( this.getProduct.bind( this ), 200 );
38+
}
39+
40+
componentDidMount() {
41+
this.getProduct();
42+
}
43+
44+
componentDidUpdate( prevProps ) {
45+
if ( prevProps.attributes.productId !== this.props.attributes.productId ) {
46+
this.debouncedGetProduct();
47+
}
48+
}
49+
50+
getProduct() {
51+
const { productId } = this.props.attributes;
52+
if ( ! productId ) {
53+
// We've removed the selected product, or no product is selected yet.
54+
this.setState( { product: false, loaded: true } );
55+
return;
56+
}
57+
apiFetch( {
58+
path: `/wc-pb/v3/products/${ productId }`,
59+
} )
60+
.then( ( product ) => {
61+
this.setState( { product, loaded: true } );
62+
} )
63+
.catch( () => {
64+
this.setState( { product: false, loaded: true } );
65+
} );
66+
}
67+
68+
getInspectorControls() {
69+
const { attributes, setAttributes } = this.props;
70+
71+
return (
72+
<InspectorControls key="inspector">
73+
<PanelBody
74+
title={ __( 'Product', 'woo-gutenberg-products-block' ) }
75+
initialOpen={ false }
76+
>
77+
<ProductControl
78+
selected={ attributes.productId || 0 }
79+
onChange={ ( value = [] ) => {
80+
const id = value[ 0 ] ? value[ 0 ].id : 0;
81+
setAttributes( { productId: id } );
82+
} }
83+
/>
84+
</PanelBody>
85+
</InspectorControls>
86+
);
87+
}
88+
89+
renderEditMode() {
90+
const { attributes, debouncedSpeak, setAttributes } = this.props;
91+
const onDone = () => {
92+
setAttributes( { editMode: false } );
93+
debouncedSpeak(
94+
__(
95+
'Showing Featured Product block preview.',
96+
'woo-gutenberg-products-block'
97+
)
98+
);
99+
};
100+
101+
return (
102+
<Placeholder
103+
icon={ <IconStar /> }
104+
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
105+
className="wc-block-featured-product"
106+
>
107+
{ __(
108+
'Visually highlight a product and encourage prompt action',
109+
'woo-gutenberg-products-block'
110+
) }
111+
<div className="wc-block-handpicked-products__selection">
112+
<ProductControl
113+
selected={ attributes.productId || 0 }
114+
onChange={ ( value = [] ) => {
115+
const id = value[ 0 ] ? value[ 0 ].id : 0;
116+
setAttributes( { productId: id } );
117+
} }
118+
/>
119+
<Button isDefault onClick={ onDone }>
120+
{ __( 'Done', 'woo-gutenberg-products-block' ) }
121+
</Button>
122+
</div>
123+
</Placeholder>
124+
);
125+
}
126+
127+
render() {
128+
const { attributes, setAttributes } = this.props;
129+
const { editMode } = attributes;
130+
const { loaded, product } = this.state;
131+
const classes = [ 'wc-block-featured-product' ];
132+
if ( ! product ) {
133+
if ( ! loaded ) {
134+
classes.push( 'is-loading' );
135+
} else {
136+
classes.push( 'is-not-found' );
137+
}
138+
}
139+
140+
return (
141+
<Fragment>
142+
<BlockControls>
143+
<Toolbar
144+
controls={ [
145+
{
146+
icon: 'edit',
147+
title: __( 'Edit' ),
148+
onClick: () => setAttributes( { editMode: ! editMode } ),
149+
isActive: editMode,
150+
},
151+
] }
152+
/>
153+
</BlockControls>
154+
{ this.getInspectorControls() }
155+
{ editMode ? (
156+
this.renderEditMode()
157+
) : (
158+
<div className={ classes.join( ' ' ) }>
159+
{ !! product ? (
160+
<ProductPreview product={ product } key={ product.id } />
161+
) : (
162+
<Placeholder
163+
icon={ <IconStar /> }
164+
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
165+
>
166+
{ ! loaded ? (
167+
<Spinner />
168+
) : (
169+
__( 'No product is selected.', 'woo-gutenberg-products-block' )
170+
) }
171+
</Placeholder>
172+
) }
173+
</div>
174+
) }
175+
</Fragment>
176+
);
177+
}
178+
}
179+
180+
FeaturedProduct.propTypes = {
181+
/**
182+
* The attributes for this block
183+
*/
184+
attributes: PropTypes.object.isRequired,
185+
/**
186+
* The register block name.
187+
*/
188+
name: PropTypes.string.isRequired,
189+
/**
190+
* A callback to update attributes
191+
*/
192+
setAttributes: PropTypes.func.isRequired,
193+
// from withSpokenMessages
194+
debouncedSpeak: PropTypes.func.isRequired,
195+
};
196+
197+
export default withSpokenMessages( FeaturedProduct );
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { __ } from '@wordpress/i18n';
5+
import { registerBlockType } from '@wordpress/blocks';
6+
7+
/**
8+
* Internal dependencies
9+
*/
10+
import Block from './block';
11+
import { IconStar } from '../../components/icons';
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: <IconStar />,
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+
* Toggle for edit mode in the block preview.
31+
*/
32+
editMode: {
33+
type: 'boolean',
34+
default: true,
35+
},
36+
37+
/**
38+
* The product ID to display
39+
*/
40+
productId: {
41+
type: 'number',
42+
},
43+
},
44+
45+
/**
46+
* Renders and manages the block.
47+
*/
48+
edit( props ) {
49+
return <Block { ...props } />;
50+
},
51+
52+
/**
53+
* Block content is rendered in PHP, not via save function.
54+
*/
55+
save() {
56+
return null;
57+
},
58+
} );

assets/js/blocks/product-category/block.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ class ProductByCategoryBlock extends Component {
139139
<ProductAttributeControl
140140
selected={ attributes.attributes }
141141
onChange={ ( value = [] ) => {
142-
const selected = value.map( ( { id, attr_slug } ) => ( {
142+
const selected = value.map( ( { id, attr_slug } ) => ( { // eslint-disable-line camelcase
143143
id,
144144
attr_slug,
145-
} ) ); // eslint-disable-line camelcase
145+
} ) );
146146
setAttributes( { attributes: selected } );
147147
} }
148148
/>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 } from '@wordpress/element';
8+
import { find } from 'lodash';
9+
import PropTypes from 'prop-types';
10+
11+
/**
12+
* Internal dependencies
13+
*/
14+
import SearchListControl from '../search-list-control';
15+
16+
class ProductControl extends Component {
17+
constructor() {
18+
super( ...arguments );
19+
this.state = {
20+
list: [],
21+
loading: true,
22+
};
23+
}
24+
25+
componentDidMount() {
26+
apiFetch( {
27+
path: addQueryArgs( '/wc-pb/v3/products', {
28+
per_page: -1,
29+
status: 'publish',
30+
} ),
31+
} )
32+
.then( ( list ) => {
33+
this.setState( { list, loading: false } );
34+
} )
35+
.catch( () => {
36+
this.setState( { list: [], loading: false } );
37+
} );
38+
}
39+
40+
render() {
41+
const { list, loading } = this.state;
42+
const { onChange, selected } = this.props;
43+
const messages = {
44+
list: __( 'Products', 'woo-gutenberg-products-block' ),
45+
noItems: __(
46+
"Your store doesn't have any products.",
47+
'woo-gutenberg-products-block'
48+
),
49+
search: __(
50+
'Search for a product to display',
51+
'woo-gutenberg-products-block'
52+
),
53+
updated: __(
54+
'Product search results updated.',
55+
'woo-gutenberg-products-block'
56+
),
57+
};
58+
59+
// Note: selected prop still needs to be array for SearchListControl.
60+
return (
61+
<Fragment>
62+
<SearchListControl
63+
className="woocommerce-products"
64+
list={ list }
65+
isLoading={ loading }
66+
isSingle
67+
selected={ [ find( list, { id: selected } ) ] }
68+
onChange={ onChange }
69+
messages={ messages }
70+
/>
71+
</Fragment>
72+
);
73+
}
74+
}
75+
76+
ProductControl.propTypes = {
77+
/**
78+
* Callback to update the selected products.
79+
*/
80+
onChange: PropTypes.func.isRequired,
81+
/**
82+
* The ID of the currently selected product.
83+
*/
84+
selected: PropTypes.number.isRequired,
85+
};
86+
87+
export default ProductControl;

0 commit comments

Comments
 (0)