diff --git a/packages/block-library/src/buttons/transforms.js b/packages/block-library/src/buttons/transforms.js index 3e89b4973e372..9848299f3a99f 100644 --- a/packages/block-library/src/buttons/transforms.js +++ b/packages/block-library/src/buttons/transforms.js @@ -4,6 +4,11 @@ import { createBlock } from '@wordpress/blocks'; import { __unstableCreateElement as createElement } from '@wordpress/rich-text'; +/** + * Internal dependencies + */ +import { getTransformedMetadata } from '../utils/get-transformed-metadata'; + const transforms = { from: [ { @@ -33,10 +38,8 @@ const transforms = { {}, // Loop the selected buttons. buttons.map( ( attributes ) => { - const element = createElement( - document, - attributes.content - ); + const { content, metadata } = attributes; + const element = createElement( document, content ); // Remove any HTML tags. const text = element.innerText || ''; // Get first url. @@ -46,6 +49,13 @@ const transforms = { return createBlock( 'core/button', { text, url, + metadata: getTransformedMetadata( + metadata, + 'core/button', + ( { content: contentBinding } ) => ( { + text: contentBinding, + } ) + ), } ); } ) ), diff --git a/packages/block-library/src/code/transforms.js b/packages/block-library/src/code/transforms.js index af6d4686af812..e537db342b8d5 100644 --- a/packages/block-library/src/code/transforms.js +++ b/packages/block-library/src/code/transforms.js @@ -4,6 +4,11 @@ import { createBlock } from '@wordpress/blocks'; import { create, toHTMLString } from '@wordpress/rich-text'; +/** + * Internal dependencies + */ +import { getTransformedMetadata } from '../utils/get-transformed-metadata'; + const transforms = { from: [ { @@ -14,17 +19,21 @@ const transforms = { { type: 'block', blocks: [ 'core/paragraph' ], - transform: ( { content } ) => - createBlock( 'core/code', { content } ), + transform: ( { content, metadata } ) => + createBlock( 'core/code', { + content, + metadata: getTransformedMetadata( metadata, 'core/code' ), + } ), }, { type: 'block', blocks: [ 'core/html' ], - transform: ( { content: text } ) => { + transform: ( { content: text, metadata } ) => { return createBlock( 'core/code', { // The HTML is plain text (with plain line breaks), so // convert it to rich text. content: toHTMLString( { value: create( { text } ) } ), + metadata: getTransformedMetadata( metadata, 'core/code' ), } ); }, }, @@ -51,8 +60,14 @@ const transforms = { { type: 'block', blocks: [ 'core/paragraph' ], - transform: ( { content } ) => - createBlock( 'core/paragraph', { content } ), + transform: ( { content, metadata } ) => + createBlock( 'core/paragraph', { + content, + metadata: getTransformedMetadata( + metadata, + 'core/paragraph' + ), + } ), }, ], }; diff --git a/packages/block-library/src/heading/transforms.js b/packages/block-library/src/heading/transforms.js index a4db788462096..f040ff06e37e8 100644 --- a/packages/block-library/src/heading/transforms.js +++ b/packages/block-library/src/heading/transforms.js @@ -7,6 +7,7 @@ import { createBlock, getBlockAttributes } from '@wordpress/blocks'; * Internal dependencies */ import { getLevelFromHeadingNodeName } from './shared'; +import { getTransformedMetadata } from '../utils/get-transformed-metadata'; const transforms = { from: [ @@ -15,12 +16,20 @@ const transforms = { isMultiBlock: true, blocks: [ 'core/paragraph' ], transform: ( attributes ) => - attributes.map( ( { content, anchor, align: textAlign } ) => - createBlock( 'core/heading', { - content, - anchor, - textAlign, - } ) + attributes.map( + ( { content, anchor, align: textAlign, metadata } ) => + createBlock( 'core/heading', { + content, + anchor, + textAlign, + metadata: getTransformedMetadata( + metadata, + 'core/heading', + ( { content: contentBinding } ) => ( { + content: contentBinding, + } ) + ), + } ) ), }, { @@ -82,8 +91,18 @@ const transforms = { isMultiBlock: true, blocks: [ 'core/paragraph' ], transform: ( attributes ) => - attributes.map( ( { content, textAlign: align } ) => - createBlock( 'core/paragraph', { content, align } ) + attributes.map( ( { content, textAlign: align, metadata } ) => + createBlock( 'core/paragraph', { + content, + align, + metadata: getTransformedMetadata( + metadata, + 'core/paragraph', + ( { content: contentBinding } ) => ( { + content: contentBinding, + } ) + ), + } ) ), }, ], diff --git a/packages/block-library/src/utils/get-transformed-metadata.js b/packages/block-library/src/utils/get-transformed-metadata.js new file mode 100644 index 0000000000000..53d79d3c1e42a --- /dev/null +++ b/packages/block-library/src/utils/get-transformed-metadata.js @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { getBlockType } from '@wordpress/blocks'; + +/** + * Transform the metadata attribute with only the values and bindings specified by each transform. + * Returns `undefined` if the input metadata is falsy. + * + * @param {Object} metadata Original metadata attribute from the block that is being transformed. + * @param {Object} newBlockName Name of the final block after the transformation. + * @param {Function} bindingsCallback Optional callback to transform the `bindings` property object. + * @return {Object|undefined} New metadata object only with the relevant properties. + */ +export function getTransformedMetadata( + metadata, + newBlockName, + bindingsCallback +) { + if ( ! metadata ) { + return; + } + const { supports } = getBlockType( newBlockName ); + // Fixed until an opt-in mechanism is implemented. + const BLOCK_BINDINGS_SUPPORTED_BLOCKS = [ + 'core/paragraph', + 'core/heading', + 'core/image', + 'core/button', + ]; + // The metadata properties that should be preserved after the transform. + const transformSupportedProps = []; + // If it support bindings, and there is a transform bindings callback, add the `id` and `bindings` properties. + if ( + BLOCK_BINDINGS_SUPPORTED_BLOCKS.includes( newBlockName ) && + bindingsCallback + ) { + transformSupportedProps.push( 'id', 'bindings' ); + } + // If it support block naming (true by default), add the `name` property. + if ( supports.renaming !== false ) { + transformSupportedProps.push( 'name' ); + } + + // Return early if no supported properties. + if ( ! transformSupportedProps.length ) { + return; + } + + const newMetadata = Object.entries( metadata ).reduce( + ( obj, [ prop, value ] ) => { + // If prop is not supported, don't add it to the new metadata object. + if ( ! transformSupportedProps.includes( prop ) ) { + return obj; + } + obj[ prop ] = + prop === 'bindings' ? bindingsCallback( value ) : value; + return obj; + }, + {} + ); + + // Return undefined if object is empty. + return Object.keys( newMetadata ).length ? newMetadata : undefined; +} diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js index dcddfca2b5b28..f62732470d974 100644 --- a/test/e2e/specs/editor/blocks/buttons.spec.js +++ b/test/e2e/specs/editor/blocks/buttons.spec.js @@ -405,4 +405,80 @@ test.describe( 'Buttons', () => { ` ); } ); + + test.describe( 'Block transforms', () => { + test.describe( 'FROM paragraph', () => { + test( 'should preserve the content', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/buttons' ); + const buttonBlock = ( await editor.getBlocks() )[ 0 ] + .innerBlocks[ 0 ]; + expect( buttonBlock.name ).toBe( 'core/button' ); + expect( buttonBlock.attributes.text ).toBe( 'initial content' ); + } ); + + test( 'should preserve the metadata attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + metadata: { + name: 'Custom name', + }, + }, + } ); + + await editor.transformBlockTo( 'core/buttons' ); + const buttonBlock = ( await editor.getBlocks() )[ 0 ] + .innerBlocks[ 0 ]; + expect( buttonBlock.name ).toBe( 'core/button' ); + expect( buttonBlock.attributes.metadata ).toMatchObject( { + name: 'Custom name', + } ); + } ); + + test( 'should preserve the block bindings', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'custom_field', + }, + }, + }, + }, + }, + } ); + + await editor.transformBlockTo( 'core/buttons' ); + const buttonBlock = ( await editor.getBlocks() )[ 0 ] + .innerBlocks[ 0 ]; + expect( buttonBlock.name ).toBe( 'core/button' ); + expect( + buttonBlock.attributes.metadata.bindings + ).toMatchObject( { + text: { + source: 'core/post-meta', + args: { + key: 'custom_field', + }, + }, + } ); + } ); + } ); + } ); } ); diff --git a/test/e2e/specs/editor/blocks/code.spec.js b/test/e2e/specs/editor/blocks/code.spec.js index 6abfb15d10b83..ba5af46f69cfd 100644 --- a/test/e2e/specs/editor/blocks/code.spec.js +++ b/test/e2e/specs/editor/blocks/code.spec.js @@ -46,4 +46,120 @@ test.describe( 'Code', () => { expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); + + test.describe( 'Block transforms', () => { + test.describe( 'FROM paragraph', () => { + test( 'should preserve the content', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/code' ); + const codeBlock = ( await editor.getBlocks() )[ 0 ]; + expect( codeBlock.name ).toBe( 'core/code' ); + expect( codeBlock.attributes.content ).toBe( + 'initial content' + ); + } ); + + test( 'should preserve the metadata name attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + metadata: { + name: 'Custom name', + }, + }, + } ); + + await editor.transformBlockTo( 'core/code' ); + const codeBlock = ( await editor.getBlocks() )[ 0 ]; + expect( codeBlock.name ).toBe( 'core/code' ); + expect( codeBlock.attributes.metadata ).toMatchObject( { + name: 'Custom name', + } ); + } ); + } ); + + test.describe( 'FROM HTML', () => { + test( 'should preserve the content', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/html', + attributes: { + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/code' ); + const codeBlock = ( await editor.getBlocks() )[ 0 ]; + expect( codeBlock.name ).toBe( 'core/code' ); + expect( codeBlock.attributes.content ).toBe( + 'initial content' + ); + } ); + + test( 'should preserve the metadata name attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/html', + attributes: { + content: 'initial content', + metadata: { + name: 'Custom name', + }, + }, + } ); + + await editor.transformBlockTo( 'core/code' ); + const codeBlock = ( await editor.getBlocks() )[ 0 ]; + expect( codeBlock.name ).toBe( 'core/code' ); + expect( codeBlock.attributes.metadata ).toMatchObject( { + name: 'Custom name', + } ); + } ); + } ); + + test.describe( 'TO paragraph', () => { + test( 'should preserve the content', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/code', + attributes: { + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/paragraph' ); + const codeBlock = ( await editor.getBlocks() )[ 0 ]; + expect( codeBlock.name ).toBe( 'core/paragraph' ); + expect( codeBlock.attributes.content ).toBe( + 'initial content' + ); + } ); + + test( 'should preserve the metadata name attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/code', + attributes: { + content: 'initial content', + metadata: { + name: 'Custom name', + }, + }, + } ); + + await editor.transformBlockTo( 'core/paragraph' ); + const codeBlock = ( await editor.getBlocks() )[ 0 ]; + expect( codeBlock.name ).toBe( 'core/paragraph' ); + expect( codeBlock.attributes.metadata ).toMatchObject( { + name: 'Custom name', + } ); + } ); + } ); + } ); } ); diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js index 705bce2c3f2c9..f0271a8f6e897 100644 --- a/test/e2e/specs/editor/blocks/heading.spec.js +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -291,4 +291,184 @@ test.describe( 'Heading', () => { }, ] ); } ); + + test.describe( 'Block transforms', () => { + test.describe( 'FROM paragraph', () => { + test( 'should preserve the content', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/heading' ); + const headingBlock = ( await editor.getBlocks() )[ 0 ]; + expect( headingBlock.name ).toBe( 'core/heading' ); + expect( headingBlock.attributes.content ).toBe( + 'initial content' + ); + } ); + + test( 'should preserve the text align attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + align: 'right', + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/heading' ); + const headingBlock = ( await editor.getBlocks() )[ 0 ]; + expect( headingBlock.name ).toBe( 'core/heading' ); + expect( headingBlock.attributes.textAlign ).toBe( 'right' ); + } ); + + test( 'should preserve the metadata attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + metadata: { + name: 'Custom name', + }, + }, + } ); + + await editor.transformBlockTo( 'core/heading' ); + const headingBlock = ( await editor.getBlocks() )[ 0 ]; + expect( headingBlock.name ).toBe( 'core/heading' ); + expect( headingBlock.attributes.metadata ).toMatchObject( { + name: 'Custom name', + } ); + } ); + + test( 'should preserve the block bindings', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'initial content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'custom_field', + }, + }, + }, + }, + }, + } ); + + await editor.transformBlockTo( 'core/heading' ); + const headingBlock = ( await editor.getBlocks() )[ 0 ]; + expect( headingBlock.name ).toBe( 'core/heading' ); + expect( + headingBlock.attributes.metadata.bindings + ).toMatchObject( { + content: { + source: 'core/post-meta', + args: { + key: 'custom_field', + }, + }, + } ); + } ); + } ); + + test.describe( 'TO paragraph', () => { + test( 'should preserve the content', async ( { editor } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/paragraph' ); + const paragraphBlock = ( await editor.getBlocks() )[ 0 ]; + expect( paragraphBlock.name ).toBe( 'core/paragraph' ); + expect( paragraphBlock.attributes.content ).toBe( + 'initial content' + ); + } ); + + test( 'should preserve the text align attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + textAlign: 'right', + content: 'initial content', + }, + } ); + await editor.transformBlockTo( 'core/paragraph' ); + const paragraphBlock = ( await editor.getBlocks() )[ 0 ]; + expect( paragraphBlock.name ).toBe( 'core/paragraph' ); + expect( paragraphBlock.attributes.align ).toBe( 'right' ); + } ); + + test( 'should preserve the metadata attribute', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'initial content', + metadata: { + name: 'Custom name', + }, + }, + } ); + + await editor.transformBlockTo( 'core/paragraph' ); + const paragraphBlock = ( await editor.getBlocks() )[ 0 ]; + expect( paragraphBlock.name ).toBe( 'core/paragraph' ); + expect( paragraphBlock.attributes.metadata ).toMatchObject( { + name: 'Custom name', + } ); + } ); + + test( 'should preserve the block bindings', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'initial content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'custom_field', + }, + }, + }, + }, + }, + } ); + + await editor.transformBlockTo( 'core/paragraph' ); + const paragraphBlock = ( await editor.getBlocks() )[ 0 ]; + expect( paragraphBlock.name ).toBe( 'core/paragraph' ); + expect( + paragraphBlock.attributes.metadata.bindings + ).toMatchObject( { + content: { + source: 'core/post-meta', + args: { + key: 'custom_field', + }, + }, + } ); + } ); + } ); + } ); } );