Automatic migration path from Products to Product Collection - step 1 - automatic migration (https://github.com/woocommerce/woocommerce-blocks/pull/10115)

* Foundation of the Products block replacement with Product Collection

* Provide logic to replace Products with Product Collection

* Make sure the blocks can be replaced

* Add types and refactor replacement a bit

* Fix the query attributes transform

* Add upgrade Notice to the Product Collection block

* Force upgrade notice to be displayed at the top of the Inspector Controls

* Externalise migration code so it can be reused in both ways

* Add util to get block IDs byt its name

* Add a way to revert the Product Collection blocks to Products

* Move the subscription to another place where it's triggered only once

* Add default values

* Remove attributes from Products block if they're not used to avoid incorrect query

* WIP logic of unseen/seen/reverted notice

* Change the state reading

* Revert changes regarding notice displaying logic

* Change the logc of firing replacement and bail early if there's no core/query blocks

* Add todos

* Implement inner blocks migration

* Implement the revert transformation of inner blocks

* Refactor types

* Add layout transformation from Products to Product Collection

* Add layout migration from Product Collection to Products

* Disable migration by default

* Simplify some parts of code

* Remove additional keyword from Product Collection to move it to another PR

* Adjust the logic to introduce the first step: conversion from Products to Product Collection

* Disable automatic migration
This commit is contained in:
Karol Manijak 2023-07-12 10:01:36 +02:00 committed by GitHub
parent 98a61d3a4d
commit 9b0c610ced
5 changed files with 330 additions and 1 deletions

View File

@ -13,6 +13,8 @@ import { VARIATION_NAME as PRODUCT_TITLE_ID } from './variations/elements/produc
import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-template'; import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-template';
import { ImageSizing } from '../../atomic/blocks/product-elements/image/types'; import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';
export const REPLACE_PRODUCTS_WITH_PRODUCT_COLLECTION = false;
export const EDIT_ATTRIBUTES_URL = export const EDIT_ATTRIBUTES_URL =
'/wp-admin/edit.php?post_type=product&page=product_attributes'; '/wp-admin/edit.php?post_type=product&page=product_attributes';

View File

@ -4,11 +4,12 @@
import type { ElementType } from 'react'; import type { ElementType } from 'react';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor'; import { InspectorControls } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data'; import { useSelect, subscribe } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks'; import { addFilter } from '@wordpress/hooks';
import { ProductQueryFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt'; import { ProductQueryFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt';
import { EditorBlock } from '@woocommerce/types'; import { EditorBlock } from '@woocommerce/types';
import { usePrevious } from '@woocommerce/base-hooks'; import { usePrevious } from '@woocommerce/base-hooks';
import { isWpVersion } from '@woocommerce/settings';
import { import {
FormTokenField, FormTokenField,
ToggleControl, ToggleControl,
@ -37,10 +38,12 @@ import {
QUERY_DEFAULT_ATTRIBUTES, QUERY_DEFAULT_ATTRIBUTES,
QUERY_LOOP_ID, QUERY_LOOP_ID,
STOCK_STATUS_OPTIONS, STOCK_STATUS_OPTIONS,
REPLACE_PRODUCTS_WITH_PRODUCT_COLLECTION,
} from './constants'; } from './constants';
import { AttributesFilter } from './inspector-controls/attributes-filter'; import { AttributesFilter } from './inspector-controls/attributes-filter';
import { PopularPresets } from './inspector-controls/popular-presets'; import { PopularPresets } from './inspector-controls/popular-presets';
import { ProductSelector } from './inspector-controls/product-selector'; import { ProductSelector } from './inspector-controls/product-selector';
import { replaceProductsWithProductCollection } from '../shared/scripts';
import './editor.scss'; import './editor.scss';
@ -266,3 +269,20 @@ export const withProductQueryControls =
}; };
addFilter( 'editor.BlockEdit', QUERY_LOOP_ID, withProductQueryControls ); addFilter( 'editor.BlockEdit', QUERY_LOOP_ID, withProductQueryControls );
if ( isWpVersion( '6.1', '>=' ) ) {
let unsubscribe: ( () => void ) | undefined;
if ( REPLACE_PRODUCTS_WITH_PRODUCT_COLLECTION && ! unsubscribe ) {
// console.info( 'Subscribed to allow Products block migration' );
unsubscribe = subscribe( () => {
replaceProductsWithProductCollection( () => {
// console.info(
// 'Unsubscribed and disallow further Products block migration'
// );
if ( unsubscribe ) {
unsubscribe();
}
} );
}, 'core/block-editor' );
}
}

View File

@ -0,0 +1 @@
export * from './migration-from-products-to-product-collection';

View File

@ -0,0 +1,220 @@
/**
* External dependencies
*/
import { createBlock, BlockInstance } from '@wordpress/blocks';
import { select, dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import {
getProductsBlockClientIds,
checkIfBlockCanBeInserted,
postTemplateHasSupportForGridView,
type TransformBlock,
type IsBlockType,
type ProductGridLayout,
type ProductGridLayoutTypes,
type PostTemplateLayout,
type PostTemplateLayoutTypes,
} from './migration-utils';
const mapAttributes = ( attributes: Record< string, unknown > ) => {
const { query, namespace, ...restAttributes } = attributes;
const {
__woocommerceAttributes,
__woocommerceStockStatus,
__woocommerceOnSale,
include,
...restQuery
} = query;
return {
...restAttributes,
query: {
woocommerceAttributes: __woocommerceAttributes,
woocommerceStockStatus: __woocommerceStockStatus,
woocommerceOnSale: __woocommerceOnSale,
woocommerceHandPickedProducts: include,
taxQuery: {},
parents: [],
isProductCollectionBlock: true,
...restQuery,
},
displayUpgradeNotice: true,
};
};
const isPostTemplate: IsBlockType = ( { name, attributes } ) =>
name === 'core/post-template' &&
attributes.__woocommerceNamespace ===
'woocommerce/product-query/product-template';
const isPostTitle: IsBlockType = ( { name, attributes } ) =>
name === 'core/post-title' &&
attributes.__woocommerceNamespace ===
'woocommerce/product-query/product-title';
const isPostSummary: IsBlockType = ( { name, attributes } ) =>
name === 'core/post-excerpt' &&
attributes.__woocommerceNamespace ===
'woocommerce/product-query/product-summary';
const transformPostTemplate: TransformBlock = ( block, innerBlocks ) => {
const { __woocommerceNamespace, className, layout, ...restAttrributes } =
block.attributes;
return createBlock(
'woocommerce/product-template',
restAttrributes,
innerBlocks
);
};
const transformPostTitle: TransformBlock = ( block, innerBlocks ) => {
const { __woocommerceNamespace, ...restAttrributes } = block.attributes;
return createBlock(
'core/post-title',
{
__woocommerceNamespace:
'woocommerce/product-collection/product-title',
...restAttrributes,
},
innerBlocks
);
};
const transformPostSummary: TransformBlock = ( block, innerBlocks ) => {
const { __woocommerceNamespace, ...restAttrributes } = block.attributes;
return createBlock(
'core/post-excerpt',
{
__woocommerceNamespace:
'woocommerce/product-collection/product-summary',
...restAttrributes,
},
innerBlocks
);
};
const mapLayoutType = (
type: PostTemplateLayoutTypes
): ProductGridLayoutTypes => {
if ( type === 'grid' ) {
return 'flex';
}
if ( type === 'default' ) {
return 'list';
}
return 'flex';
};
const mapLayoutPropertiesFromPostTemplateToProductCollection = (
layout: PostTemplateLayout
): ProductGridLayout => {
const { type, columnCount } = layout;
return {
type: mapLayoutType( type ),
columns: columnCount,
};
};
const getLayoutAttribute = (
attributes,
innerBlocks: BlockInstance[]
): ProductGridLayout => {
// Starting from GB 16, it's not Query Loop that keeps the layout, but the Post Template block.
// We need to account for that and in that case, move the layout properties
// from Post Template to Product Collection.
const postTemplate = innerBlocks.find( isPostTemplate );
const { layout: postTemplateLayout } = postTemplate?.attributes || {};
return postTemplateHasSupportForGridView
? mapLayoutPropertiesFromPostTemplateToProductCollection(
postTemplateLayout
)
: attributes.displayLayout;
};
const mapInnerBlocks = ( innerBlocks: BlockInstance[] ): BlockInstance[] => {
const mappedInnerBlocks = innerBlocks.map( ( innerBlock ) => {
const { name, attributes } = innerBlock;
const mappedInnerInnerBlocks = mapInnerBlocks( innerBlock.innerBlocks );
if ( isPostTemplate( innerBlock ) ) {
return transformPostTemplate( innerBlock, mappedInnerInnerBlocks );
}
if ( isPostTitle( innerBlock ) ) {
return transformPostTitle( innerBlock, mappedInnerInnerBlocks );
}
if ( isPostSummary( innerBlock ) ) {
return transformPostSummary( innerBlock, mappedInnerInnerBlocks );
}
return createBlock( name, attributes, mappedInnerInnerBlocks );
} );
return mappedInnerBlocks;
};
const replaceProductsBlock = ( clientId: string ) => {
const productsBlock = select( 'core/block-editor' ).getBlock( clientId );
const canBeInserted = checkIfBlockCanBeInserted(
clientId,
'woocommerce/product-collection'
);
if ( productsBlock && canBeInserted ) {
const { attributes = {}, innerBlocks = [] } = productsBlock;
const displayLayout = getLayoutAttribute( attributes, innerBlocks );
const adjustedAttributes = mapAttributes( {
...attributes,
displayLayout,
} );
const adjustedInnerBlocks = mapInnerBlocks( innerBlocks );
const productCollectionBlock = createBlock(
'woocommerce/product-collection',
adjustedAttributes,
adjustedInnerBlocks
);
dispatch( 'core/block-editor' ).replaceBlock(
clientId,
productCollectionBlock
);
return true;
}
return false;
};
const replaceProductsBlocks = ( productsBlockClientIds: string[] ) => {
const results = productsBlockClientIds.map( replaceProductsBlock );
return !! results.length && results.every( ( result ) => !! result );
};
export const replaceProductsWithProductCollection = (
unsubscribe: () => void
) => {
const queryBlocksCount =
select( 'core/block-editor' ).getGlobalBlockCount( 'core/query' );
if ( queryBlocksCount === 0 ) {
return;
}
const blocks = select( 'core/block-editor' ).getBlocks();
const productsBlockClientIds = getProductsBlockClientIds( blocks );
const productsBlocksCount = productsBlockClientIds.length;
if ( productsBlocksCount === 0 ) {
return;
}
const replaced = replaceProductsBlocks( productsBlockClientIds );
if ( replaced ) {
// @todo: unsubscribe on user reverting migration
unsubscribe();
}
};

View File

@ -0,0 +1,86 @@
/**
* External dependencies
*/
import { getSettingWithCoercion } from '@woocommerce/settings';
import { type BlockInstance } from '@wordpress/blocks';
import { select } from '@wordpress/data';
import { isBoolean } from '@woocommerce/types';
type GetBlocksClientIds = ( blocks: BlockInstance[] ) => string[];
export type IsBlockType = ( block: BlockInstance ) => boolean;
export type TransformBlock = (
block: BlockInstance,
innerBlock: BlockInstance[]
) => BlockInstance;
export type ProductGridLayoutTypes = 'flex' | 'list';
export type PostTemplateLayoutTypes = 'grid' | 'default';
export type ProductGridLayout = {
type: ProductGridLayoutTypes;
columns: number;
};
export type PostTemplateLayout = {
type: PostTemplateLayoutTypes;
columnCount: number;
};
const isProductsBlock: IsBlockType = ( block ) =>
block.name === 'core/query' &&
block.attributes.namespace === 'woocommerce/product-query';
const isProductCollectionBlock: IsBlockType = ( block ) =>
block.name === 'woocommerce/product-collection';
const getBlockClientIdsByPredicate = (
blocks: BlockInstance[],
predicate: ( block: BlockInstance ) => boolean
): string[] => {
let clientIds: string[] = [];
blocks.forEach( ( block ) => {
if ( predicate( block ) ) {
clientIds = [ ...clientIds, block.clientId ];
}
clientIds = [
...clientIds,
...getBlockClientIdsByPredicate( block.innerBlocks, predicate ),
];
} );
return clientIds;
};
const getProductsBlockClientIds: GetBlocksClientIds = ( blocks ) =>
getBlockClientIdsByPredicate( blocks, isProductsBlock );
const getProductCollectionBlockClientIds: GetBlocksClientIds = ( blocks ) =>
getBlockClientIdsByPredicate( blocks, isProductCollectionBlock );
const checkIfBlockCanBeInserted = (
clientId: string,
blockToBeInserted: string
) => {
// We need to duplicate checks that are happening within replaceBlocks method
// as replacement is initially blocked and there's no information returned
// that would determine if replacement happened or not.
// https://github.com/WordPress/gutenberg/issues/46740
const rootClientId =
select( 'core/block-editor' ).getBlockRootClientId( clientId ) ||
undefined;
return select( 'core/block-editor' ).canInsertBlockType(
blockToBeInserted,
rootClientId
);
};
const postTemplateHasSupportForGridView = getSettingWithCoercion(
'post_template_has_support_for_grid_view',
false,
isBoolean
);
export {
getProductsBlockClientIds,
getProductCollectionBlockClientIds,
checkIfBlockCanBeInserted,
postTemplateHasSupportForGridView,
};