From ab7a429db76c3484da91012714648f592b09fae0 Mon Sep 17 00:00:00 2001 From: Jorge Date: Tue, 10 Apr 2018 19:35:02 +0100 Subject: [PATCH] Add possibility to manage innerBlocks while migrating deprecated blocks. (#5932) Before the migrate function received the existing attributes and returned the new attributes now. Now migrate function can also receive the innerBlocks and add or remove them. This allows, for example, to migrate existing cover images to the nested version. --- blocks/api/parser.js | 34 +++++++++----- blocks/api/test/parser.js | 86 ++++++++++++++++++++++++++++++++---- docs/deprecated-blocks.md | 93 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 19 deletions(-) diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 4cfb87094ce109..99d13fbb83e8fb 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -2,7 +2,7 @@ * External dependencies */ import { parse as hpqParse } from 'hpq'; -import { mapValues, omit } from 'lodash'; +import { castArray, mapValues, omit } from 'lodash'; /** * WordPress dependencies @@ -155,13 +155,14 @@ export function getBlockAttributes( blockType, innerHTML, attributes ) { * Attempt to parse the innerHTML using using a supplied `deprecated` * definition. * - * @param {?Object} blockType Block type. - * @param {string} innerHTML Raw block content. - * @param {?Object} attributes Known block attributes (from delimiters). + * @param {?Object} blockType Block type. + * @param {string} innerHTML Raw block content. + * @param {?Object} attributes Known block attributes (from delimiters). + * @param {?Array} innerBlocks Array of innerBlocks. * * @return {Object} Block attributes. */ -export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attributes ) { +export function getAttributesAndInnerBlocksFromDeprecatedVersion( blockType, innerHTML, attributes, innerBlocks ) { // Not all blocks need a deprecated definition so avoid unnecessary computational cycles // as early as possible when `deprecated` property is not supplied. if ( ! blockType.deprecated || ! blockType.deprecated.length ) { @@ -181,12 +182,22 @@ export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attrib try { // Handle migration of older attributes into current version if necessary. const deprecatedBlockAttributes = getBlockAttributes( deprecatedBlockType, innerHTML, attributes ); - const migratedBlockAttributes = deprecatedBlockType.migrate ? deprecatedBlockType.migrate( deprecatedBlockAttributes ) : deprecatedBlockAttributes; // Attempt to validate the parsed block. Ignore if the the validation step fails. const isValid = isValidBlock( innerHTML, deprecatedBlockType, deprecatedBlockAttributes ); if ( isValid ) { - return migratedBlockAttributes; + const migratedBlockAttributesAndInnerBlocks = deprecatedBlockType.migrate && + deprecatedBlockType.migrate( deprecatedBlockAttributes, innerBlocks ); + + if ( migratedBlockAttributesAndInnerBlocks ) { + const [ + migratedAttributes, + migratedInnerBlocks = innerBlocks, + ] = castArray( migratedBlockAttributesAndInnerBlocks ); + return { attributes: migratedAttributes, innerBlocks: migratedInnerBlocks }; + } + + return { attributes: deprecatedBlockAttributes, innerBlocks }; } } catch ( error ) { // Ignore error, it means this deprecated version is invalid. @@ -275,13 +286,14 @@ export function createBlockWithFallback( blockNode ) { // This enables blocks to modify its attributes and markup structure without // invalidating content written in previous formats. if ( ! block.isValid ) { - const attributesParsedWithDeprecatedVersion = getAttributesFromDeprecatedVersion( - blockType, innerHTML, attributes + const attributesAndInnerBlocksParsedWithDeprecatedVersion = getAttributesAndInnerBlocksFromDeprecatedVersion( + blockType, innerHTML, attributes, block.innerBlocks ); - if ( attributesParsedWithDeprecatedVersion ) { + if ( attributesAndInnerBlocksParsedWithDeprecatedVersion ) { block.isValid = true; - block.attributes = attributesParsedWithDeprecatedVersion; + block.attributes = attributesAndInnerBlocksParsedWithDeprecatedVersion.attributes; + block.innerBlocks = attributesAndInnerBlocksParsedWithDeprecatedVersion.innerBlocks || []; } } diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index ac304aa1a12a72..36208909138aa5 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -6,7 +6,7 @@ import { getBlockAttributes, asType, createBlockWithFallback, - getAttributesFromDeprecatedVersion, + getAttributesAndInnerBlocksFromDeprecatedVersion, default as parse, parseWithAttributeSchema, } from '../parser'; @@ -185,18 +185,18 @@ describe( 'block parser', () => { } ); } ); - describe( 'getAttributesFromDeprecatedVersion', () => { + describe( 'getAttributesAndInnerBlocksFromDeprecatedVersion', () => { it( 'should return undefined if the block has no deprecated versions', () => { - const attributes = getAttributesFromDeprecatedVersion( + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( defaultBlockSettings, 'Bananas', {}, ); - expect( attributes ).toBeUndefined(); + expect( attributesAndInnerBlocks ).toBeUndefined(); } ); it( 'should return undefined if no valid deprecated version found', () => { - const attributes = getAttributesFromDeprecatedVersion( + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( { name: 'core/test-block', ...defaultBlockSettings, @@ -211,13 +211,13 @@ describe( 'block parser', () => { 'Bananas', {}, ); - expect( attributes ).toBeUndefined(); + expect( attributesAndInnerBlocks ).toBeUndefined(); expect( console ).toHaveErrored(); expect( console ).toHaveWarned(); } ); it( 'should return the attributes parsed by the deprecated version', () => { - const attributes = getAttributesFromDeprecatedVersion( + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( { name: 'core/test-block', ...defaultBlockSettings, @@ -238,7 +238,77 @@ describe( 'block parser', () => { 'Bananas', {}, ); - expect( attributes ).toEqual( { fruit: 'Bananas' } ); + expect( attributesAndInnerBlocks.attributes ).toEqual( { fruit: 'Bananas' } ); + } ); + + it( 'should return the innerBlocks if they are passed', () => { + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( + { + name: 'core/test-block', + ...defaultBlockSettings, + save: ( props ) =>
{ props.attributes.fruit }
, + deprecated: [ + { + attributes: { + fruit: { + type: 'string', + source: 'text', + selector: 'span', + }, + }, + save: ( props ) => { props.attributes.fruit }, + }, + ], + }, + 'Bananas', + {}, + [ { + name: 'core/test-block', + attributes: { aaa: 'bbb' }, + } ] + ); + + expect( attributesAndInnerBlocks.innerBlocks ).toHaveLength( 1 ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].name ).toEqual( 'core/test-block' ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].attributes ).toEqual( { aaa: 'bbb' } ); + } ); + + it( 'should be able to migrate attributes and innerBlocks', () => { + const attributesAndInnerBlocks = getAttributesAndInnerBlocksFromDeprecatedVersion( + { + name: 'core/test-block', + ...defaultBlockSettings, + save: ( props ) =>
{ props.attributes.fruit }
, + deprecated: [ + { + attributes: { + fruit: { + type: 'string', + source: 'text', + selector: 'span', + }, + }, + save: ( props ) => { props.attributes.fruit }, + migrate: ( attributes ) => { + return [ + { newFruit: attributes.fruit }, + [ { + name: 'core/test-block', + attributes: { aaa: 'bbb' }, + } ], + ]; + }, + }, + ], + }, + 'Bananas', + {}, + ); + + expect( attributesAndInnerBlocks.attributes ).toEqual( { newFruit: 'Bananas' } ); + expect( attributesAndInnerBlocks.innerBlocks ).toHaveLength( 1 ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].name ).toEqual( 'core/test-block' ); + expect( attributesAndInnerBlocks.innerBlocks[ 0 ].attributes ).toEqual( { aaa: 'bbb' } ); } ); } ); diff --git a/docs/deprecated-blocks.md b/docs/deprecated-blocks.md index 345c398a734b56..3b952088e717df 100644 --- a/docs/deprecated-blocks.md +++ b/docs/deprecated-blocks.md @@ -173,3 +173,96 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { {% end %} In the example above we updated the markup of the block to use a `div` instead of `p` and rename the `text` attribute to `content`. + + +## Changing the innerBlocks + +Situations may exist where when migrating the block we may need to add or remove innerBlocks. +E.g: a block wants to migrate a title attribute to a paragraph innerBlock. + +### Example: + +{% codetabs %} +{% ES5 %} +```js +var el = wp.element.createElement, + registerBlockType = wp.blocks.registerBlockType; + +registerBlockType( 'gutenberg/block-with-deprecated-version', { + + // ... block properties go here + + deprecated: [ + { + attributes: { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + migrate: function( attributes, innerBlocks ) { + return [ + omit( attributes, 'title' ), + [ + createBlock( 'core/paragraph', { + content: attributes.title, + fontSize: 'large', + } ), + ].concat( innerBlocks ), + ]; + }, + + save: function( props ) { + return el( 'p', {}, props.attributes.title ); + }, + } + ] +} ); +``` +{% ESNext %} +```js +const { registerBlockType } = wp.blocks; + +registerBlockType( 'gutenberg/block-with-deprecated-version', { + + // ... block properties go here + + save( props ) { + return

{ props.attributes.title }; + }, + + deprecated: [ + { + attributes: { + title: { + type: 'array', + source: 'children', + selector: 'p', + }, + }, + + migrate( attributes, innerBlocks ) { + return [ + omit( attributes, 'title' ), + [ + createBlock( 'core/paragraph', { + content: attributes.title, + fontSize: 'large', + } ), + ...innerBlocks, + ], + ]; + }, + + save( props ) { + return

{ props.attributes.title }; + }, + } + ] +} ); +``` +{% end %} + +In the example above we updated the block to use an inner paragraph block with a title instead of a title attribute.