diff --git a/assets/js/atomic/blocks/product-elements/button/block.json b/assets/js/atomic/blocks/product-elements/button/block.json index 62f1889af91..4b281d48691 100644 --- a/assets/js/atomic/blocks/product-elements/button/block.json +++ b/assets/js/atomic/blocks/product-elements/button/block.json @@ -5,7 +5,7 @@ "description": "Display a call to action button which either adds the product to the cart, or links to the product page.", "category": "woocommerce", "keywords": [ "WooCommerce" ], - "usesContext": [ "query", "queryId", "postId" ], + "usesContext": [ "query", "queryId", "postId", "product" ], "textdomain": "woo-gutenberg-products-block", "attributes": { "productId": { diff --git a/assets/js/atomic/blocks/product-elements/image/index.ts b/assets/js/atomic/blocks/product-elements/image/index.ts index f2edff84430..1f650232352 100644 --- a/assets/js/atomic/blocks/product-elements/image/index.ts +++ b/assets/js/atomic/blocks/product-elements/image/index.ts @@ -26,7 +26,7 @@ const blockConfig: BlockConfiguration = { icon: { src: icon }, keywords: [ 'WooCommerce' ], description, - usesContext: [ 'query', 'queryId', 'postId' ], + usesContext: [ 'query', 'queryId', 'postId', 'product' ], ancestor: [ 'woocommerce/all-products', 'woocommerce/single-product', diff --git a/assets/js/atomic/blocks/product-elements/price/index.tsx b/assets/js/atomic/blocks/product-elements/price/index.tsx index 6447673b9f2..fb3ffafb429 100644 --- a/assets/js/atomic/blocks/product-elements/price/index.tsx +++ b/assets/js/atomic/blocks/product-elements/price/index.tsx @@ -23,7 +23,7 @@ const blockConfig = { apiVersion: 2, title, description, - usesContext: [ 'query', 'queryId', 'postId' ], + usesContext: [ 'query', 'queryId', 'postId', 'product' ], icon: { src: icon }, attributes, supports, diff --git a/assets/js/atomic/blocks/product-elements/sale-badge/index.ts b/assets/js/atomic/blocks/product-elements/sale-badge/index.ts index 8769f7f3c76..a0eae5816ef 100644 --- a/assets/js/atomic/blocks/product-elements/sale-badge/index.ts +++ b/assets/js/atomic/blocks/product-elements/sale-badge/index.ts @@ -26,7 +26,7 @@ const blockConfig: BlockConfiguration = { supports, attributes, edit, - usesContext: [ 'query', 'queryId', 'postId' ], + usesContext: [ 'query', 'queryId', 'postId', 'product' ], ancestor: [ 'woocommerce/all-products', 'woocommerce/single-product', diff --git a/assets/js/atomic/blocks/product-elements/shared/config.tsx b/assets/js/atomic/blocks/product-elements/shared/config.tsx index 86d07aedecb..93d554a46bc 100644 --- a/assets/js/atomic/blocks/product-elements/shared/config.tsx +++ b/assets/js/atomic/blocks/product-elements/shared/config.tsx @@ -28,7 +28,11 @@ const sharedConfig: Omit< BlockConfiguration, 'attributes' | 'title' > = { supports: { html: false, }, - ancestor: [ 'woocommerce/all-products', 'woocommerce/single-product' ], + ancestor: [ + 'woocommerce/all-products', + 'woocommerce/single-product', + 'woocommerce/product-collection', + ], save, deprecated: [ { diff --git a/assets/js/atomic/blocks/product-elements/sku/index.tsx b/assets/js/atomic/blocks/product-elements/sku/index.tsx index 4edc1dfcbe2..23ee391964b 100644 --- a/assets/js/atomic/blocks/product-elements/sku/index.tsx +++ b/assets/js/atomic/blocks/product-elements/sku/index.tsx @@ -25,7 +25,7 @@ const blockConfig: BlockConfiguration = { title, description, icon: { src: icon }, - usesContext: [ 'query', 'queryId', 'postId' ], + usesContext: [ 'query', 'queryId', 'postId', 'product' ], attributes, ancestor: [ 'woocommerce/all-products', diff --git a/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts b/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts index 5c7fed8b68f..d8ab9b4cbd8 100644 --- a/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts +++ b/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts @@ -27,7 +27,7 @@ const blockConfig: BlockConfiguration = { attributes, supports, edit, - usesContext: [ 'query', 'queryId', 'postId' ], + usesContext: [ 'query', 'queryId', 'postId', 'product' ], ancestor: [ 'woocommerce/all-products', 'woocommerce/single-product', diff --git a/assets/js/blocks/product-collection/types.ts b/assets/js/blocks/product-collection/types.ts index 32e66a5e49c..6c48bb4e781 100644 --- a/assets/js/blocks/product-collection/types.ts +++ b/assets/js/blocks/product-collection/types.ts @@ -22,18 +22,18 @@ export interface ProductCollectionDisplayLayout { } export interface ProductCollectionQuery { - author: string; - exclude: string[]; + author?: string; + exclude?: string[]; inherit: boolean | null; offset: number; order: TProductCollectionOrder; orderBy: TProductCollectionOrderBy; pages: number; - parents: number[]; + parents?: number[]; perPage: number; - postType: string; - search: string; - sticky: string; + postType?: string; + search?: string; + sticky?: string; taxQuery: Record< string, number[] >; woocommerceOnSale: boolean; /** diff --git a/assets/js/blocks/product-template/edit.tsx b/assets/js/blocks/product-template/edit.tsx index d4885a01504..a3dc51326f6 100644 --- a/assets/js/blocks/product-template/edit.tsx +++ b/assets/js/blocks/product-template/edit.tsx @@ -3,7 +3,7 @@ * External dependencies */ import classnames from 'classnames'; -import { memo, useMemo, useState } from '@wordpress/element'; +import { memo, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { @@ -16,7 +16,21 @@ import { import { Spinner } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import type { BlockEditProps } from '@wordpress/blocks'; -import { ProductCollectionAttributes } from '@woocommerce/blocks/product-collection/types'; +import { + ProductCollectionAttributes, + ProductCollectionQuery, +} from '@woocommerce/blocks/product-collection/types'; + +/** + * Internal dependencies + */ +import { Taxonomy, ProductTemplateQuery } from './types'; +import { + productCollectionApiFetchMiddleware, + transformProductData, +} from './products-middleware'; + +productCollectionApiFetchMiddleware( transformProductData ); const ProductTemplateInnerBlocks = () => { const innerBlocksProps = useInnerBlocksProps( @@ -33,9 +47,9 @@ const ProductTemplateBlockPreview = ( { setActiveBlockContextId, }: { blocks: object[]; - blockContextId: string; + blockContextId: number; isHidden: boolean; - setActiveBlockContextId: ( blockContextId: string ) => void; + setActiveBlockContextId: ( blockContextId: number ) => void; } ) => { const blockPreviewProps = useBlockPreview( { blocks, @@ -67,24 +81,79 @@ const ProductTemplateBlockPreview = ( { const MemoizedProductTemplateBlockPreview = memo( ProductTemplateBlockPreview ); +// We have to build the tax query for the REST API and use as +// keys the taxonomies `rest_base` with the `term ids` as values. +const buildTaxQuery = ( + taxQuery: Record< string, number[] >, + taxonomies?: Taxonomy[] +) => + Object.entries( taxQuery ).reduce( + ( accumulator, [ taxonomySlug, terms ] ) => { + const taxonomy = taxonomies?.find( + ( { slug } ) => slug === taxonomySlug + ); + if ( taxonomy?.rest_base ) { + accumulator[ taxonomy?.rest_base ] = terms; + } + return accumulator; + }, + {} + ); + +const buildQuery = ( { + query, + taxonomies, + templateCategory, + page, +}: { + query: ProductCollectionQuery; + taxonomies?: Taxonomy[]; + templateCategory?: object[]; + page: number; +} ): ProductTemplateQuery => { + const { + perPage, + offset = 0, + orderBy, + author, + search, + exclude, + sticky, + inherit, + taxQuery, + parents, + pages, + ...restQueryArgs + } = query; + + // There is no need to build the taxQuery if we inherit. + const builtTaxQuery = + taxQuery && ! inherit ? buildTaxQuery( taxQuery, taxonomies ) : {}; + return { + page, + offset: perPage ? perPage * ( page - 1 ) + offset : 0, + orderby: orderBy, + per_page: perPage, + author: author || undefined, + search: search || undefined, + exclude: exclude?.length ? exclude : undefined, + parent: parents?.length ? parents : undefined, + // If sticky is not set, it will return all products in the results. + // If sticky is set to `only`, it will limit the results to sticky products only. + // If it is anything else, it will exclude sticky products from results. For the record the value stored is `exclude`. + sticky: sticky === 'only' || undefined, + // If `inherit` is truthy, adjust conditionally the query to create a better preview. + categories: + inherit && templateCategory ? templateCategory[ 0 ]?.id : undefined, + ...builtTaxQuery, + ...restQueryArgs, + }; +}; + const ProductTemplateEdit = ( { clientId, context: { - query: { - perPage, - offset = 0, - order, - orderBy, - author, - search, - exclude, - sticky, - inherit, - taxQuery, - parents, - pages, - ...restQueryArgs - }, + query, queryContext = [ { page: 1 } ], templateSlug, displayLayout: { type: layoutType, columns } = { @@ -100,113 +169,42 @@ const ProductTemplateEdit = ( { __unstableLayoutClassNames: string; } ) => { const [ { page } ] = queryContext; + const { taxQuery, inherit } = query; const [ activeBlockContextId, setActiveBlockContextId ] = useState(); - const postType = 'product'; - const { products, blocks } = useSelect( - ( select ) => { - const { getEntityRecords, getTaxonomies } = select( coreStore ); - const { getBlocks } = select( blockEditorStore ); - const taxonomies = getTaxonomies( { - type: postType, - per_page: -1, + const { blocks, products } = useSelect( ( select ) => { + const { getBlocks } = select( blockEditorStore ); + const { getTaxonomies, getEntityRecords } = select( coreStore ); + const taxonomies = taxQuery + ? getTaxonomies( { + type: 'product', + per_page: -1, + context: 'view', + } ) + : []; + const templateCategory = + inherit && + templateSlug?.startsWith( 'category-' ) && + getEntityRecords( 'postType', 'category', { context: 'view', + per_page: 1, + _fields: [ 'id' ], + slug: templateSlug.replace( 'category-', '' ), } ); - const templateCategory = - inherit && - templateSlug?.startsWith( 'category-' ) && - getEntityRecords( 'taxonomy', 'category', { - context: 'view', - per_page: 1, - _fields: [ 'id' ], - slug: templateSlug.replace( 'category-', '' ), - } ); - const query: Record< string, unknown > = { - offset: perPage ? perPage * ( page - 1 ) + offset : 0, - order, - orderby: orderBy, - }; - // There is no need to build the taxQuery if we inherit. - if ( taxQuery && ! inherit ) { - // We have to build the tax query for the REST API and use as - // keys the taxonomies `rest_base` with the `term ids` as values. - const builtTaxQuery = Object.entries( taxQuery ).reduce( - ( accumulator, [ taxonomySlug, terms ] ) => { - const taxonomy = taxonomies?.find( - ( { slug } ) => slug === taxonomySlug - ); - if ( taxonomy?.rest_base ) { - accumulator[ taxonomy?.rest_base ] = terms; - } - return accumulator; - }, - {} - ); - if ( !! Object.keys( builtTaxQuery ).length ) { - Object.assign( query, builtTaxQuery ); - } - } - if ( perPage ) { - query.per_page = perPage; - } - if ( author ) { - query.author = author; - } - if ( search ) { - query.search = search; - } - if ( exclude?.length ) { - query.exclude = exclude; - } - if ( parents?.length ) { - query.parent = parents; - } - // If sticky is not set, it will return all products in the results. - // If sticky is set to `only`, it will limit the results to sticky products only. - // If it is anything else, it will exclude sticky products from results. For the record the value stored is `exclude`. - if ( sticky ) { - query.sticky = sticky === 'only'; - } - // If `inherit` is truthy, adjust conditionally the query to create a better preview. - if ( inherit ) { - if ( templateCategory ) { - query.categories = templateCategory[ 0 ]?.id; - } - } - return { - products: getEntityRecords( 'postType', postType, { - ...query, - ...restQueryArgs, - } ), - blocks: getBlocks( clientId ), - }; - }, - [ - perPage, + + const finalQuery = buildQuery( { + query, page, - offset, - order, - orderBy, - clientId, - author, - search, - postType, - exclude, - sticky, - inherit, - templateSlug, - taxQuery, - parents, - restQueryArgs, - ] - ); - const blockContexts = useMemo( - () => - products?.map( ( product ) => ( { - postType: product.type, - postId: product.id, - } ) ), - [ products ] - ); + taxonomies, + templateCategory, + source: 'product-collection', + } ); + + return { + blocks: getBlocks( clientId ), + products: getEntityRecords( 'postType', 'product', finalQuery ), + }; + } ); + const hasLayoutFlex = layoutType === 'flex' && columns > 1; const blockProps = useBlockProps( { className: classnames( @@ -231,40 +229,43 @@ const ProductTemplateEdit = ( { return (
{ ' ' } - { __( 'No results found.', 'woo-gutenberg-products-block' ) } + { __( 'No products found.', 'woo-gutenberg-products-block' ) }
); } + const buildContext = ( product ) => ( { + postType: 'product', + postId: product.id, + product, + } ); + // To avoid flicker when switching active block contexts, a preview is rendered // for each block context, but the preview for the active block context is hidden. // This ensures that when it is displayed again, the cached rendering of the // block preview is used, instead of having to re-render the preview from scratch. return (${ description }
`; +}; + +const createShortDescriptionProperty = ( input: ProductItem ) => { + const { short_description } = input; + return `${ short_description }
`; +}; + +const createImagesProperty = ( input: ProductItem ) => { + const { images } = input; + const adjustImage = ( image ) => { + const { + date_created, + date_created_gmt, + date_modified, + date_modified_gmt, + ...rest + } = image; + return { + ...rest, + sizes: '(max-width: 800px) 100vw, 800px', + srcset: 'http://another-try.local/wp-content/uploads/2023/04/album-1.jpg 800w, http://another-try.local/wp-content/uploads/2023/04/album-1-450x450.jpg 450w, http://another-try.local/wp-content/uploads/2023/04/album-1-100x100.jpg 100w, http://another-try.local/wp-content/uploads/2023/04/album-1-600x600.jpg 600w, http://another-try.local/wp-content/uploads/2023/04/album-1-300x300.jpg 300w, http://another-try.local/wp-content/uploads/2023/04/album-1-150x150.jpg 150w, http://another-try.local/wp-content/uploads/2023/04/album-1-768x768.jpg 768w', + thumbnail: image.src, + }; + }; + return images.map( adjustImage ); +}; + +export const transformProductData = ( + input: ProductItem +): WooCommerceBlocksAPIproductResponse => { + const passedProperties = passProperties( input ); + return { + ...passedProperties, + prices: createPricesProperty( input ), + add_to_cart: createAddToCartProperty( input ), + categories: createCategoriesProperty( input ), + description: createDescriptionProperty( input ), + short_description: createShortDescriptionProperty( input ), + images: createImagesProperty( input ), + is_in_stock: input.stock_status === 'instock', + is_on_backorder: input.stock_status === 'onbackorder', + is_purchasable: input.purchasable, + low_stock_remaining: input.low_stock_amount, + parent: input.parent_id, + review_count: input.rating_count, + variation: '', + extensions: {}, + title: { + raw: input.name, + rendered: input.name, + }, + excerpt: { + protected: false, + raw: input.short_description, + rendered: `${ input.short_description }
`, + }, + }; +}; + +const defaultTransform = ( input: ProductItem ) => input; + +export const productCollectionApiFetchMiddleware = ( + transform = defaultTransform +) => { + apiFetch.use( async ( options, next ) => { + const regex = /^\/wp\/v2\/product.*isProductCollectionBlock=true/; + + if ( options.path && regex.test( options?.path ) ) { + const from = '/wp/v2/product'; + const to = '/wc/v3/products'; + + const amendedPath = options.path.replace( from, to ); + const response = await next( { ...options, path: amendedPath } ); + const output = response.map( transform ); + + return output; + } + + return next( options ); + } ); +}; diff --git a/assets/js/blocks/product-template/test/mocks.ts b/assets/js/blocks/product-template/test/mocks.ts new file mode 100644 index 00000000000..c98d6181c3c --- /dev/null +++ b/assets/js/blocks/product-template/test/mocks.ts @@ -0,0 +1,405 @@ +export const wooCommerceAPIResponse = [ + { + id: 15, + name: 'Album', + slug: 'album', + permalink: 'http://your-shop-domain/hoodie-with-logo/music/album/', + date_created: '2023-04-19T12:15:04', + date_created_gmt: '2023-04-19T12:15:04', + date_modified: '2023-04-19T12:15:15', + date_modified_gmt: '2023-04-19T12:15:15', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.', + short_description: 'This is a simple, virtual product.', + sku: 'woo-album', + price: '15', + regular_price: '15', + sale_price: '', + date_on_sale_from: null, + date_on_sale_from_gmt: null, + date_on_sale_to: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: true, + downloadable: true, + downloads: [ + { + id: '47e4cef1-e656-41b7-b922-1610e6f63a6a', + name: 'Single 1', + file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', + }, + { + id: '9d205e64-847c-4112-b843-c1f02e80a284', + name: 'Single 2', + file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg', + }, + ], + download_limit: 1, + download_expiry: 1, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '', + dimensions: { + length: '', + width: '', + height: '', + }, + shipping_required: false, + shipping_taxable: false, + shipping_class: '', + shipping_class_id: 0, + reviews_allowed: true, + average_rating: '0', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: 7, + name: 'Music', + slug: 'music', + }, + ], + tags: [], + images: [ + { + id: 44, + date_created: '2023-04-19T12:15:15', + date_created_gmt: '2023-04-19T12:15:15', + date_modified: '2023-04-19T12:15:15', + date_modified_gmt: '2023-04-19T12:15:15', + src: 'http://your-shop-domain/wp-content/uploads/2023/04/album-1.jpg', + name: 'album-1.jpg', + alt: '', + }, + ], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + price_html: + '15,00 zł', + related_ids: [ 16 ], + meta_data: [ + { + id: 550, + key: '_wpcom_is_markdown', + value: '1', + }, + ], + stock_status: 'instock', + has_options: false, + permalink_template: + 'http://your-shop-domain/hoodie-with-logo/music/%pagename%/', + generated_slug: 'album', + _links: { + self: [ + { + href: 'http://your-shop-domain/wp-json/wc/v3/products/15', + }, + ], + collection: [ + { + href: 'http://your-shop-domain/wp-json/wc/v3/products', + }, + ], + }, + }, + { + id: 7, + name: 'Beanie', + slug: 'beanie', + permalink: + 'http://your-shop-domain/hoodie-with-logo/clothing/accessories/beanie/', + date_created: '2023-04-19T12:15:04', + date_created_gmt: '2023-04-19T12:15:04', + date_modified: '2023-04-19T12:15:10', + date_modified_gmt: '2023-04-19T12:15:10', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description: + 'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.', + short_description: 'This is a simple product.', + sku: 'woo-beanie', + price: '18', + regular_price: '20', + sale_price: '18', + date_on_sale_from: null, + date_on_sale_from_gmt: null, + date_on_sale_to: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '', + dimensions: { + length: '', + width: '', + height: '', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + shipping_class_id: 0, + reviews_allowed: true, + average_rating: '0', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: 6, + name: 'Accessories', + slug: 'accessories', + }, + ], + tags: [], + images: [ + { + id: 36, + date_created: '2023-04-19T12:15:10', + date_created_gmt: '2023-04-19T12:15:10', + date_modified: '2023-04-19T12:15:10', + date_modified_gmt: '2023-04-19T12:15:10', + src: 'http://your-shop-domain/wp-content/uploads/2023/04/beanie-2.jpg', + name: 'beanie-2.jpg', + alt: '', + }, + ], + attributes: [ + { + id: 1, + name: 'Color', + position: 0, + visible: true, + variation: false, + options: [ 'Red' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + price_html: + ' 18,00 zł', + related_ids: [ 8, 193, 195, 9, 24 ], + meta_data: [ + { + id: 477, + key: '_wpcom_is_markdown', + value: '1', + }, + ], + stock_status: 'instock', + has_options: false, + permalink_template: + 'http://your-shop-domain/hoodie-with-logo/clothing/accessories/%pagename%/', + generated_slug: 'beanie', + _links: { + self: [ + { + href: 'http://your-shop-domain/wp-json/wc/v3/products/7', + }, + ], + collection: [ + { + href: 'http://your-shop-domain/wp-json/wc/v3/products', + }, + ], + }, + }, +]; + +export const wooCommerceBlocksStoreAPIResponse = [ + { + id: 15, + name: 'Album', + slug: 'album', + parent: 0, + type: 'simple', + variation: '', + permalink: 'http://your-shop-domain/hoodie-with-logo/music/album/', + sku: 'woo-album', + short_description: 'This is a simple, virtual product.
', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.
', + on_sale: false, + prices: { + price: '1500', + regular_price: '1500', + sale_price: '1500', + price_range: null, + currency_code: 'PLN', + currency_symbol: 'z\u0142', + currency_minor_unit: 2, + currency_decimal_separator: ',', + currency_thousand_separator: '', + currency_prefix: '', + currency_suffix: ' z\u0142', + }, + price_html: + '15,00 zł', + average_rating: '0', + review_count: 0, + images: [ + { + id: 44, + src: 'http://your-shop-domain/wp-content/uploads/2023/04/album-1.jpg', + thumbnail: + 'http://your-shop-domain/wp-content/uploads/2023/04/album-1-450x450.jpg', + srcset: 'http://your-shop-domain/wp-content/uploads/2023/04/album-1.jpg 800w, http://your-shop-domain/wp-content/uploads/2023/04/album-1-450x450.jpg 450w, http://your-shop-domain/wp-content/uploads/2023/04/album-1-100x100.jpg 100w, http://your-shop-domain/wp-content/uploads/2023/04/album-1-600x600.jpg 600w, http://your-shop-domain/wp-content/uploads/2023/04/album-1-300x300.jpg 300w, http://your-shop-domain/wp-content/uploads/2023/04/album-1-150x150.jpg 150w, http://your-shop-domain/wp-content/uploads/2023/04/album-1-768x768.jpg 768w', + sizes: '(max-width: 800px) 100vw, 800px', + name: 'album-1.jpg', + alt: '', + }, + ], + categories: [ + { + id: 7, + name: 'Music', + slug: 'music', + link: 'http://your-shop-domain/product-category/music/', + }, + ], + tags: [], + attributes: [], + variations: [], + has_options: false, + is_purchasable: true, + is_in_stock: true, + is_on_backorder: false, + low_stock_remaining: null, + sold_individually: false, + add_to_cart: { + text: 'Add to cart', + description: 'Add “Album” to your cart', + url: '?add-to-cart=15', + minimum: 1, + maximum: 9999, + multiple_of: 1, + }, + extensions: {}, + }, + { + id: 7, + name: 'Beanie', + slug: 'beanie', + parent: 0, + type: 'simple', + variation: '', + permalink: + 'http://your-shop-domain/hoodie-with-logo/clothing/accessories/beanie/', + sku: 'woo-beanie', + short_description: 'This is a simple product.
', + description: + 'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.
', + on_sale: true, + prices: { + price: '1800', + regular_price: '2000', + sale_price: '1800', + price_range: null, + currency_code: 'PLN', + currency_symbol: 'z\u0142', + currency_minor_unit: 2, + currency_decimal_separator: ',', + currency_thousand_separator: '', + currency_prefix: '', + currency_suffix: ' z\u0142', + }, + price_html: + ' 18,00 zł', + average_rating: '0', + review_count: 0, + images: [ + { + id: 36, + src: 'http://your-shop-domain/wp-content/uploads/2023/04/beanie-2.jpg', + thumbnail: + 'http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-450x450.jpg', + srcset: 'http://your-shop-domain/wp-content/uploads/2023/04/beanie-2.jpg 801w, http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-450x450.jpg 450w, http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-100x100.jpg 100w, http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-600x600.jpg 600w, http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-300x300.jpg 300w, http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-150x150.jpg 150w, http://your-shop-domain/wp-content/uploads/2023/04/beanie-2-768x768.jpg 768w', + sizes: '(max-width: 801px) 100vw, 801px', + name: 'beanie-2.jpg', + alt: '', + }, + ], + categories: [ + { + id: 6, + name: 'Accessories', + slug: 'accessories', + link: 'http://your-shop-domain/product-category/clothing/accessories/', + }, + ], + tags: [], + attributes: [ + { + id: 1, + name: 'Color', + taxonomy: 'pa_color', + has_variations: false, + terms: [ + { + id: 13, + name: 'Red', + slug: 'red', + }, + ], + }, + ], + variations: [], + has_options: false, + is_purchasable: true, + is_in_stock: true, + is_on_backorder: false, + low_stock_remaining: null, + sold_individually: false, + add_to_cart: { + text: 'Add to cart', + description: 'Add “Beanie” to your cart', + url: '?add-to-cart=7', + minimum: 1, + maximum: 9999, + multiple_of: 1, + }, + extensions: {}, + }, +]; diff --git a/assets/js/blocks/product-template/test/products-middleware.ts b/assets/js/blocks/product-template/test/products-middleware.ts new file mode 100644 index 00000000000..f843e1c26dc --- /dev/null +++ b/assets/js/blocks/product-template/test/products-middleware.ts @@ -0,0 +1,21 @@ +/** + * Internal dependencies + */ +import { transformProductData } from '../products-middleware'; +import { + wooCommerceAPIResponse, + wooCommerceBlocksStoreAPIResponse, +} from './mocks'; + +describe( 'transformProductData', () => { + it( 'changes WooCommerce API response to WooCommerce Blocks Store API response', () => { + const product0 = wooCommerceAPIResponse[ 0 ]; + const expected0 = wooCommerceBlocksStoreAPIResponse[ 0 ]; + + const product1 = wooCommerceAPIResponse[ 1 ]; + const expected1 = wooCommerceBlocksStoreAPIResponse[ 1 ]; + + expect( transformProductData( product0 ) ).toEqual( expected0 ); + expect( transformProductData( product1 ) ).toEqual( expected1 ); + } ); +} ); diff --git a/assets/js/blocks/product-template/types.ts b/assets/js/blocks/product-template/types.ts new file mode 100644 index 00000000000..b2bb93a7c4b --- /dev/null +++ b/assets/js/blocks/product-template/types.ts @@ -0,0 +1,149 @@ +export type Taxonomy = { + slug: string; + rest_base: string; +}; + +export type ProductTemplateQuery = { + page: number; + offset: number; + order: 'asc' | 'desc'; + orderby: 'date' | 'relevance' | 'title'; + per_page: number; + author: string | undefined; + exclude: string[] | undefined; + parent: number[] | undefined; + search: string | undefined; + sticky: boolean | undefined; + categories: string | undefined; +}; + +// WooCommerce does not export the types of Products returned from API +type ItemImage = { + id: number; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + src: string; + name: string; + alt: string; +}; + +export type ProductItem = { + id: number; + name: string; + slug: string; + permalink: string; + attributes: Array< { + id: number; + name: string; + position: number; + visible: boolean; + variation: boolean; + options: string[]; + } >; + average_rating: string; + backordered: boolean; + backorders: string; + backorders_allowed: boolean; + button_text: string; + catalog_visibility: string; + categories: Array< { + id: number; + name: string; + slug: string; + } >; + cross_sell_ids: number[]; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + date_on_sale_from: null | string; + date_on_sale_from_gmt: null | string; + date_on_sale_to: null | string; + date_on_sale_to_gmt: null | string; + default_attributes: Array< { + id: number; + name: string; + option: string; + } >; + description: string; + dimensions: { length: string; width: string; height: string }; + download_expiry: number; + download_limit: number; + downloadable: boolean; + downloads: Array< { + id: number; + name: string; + file: string; + } >; + external_url: string; + featured: boolean; + grouped_products: Array< number >; + has_options: boolean; + images: Array< ItemImage >; + low_stock_amount: null | number; + manage_stock: boolean; + menu_order: number; + meta_data: Array< { + id: number; + key: string; + value: string; + } >; + on_sale: boolean; + parent_id: number; + price: string; + price_html: string; + purchasable: boolean; + purchase_note: string; + rating_count: number; + regular_price: string; + related_ids: number[]; + reviews_allowed: boolean; + sale_price: string; + shipping_class: string; + shipping_class_id: number; + shipping_required: boolean; + shipping_taxable: boolean; + short_description: string; + sku: string; + sold_individually: boolean; + status: string; + stock_quantity: number; + stock_status: string; + tags: Array< { + id: number; + name: string; + slug: string; + } >; + tax_class: string; + tax_status: string; + total_sales: number; + type: string; + upsell_ids: number[]; + variations: Array< { + id: number; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + attributes: Array< { + id: number; + name: string; + option: string; + } >; + image: string; + price: string; + regular_price: string; + sale_price: string; + sku: string; + stock_quantity: number; + tax_class: string; + tax_status: string; + total_sales: number; + weight: string; + } >; + virtual: boolean; + weight: string; + last_order_date: string; +}; diff --git a/assets/js/types/type-defs/hooks.ts b/assets/js/types/type-defs/hooks.ts index a9ee2aa58ef..8e02c4b7c1c 100644 --- a/assets/js/types/type-defs/hooks.ts +++ b/assets/js/types/type-defs/hooks.ts @@ -63,7 +63,7 @@ export interface StoreCart { } export type Query = { - catalog_visibility: 'catalog'; + catalog_visibility?: 'catalog'; per_page: number; page: number; orderby: string; diff --git a/src/BlockTypes/AddToCartForm.php b/src/BlockTypes/AddToCartForm.php index 97a145360fc..7e9eb182b02 100644 --- a/src/BlockTypes/AddToCartForm.php +++ b/src/BlockTypes/AddToCartForm.php @@ -90,9 +90,9 @@ protected function render( $attributes, $content, $block ) { return ''; } - $parsed_attributes = $this->parse_attributes( $attributes ); + $parsed_attributes = $this->parse_attributes( $attributes ); $is_descendent_of_single_product_block = $parsed_attributes['isDescendentOfSingleProductBlock']; - $product = $this->add_is_descendent_of_single_product_block_hidden_input_to_product_form( $product, $is_descendent_of_single_product_block ); + $product = $this->add_is_descendent_of_single_product_block_hidden_input_to_product_form( $product, $is_descendent_of_single_product_block ); $classname = $attributes['className'] ?? ''; $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); @@ -124,7 +124,7 @@ protected function add_is_descendent_of_single_product_block_hidden_input_to_pro '', $is_descendent_of_single_product_block ? 'true' : 'false' ); - $regex_pattern = '/