rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -0,0 +1,172 @@
/**
* External dependencies
*/
import {
createBlock,
createBlocksFromInnerBlocksTemplate,
type BlockInstance,
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/settings';
import { __, sprintf } from '@wordpress/i18n';
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
} from '@woocommerce/blocks/product-query/constants';
/**
* Internal dependencies
*/
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) =>
createBlock(
'core/query',
{
...productsQueryDefaultAttributes,
...inheritedAttributes,
namespace: productsVariationName,
query: {
...productsQueryDefaultAttributes.query,
inherit: true,
},
},
createBlocksFromInnerBlocksTemplate( productsInnerBlocksTemplate )
);
const getBlockifiedTemplate = (
inheritedAttributes: InheritedAttributes,
withTermDescription = false
) =>
[
createBlock( 'woocommerce/breadcrumbs', inheritedAttributes ),
createArchiveTitleBlock( 'archive-title', inheritedAttributes ),
withTermDescription
? createBlock( 'core/term-description', inheritedAttributes )
: null,
createBlock( 'woocommerce/store-notices', inheritedAttributes ),
createRowBlock(
[
createBlock( 'woocommerce/product-results-count' ),
createBlock( 'woocommerce/catalog-sorting' ),
],
inheritedAttributes
),
createProductsBlock( inheritedAttributes ),
].filter( Boolean ) as BlockInstance[];
const getBlockifiedTemplateWithTermDescription = (
inheritedAttributes: InheritedAttributes
) => getBlockifiedTemplate( inheritedAttributes, true );
const isConversionPossible = () => {
// Blockification is possible for the WP version 6.1 and above,
// which are the versions the Products block supports.
return isWpVersion( '6.1', '>=' );
};
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
);
const getDescriptionDisallowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
'This block serves as a placeholder for your %s. It will display the actual product image, title, price in your store. You can move this placeholder around and add more blocks around to customize the template.',
'woo-gutenberg-products-block'
),
templateTitle
);
const getDescription = ( templateTitle: string, canConvert: boolean ) => {
if ( canConvert ) {
return getDescriptionAllowingConversion( templateTitle );
}
return getDescriptionDisallowingConversion( templateTitle );
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const onClickCallback = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const onClickCallbackWithTermDescription = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes, true ) );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const productCatalogBlockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
const productTaxonomyBlockifyConfig = {
getButtonLabel,
onClickCallback: onClickCallbackWithTermDescription,
getBlockifiedTemplate: getBlockifiedTemplateWithTermDescription,
};
export const blockifiedProductCatalogConfig = {
isConversionPossible,
getDescription,
blockifyConfig: productCatalogBlockifyConfig,
};
export const blockifiedProductTaxonomyConfig = {
isConversionPossible,
getDescription,
blockifyConfig: productTaxonomyBlockifyConfig,
};

View File

