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

Commit 10d4fb8

Browse files
nerradmikejolley
authored andcommitted
Followup: switch namespace on useStoreProducts hook. (#1102)
* add missing `add_to_cart` properties to product schema Also camelcase properties. * switch namespace to `/wc/store/` * add experimental action for perisisting and item to a given collection * refactor ProductButton to use hooks (initial pass) This is just the initial refactor to figure out the logic. I’m going to do another pass to see about extracting some of this to a custom hook (because it’s kind of gnarly to have to repeat… and it’s possible it can be simplified as well). * add new properties to tests and ensure test is using the same product instance values as the rest request * refactor to add custom internal only useAddToCart hook. * fix value extraction from product object * revert casing changes
1 parent 0b952ea commit 10d4fb8

File tree

3 files changed

+189
-109
lines changed

3 files changed

+189
-109
lines changed

assets/js/atomic/components/product/button/index.js

Lines changed: 141 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,129 +3,162 @@
33
*/
44
import PropTypes from 'prop-types';
55
import classnames from 'classnames';
6-
import apiFetch from '@wordpress/api-fetch';
76
import { __, sprintf } from '@wordpress/i18n';
8-
import { Component } from 'react';
9-
import { addQueryArgs } from '@wordpress/url';
7+
import {
8+
useMemo,
9+
useCallback,
10+
useState,
11+
useEffect,
12+
useRef,
13+
} from '@wordpress/element';
14+
import { useDispatch } from '@wordpress/data';
15+
import { find } from 'lodash';
1016

11-
class ProductButton extends Component {
12-
static propTypes = {
13-
className: PropTypes.string,
14-
product: PropTypes.object.isRequired,
15-
};
16-
17-
state = {
18-
addedToCart: false,
19-
addingToCart: false,
20-
cartQuantity: null,
21-
};
22-
23-
onAddToCart = () => {
24-
const { product } = this.props;
25-
26-
this.setState( { addingToCart: true } );
27-
28-
return apiFetch( {
29-
method: 'POST',
30-
path: '/wc/blocks/cart/add',
31-
data: {
32-
product_id: product.id,
33-
quantity: 1,
34-
},
35-
cache: 'no-store',
36-
} )
37-
.then( ( response ) => {
38-
const newQuantity = response.quantity;
39-
40-
this.setState( {
41-
addedToCart: true,
42-
addingToCart: false,
43-
cartQuantity: newQuantity,
44-
} );
45-
} )
46-
.catch( ( response ) => {
47-
if ( response.code ) {
48-
return ( document.location.href = addQueryArgs(
49-
product.permalink,
50-
{ wc_error: response.message }
51-
) );
52-
}
17+
/**
18+
* Internal dependencies
19+
*/
20+
import { useCollection } from '@woocommerce/base-hooks';
21+
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
5322

54-
document.location.href = product.permalink;
55-
} );
23+
/**
24+
* A custom hook for exposing cart related data for a given product id and an
25+
* action for adding a single quantity of the product _to_ the cart.
26+
*
27+
* Currently this is internal only to the ProductButton component until we have
28+
* a clearer idea of the pattern that should emerge for a cart hook.
29+
*
30+
* @param {number} productId The product id for the product connection to the
31+
* cart.
32+
*
33+
* @return {Object} Returns an object with the following properties:
34+
* @type {number} cartQuantity The quantity of the product currently in
35+
* the cart.
36+
* @type {bool} addingToCart Whether the product is currently being
37+
* added to the cart (true).
38+
* @type {bool} cartIsLoading Whether the cart is being loaded.
39+
* @type {function} addToCart An action dispatcher for adding a single
40+
* quantity of the product to the cart.
41+
* Receives no arguments, it operates on the
42+
* current product.
43+
*/
44+
const useAddToCart = ( productId ) => {
45+
const { results: cartResults, isLoading: cartIsLoading } = useCollection( {
46+
namespace: '/wc/store',
47+
resourceName: 'cart/items',
48+
} );
49+
const currentCartResults = useRef( null );
50+
const { __experimentalPersistItemToCollection } = useDispatch( storeKey );
51+
const cartQuantity = useMemo( () => {
52+
const productItem = find( cartResults, { id: productId } );
53+
return productItem ? productItem.quantity : 0;
54+
}, [ cartResults, productId ] );
55+
const [ addingToCart, setAddingToCart ] = useState( false );
56+
const addToCart = useCallback( () => {
57+
setAddingToCart( true );
58+
// exclude this item from the cartResults for adding to the new
59+
// collection (so it's updated correctly!)
60+
const collection = cartResults.filter( ( cartItem ) => {
61+
return cartItem.id !== productId;
62+
} );
63+
__experimentalPersistItemToCollection(
64+
'/wc/store',
65+
'cart/items',
66+
collection,
67+
{ id: productId, quantity: 1 }
68+
);
69+
}, [ productId, cartResults ] );
70+
useEffect( () => {
71+
if ( currentCartResults.current !== cartResults ) {
72+
if ( addingToCart ) {
73+
setAddingToCart( false );
74+
}
75+
currentCartResults.current = cartResults;
76+
}
77+
}, [ cartResults, addingToCart ] );
78+
return {
79+
cartQuantity,
80+
addingToCart,
81+
cartIsLoading,
82+
addToCart,
5683
};
57-
58-
getButtonText = () => {
59-
const { product } = this.props;
60-
const { cartQuantity } = this.state;
61-
62-
if ( Number.isFinite( cartQuantity ) ) {
84+
};
85+
86+
const ProductButton = ( { product, className } ) => {
87+
const {
88+
id,
89+
permalink,
90+
add_to_cart: productCartDetails,
91+
has_options: hasOptions,
92+
is_purchasable: isPurchasable,
93+
is_in_stock: isInStock,
94+
} = product;
95+
const {
96+
cartQuantity,
97+
addingToCart,
98+
cartIsLoading,
99+
addToCart,
100+
} = useAddToCart( id );
101+
const addedToCart = cartQuantity > 0;
102+
const getButtonText = () => {
103+
if ( Number.isFinite( cartQuantity ) && addedToCart ) {
63104
return sprintf(
64105
__( '%d in cart', 'woo-gutenberg-products-block' ),
65106
cartQuantity
66107
);
67108
}
68-
69-
return product.add_to_cart.text;
109+
return productCartDetails.text;
70110
};
71-
72-
render = () => {
73-
const { product, className } = this.props;
74-
const { addingToCart, addedToCart } = this.state;
75-
76-
const wrapperClasses = classnames(
77-
className,
78-
'wc-block-grid__product-add-to-cart',
79-
'wp-block-button'
80-
);
81-
82-
const buttonClasses = classnames(
83-
'wp-block-button__link',
84-
'add_to_cart_button',
85-
{
86-
loading: addingToCart,
87-
added: addedToCart,
88-
}
89-
);
90-
91-
if ( Object.keys( product ).length === 0 ) {
92-
return (
93-
<div className={ wrapperClasses }>
94-
<button className={ buttonClasses } disabled={ true } />
95-
</div>
96-
);
111+
const wrapperClasses = classnames(
112+
className,
113+
'wc-block-grid__product-add-to-cart',
114+
'wp-block-button'
115+
);
116+
117+
const buttonClasses = classnames(
118+
'wp-block-button__link',
119+
'add_to_cart_button',
120+
{
121+
loading: addingToCart,
122+
added: addedToCart,
97123
}
124+
);
98125

99-
const allowAddToCart =
100-
! product.has_options &&
101-
product.is_purchasable &&
102-
product.is_in_stock;
103-
const buttonText = this.getButtonText();
104-
126+
if ( Object.keys( product ).length === 0 || cartIsLoading ) {
105127
return (
106128
<div className={ wrapperClasses }>
107-
{ allowAddToCart ? (
108-
<button
109-
onClick={ this.onAddToCart }
110-
aria-label={ product.add_to_cart.description }
111-
className={ buttonClasses }
112-
disabled={ addingToCart }
113-
>
114-
{ buttonText }
115-
</button>
116-
) : (
117-
<a
118-
href={ product.permalink }
119-
aria-label={ product.add_to_cart.description }
120-
className={ buttonClasses }
121-
rel="nofollow"
122-
>
123-
{ buttonText }
124-
</a>
125-
) }
129+
<button className={ buttonClasses } disabled={ true } />
126130
</div>
127131
);
128-
};
129-
}
132+
}
133+
const allowAddToCart = ! hasOptions && isPurchasable && isInStock;
134+
return (
135+
<div className={ wrapperClasses }>
136+
{ allowAddToCart ? (
137+
<button
138+
onClick={ addToCart }
139+
aria-label={ productCartDetails.description }
140+
className={ buttonClasses }
141+
disabled={ addingToCart }
142+
>
143+
{ getButtonText() }
144+
</button>
145+
) : (
146+
<a
147+
href={ permalink }
148+
aria-label={ productCartDetails.description }
149+
className={ buttonClasses }
150+
rel="nofollow"
151+
>
152+
{ getButtonText() }
153+
</a>
154+
) }
155+
</div>
156+
);
157+
};
158+
159+
ProductButton.propTypes = {
160+
className: PropTypes.string,
161+
product: PropTypes.object.isRequired,
162+
};
130163

131164
export default ProductButton;

assets/js/base/hooks/use-store-products.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const useStoreProducts = ( query ) => {
2525
// @todo see @https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1097
2626
// where the namespace is going to be changed. Not doing in this pull.
2727
const collectionOptions = {
28-
namespace: '/wc/blocks',
28+
namespace: '/wc/store',
2929
resourceName: 'products',
3030
query,
3131
};

assets/js/data/collections/actions.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { apiFetch, select } from '@wordpress/data-controls';
5+
6+
/**
7+
* Internal dependencies
8+
*/
19
import { ACTION_TYPES as types } from './action-types';
10+
import { STORE_KEY as SCHEMA_STORE_KEY } from '../schema/constants';
211

312
let Headers = window.Headers || null;
413
Headers = Headers
@@ -53,3 +62,41 @@ export function receiveCollection(
5362
response,
5463
};
5564
}
65+
66+
export function* __experimentalPersistItemToCollection(
67+
namespace,
68+
resourceName,
69+
currentCollection,
70+
data = {}
71+
) {
72+
const newCollection = [ ...currentCollection ];
73+
const route = yield select(
74+
SCHEMA_STORE_KEY,
75+
'getRoute',
76+
namespace,
77+
resourceName
78+
);
79+
if ( ! route ) {
80+
return;
81+
}
82+
const item = yield apiFetch( {
83+
path: route,
84+
method: 'POST',
85+
data,
86+
cache: 'no-store',
87+
} );
88+
if ( item ) {
89+
newCollection.push( item );
90+
yield receiveCollection(
91+
namespace,
92+
resourceName,
93+
'',
94+
[],
95+
{
96+
items: newCollection,
97+
headers: Headers,
98+
},
99+
true
100+
);
101+
}
102+
}

0 commit comments

Comments
 (0)