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:
parent
98a61d3a4d
commit
9b0c610ced
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './migration-from-products-to-product-collection';
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
Loading…
Reference in New Issue