@@ -0,0 +1,95 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { TemplateDetails } from './types';
export const BLOCK_SLUG = 'woocommerce/legacy-template';
export const TYPES = {
singleProduct: 'single-product',
productCatalog: 'product-catalog',
productTaxonomy: 'product-taxonomy',
productSearchResults: 'product-search-results',
orderConfirmation: 'order-confirmation',
checkoutHeader: 'checkout-header',
};
export const PLACEHOLDERS = {
singleProduct: 'single-product',
archiveProduct: 'archive-product',
orderConfirmation: 'fallback',
checkoutHeader: 'checkout-header',
};
export const TEMPLATES: TemplateDetails = {
'single-product': {
type: TYPES.singleProduct,
title: __(
'WooCommerce Single Product Block',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.singleProduct,
},
'archive-product': {
type: TYPES.productCatalog,
title: __(
'WooCommerce Product Grid Block',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
'taxonomy-product_cat': {
type: TYPES.productTaxonomy,
title: __(
'WooCommerce Product Taxonomy Block',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
'taxonomy-product_tag': {
type: TYPES.productTaxonomy,
title: __(
'WooCommerce Product Tag Block',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
'taxonomy-product_attribute': {
type: TYPES.productTaxonomy,
title: __(
'WooCommerce Product Attribute Block',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
// Since that it is a fallback value, it has to be the last one.
'taxonomy-product': {
type: TYPES.productTaxonomy,
title: __(
"WooCommerce Product's Custom Taxonomy Block",
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
'product-search-results': {
type: TYPES.productSearchResults,
title: __(
'WooCommerce Product Search Results Block',
'woo-gutenberg-products-block'
),
placeholder: PLACEHOLDERS.archiveProduct,
},
'checkout-header': {
type: TYPES.checkoutHeader,
title: __( 'Checkout Header', 'woo-gutenberg-products-block' ),
placeholder: 'checkout-header',
},
'order-confirmation': {
type: TYPES.orderConfirmation,
title: __( 'Order Confirmation Block', 'woo-gutenberg-products-block' ),
placeholder: PLACEHOLDERS.orderConfirmation,
},
};

View File

@@ -0,0 +1,88 @@
:where(.wp-block-woocommerce-legacy-template) {
margin-left: auto;
margin-right: auto;
}
.wp-block-woocommerce-classic-template__placeholder-warning {
border-left: 5px solid #2181d2;
padding-left: em(40px);
}
.wp-block-woocommerce-classic-template__placeholder .components-placeholder__fieldset {
display: grid;
grid-template-columns: 1fr;
}
.wp-block-woocommerce-classic-template__placeholder-wireframe,
.wp-block-woocommerce-classic-template__placeholder-copy {
grid-row-start: 1;
grid-column-start: 1;
transition: 0.3s all ease;
}
.wp-block-woocommerce-classic-template__placeholder-copy {
border: 1px solid $gray-900;
background-color: #fff;
padding: $gap-large $gap-larger;
border-radius: $universal-border-radius;
display: flex;
flex-direction: column;
max-width: 900px;
width: 400px;
margin: auto;
opacity: 0;
z-index: 10;
.wp-block-woocommerce-classic-template__placeholder-copy__icon-container {
margin: 0 0 $gap;
span {
@include font-size(larger);
display: block;
}
.woo-icon {
color: #{$studio-woocommerce-purple};
@include font-size(large);
svg {
vertical-align: middle;
}
}
}
p {
margin: 0 0 $gap;
}
.wp-block-woocommerce-classic-template__placeholder-migration-button-container {
justify-content: center;
margin: $gap 0;
}
}
.wp-block-woocommerce-classic-template__placeholder-wireframe {
pointer-events: none;
// Image based placeholders should fill horizontal width.
> img {
width: 100%;
}
}
.wp-block-woocommerce-legacy-template {
.components-placeholder {
box-shadow: none;
padding: 0;
}
}
.wp-block-woocommerce-legacy-template.is-selected {
.wp-block-woocommerce-classic-template__placeholder-wireframe {
filter: blur(3px);
opacity: 0.5;
* {
color: $gray-200 !important;
border-color: $gray-200 !important;
}
}
.wp-block-woocommerce-classic-template__placeholder-copy {
opacity: 1;
}
.components-placeholder {
box-shadow: inherit;
}
}

View File

@@ -0,0 +1,477 @@
/**
* External dependencies
*/
import {
BlockInstance,
createBlock,
getBlockType,
registerBlockType,
unregisterBlockType,
parse,
} from '@wordpress/blocks';
import type { BlockEditProps } from '@wordpress/blocks';
import { WC_BLOCKS_IMAGE_URL } from '@woocommerce/block-settings';
import {
useBlockProps,
BlockPreview,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { Button, Placeholder, Popover } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { box, Icon } from '@wordpress/icons';
import {
useDispatch,
subscribe,
useSelect,
select,
dispatch,
} from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { useEntityRecord } from '@wordpress/core-data';
import { debounce } from '@woocommerce/base-utils';
import { woo } from '@woocommerce/icons';
import { isNumber } from '@woocommerce/types';
/**
* Internal dependencies
*/
import './editor.scss';
import './style.scss';
import { BLOCK_SLUG, TEMPLATES, TYPES } from './constants';
import {
isClassicTemplateBlockRegisteredWithAnotherTitle,
hasTemplateSupportForClassicTemplateBlock,
getTemplateDetailsBySlug,
} from './utils';
import {
blockifiedProductCatalogConfig,
blockifiedProductTaxonomyConfig,
} from './archive-product';
import * as blockifiedSingleProduct from './single-product';
import * as blockifiedProductSearchResults from './product-search-results';
import * as blockifiedOrderConfirmation from './order-confirmation';
import type { BlockifiedTemplateConfig } from './types';
type Attributes = {
template: string;
align: string;
};
const blockifiedFallbackConfig = {
isConversionPossible: () => false,
getBlockifiedTemplate: () => [],
getDescription: () => '',
onClickCallback: () => void 0,
};
const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
[ TYPES.productCatalog ]: blockifiedProductCatalogConfig,
[ TYPES.productTaxonomy ]: blockifiedProductTaxonomyConfig,
[ TYPES.singleProduct ]: blockifiedSingleProduct,
[ TYPES.productSearchResults ]: blockifiedProductSearchResults,
[ TYPES.orderConfirmation ]: blockifiedOrderConfirmation,
fallback: blockifiedFallbackConfig,
};
const pickBlockClientIds = ( blocks: Array< BlockInstance > ) =>
blocks.reduce< Array< string > >( ( acc, block ) => {
if ( block.name === 'core/template-part' ) {
return acc;
}
return [ ...acc, block.clientId ];
}, [] );
const ConvertTemplate = ( { blockifyConfig, clientId, attributes } ) => {
const { getButtonLabel, onClickCallback, getBlockifiedTemplate } =
blockifyConfig;
const [ isPopoverOpen, setIsPopoverOpen ] = useState( false );
const { replaceBlock, selectBlock, replaceBlocks } =
useDispatch( blockEditorStore );
const { getBlocks } = useSelect( ( sel ) => {
return {
getBlocks: sel( blockEditorStore ).getBlocks,
};
}, [] );
const { createInfoNotice } = useDispatch( noticesStore );
return (
<div className="wp-block-woocommerce-classic-template__placeholder-migration-button-container">
<Button
isPrimary
onClick={ () => {
onClickCallback( {
clientId,
getBlocks,
attributes,
replaceBlock,
selectBlock,
} );
createInfoNotice(
__(
'Template transformed into blocks!',
'woo-gutenberg-products-block'
),
{
actions: [
{
label: __(
'Undo',
'woo-gutenberg-products-block'
),
onClick: () => {
const clientIds = pickBlockClientIds(
getBlocks()
);
replaceBlocks(
clientIds,
createBlock(
'core/group',
{
layout: {
inherit: true,
type: 'constrained',
},
},
[
createBlock(
'woocommerce/legacy-template',
{
template:
attributes.template,
}
),
]
)
);
},
},
],
type: 'snackbar',
}
);
} }
onMouseEnter={ () => setIsPopoverOpen( true ) }
onMouseLeave={ () => setIsPopoverOpen( false ) }
text={ getButtonLabel ? getButtonLabel() : '' }
>
{ isPopoverOpen && (
<Popover resize={ false } placement="right-end">
<div
style={ {
minWidth: '250px',
width: '250px',
maxWidth: '250px',
minHeight: '300px',
height: '300px',
maxHeight: '300px',
cursor: 'pointer',
} }
>
<BlockPreview
blocks={ getBlockifiedTemplate( {
...attributes,
isPreview: true,
} ) }
viewportWidth={ 1200 }
additionalStyles={ [
{
css: 'body { padding: 20px !important; height: fit-content !important; overflow:hidden}',
},
] }
/>
</div>
</Popover>
) }
</Button>
</div>
);
};
const Edit = ( {
clientId,
attributes,
setAttributes,
}: BlockEditProps< Attributes > ) => {
const blockProps = useBlockProps();
const { editedPostId } = useSelect( ( sel ) => {
return {
editedPostId: sel( 'core/edit-site' ).getEditedPostId(),
};
}, [] );
const template = useEntityRecord< {
slug: string;
title: {
rendered?: string;
row: string;
};
} >( 'postType', 'wp_template', editedPostId );
const templateDetails = getTemplateDetailsBySlug(
attributes.template,
TEMPLATES
);
const templateTitle =
template.record?.title.rendered?.toLowerCase() ?? attributes.template;
const templatePlaceholder = templateDetails?.placeholder ?? 'fallback';
const templateType = templateDetails?.type ?? 'fallback';
useEffect(
() =>
setAttributes( {
template: attributes.template,
align: attributes.align ?? 'wide',
} ),
[ attributes.align, attributes.template, setAttributes ]
);
const {
isConversionPossible,
getDescription,
getSkeleton,
blockifyConfig,
} = conversionConfig[ templateType ];
const skeleton = getSkeleton ? (
getSkeleton()
) : (
<img
className="wp-block-woocommerce-classic-template__placeholder-image"
src={ `${ WC_BLOCKS_IMAGE_URL }template-placeholders/${ templatePlaceholder }.svg` }
alt={ templateTitle }
/>
);
const canConvert = isConversionPossible();
const placeholderDescription = getDescription( templateTitle, canConvert );
return (
<div { ...blockProps }>
<Placeholder className="wp-block-woocommerce-classic-template__placeholder">
<div className="wp-block-woocommerce-classic-template__placeholder-wireframe">
{ skeleton }
</div>
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
<div className="wp-block-woocommerce-classic-template__placeholder-copy__icon-container">
<span className="woo-icon">
<Icon icon={ woo } />{ ' ' }
{ __(
'WooCommerce',
'woo-gutenberg-products-block'
) }
</span>
<span>
{ __(
'Classic Template Placeholder',
'woo-gutenberg-products-block'
) }
</span>
</div>
<p
dangerouslySetInnerHTML={ {
__html: placeholderDescription,
} }
/>
<p>
{ __(
'You cannot edit the content of this block. However, you can move it and place other blocks around it.',
'woo-gutenberg-products-block'
) }
</p>
{ canConvert && blockifyConfig && (
<ConvertTemplate
clientId={ clientId }
blockifyConfig={ blockifyConfig }
attributes={ attributes }
/>
) }
</div>
</Placeholder>
</div>
);
};
const registerClassicTemplateBlock = ( {
template,
inserter,
}: {
template?: string;
inserter: boolean;
} ) => {
/**
* The 'WooCommerce Legacy Template' block was renamed to 'WooCommerce Classic Template', however, the internal block
* name 'woocommerce/legacy-template' needs to remain the same. Otherwise, it would result in a corrupt block when
* loaded for users who have customized templates using the legacy-template (since the internal block name is
* stored in the database).
*
* See https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5861 for more context
*/
registerBlockType( BLOCK_SLUG, {
title:
template && TEMPLATES[ template ]
? TEMPLATES[ template ].title
: __(
'WooCommerce Classic Template',
'woo-gutenberg-products-block'
),
icon: (
<Icon
icon={ box }
className="wc-block-editor-components-block-icon"
/>
),
category: 'woocommerce',
apiVersion: 2,
keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
description: __(
'Renders classic WooCommerce PHP templates.',
'woo-gutenberg-products-block'
),
supports: {
align: [ 'wide', 'full' ],
html: false,
multiple: false,
reusable: false,
inserter,
},
attributes: {
/**
* Template attribute is used to determine which core PHP template gets rendered.
*/
template: {
type: 'string',
default: 'any',
},
align: {
type: 'string',
default: 'wide',
},
},
edit: ( {
attributes,
clientId,
setAttributes,
}: BlockEditProps< Attributes > ) => {
const newTemplate = template ?? attributes.template;
return (
<Edit
attributes={ {
...attributes,
template: newTemplate,
} }
setAttributes={ setAttributes }
clientId={ clientId }
/>
);
},
save: () => null,
} );
};
/**
* Attempts to recover the Classic Template block if it fails to render on the Single Product template
* due to the user resetting customizations without refreshing the page.
*
* When the Classic Template block fails to render, it is replaced by the 'core/missing' block, which
* displays an error message stating that the WooCommerce Classic template block is unsupported.
*
* This function replaces the 'core/missing' block with the original Classic Template block that failed
* to render, allowing the block to be displayed correctly.
*
* @see {@link https://github.com/woocommerce/woocommerce-blocks/issues/9637|Issue: Block error is displayed on clearing customizations for Woo Templates}
*
*/
const tryToRecoverClassicTemplateBlockWhenItFailsToRender = debounce( () => {
const blocks = select( 'core/block-editor' ).getBlocks();
const blocksIncludingInnerBlocks = blocks.flatMap( ( block ) => [
block,
...block.innerBlocks,
] );
const classicTemplateThatFailedToRender = blocksIncludingInnerBlocks.find(
( block ) =>
block.name === 'core/missing' &&
block.attributes.originalName === BLOCK_SLUG
);
if ( classicTemplateThatFailedToRender ) {
const blockToReplaceClassicTemplateBlockThatFailedToRender = parse(
classicTemplateThatFailedToRender.attributes.originalContent
);
if ( blockToReplaceClassicTemplateBlockThatFailedToRender ) {
dispatch( 'core/block-editor' ).replaceBlock(
classicTemplateThatFailedToRender.clientId,
blockToReplaceClassicTemplateBlockThatFailedToRender
);
}
}
}, 100 );
// @todo Refactor when there will be possible to show a block according on a template/post with a Gutenberg API. https://github.com/WordPress/gutenberg/pull/41718
let currentTemplateId: string | undefined;
subscribe( () => {
const previousTemplateId = currentTemplateId;
const store = select( 'core/edit-site' );
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
const editedPostId = store?.getEditedPostId() as
| string
| number
| undefined;
currentTemplateId = isNumber( editedPostId ) ? undefined : editedPostId;
const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ];
if ( parsedTemplate === null || parsedTemplate === undefined ) {
return;
}
const block = getBlockType( BLOCK_SLUG );
const isBlockRegistered = Boolean( block );
if (
isBlockRegistered &&
hasTemplateSupportForClassicTemplateBlock( parsedTemplate, TEMPLATES )
) {
tryToRecoverClassicTemplateBlockWhenItFailsToRender();
}
if ( previousTemplateId === currentTemplateId ) {
return;
}
if (
isBlockRegistered &&
( ! hasTemplateSupportForClassicTemplateBlock(
parsedTemplate,
TEMPLATES
) ||
isClassicTemplateBlockRegisteredWithAnotherTitle(
block,
parsedTemplate
) )
) {
unregisterBlockType( BLOCK_SLUG );
currentTemplateId = undefined;
return;
}
if (
! isBlockRegistered &&
hasTemplateSupportForClassicTemplateBlock( parsedTemplate, TEMPLATES )
) {
registerClassicTemplateBlock( {
template: parsedTemplate,
inserter: true,
} );
}
}, 'core/blocks-editor' );

View File

@@ -0,0 +1,237 @@
/**
* External dependencies
*/
import { createBlock, type BlockInstance } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import type { OnClickCallbackParameter, InheritedAttributes } from './types';
const isConversionPossible = () => {
return true;
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
[
createBlock( 'woocommerce/order-confirmation-status', {
...inheritedAttributes,
fontSize: 'large',
} ),
createBlock(
'woocommerce/order-confirmation-summary',
inheritedAttributes
),
createBlock(
'woocommerce/order-confirmation-totals-wrapper',
inheritedAttributes
),
createBlock(
'woocommerce/order-confirmation-downloads-wrapper',
inheritedAttributes
),
createBlock(
'core/columns',
{
...inheritedAttributes,
className: 'woocommerce-order-confirmation-address-wrapper',
},
[
createBlock( 'core/column', inheritedAttributes, [
createBlock(
'woocommerce/order-confirmation-shipping-wrapper',
inheritedAttributes
),
] ),
createBlock( 'core/column', inheritedAttributes, [
createBlock(
'woocommerce/order-confirmation-billing-wrapper',
inheritedAttributes
),
] ),
]
),
createBlock(
'woocommerce/order-confirmation-additional-information',
inheritedAttributes
),
].filter( Boolean ) as BlockInstance[];
const onClickCallback = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const getDescription = () => {
return __(
'This block represents the classic template used to display the order confirmation. The actual rendered template may appear different from this placeholder.',
'woo-gutenberg-products-block'
);
};
const getSkeleton = () => {
return (
<div className="woocommerce-page">
<div className="woocommerce-order">
<h1>
{ __( 'Order received', 'woo-gutenberg-products-block' ) }
</h1>
<p className="woocommerce-notice woocommerce-notice--success woocommerce-thankyou-order-confirmation">
{ __(
'Thank you. Your order has been received.',
'woo-gutenberg-products-block'
) }
</p>
<ul className="woocommerce-order-overview woocommerce-thankyou-order-details order_details">
<li className="woocommerce-order-overview__order order">
{ __( 'Order number', 'woo-gutenberg-products-block' ) }
: <strong>123</strong>
</li>
<li className="woocommerce-order-overview__date date">
{ __( 'Date', 'woo-gutenberg-products-block' ) }:{ ' ' }
<strong>May 25, 2023</strong>
</li>
<li className="woocommerce-order-overview__email email">
{ __( 'Email', 'woo-gutenberg-products-block' ) }:{ ' ' }
<strong>shopper@woo.com</strong>
</li>
<li className="woocommerce-order-overview__total total">
{ __( 'Total', 'woo-gutenberg-products-block' ) }:{ ' ' }
<strong>$20.00</strong>
</li>
</ul>
<section className="woocommerce-order-details">
<h2 className="woocommerce-order-details__title">
{ __(
'Order details',
'woo-gutenberg-products-block'
) }
</h2>
<table className="woocommerce-table woocommerce-table--order-details shop_table order_details">
<thead>
<tr>
<th className="woocommerce-table__product-name product-name">
{ __(
'Product',
'woo-gutenberg-products-block'
) }
</th>
<th className="woocommerce-table__product-table product-total">
{ __(
'Total',
'woo-gutenberg-products-block'
) }
</th>
</tr>
</thead>
<tbody>
<tr className="woocommerce-table__line-item order_item">
<td className="woocommerce-table__product-name product-name">
Sample Product{ ' ' }
<strong className="product-quantity">
×&nbsp;2
</strong>{ ' ' }
</td>
<td className="woocommerce-table__product-total product-total">
$20.00
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">
{ __(
'Subtotal',
'woo-gutenberg-products-block'
) }
:
</th>
<td>$20.00</td>
</tr>
<tr>
<th scope="row">
{ __(
'Total',
'woo-gutenberg-products-block'
) }
:
</th>
<td>$20.00</td>
</tr>
</tfoot>
</table>
</section>
<section className="woocommerce-customer-details">
<section className="woocommerce-columns woocommerce-columns--2 woocommerce-columns--addresses col2-set addresses">
<div className="woocommerce-column woocommerce-column--1 woocommerce-column--billing-address col-1">
<h2 className="woocommerce-column__title">
{ __(
'Billing address',
'woo-gutenberg-products-block'
) }
</h2>
<address>
123 Main St
<br />
New York, NY 10001
<br />
United States (US)
</address>
</div>
<div className="woocommerce-column woocommerce-column--2 woocommerce-column--shipping-address col-2">
<h2 className="woocommerce-column__title">
{ __(
'Shipping address',
'woo-gutenberg-products-block'
) }
</h2>
<address>
123 Main St
<br />
New York, NY 10001
<br />
United States (US)
</address>
</div>
</section>
</section>
</div>
</div>
);
};
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { blockifyConfig, isConversionPossible, getDescription, getSkeleton };

View File

@@ -0,0 +1,178 @@
/**
* External dependencies
*/
import {
createBlock,
createBlocksFromInnerBlocksTemplate,
type BlockInstance,
type InnerBlockTemplate,
} from '@wordpress/blocks';
import { isWpVersion } from '@woocommerce/settings';
import { __, sprintf } from '@wordpress/i18n';
import {
INNER_BLOCKS_TEMPLATE as productsInnerBlocksTemplate,
QUERY_DEFAULT_ATTRIBUTES as productsQueryDefaultAttributes,
PRODUCT_QUERY_VARIATION_NAME as productsVariationName,
} from '@woocommerce/blocks/product-query/constants';
/**
* Internal dependencies
*/
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
const createNoResultsParagraph = () =>
createBlock( 'core/paragraph', {
content: __(
'No products were found matching your selection.',
'woo-gutenberg-products-block'
),
} );
const createProductSearch = () =>
createBlock( 'core/search', {
buttonPosition: 'button-outside',
buttonText: __( 'Search', 'woo-gutenberg-products-block' ),
buttonUseIcon: false,
showLabel: false,
placeholder: __( 'Search products…', 'woo-gutenberg-products-block' ),
query: { post_type: 'product' },
} );
const extendInnerBlocksWithNoResultsContent = (
innerBlocks: InnerBlockTemplate[],
inheritedAttributes: InheritedAttributes
) => {
const noResultsContent = [
createNoResultsParagraph(),
createProductSearch(),
];
const noResultsBlockName = 'core/query-no-results';
const noResultsBlockIndex = innerBlocks.findIndex(
( block ) => block[ 0 ] === noResultsBlockName
);
const noResultsBlock = innerBlocks[ noResultsBlockIndex ];
const attributes = {
...( noResultsBlock[ 1 ] || {} ),
...inheritedAttributes,
};
const extendedNoResults = [
noResultsBlockName,
attributes,
noResultsContent,
];
return [
...productsInnerBlocksTemplate.slice( 0, noResultsBlockIndex ),
extendedNoResults,
...productsInnerBlocksTemplate.slice( noResultsBlockIndex + 1 ),
];
};
const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) => {
const productsInnerBlocksWithNoResults =
extendInnerBlocksWithNoResultsContent(
productsInnerBlocksTemplate,
inheritedAttributes
);
return createBlock(
'core/query',
{
...productsQueryDefaultAttributes,
...inheritedAttributes,
namespace: productsVariationName,
query: {
...productsQueryDefaultAttributes.query,
inherit: true,
},
},
createBlocksFromInnerBlocksTemplate( productsInnerBlocksWithNoResults )
);
};
const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) =>
[
createArchiveTitleBlock( 'search-title', inheritedAttributes ),
createBlock( 'woocommerce/store-notices', inheritedAttributes ),
createRowBlock(
[
createBlock( 'woocommerce/product-results-count' ),
createBlock( 'woocommerce/catalog-sorting' ),
],
inheritedAttributes
),
createProductsBlock( inheritedAttributes ),
].filter( Boolean ) as BlockInstance[];
const isConversionPossible = () => {
// Blockification is possible for the WP version 6.1 and above,
// which are the versions the Products block supports.
return isWpVersion( '6.1', '>=' );
};
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
);
const getDescriptionDisallowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
'This block serves as a placeholder for your %s. It will display the actual product image, title, price in your store. You can move this placeholder around and add more blocks around to customize the template.',
'woo-gutenberg-products-block'
),
templateTitle
);
const getDescription = ( templateTitle: string, canConvert: boolean ) => {
if ( canConvert ) {
return getDescriptionAllowingConversion( templateTitle );
}
return getDescriptionDisallowingConversion( templateTitle );
};
const onClickCallback = ( {
clientId,
attributes,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate( attributes ) );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) =>
innerBlock.name === 'woocommerce/store-notices'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { isConversionPossible, getDescription, blockifyConfig };

View File

@@ -0,0 +1,124 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { isWpVersion } from '@woocommerce/settings';
import { BlockInstance, createBlock } from '@wordpress/blocks';
import { VARIATION_NAME as PRODUCT_TITLE_VARIATION_NAME } from '@woocommerce/blocks/product-query/variations/elements/product-title';
import { VARIATION_NAME as PRODUCT_SUMMARY_VARIATION_NAME } from '@woocommerce/blocks/product-query/variations/elements/product-summary';
/**
* Internal dependencies
*/
import { OnClickCallbackParameter } from './types';
const getBlockifiedTemplate = () =>
[
createBlock( 'woocommerce/breadcrumbs' ),
createBlock( 'woocommerce/store-notices' ),
createBlock(
'core/columns',
{
align: 'wide',
},
[
createBlock(
'core/column',
{
type: 'constrained',
justifyContent: 'right',
width: '512px',
},
[ createBlock( 'woocommerce/product-image-gallery' ) ]
),
createBlock( 'core/column', {}, [
createBlock( 'core/post-title', {
__woocommerceNamespace: PRODUCT_TITLE_VARIATION_NAME,
level: 1,
} ),
createBlock( 'woocommerce/product-rating' ),
createBlock( 'woocommerce/product-price', {
fontSize: 'large',
} ),
createBlock( 'core/post-excerpt', {
__woocommerceNamespace: PRODUCT_SUMMARY_VARIATION_NAME,
} ),
createBlock( 'woocommerce/add-to-cart-form' ),
createBlock( 'woocommerce/product-meta' ),
] ),
]
),
createBlock( 'woocommerce/product-details', {
align: 'wide',
} ),
createBlock( 'woocommerce/related-products', {
align: 'wide',
} ),
].filter( Boolean ) as BlockInstance[];
const isConversionPossible = () => {
// Blockification is possible for the WP version 6.1 and above,
// which are the versions the Products block supports.
return isWpVersion( '6.1', '>=' );
};
const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
);
const getDescriptionDisallowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
'This block serves as a placeholder for your %s. It will display the actual product image, title, price in your store. You can move this placeholder around and add more blocks around to customize the template.',
'woo-gutenberg-products-block'
),
templateTitle
);
const getDescription = ( templateTitle: string, canConvert: boolean ) => {
if ( canConvert ) {
return getDescriptionAllowingConversion( templateTitle );
}
return getDescriptionDisallowingConversion( templateTitle );
};
const getButtonLabel = () =>
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
const onClickCallback = ( {
clientId,
getBlocks,
replaceBlock,
selectBlock,
}: OnClickCallbackParameter ) => {
replaceBlock( clientId, getBlockifiedTemplate() );
const blocks = getBlocks();
const groupBlock = blocks.find(
( block ) =>
block.name === 'core/group' &&
block.innerBlocks.some(
( innerBlock ) => innerBlock.name === 'woocommerce/breadcrumbs'
)
);
if ( groupBlock ) {
selectBlock( groupBlock.clientId );
}
};
const blockifyConfig = {
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
};
export { isConversionPossible, getDescription, blockifyConfig };

View File

@@ -0,0 +1,5 @@
:where(div[data-block-name="woocommerce/legacy-template"]) {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
}

View File

@@ -0,0 +1,35 @@
/**
* Internal dependencies
*/
import { TEMPLATES } from '../constants';
import { getTemplateDetailsBySlug } from '../utils';
describe( 'getTemplateDetailsBySlug', function () {
it( 'should return single-product object when given an exact match', () => {
expect(
getTemplateDetailsBySlug( 'single-product', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'single-product' ] );
} );
it( 'should return single-product object when given a partial match', () => {
expect(
getTemplateDetailsBySlug( 'single-product-hoodie', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'single-product' ] );
} );
it( 'should return taxonomy-product object when given a partial match', () => {
expect(
getTemplateDetailsBySlug( 'taxonomy-product_tag', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product_tag' ] );
} );
it( 'should return taxonomy-product object when given an exact match', () => {
expect(
getTemplateDetailsBySlug( 'taxonomy-product_brands', TEMPLATES )
).toStrictEqual( TEMPLATES[ 'taxonomy-product' ] );
} );
it( 'should return null object when given an incorrect match', () => {
expect( getTemplateDetailsBySlug( 'void', TEMPLATES ) ).toBeNull();
} );
} );

View File

@@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { type BlockInstance } from '@wordpress/blocks';
type TemplateDetail = {
type: string;
title: string;
placeholder: string;
};
export type TemplateDetails = Record< string, TemplateDetail >;
export type InheritedAttributes = {
align?: string;
};
export type OnClickCallbackParameter = {
clientId: string;
attributes: Record< string, unknown >;
getBlocks: () => BlockInstance[];
replaceBlock: ( clientId: string, blocks: BlockInstance[] ) => void;
selectBlock: ( clientId: string ) => void;
};
type ConversionConfig = {
onClickCallback: ( params: OnClickCallbackParameter ) => void;
getButtonLabel: () => string;
getBlockifiedTemplate: (
inheritedAttributes: InheritedAttributes
) => BlockInstance[];
};
export type BlockifiedTemplateConfig = {
// Description of the template, shown in the block placeholder.
getDescription: ( templateTitle: string, canConvert: boolean ) => string;
// Returns the skeleton HTML for the template, or can be left blank to use the default fallback image.
getSkeleton?: ( () => JSX.Element ) | undefined;
// Is conversion possible for the template?
isConversionPossible: () => boolean;
// If conversion is possible, returns the config for the template to be blockified.
blockifyConfig?: ConversionConfig | undefined;
};

View File

@@ -0,0 +1,108 @@
/**
* External dependencies
*/
import {
type Block,
type BlockInstance,
getBlockType,
createBlock,
} from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { TEMPLATES } from './constants';
import { TemplateDetails, InheritedAttributes } from './types';
// Finds the most appropriate template details object for specific template keys such as single-product-hoodie.
export function getTemplateDetailsBySlug(
parsedTemplate: string,
templates: TemplateDetails
) {
const templateKeys = Object.keys( templates );
let templateDetails = null;
for ( let i = 0; templateKeys.length > i; i++ ) {
const keyToMatch = parsedTemplate.substr( 0, templateKeys[ i ].length );
const maybeTemplate = templates[ keyToMatch ];
if ( maybeTemplate ) {
templateDetails = maybeTemplate;
break;
}
}
return templateDetails;
}
export function isClassicTemplateBlockRegisteredWithAnotherTitle(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
block: Block< any > | undefined,
parsedTemplate: string
) {
const templateDetails = getTemplateDetailsBySlug(
parsedTemplate,
TEMPLATES
);
return block?.title !== templateDetails?.title;
}
export function hasTemplateSupportForClassicTemplateBlock(
parsedTemplate: string,
templates: TemplateDetails
): boolean {
return getTemplateDetailsBySlug( parsedTemplate, templates ) ? true : false;
}
export const createArchiveTitleBlock = (
variationName: string,
inheritedAttributes: InheritedAttributes
) => {
const queryTitleBlockName = 'core/query-title';
const queryTitleBlockVariations =
getBlockType( queryTitleBlockName )?.variations || [];
const archiveTitleVariation = queryTitleBlockVariations.find(
( { name }: { name: string } ) => name === variationName
);
if ( ! archiveTitleVariation ) {
return null;
}
const { attributes } = archiveTitleVariation;
const extendedAttributes = {
...attributes,
...inheritedAttributes,
showPrefix: false,
};
return createBlock( queryTitleBlockName, extendedAttributes );
};
export const createRowBlock = (
innerBlocks: Array< BlockInstance >,
inheritedAttributes: InheritedAttributes
) => {
const groupBlockName = 'core/group';
const rowVariationName = `group-row`;
const groupBlockVariations =
getBlockType( groupBlockName )?.variations || [];
const rowVariation = groupBlockVariations.find(
( { name }: { name: string } ) => name === rowVariationName
);
if ( ! rowVariation ) {
return null;
}
const { attributes } = rowVariation;
const extendedAttributes = {
...attributes,
...inheritedAttributes,
layout: {
...attributes.layout,
justifyContent: 'space-between',
},
};
return createBlock( groupBlockName, extendedAttributes, innerBlocks );
};