diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 8a2d6c26a7dabf..7986be28bee4bc 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -311,6 +311,40 @@ transforms: { ``` {% end %} +In addition to accepting an array of known block types, the `blocks` option also accepts a "wildcard" (`"*"`). This allows for transformations which apply to _all_ block types (eg: all blocks can transform into `core/group`): + +{% codetabs %} +{% ES5 %} +```js +transforms: { + from: [ + { + type: 'block', + blocks: [ '*' ], // wildcard - match any block + transform: function( attributes, innerBlocks ) { + // transform logic here + }, + }, + ], +}, +``` +{% ESNext %} +```js +transforms: { + from: [ + { + type: 'block', + blocks: [ '*' ], // wildcard - match any block + transform: ( attributes, innerBlocks ) => { + // transform logic here + }, + }, + ], +}, +``` +{% end %} + + A block with innerBlocks can also be transformed from and to another block with innerBlocks. {% codetabs %} diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index 3b3f8032432e8b..c10d03a5c060f0 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -8,13 +8,15 @@ import { castArray, first, last, every } from 'lodash'; */ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; -import { cloneBlock, hasBlockSupport } from '@wordpress/blocks'; +import { cloneBlock, hasBlockSupport, switchToBlockType } from '@wordpress/blocks'; function BlockActions( { onDuplicate, onRemove, onInsertBefore, onInsertAfter, + onGroup, + onUngroup, isLocked, canDuplicate, children, @@ -24,6 +26,8 @@ function BlockActions( { onRemove, onInsertAfter, onInsertBefore, + onGroup, + onUngroup, isLocked, canDuplicate, } ); @@ -65,6 +69,7 @@ export default compose( [ multiSelect, removeBlocks, insertDefaultBlock, + replaceBlocks, } = dispatch( 'core/block-editor' ); return { @@ -107,6 +112,39 @@ export default compose( [ insertDefaultBlock( {}, rootClientId, lastSelectedIndex + 1 ); } }, + onGroup() { + if ( ! blocks.length ) { + return; + } + + // Activate the `transform` on `core/group` which does the conversion + const newBlocks = switchToBlockType( blocks, 'core/group' ); + + if ( ! newBlocks ) { + return; + } + replaceBlocks( + clientIds, + newBlocks + ); + }, + + onUngroup() { + if ( ! blocks.length ) { + return; + } + + const innerBlocks = blocks[ 0 ].innerBlocks; + + if ( ! innerBlocks.length ) { + return; + } + + replaceBlocks( + clientIds, + innerBlocks + ); + }, }; } ), ] )( BlockActions ); diff --git a/packages/block-library/src/group/index.js b/packages/block-library/src/group/index.js index 9bab779f5863b5..386ab20a80955c 100644 --- a/packages/block-library/src/group/index.js +++ b/packages/block-library/src/group/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -25,6 +26,45 @@ export const settings = { anchor: true, html: false, }, + + transforms: { + from: [ + { + type: 'block', + isMultiBlock: true, + blocks: [ '*' ], + convert( blocks ) { + // Avoid transforming a single `core/group` Block + if ( blocks.length === 1 && blocks[ 0 ].name === 'core/group' ) { + return; + } + + const alignments = [ 'wide', 'full' ]; + + // Determine the widest setting of all the blocks to be grouped + const widestAlignment = blocks.reduce( ( result, block ) => { + const { align } = block.attributes; + return alignments.indexOf( align ) > alignments.indexOf( result ) ? align : result; + }, undefined ); + + // Clone the Blocks to be Grouped + // Failing to create new block references causes the original blocks + // to be replaced in the switchToBlockType call thereby meaning they + // are removed both from their original location and within the + // new group block. + const groupInnerBlocks = blocks.map( ( block ) => { + return createBlock( block.name, block.attributes, block.innerBlocks ); + } ); + + return createBlock( 'core/group', { + align: widestAlignment, + }, groupInnerBlocks ); + }, + }, + + ], + }, + edit, save, }; diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index b13c58601e1e99..df5b183e688d45 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -3,6 +3,8 @@ ### New Feature - Added a default implementation for `save` setting in `registerBlockType` which saves no markup in the post content. +- Added wildcard block transforms which allows for transforming all/any blocks in another block. +- Added `convert()` method option to `transforms` definition. It receives complete block object(s) as it's argument(s). It is now preferred over the older `transform()` (note that `transform()` is still fully supported). ## 6.1.0 (2019-03-06) diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 9bc199a54452f2..b7a8e5fd70c9dd 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -679,7 +679,7 @@ _Parameters_ _Returns_ -- `Array`: Array of blocks. +- `?Array`: Array of blocks or null. # **synchronizeBlocksWithTemplate** diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index c7bd01f2c699c4..304329d0be657f 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -11,6 +11,7 @@ import { filter, first, flatMap, + has, uniq, isFunction, isEmpty, @@ -117,26 +118,41 @@ const isPossibleTransformForSource = ( transform, direction, blocks ) => { return false; } - // If multiple blocks are selected, only multi block transforms are allowed. + // If multiple blocks are selected, only multi block transforms + // or wildcard transforms are allowed. const isMultiBlock = blocks.length > 1; - const isValidForMultiBlocks = ! isMultiBlock || transform.isMultiBlock; + const firstBlockName = first( blocks ).name; + const isValidForMultiBlocks = isWildcardBlockTransform( transform ) || ! isMultiBlock || transform.isMultiBlock; if ( ! isValidForMultiBlocks ) { return false; } + // Check non-wildcard transforms to ensure that transform is valid + // for a block selection of multiple blocks of different types + if ( ! isWildcardBlockTransform( transform ) && ! every( blocks, { name: firstBlockName } ) ) { + return false; + } + // Only consider 'block' type transforms as valid. const isBlockType = transform.type === 'block'; if ( ! isBlockType ) { return false; } - // Check if the transform's block name matches the source block only if this is a transform 'from'. + // Check if the transform's block name matches the source block (or is a wildcard) + // only if this is a transform 'from'. const sourceBlock = first( blocks ); - const hasMatchingName = direction !== 'from' || transform.blocks.indexOf( sourceBlock.name ) !== -1; + const hasMatchingName = direction !== 'from' || transform.blocks.indexOf( sourceBlock.name ) !== -1 || isWildcardBlockTransform( transform ); if ( ! hasMatchingName ) { return false; } + // Don't allow single 'core/group' blocks to be transformed into + // a 'core/group' block. + if ( ! isMultiBlock && isContainerGroupBlock( sourceBlock.name ) && isContainerGroupBlock( transform.blockName ) ) { + return false; + } + // If the transform has a `isMatch` function specified, check that it returns true. if ( isFunction( transform.isMatch ) ) { const attributes = transform.isMultiBlock ? blocks.map( ( block ) => block.attributes ) : sourceBlock.attributes; @@ -171,7 +187,9 @@ const getBlockTypesForPossibleFromTransforms = ( blocks ) => { return !! findTransform( fromTransforms, - ( transform ) => isPossibleTransformForSource( transform, 'from', blocks ) + ( transform ) => { + return isPossibleTransformForSource( transform, 'from', blocks ); + } ); }, ); @@ -199,7 +217,9 @@ const getBlockTypesForPossibleToTransforms = ( blocks ) => { // filter all 'to' transforms to find those that are possible. const possibleTransforms = filter( transformsTo, - ( transform ) => isPossibleTransformForSource( transform, 'to', blocks ) + ( transform ) => { + return transform && isPossibleTransformForSource( transform, 'to', blocks ); + } ); // Build a list of block names using the possible 'to' transforms. @@ -212,6 +232,45 @@ const getBlockTypesForPossibleToTransforms = ( blocks ) => { return blockNames.map( ( name ) => getBlockType( name ) ); }; +/** + * Determines whether transform is a "block" type + * and if so whether it is a "wildcard" transform + * ie: targets "any" block type + * + * @param {Object} t the Block transform object + * + * @return {boolean} whether transform is a wildcard transform + */ +export const isWildcardBlockTransform = ( t ) => t && t.type === 'block' && Array.isArray( t.blocks ) && t.blocks.includes( '*' ); + +/** + * Determines whether the given Block is the core Block which + * acts as a container Block for other Blocks as part of the + * Grouping mechanics + * + * @param {string} name the name of the Block to test against + * + * @return {boolean} whether or not the Block is the container Block type + */ +export const isContainerGroupBlock = ( name ) => name === 'core/group'; + +/** + * Determines whether the provided Blocks are of the same type + * (eg: all `core/paragraph`). + * + * @param {Array} blocksArray the Block definitions + * + * @return {boolean} whether or not the given Blocks pass the criteria + */ +export const isBlockSelectionOfSameType = ( blocksArray = [] ) => { + if ( ! blocksArray.length ) { + return false; + } + const sourceName = blocksArray[ 0 ].name; + + return every( blocksArray, [ 'name', sourceName ] ); +}; + /** * Returns an array of block types that the set of blocks received as argument * can be transformed into. @@ -225,12 +284,6 @@ export function getPossibleBlockTransformations( blocks ) { return []; } - const sourceBlock = first( blocks ); - const isMultiBlock = blocks.length > 1; - if ( isMultiBlock && ! every( blocks, { name: sourceBlock.name } ) ) { - return []; - } - const blockTypesForFromTransforms = getBlockTypesForPossibleFromTransforms( blocks ); const blockTypesForToTransforms = getBlockTypesForPossibleToTransforms( blocks ); @@ -313,7 +366,7 @@ export function getBlockTransforms( direction, blockTypeOrName ) { * @param {Array|Object} blocks Blocks array or block object. * @param {string} name Block name. * - * @return {Array} Array of blocks. + * @return {?Array} Array of blocks or null. */ export function switchToBlockType( blocks, name ) { const blocksArray = castArray( blocks ); @@ -321,7 +374,10 @@ export function switchToBlockType( blocks, name ) { const firstBlock = blocksArray[ 0 ]; const sourceName = firstBlock.name; - if ( isMultiBlock && ! every( blocksArray, ( block ) => ( block.name === sourceName ) ) ) { + // Unless it's a `core/group` Block then for multi block selections + // check that all Blocks are of the same type otherwise + // we can't run a conversion + if ( ! isContainerGroupBlock( name ) && isMultiBlock && ! isBlockSelectionOfSameType( blocksArray ) ) { return null; } @@ -329,14 +385,15 @@ export function switchToBlockType( blocks, name ) { // transformation. const transformationsFrom = getBlockTransforms( 'from', name ); const transformationsTo = getBlockTransforms( 'to', sourceName ); + const transformation = findTransform( transformationsTo, - ( t ) => t.type === 'block' && t.blocks.indexOf( name ) !== -1 && ( ! isMultiBlock || t.isMultiBlock ) + ( t ) => t.type === 'block' && ( ( isWildcardBlockTransform( t ) ) || t.blocks.indexOf( name ) !== -1 ) && ( ! isMultiBlock || t.isMultiBlock ) ) || findTransform( transformationsFrom, - ( t ) => t.type === 'block' && t.blocks.indexOf( sourceName ) !== -1 && ( ! isMultiBlock || t.isMultiBlock ) + ( t ) => t.type === 'block' && ( ( isWildcardBlockTransform( t ) ) || t.blocks.indexOf( sourceName ) !== -1 ) && ( ! isMultiBlock || t.isMultiBlock ) ); // Stop if there is no valid transformation. @@ -345,11 +402,18 @@ export function switchToBlockType( blocks, name ) { } let transformationResults; + if ( transformation.isMultiBlock ) { - transformationResults = transformation.transform( - blocksArray.map( ( currentBlock ) => currentBlock.attributes ), - blocksArray.map( ( currentBlock ) => currentBlock.innerBlocks ) - ); + if ( has( transformation, 'convert' ) ) { + transformationResults = transformation.convert( blocksArray ); + } else { + transformationResults = transformation.transform( + blocksArray.map( ( currentBlock ) => currentBlock.attributes ), + blocksArray.map( ( currentBlock ) => currentBlock.innerBlocks ), + ); + } + } else if ( has( transformation, 'convert' ) ) { + transformationResults = transformation.convert( firstBlock ); } else { transformationResults = transformation.transform( firstBlock.attributes, firstBlock.innerBlocks ); } diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index 11249b08d36ced..3e0d42c103bb9a 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -2,7 +2,7 @@ * External dependencies */ import deepFreeze from 'deep-freeze'; -import { noop } from 'lodash'; +import { noop, times } from 'lodash'; /** * Internal dependencies @@ -14,6 +14,9 @@ import { switchToBlockType, getBlockTransforms, findTransform, + isWildcardBlockTransform, + isContainerGroupBlock, + isBlockSelectionOfSameType, } from '../factory'; import { getBlockType, @@ -776,6 +779,81 @@ describe( 'block factory', () => { expect( isMatch ).toHaveBeenCalledWith( [ { value: 'ribs' }, { value: 'halloumi' } ] ); } ); + + describe( 'wildcard block transforms', () => { + beforeEach( () => { + registerBlockType( 'core/group', { + attributes: { + value: { + type: 'string', + }, + }, + transforms: { + from: [ { + type: 'block', + blocks: [ '*' ], + transform: noop, + } ], + }, + save: noop, + category: 'common', + title: 'A block that groups other blocks.', + } ); + } ); + + it( 'should should show wildcard "from" transformation as available for multiple blocks of the same type', () => { + registerBlockType( 'core/text-block', defaultBlockSettings ); + registerBlockType( 'core/image-block', defaultBlockSettings ); + + const textBlocks = times( 4, ( index ) => { + return createBlock( 'core/text-block', { + value: `textBlock${ index + 1 }`, + } ); + } ); + + const availableBlocks = getPossibleBlockTransformations( textBlocks ); + + expect( availableBlocks ).toHaveLength( 1 ); + expect( availableBlocks[ 0 ].name ).toBe( 'core/group' ); + } ); + + it( 'should should show wildcard "from" transformation as available for multiple blocks of different types', () => { + registerBlockType( 'core/text-block', defaultBlockSettings ); + registerBlockType( 'core/image-block', defaultBlockSettings ); + + const textBlocks = times( 2, ( index ) => { + return createBlock( 'core/text-block', { + value: `textBlock${ index + 1 }`, + } ); + } ); + + const imageBlocks = times( 2, ( index ) => { + return createBlock( 'core/image-block', { + value: `imageBlock${ index + 1 }`, + } ); + } ); + + const availableBlocks = getPossibleBlockTransformations( [ ...textBlocks, ...imageBlocks ] ); + + expect( availableBlocks ).toHaveLength( 1 ); + expect( availableBlocks[ 0 ].name ).toBe( 'core/group' ); + } ); + + it( 'should should show wildcard "from" transformation as available for single blocks', () => { + registerBlockType( 'core/text-block', defaultBlockSettings ); + + const blocks = times( 1, ( index ) => { + return createBlock( 'core/text-block', { + value: `textBlock${ index + 1 }`, + } ); + } ); + + const availableBlocks = getPossibleBlockTransformations( blocks ); + + expect( availableBlocks ).toHaveLength( 1 ); + expect( availableBlocks[ 0 ].name ).toBe( 'core/group' ); + } ); + } ); } ); describe( 'switchToBlockType()', () => { @@ -1222,6 +1300,94 @@ describe( 'block factory', () => { expect( transformedBlocks[ 1 ].innerBlocks ).toHaveLength( 1 ); expect( transformedBlocks[ 1 ].innerBlocks[ 0 ].attributes.value ).toBe( 'after1' ); } ); + + it( 'should pass entire block object(s) to the "convert" method if defined', () => { + registerBlockType( 'core/test-group-block', { + attributes: { + value: { + type: 'string', + }, + }, + transforms: { + from: [ { + type: 'block', + blocks: [ '*' ], + isMultiBlock: true, + convert( blocks ) { + const groupInnerBlocks = blocks.map( ( { name, attributes, innerBlocks } ) => { + return createBlock( name, attributes, innerBlocks ); + } ); + + return createBlock( 'core/test-group-block', {}, groupInnerBlocks ); + }, + } ], + }, + save: noop, + category: 'common', + title: 'Test Group Block', + } ); + + registerBlockType( 'core/text-block', defaultBlockSettings ); + + const numOfBlocksToGroup = 4; + const blocks = times( numOfBlocksToGroup, ( index ) => { + return createBlock( 'core/text-block', { + value: `textBlock${ index + 1 }`, + } ); + } ); + + const transformedBlocks = switchToBlockType( blocks, 'core/test-group-block' ); + + expect( transformedBlocks ).toHaveLength( 1 ); + expect( transformedBlocks[ 0 ].name ).toBe( 'core/test-group-block' ); + expect( transformedBlocks[ 0 ].innerBlocks ).toHaveLength( numOfBlocksToGroup ); + } ); + + it( 'should prefer "convert" method over "transform" method when running a transformation', () => { + const convertSpy = jest.fn( ( blocks ) => { + const groupInnerBlocks = blocks.map( ( { name, attributes, innerBlocks } ) => { + return createBlock( name, attributes, innerBlocks ); + } ); + + return createBlock( 'core/test-group-block', {}, groupInnerBlocks ); + } ); + const transformSpy = jest.fn(); + + registerBlockType( 'core/test-group-block', { + attributes: { + value: { + type: 'string', + }, + }, + transforms: { + from: [ { + type: 'block', + blocks: [ '*' ], + isMultiBlock: true, + convert: convertSpy, + transform: transformSpy, + } ], + }, + save: noop, + category: 'common', + title: 'Test Group Block', + } ); + + registerBlockType( 'core/text-block', defaultBlockSettings ); + + const numOfBlocksToGroup = 4; + const blocks = times( numOfBlocksToGroup, ( index ) => { + return createBlock( 'core/text-block', { + value: `textBlock${ index + 1 }`, + } ); + } ); + + const transformedBlocks = switchToBlockType( blocks, 'core/test-group-block' ); + + expect( transformedBlocks ).toHaveLength( 1 ); + expect( convertSpy.mock.calls ).toHaveLength( 1 ); + expect( transformSpy.mock.calls ).toHaveLength( 0 ); + } ); } ); describe( 'getBlockTransforms', () => { @@ -1336,4 +1502,107 @@ describe( 'block factory', () => { expect( transform ).toBe( null ); } ); } ); + + describe( 'isWildcardBlockTransform', () => { + it( 'should return true for transforms with type of block and "*" alias as blocks', () => { + const validWildcardBlockTransform = { + type: 'block', + blocks: [ + 'core/some-other-block-first', // unlikely to happen but... + '*', + ], + blockName: 'core/test-block', + }; + + expect( isWildcardBlockTransform( validWildcardBlockTransform ) ).toBe( true ); + } ); + + it( 'should return false for transforms with a type which is not "block"', () => { + const invalidWildcardBlockTransform = { + type: 'file', + blocks: [ + '*', + ], + blockName: 'core/test-block', + }; + + expect( isWildcardBlockTransform( invalidWildcardBlockTransform ) ).toBe( false ); + } ); + + it( 'should return false for transforms which do not include "*" alias in "block" array', () => { + const invalidWildcardBlockTransform = { + type: 'block', + blocks: [ + 'core/some-block', + 'core/another-block', + ], + blockName: 'core/test-block', + }; + + expect( isWildcardBlockTransform( invalidWildcardBlockTransform ) ).toBe( false ); + } ); + + it( 'should return false for transforms which do not provide an array as the "blocks" option', () => { + const invalidWildcardBlockTransform = { + type: 'block', + blocks: noop, + blockName: 'core/test-block', + }; + + expect( isWildcardBlockTransform( invalidWildcardBlockTransform ) ).toBe( false ); + } ); + } ); + + describe( 'isContainerGroupBlock', () => { + it( 'should return true when passed block name matches "core/group"', () => { + expect( isContainerGroupBlock( 'core/group' ) ).toBe( true ); + } ); + + it( 'should return false when passed block name does not match "core/group"', () => { + expect( isContainerGroupBlock( 'core/some-test-name' ) ).toBe( false ); + } ); + } ); + + describe( 'isBlockSelectionOfSameType', () => { + it( 'should return false when all blocks do not match the name of the first block', () => { + const blocks = [ + { + name: 'core/test-block', + }, + { + name: 'core/test-block', + }, + { + name: 'core/test-block', + }, + { + name: 'core/another-block', + }, + { + name: 'core/test-block', + }, + ]; + + expect( isBlockSelectionOfSameType( blocks ) ).toBe( false ); + } ); + + it( 'should return true when all blocks match the name of the first block', () => { + const blocks = [ + { + name: 'core/test-block', + }, + { + name: 'core/test-block', + }, + { + name: 'core/test-block', + }, + { + name: 'core/test-block', + }, + ]; + + expect( isBlockSelectionOfSameType( blocks ) ).toBe( true ); + } ); + } ); } ); diff --git a/packages/e2e-tests/fixtures/block-transforms.js b/packages/e2e-tests/fixtures/block-transforms.js index 79d4d7bc804566..913953d69d2b3e 100644 --- a/packages/e2e-tests/fixtures/block-transforms.js +++ b/packages/e2e-tests/fixtures/block-transforms.js @@ -1,131 +1,166 @@ export const EXPECTED_TRANSFORMS = { core__archives: { originalBlock: 'Archives', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__archives__showPostCounts: { originalBlock: 'Archives', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__audio: { originalBlock: 'Audio', availableTransforms: [ 'File', + 'Group', ], }, core__button__center: { originalBlock: 'Button', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__calendar: { originalBlock: 'Calendar', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__media-text': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Image', ], }, 'core__media-text__image-alt-no-align': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Image', ], }, 'core__media-text__image-fill-no-focal-point-selected': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Image', ], }, 'core__media-text__image-fill-with-focal-point-selected': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Image', ], }, 'core__media-text__is-stacked-on-mobile': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Video', ], }, 'core__media-text__media-right-custom-width': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Video', ], }, 'core__media-text__vertical-align-bottom': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Image', ], }, 'core__media-text__video': { originalBlock: 'Media & Text', availableTransforms: [ + 'Group', 'Video', ], }, core__categories: { originalBlock: 'Categories', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__code: { originalBlock: 'Code', availableTransforms: [ + 'Group', 'Preformatted', ], }, core__columns: { originalBlock: 'Columns', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__cover: { availableTransforms: [ + 'Group', 'Image', ], originalBlock: 'Cover', }, core__cover__video: { availableTransforms: [ + 'Group', 'Video', ], originalBlock: 'Cover', }, 'core__cover__video-overlay': { availableTransforms: [ + 'Group', 'Video', ], originalBlock: 'Cover', }, core__embed: { originalBlock: 'Embed', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__file__new-window': { originalBlock: 'File', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__file__no-download-button': { originalBlock: 'File', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__file__no-text-link': { originalBlock: 'File', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__gallery: { originalBlock: 'Gallery', availableTransforms: [ + 'Group', 'Image', ], }, core__gallery__columns: { originalBlock: 'Gallery', availableTransforms: [ + 'Group', 'Image', ], }, @@ -137,6 +172,7 @@ export const EXPECTED_TRANSFORMS = { originalBlock: 'Heading', availableTransforms: [ 'Quote', + 'Group', 'Paragraph', ], }, @@ -144,12 +180,15 @@ export const EXPECTED_TRANSFORMS = { originalBlock: 'Heading', availableTransforms: [ 'Quote', + 'Group', 'Paragraph', ], }, core__html: { originalBlock: 'Custom HTML', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__image: { originalBlock: 'Image', @@ -157,6 +196,7 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, @@ -166,6 +206,7 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, @@ -175,6 +216,7 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, @@ -184,6 +226,7 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, @@ -193,6 +236,7 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, @@ -202,6 +246,7 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, @@ -211,43 +256,59 @@ export const EXPECTED_TRANSFORMS = { 'Gallery', 'Cover', 'File', + 'Group', 'Media & Text', ], }, 'core__latest-comments': { originalBlock: 'Latest Comments', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__latest-posts': { originalBlock: 'Latest Posts', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__latest-posts__displayPostDate': { originalBlock: 'Latest Posts', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__legacy-widget': { originalBlock: 'Legacy Widget (Experimental)', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__list__ul: { originalBlock: 'List', availableTransforms: [ + 'Group', 'Paragraph', 'Quote', ], }, core__more: { originalBlock: 'More', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__more__custom-text-teaser': { originalBlock: 'More', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__nextpage: { originalBlock: 'Page Break', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__paragraph__align-right': { originalBlock: 'Paragraph', @@ -255,6 +316,7 @@ export const EXPECTED_TRANSFORMS = { 'Heading', 'List', 'Quote', + 'Group', 'Preformatted', 'Verse', ], @@ -262,6 +324,7 @@ export const EXPECTED_TRANSFORMS = { core__preformatted: { originalBlock: 'Preformatted', availableTransforms: [ + 'Group', 'Paragraph', ], }, @@ -269,18 +332,21 @@ export const EXPECTED_TRANSFORMS = { originalBlock: 'Pullquote', availableTransforms: [ 'Quote', + 'Group', ], }, 'core__pullquote__multi-paragraph': { originalBlock: 'Pullquote', availableTransforms: [ 'Quote', + 'Group', ], }, 'core__quote__style-1': { originalBlock: 'Quote', availableTransforms: [ 'List', + 'Group', 'Paragraph', 'Heading', 'Pullquote', @@ -290,6 +356,7 @@ export const EXPECTED_TRANSFORMS = { originalBlock: 'Quote', availableTransforms: [ 'List', + 'Group', 'Paragraph', 'Heading', 'Pullquote', @@ -297,44 +364,62 @@ export const EXPECTED_TRANSFORMS = { }, core__rss: { originalBlock: 'RSS', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__search: { originalBlock: 'Search', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__search__custom-text': { originalBlock: 'Search', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__separator: { originalBlock: 'Separator', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__shortcode: { originalBlock: 'Shortcode', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__spacer: { originalBlock: 'Spacer', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, core__table: { originalBlock: 'Table', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__tag-cloud': { originalBlock: 'Tag Cloud', - availableTransforms: [], + availableTransforms: [ + 'Group', + ], }, 'core__tag-cloud__showTagCounts': { originalBlock: 'Tag Cloud', availableTransforms: [ + 'Group', ], }, core__verse: { originalBlock: 'Verse', availableTransforms: [ + 'Group', 'Paragraph', ], }, @@ -343,6 +428,7 @@ export const EXPECTED_TRANSFORMS = { availableTransforms: [ 'Cover', 'File', + 'Group', 'Media & Text', ], }, diff --git a/packages/e2e-tests/specs/__snapshots__/block-grouping.test.js.snap b/packages/e2e-tests/specs/__snapshots__/block-grouping.test.js.snap new file mode 100644 index 00000000000000..af571b2010ed25 --- /dev/null +++ b/packages/e2e-tests/specs/__snapshots__/block-grouping.test.js.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Block Grouping Group creation creates a group from multiple blocks of different types via block transforms 1`] = ` +" +
Some paragraph
+First Paragraph
+ + + +Second Paragraph
+ + + +Third Paragraph
+First Paragraph
+ + + +Second Paragraph
+ + + +Third Paragraph
+Some paragraph
+Some paragraph
+" +`; + +exports[`Block Grouping Preserving selected blocks attributes preserves width alignment settings of selected blocks 1`] = ` +" +Some paragraph
+... like this one, which is separate from the above and right aligned.
+