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

Single Product Block #2399

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f9f00a5
Create placeholder code for single product block
mikejolley May 4, 2020
39588eb
Edit mode
mikejolley May 5, 2020
8853f01
Put block behind experimental flag
mikejolley May 5, 2020
ed59e30
Share atomic blocks with all products
mikejolley May 5, 2020
1d2694c
frontend render
mikejolley May 5, 2020
117b378
Fix notice in admin
mikejolley May 5, 2020
28b42e3
Update string
mikejolley May 6, 2020
bdf053a
Script behind flag
mikejolley May 6, 2020
2d3b072
conflict
mikejolley May 12, 2020
a515893
FIx height of control items
mikejolley May 12, 2020
6c3ed98
Basic insertion test
mikejolley May 12, 2020
beff720
Restore icon
mikejolley May 19, 2020
e971850
withBlockErrorBoundary HOC
mikejolley May 19, 2020
a257cdf
Consolodate contexts
mikejolley May 19, 2020
4ef09cf
Add styles
mikejolley May 19, 2020
8e9e22c
getAllowedInnerBlocks util
mikejolley May 19, 2020
16c0802
getAttributesFromDataset support for arrays and objects
mikejolley May 19, 2020
518a5fb
useSyncedLayoutConfig hook
mikejolley May 19, 2020
c3e9dff
Split up edit controls
mikejolley May 19, 2020
f035b35
Edit layout progress
mikejolley May 19, 2020
41b08a1
Add core columns in block map
mikejolley May 20, 2020
261f3a0
Add specific props to context provider
mikejolley May 20, 2020
3e5ba21
Revert BlockErrorBoundary changes
mikejolley May 20, 2020
2709449
introduce a new shared contexts build
nerrad May 20, 2020
446d05d
Clean up atomic imports
mikejolley May 21, 2020
96d19eb
Atomic component context support
mikejolley May 21, 2020
f5ebad7
update jest config
mikejolley May 21, 2020
956f691
[Experiment] Alternative layout rendering (#2542)
mikejolley May 26, 2020
6150303
Fix import
mikejolley May 26, 2020
b9280f8
Refactor components for attribute support
mikejolley May 26, 2020
a2121de
Remove readme
mikejolley May 26, 2020
da55665
Move back old components files
mikejolley May 26, 2020
88b1893
Mv files
mikejolley May 26, 2020
995b931
Revert "Mv files"
mikejolley May 26, 2020
3d46cd6
mv files again
mikejolley May 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/js/atomic/blocks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './product';
150 changes: 150 additions & 0 deletions assets/js/atomic/blocks/product/button/block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* External dependencies
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved contents of Atomic Components to a block.js per atomic block. This is so we can reuse the same block for edit.js and frontend.js, and also allows frontend.js to see the defined blockAttributes so it can map dataset to block attributes correctly.

*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { __, _n, sprintf } from '@wordpress/i18n';
import { useEffect, useRef } from '@wordpress/element';
import { useStoreAddToCart } from '@woocommerce/base-hooks';
import {
useInnerBlockConfigurationContext,
useProductDataContextContext,
} from '@woocommerce/shared-context';
import { decodeEntities } from '@wordpress/html-entities';
import { triggerFragmentRefresh } from '@woocommerce/base-utils';

const ProductButton = ( { className } ) => {
const { product } = useProductDataContextContext();
const { layoutStyleClassPrefix } = useInnerBlockConfigurationContext();
const componentClass = `${ layoutStyleClassPrefix }__product-add-to-cart`;

return (
<div
className={ classnames(
className,
componentClass,
'wp-block-button',
{
'is-loading': ! product,
}
) }
>
{ product ? (
<AddToCartButton product={ product } />
) : (
<AddToCartButtonPlaceholder />
) }
</div>
);
};

const AddToCartButton = ( { product } ) => {
const firstMount = useRef( true );

const {
id,
permalink,
add_to_cart: productCartDetails,
has_options: hasOptions,
is_purchasable: isPurchasable,
is_in_stock: isInStock,
} = product;

const {
cartQuantity,
addingToCart,
cartIsLoading,
addToCart,
} = useStoreAddToCart( id );

useEffect( () => {
// Avoid running on first mount when cart quantity is first set.
if ( firstMount.current ) {
firstMount.current = false;
return;
}
triggerFragmentRefresh();
}, [ cartQuantity ] );

if ( cartIsLoading ) {
return <AddToCartButtonPlaceholder />;
}

const addedToCart = Number.isFinite( cartQuantity ) && cartQuantity > 0;
const allowAddToCart = ! hasOptions && isPurchasable && isInStock;
const buttonAriaLabel = decodeEntities(
productCartDetails?.description || ''
);
const buttonText = addedToCart
? sprintf(
// translators: %s number of products in cart.
_n(
'%d in cart',
'%d in cart',
cartQuantity,
'woo-gutenberg-products-block'
),
cartQuantity
)
: decodeEntities(
productCartDetails?.text ||
__( 'Add to cart', 'woo-gutenberg-products-block' )
);

if ( ! allowAddToCart ) {
return (
<a
href={ permalink }
aria-label={ buttonAriaLabel }
className={ classnames(
'wp-block-button__link',
'add_to_cart_button',
{
loading: addingToCart,
added: addedToCart,
}
) }
rel="nofollow"
>
{ buttonText }
</a>
);
}

return (
<button
onClick={ addToCart }
aria-label={ buttonAriaLabel }
className={ classnames(
'wp-block-button__link',
'add_to_cart_button',
{
loading: addingToCart,
added: addedToCart,
}
) }
disabled={ addingToCart }
>
{ buttonText }
</button>
);
};

const AddToCartButtonPlaceholder = () => {
return (
<button
className={ classnames(
'wp-block-button__link',
'add_to_cart_button'
) }
disabled={ true }
/>
);
};

ProductButton.propTypes = {
className: PropTypes.string,
product: PropTypes.object,
};

export default ProductButton;
17 changes: 17 additions & 0 deletions assets/js/atomic/blocks/product/button/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* External dependencies
*/
import { Disabled } from '@wordpress/components';

/**
* Internal dependencies
*/
import Block from './block';

export default () => {
return (
<Disabled>
<Block />
</Disabled>
);
};
13 changes: 13 additions & 0 deletions assets/js/atomic/blocks/product/button/frontend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Internal dependencies
*/
import Block from './block';

/**
* Wrapper component used on the frontend.
*/
const FrontendBlock = () => {
return <Block />;
};

export default FrontendBlock;
13 changes: 2 additions & 11 deletions assets/js/atomic/blocks/product/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
*/
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { Disabled } from '@wordpress/components';
import { Icon, cart } from '@woocommerce/icons';
import { ProductButton } from '@woocommerce/atomic-components/product';

/**
* Internal dependencies
*/
import sharedConfig from '../shared-config';
import edit from './edit';

const blockConfig = {
title: __( 'Add to Cart Button', 'woo-gutenberg-products-block' ),
Expand All @@ -22,15 +21,7 @@ const blockConfig = {
src: <Icon srcElement={ cart } />,
foreground: '#96588a',
},
edit( props ) {
const { attributes } = props;

return (
<Disabled>
<ProductButton product={ attributes.product } />
</Disabled>
);
},
edit,
};

registerBlockType( 'woocommerce/product-button', {
Expand Down
19 changes: 19 additions & 0 deletions assets/js/atomic/blocks/product/image/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Internal dependencies
*/
export const blockAttributes = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining these separately so the frontend.js can validate attributes against them.

productLink: {
type: 'boolean',
default: true,
},
showSaleBadge: {
type: 'boolean',
default: true,
},
saleBadgeAlign: {
type: 'string',
default: 'right',
},
};

export default blockAttributes;
119 changes: 119 additions & 0 deletions assets/js/atomic/blocks/product/image/block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import { useState } from '@wordpress/element';
import classnames from 'classnames';
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/block-settings';
import {
useInnerBlockConfigurationContext,
useProductDataContextContext,
} from '@woocommerce/shared-context';

/**
* Internal dependencies
*/
import ProductSaleBadge from '../sale-badge/block.js';

const ProductImage = ( {
className,
productLink = true,
showSaleBadge = true,
saleBadgeAlign = 'right',
} ) => {
const { product } = useProductDataContextContext();
const { layoutStyleClassPrefix } = useInnerBlockConfigurationContext();
const componentClass = `${ layoutStyleClassPrefix }__product-image`;
const [ imageLoaded, setImageLoaded ] = useState( false );

if ( ! product ) {
return (
<div
className={ classnames(
className,
componentClass,
'is-loading'
) }
>
<ImagePlaceholder componentClass={ componentClass } />
</div>
);
}

const image =
product?.images && product.images.length ? product.images[ 0 ] : null;

return (
<div className={ classnames( className, componentClass ) }>
{ productLink ? (
<a href={ product.permalink } rel="nofollow">
{ showSaleBadge && (
<ProductSaleBadge align={ saleBadgeAlign } />
) }
<Image
componentClass={ componentClass }
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
/>
</a>
) : (
<>
{ showSaleBadge && (
<ProductSaleBadge align={ saleBadgeAlign } />
) }
<Image
componentClass={ componentClass }
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
/>
</>
) }
</div>
);
};

const ImagePlaceholder = ( { componentClass } ) => {
return (
<img
className={ classnames(
`${ componentClass }__image`,
`${ componentClass }__image_placeholder`
) }
src={ PLACEHOLDER_IMG_SRC }
alt=""
/>
);
};

const Image = ( { componentClass, image, onLoad, loaded } ) => {
const { thumbnail, srcset, sizes, alt } = image || {};

return (
<>
<img
className={ classnames( `${ componentClass }__image` ) }
src={ thumbnail }
srcSet={ srcset }
sizes={ sizes }
alt={ alt }
onLoad={ onLoad }
hidden={ ! loaded }
/>
{ ! loaded && (
<ImagePlaceholder componentClass={ componentClass } />
) }
</>
);
};

ProductImage.propTypes = {
className: PropTypes.string,
product: PropTypes.object,
productLink: PropTypes.bool,
showSaleBadge: PropTypes.bool,
saleBadgeAlign: PropTypes.string,
};

export default ProductImage;
Loading