Skip to content

Commit

Permalink
Create Cross-Sells product list (woocommerce#6645)
Browse files Browse the repository at this point in the history
* Create Cross-Sells product list

* Show “Read more” button for out-of-stock cross-sells products

* Update assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.tsx

Co-authored-by: Thomas Roberts <[email protected]>

* Update assets/js/blocks/cart/cart-cross-sells-product-list/index.tsx

Co-authored-by: Thomas Roberts <[email protected]>

* Remove obsolete isLoading and placeholderRows

* Fix TS errors

* Rename crossSellsProduct to product

* Fix critical error

* Lock “Cart Cross-Sells products” inner block

* Update assets/js/blocks/cart/inner-blocks/cart-cross-sells-products/block.json

Co-authored-by: Saad Tarhi <[email protected]>

* Prevent moving of the Cross-Sells block

Co-authored-by: Thomas Roberts <[email protected]>
Co-authored-by: Saad Tarhi <[email protected]>
  • Loading branch information
3 people authored and senadir committed Nov 12, 2022
1 parent 7abdd47 commit 08b7fed
Show file tree
Hide file tree
Showing 46 changed files with 810 additions and 37 deletions.
2 changes: 1 addition & 1 deletion assets/js/atomic/blocks/product-elements/button/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import './style.scss';
* @param {string} [props.className] CSS Class name for the component.
* @return {*} The component.
*/
const Block = ( props ) => {
export const Block = ( props ) => {
const { className } = props;

const { parentClassName } = useInnerBlockLayoutContext();
Expand Down
14 changes: 7 additions & 7 deletions assets/js/atomic/blocks/product-elements/image/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ import './style.scss';
/**
* Product Image Block Component.
*
* @param {Object} props Incoming props.
* @param {string} [props.className] CSS Class name for the component.
* @param {string} [props.imageSizing] Size of image to use.
* @param {boolean} [props.showProductLink] Whether or not to display a link to the product page.
* @param {boolean} [props.showSaleBadge] Whether or not to display the on sale badge.
* @param {string} [props.saleBadgeAlign] How should the sale badge be aligned if displayed.
* @param {boolean} [props.isDescendentOfQueryLoop] Whether or not be a children of Query Loop Block.
* @param {Object} props Incoming props.
* @param {string} [props.className] CSS Class name for the component.
* @param {string|undefined} [props.imageSizing] Size of image to use.
* @param {boolean|undefined} [props.showProductLink] Whether or not to display a link to the product page.
* @param {boolean} [props.showSaleBadge] Whether or not to display the on sale badge.
* @param {string|undefined} [props.saleBadgeAlign] How should the sale badge be aligned if displayed.
* @param {boolean} [props.isDescendentOfQueryLoop] Whether or not be a children of Query Loop Block.
* @return {*} The component.
*/
export const Block = ( props ) => {
Expand Down
2 changes: 1 addition & 1 deletion assets/js/atomic/blocks/product-elements/price/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { withProductDataContext } from '@woocommerce/shared-hocs';
* context will be used if this is not provided.
* @return {*} The component.
*/
const Block = ( props ) => {
export const Block = ( props ) => {
const { className, textAlign } = props;
const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext();
Expand Down
2 changes: 1 addition & 1 deletion assets/js/atomic/blocks/product-elements/rating/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import './style.scss';
* @param {string} [props.className] CSS Class name for the component.
* @return {*} The component.
*/
const Block = ( props ) => {
export const Block = ( props ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext();
const rating = getAverageRating( product );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import './style.scss';
* @param {string} [props.align] Alignment of the badge.
* @return {*} The component.
*/
const Block = ( props ) => {
export const Block = ( props ) => {
const { className, align } = props;
const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext();
Expand Down
1 change: 0 additions & 1 deletion assets/js/atomic/blocks/product-elements/title/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export interface Attributes {
headingLevel: number;
showProductLink: boolean;
linkTarget?: string;
productId: number;
align: string;
}

Expand Down
1 change: 1 addition & 0 deletions assets/js/base/context/hooks/cart/test/use-store-cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe( 'useStoreCart', () => {
const previewCartData = {
cartCoupons: previewCart.coupons,
cartItems: previewCart.items,
crossSellsProducts: previewCart.cross_sells,
cartFees: previewCart.fees,
cartItemsCount: previewCart.items_count,
cartItemsWeight: previewCart.items_weight,
Expand Down
4 changes: 4 additions & 0 deletions assets/js/base/context/hooks/cart/use-store-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CART_STORE_KEY as storeKey,
EMPTY_CART_COUPONS,
EMPTY_CART_ITEMS,
EMPTY_CART_CROSS_SELLS,
EMPTY_CART_FEES,
EMPTY_CART_ITEM_ERRORS,
EMPTY_CART_ERRORS,
Expand Down Expand Up @@ -99,6 +100,7 @@ export const defaultCartData: StoreCart = {
cartFees: EMPTY_CART_FEES,
cartItemsCount: 0,
cartItemsWeight: 0,
crossSellsProducts: EMPTY_CART_CROSS_SELLS,
cartNeedsPayment: true,
cartNeedsShipping: true,
cartItemErrors: EMPTY_CART_ITEM_ERRORS,
Expand Down Expand Up @@ -150,6 +152,7 @@ export const useStoreCart = (
return {
cartCoupons: previewCart.coupons,
cartItems: previewCart.items,
crossSellsProducts: previewCart.cross_sells,
cartFees: previewCart.fees,
cartItemsCount: previewCart.items_count,
cartItemsWeight: previewCart.items_weight,
Expand Down Expand Up @@ -211,6 +214,7 @@ export const useStoreCart = (
return {
cartCoupons,
cartItems: cartData.items,
crossSellsProducts: cartData.crossSells,
cartFees,
cartItemsCount: cartData.itemsCount,
cartItemsWeight: cartData.itemsWeight,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* External dependencies
*/
import {
InnerBlockLayoutContextProvider,
ProductDataContextProvider,
} from '@woocommerce/shared-context';
import { ProductResponseItem } from '@woocommerce/type-defs/product-response';

/**
* Internal dependencies
*/
import { Block as ProductImage } from '../../../atomic/blocks/product-elements/image/block';
import { Block as ProductName } from '../../../atomic/blocks/product-elements/title/block';
import { Block as ProductRating } from '../../../atomic/blocks/product-elements/rating/block';
import { Block as ProductSaleBadge } from '../../../atomic/blocks/product-elements/sale-badge/block';
import { Block as ProductPrice } from '../../../atomic/blocks/product-elements/price/block';
import { Block as ProductButton } from '../../../atomic/blocks/product-elements/button/block';
import AddToCartButton from '../../../atomic/blocks/product-elements/add-to-cart/block';

interface CrossSellsProductProps {
product: ProductResponseItem;
isLoading: boolean;
}

const CartCrossSellsProduct = ( {
product,
}: CrossSellsProductProps ): JSX.Element => {
return (
<div className="cross-sells-product">
<InnerBlockLayoutContextProvider
parentName={ 'woocommerce/cart-cross-sells-block' }
parentClassName={ 'wp-block-cart-cross-sells-product' }
>
<ProductDataContextProvider
// Setting isLoading to false, given this parameter is required.
isLoading={ false }
product={ product }
>
<div>
<ProductImage
className={ '' }
showSaleBadge={ false }
/>
<ProductName
align={ '' }
headingLevel={ 2 }
showProductLink={ true }
/>
<ProductRating />
<ProductSaleBadge />
<ProductPrice />
</div>
{ product.is_in_stock ? (
<AddToCartButton />
) : (
<ProductButton />
) }
</ProductDataContextProvider>
</InnerBlockLayoutContextProvider>
</div>
);
};

export default CartCrossSellsProduct;
37 changes: 37 additions & 0 deletions assets/js/blocks/cart/cart-cross-sells-product-list/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { ProductResponseItem } from '@woocommerce/type-defs/product-response';

/**
* Internal dependencies
*/
import CartCrossSellsProduct from './cart-cross-sells-product';

interface CrossSellsProductListProps {
products: ProductResponseItem[];
className?: string | undefined;
columns: number;
}

const CartCrossSellsProductList = ( {
products,
columns,
}: CrossSellsProductListProps ): JSX.Element => {
const crossSellsProducts = products.map( ( product, i ) => {
if ( i >= columns ) return null;

return (
<CartCrossSellsProduct
// Setting isLoading to false, given this parameter is required.
isLoading={ false }
product={ product }
key={ product.id }
/>
);
} );

return <div>{ crossSellsProducts }</div>;
};

export default CartCrossSellsProductList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "woocommerce/cart-cross-sells-block",
"version": "1.0.0",
"title": "Cart Cross-Sells block",
"description": "Shows the Cross-Sells block.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": true
},
"attributes": {
"lock": {
"type": "object",
"default": {
"move": true
}
}
},
"parent": [ "woocommerce/cart-items-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}
41 changes: 41 additions & 0 deletions assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* External dependencies
*/
import type { TemplateArray } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

export const Edit = (): JSX.Element => {
const blockProps = useBlockProps( {
className: 'wc-block-cart__cross-sells',
} );
const defaultTemplate = [
[
'core/heading',
{
content: __(
'You may be interested in…',
'woo-gutenberg-products-block'
),
level: 3,
},
,
[],
],
[ 'woocommerce/cart-cross-sells-products-block', {}, [] ],
] as TemplateArray;

return (
<div { ...blockProps }>
<InnerBlocks template={ defaultTemplate } templateLock={ false } />
</div>
);
};

export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { useStoreCart } from '@woocommerce/base-context/hooks';

interface Props {
children?: JSX.Element | JSX.Element[];
className?: string;
}

const FrontendBlock = ( {
children,
className = '',
}: Props ): JSX.Element | null => {
const { crossSellsProducts, cartIsLoading } = useStoreCart();

if ( cartIsLoading || crossSellsProducts.length < 1 ) {
return null;
}

return <div className={ className }>{ children }</div>;
};

export default FrontendBlock;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { Icon, column } from '@wordpress/icons';
import { registerExperimentalBlockType } from '@woocommerce/block-settings';

/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';

registerExperimentalBlockType( metadata, {
icon: {
src: (
<Icon
icon={ column }
className="wc-block-editor-components-block-icon"
/>
),
},
edit: Edit,
save: Save,
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "woocommerce/cart-cross-sells-products-block",
"version": "1.0.0",
"title": "Cart Cross-Sells products",
"description": "Shows the Cross-Sells products.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
},
"attributes": {
"columns": {
"type": "number",
"default": 3
},
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/cart-cross-sells-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { useStoreCart } from '@woocommerce/base-context/hooks';

/**
* Internal dependencies
*/
import CartCrossSellsProductList from '../../cart-cross-sells-product-list';
import metadata from './block.json';

interface BlockProps {
className?: string | undefined;
columns: number;
}

const Block = ( { className, columns }: BlockProps ): JSX.Element => {
const { crossSellsProducts } = useStoreCart();

if ( typeof columns === 'undefined' ) {
columns = metadata.attributes.columns.default;
}

return (
<CartCrossSellsProductList
className={ className }
columns={ columns }
products={ crossSellsProducts }
/>
);
};

export default Block;
Loading

0 comments on commit 08b7fed

Please sign in to comment.