* improve transform classic product template UX

* improve layout

* not update deps

* improve logic

* add bold

* fix height and width preview on hover

* fix label

* improve logic for revert button

* fix regression
This commit is contained in:
Luigi Teschio 2023-05-10 13:02:33 +02:00 committed by GitHub
parent b12cfd2d6a
commit b15cb932fb
11 changed files with 485 additions and 49 deletions

View File

@ -19,7 +19,7 @@ import {
} from '../product-query/constants';
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { type InheritedAttributes } from './types';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) =>
createBlock(
@ -71,7 +71,7 @@ const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
"This block serves as a placeholder for your %s. We recommend upgrading to the Products block for more features to edit your products visually. Don't worry, you can always revert back.",
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
@ -96,17 +96,69 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
};
const getButtonLabel = () =>
__( 'Upgrade to Products block', 'woo-gutenberg-products-block' );
__( '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 );
}
};
export const blockifiedProductCatalogConfig = {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
onClickCallback,
};
export const blockifiedProductTaxonomyConfig = {
getBlockifiedTemplate: getBlockifiedTemplateWithTermDescription,
onClickCallback: onClickCallbackWithTermDescription,
isConversionPossible,
getDescription,
getButtonLabel,

View File

@ -1,12 +1,14 @@
:where(.wp-block-woocommerce-legacy-template) {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
}
.wp-block-woocommerce-classic-template__placeholder-copy {
display: flex;
flex-direction: column;
max-width: 900px;
margin-bottom: 30px;
width: 400px;
margin: auto;
}
.wp-block-woocommerce-classic-template__placeholder-warning {
@ -15,9 +17,12 @@
}
.wp-block-woocommerce-classic-template__placeholder-wireframe {
width: 100%;
height: 250px;
background: #e5e5e5;
display: flex;
flex-wrap: wrap;
gap: $gap-large;
margin: auto;
@media only screen and (min-width: 768px) {
height: auto;
@ -27,7 +32,7 @@
.wp-block-woocommerce-classic-template__placeholder .wp-block-woocommerce-classic-template__placeholder-image {
display: none;
width: 100%;
width: 400px;
height: auto;
@media only screen and (min-width: 768px) {
@ -35,14 +40,18 @@
}
}
.wp-block-woocommerce-classic-template__placeholder-wireframe {
display: flex;
flex-direction: column;
}
.wp-block-woocommerce-classic-template__placeholder-migration-button-container {
justify-content: center;
align-items: center;
margin: 0 auto;
}
.wp-block-woocommerce-classic-template__placeholder-copy__icon-container {
display: flex;
align-items: center;
gap: $gap-small;
span {
@include font-size(larger);
}
}

View File

@ -2,6 +2,8 @@
* External dependencies
*/
import {
BlockInstance,
createBlock,
getBlockType,
registerBlockType,
unregisterBlockType,
@ -11,12 +13,18 @@ import {
isExperimentalBuild,
WC_BLOCKS_IMAGE_URL,
} from '@woocommerce/block-settings';
import { useBlockProps } from '@wordpress/block-editor';
import { Button, Placeholder } from '@wordpress/components';
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 { select, useDispatch, subscribe } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { useDispatch, subscribe, useSelect, select } from '@wordpress/data';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { useEntityRecord } from '@wordpress/core-data';
/**
* Internal dependencies
@ -47,6 +55,7 @@ const blockifiedFallbackConfig = {
getBlockifiedTemplate: () => [],
getDescription: () => '',
getButtonLabel: () => '',
onClickCallback: () => void 0,
};
const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
@ -57,19 +66,53 @@ const conversionConfig: { [ key: string ]: BlockifiedTemplateConfig } = {
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 Edit = ( {
clientId,
attributes,
setAttributes,
}: BlockEditProps< Attributes > ) => {
const { replaceBlock } = useDispatch( 'core/block-editor' );
const { replaceBlock, selectBlock, replaceBlocks } =
useDispatch( blockEditorStore );
const { getBlocks, editedPostId } = useSelect( ( sel ) => {
return {
getBlocks: sel( blockEditorStore ).getBlocks,
editedPostId: sel( 'core/edit-site' ).getEditedPostId(),
};
}, [] );
const template = useEntityRecord< {
slug: string;
title: {
rendered?: string;
row: string;
};
} >( 'postType', 'wp_template', editedPostId );
const { createInfoNotice } = useDispatch( noticesStore );
const blocks = getBlocks();
const clientIds = useMemo( () => {
pickBlockClientIds( blocks );
}, [ blocks ] );
const blockProps = useBlockProps();
const templateDetails = getTemplateDetailsBySlug(
attributes.template,
TEMPLATES
);
const templateTitle = templateDetails?.title ?? attributes.template;
const templateTitle =
template.record?.title.rendered?.toLowerCase() ?? attributes.template;
const templatePlaceholder = templateDetails?.placeholder ?? 'fallback';
const templateType = templateDetails?.type ?? 'fallback';
@ -83,40 +126,128 @@ const Edit = ( {
);
const {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
onClickCallback,
getBlockifiedTemplate,
} = conversionConfig[ templateType ];
const canConvert = isConversionPossible();
const placeholderDescription = getDescription( templateTitle, canConvert );
const [ isPopoverOpen, setIsPopoverOpen ] = useState( false );
return (
<div { ...blockProps }>
<Placeholder
icon={ box }
label={ templateTitle }
className="wp-block-woocommerce-classic-template__placeholder"
>
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
<p>{ placeholderDescription }</p>
</div>
<Placeholder className="wp-block-woocommerce-classic-template__placeholder">
<div className="wp-block-woocommerce-classic-template__placeholder-wireframe">
{ canConvert && (
<div className="wp-block-woocommerce-classic-template__placeholder-migration-button-container">
<Button
isPrimary
onClick={ () => {
replaceBlock(
clientId,
getBlockifiedTemplate( attributes )
);
} }
text={ getButtonLabel() }
/>
<div className="wp-block-woocommerce-classic-template__placeholder-copy">
<div className="wp-block-woocommerce-classic-template__placeholder-copy__icon-container">
<Icon icon={ box } />
<span>
{ __(
'Classic Product Template',
'woo-gutenberg-products-block'
) }
</span>
</div>
) }
<p>{ placeholderDescription }</p>
{ canConvert && (
<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: () => {
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() }
>
{ 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
) }
viewportWidth={ 1200 }
additionalStyles={ [
{
css: 'body { padding: 20px !important; height: fit-content !important; overflow:hidden}',
},
] }
/>
</div>
</Popover>
) }
</Button>
</div>
) }
</div>
<img
className="wp-block-woocommerce-classic-template__placeholder-image"
src={ `${ WC_BLOCKS_IMAGE_URL }template-placeholders/${ templatePlaceholder }.svg` }

View File

@ -20,7 +20,7 @@ import {
} from '../product-query/constants';
import { VARIATION_NAME as productsVariationName } from '../product-query/variations/product-query';
import { createArchiveTitleBlock, createRowBlock } from './utils';
import { type InheritedAttributes } from './types';
import { OnClickCallbackParameter, type InheritedAttributes } from './types';
const createNoResultsParagraph = () =>
createBlock( 'core/paragraph', {
@ -118,7 +118,7 @@ const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
"This block serves as a placeholder for your %s. We recommend upgrading to the Products block for more features to edit your products visually. Don't worry, you can always revert back.",
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s template.',
'woo-gutenberg-products-block'
),
templateTitle
@ -142,12 +142,38 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
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 = () =>
__( 'Upgrade to Products block', 'woo-gutenberg-products-block' );
__( 'Transform into blocks', 'woo-gutenberg-products-block' );
export {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
onClickCallback,
};

View File

@ -7,6 +7,11 @@ 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' ),
@ -58,7 +63,7 @@ const getDescriptionAllowingConversion = ( templateTitle: string ) =>
sprintf(
/* translators: %s is the template title */
__(
"This block serves as a placeholder for your %s. We recommend upgrading to the Single Products block for more features to edit your products visually. Don't worry, you can always revert back.",
'Transform this template into multiple blocks so you can add, remove, reorder, and customize your %s.',
'woo-gutenberg-products-block'
),
templateTitle
@ -83,14 +88,34 @@ const getDescription = ( templateTitle: string, canConvert: boolean ) => {
};
const getButtonLabel = () =>
__(
'Upgrade to Blockified Single Product template',
'woo-gutenberg-products-block'
__( '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 );
}
};
export {
getBlockifiedTemplate,
isConversionPossible,
getDescription,
getButtonLabel,
onClickCallback,
};

View File

@ -9,6 +9,14 @@ 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;
};
export type BlockifiedTemplateConfig = {
getBlockifiedTemplate: (
inheritedAttributes: InheritedAttributes
@ -16,4 +24,5 @@ export type BlockifiedTemplateConfig = {
isConversionPossible: () => boolean;
getDescription: ( templateTitle: string, canConvert: boolean ) => string;
getButtonLabel: () => string;
onClickCallback: ( params: OnClickCallbackParameter ) => void;
};

View File

@ -5,6 +5,7 @@ import { getCategories, setCategories } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { woo } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import '@woocommerce/templates/revert-button';
/**
* Internal dependencies

View File

@ -0,0 +1,171 @@
/**
* External dependencies
*/
import { PluginTemplateSettingPanel } from '@wordpress/edit-site';
import { subscribe, select, useSelect, useDispatch } from '@wordpress/data';
import { BlockInstance, createBlock } from '@wordpress/blocks';
import { Button, PanelBody } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { createInterpolateElement, useMemo } from '@wordpress/element';
import { useEntityRecord } from '@wordpress/core-data';
import { store as blockEditorStore } from '@wordpress/block-editor';
// @ts-expect-error: @wordpress/plugin is typed in the newer versions
// eslint-disable-next-line @woocommerce/dependency-group
import {
registerPlugin,
unregisterPlugin,
getPlugin,
} from '@wordpress/plugins';
/**
* Internal dependencies
*/
import './style.scss';
const hasLegacyTemplateBlock = ( blocks: Array< BlockInstance > ): boolean => {
return blocks.some( ( block ) => {
return (
block.name === 'woocommerce/legacy-template' ||
hasLegacyTemplateBlock( block.innerBlocks )
);
} );
};
const pickBlockClientIds = ( blocks: Array< BlockInstance > ) =>
blocks.reduce< Array< string > >( ( acc, block ) => {
if ( block.name === 'core/template-part' ) {
return acc;
}
return [ ...acc, block.clientId ];
}, [] );
const RevertClassicTemplateButton = () => {
const { blocks, editedPostId } = useSelect( ( sel ) => {
return {
blocks: sel( blockEditorStore ).getBlocks(),
editedPostId: sel( 'core/edit-site' ).getEditedPostId(),
};
}, [] );
const { replaceBlocks } = useDispatch( blockEditorStore );
const template = useEntityRecord< {
slug: string;
title: {
rendered?: string;
row: string;
};
} >( 'postType', 'wp_template', editedPostId );
const isLegacyTemplateBlockAdded = useMemo(
() => hasLegacyTemplateBlock( blocks ),
[ blocks ]
);
const clientIds = useMemo( () => pickBlockClientIds( blocks ), [ blocks ] );
return (
<>
{ ! isLegacyTemplateBlockAdded && (
<PluginTemplateSettingPanel>
<PanelBody className="wc-block-editor-revert-button-container">
<Button
variant="secondary"
onClick={ () => {
replaceBlocks(
clientIds,
createBlock(
'core/group',
{
layout: {
inherit: true,
type: 'constrained',
},
},
[
createBlock(
'woocommerce/legacy-template',
{
template:
template?.record?.slug,
}
),
]
)
);
} }
>
{ __(
'Revert to Classic Product Template',
'woo-gutenberg-products-block'
) }
</Button>
<span>
{ createInterpolateElement(
__(
`The <strongText /> template doesnt allow for reordering or customizing blocks, but might work better with your extensions`,
'woo-gutenberg-products-block'
),
{
strongText: (
<strong>
{ template?.record?.title
?.rendered ?? '' }
</strong>
),
}
) }
</span>
</PanelBody>
</PluginTemplateSettingPanel>
) }
</>
);
};
const templateSlugs = [
'single-product',
'archive-product',
'product-search-results',
'taxonomy-product_cat',
'taxonomy-product_tag',
'taxonomy-product_attribute',
];
const REVERT_BUTTON_PLUGIN_NAME = 'woocommerce-blocks-revert-button-templates';
let currentTemplateId: string | undefined;
subscribe( () => {
const previousTemplateId = currentTemplateId;
const store = select( 'core/edit-site' );
currentTemplateId = store.getEditedPostId();
if ( previousTemplateId === currentTemplateId ) {
return;
}
const isWooTemplate = templateSlugs.some( ( slug ) =>
currentTemplateId?.includes( slug )
);
const hasSupportForPluginTemplateSettingPanel =
PluginTemplateSettingPanel !== undefined;
if ( isWooTemplate && hasSupportForPluginTemplateSettingPanel ) {
if ( getPlugin( REVERT_BUTTON_PLUGIN_NAME ) ) {
return;
}
return registerPlugin( REVERT_BUTTON_PLUGIN_NAME, {
render: RevertClassicTemplateButton,
} );
}
if ( getPlugin( REVERT_BUTTON_PLUGIN_NAME ) === undefined ) {
return;
}
unregisterPlugin( REVERT_BUTTON_PLUGIN_NAME );
}, 'core/edit-site' );

View File

@ -0,0 +1,8 @@
.wc-block-editor-revert-button-container {
display: flex;
flex-direction: column;
gap: $gap;
span {
color: $gray-700;
}
}

View File

@ -89,6 +89,10 @@ const getAlias = ( options = {} ) => {
),
'@woocommerce/types': path.resolve( __dirname, `../assets/js/types/` ),
'@woocommerce/utils': path.resolve( __dirname, `../assets/js/utils/` ),
'@woocommerce/templates': path.resolve(
__dirname,
`../assets/js/templates/`
),
};
};

View File

@ -65,7 +65,7 @@
"@woocommerce/e2e-utils": [ "tests/e2e-pw/utils" ],
"@woocommerce/e2e-types": [ "tests/e2e-pw/types" ],
"@woocommerce/e2e-playwright-utils": [ "tests/e2e-pw/playwright-utils" ],
"@woocommerce/templates/*": [ "assets/js/templates/*" ],
}
}
}