From 2a1966c16f4bd981a0fe18b665471de75bdcd926 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Mon, 5 Oct 2020 14:48:53 +0300 Subject: [PATCH 1/5] Transform multiple selected blocks to Columns block --- packages/block-library/src/columns/index.js | 2 + .../block-library/src/columns/transforms.js | 39 +++++++++++++++++++ packages/blocks/README.md | 15 +++++++ packages/blocks/src/api/factory.js | 28 +++++++++++++ packages/blocks/src/api/index.js | 1 + 5 files changed, 85 insertions(+) create mode 100644 packages/block-library/src/columns/transforms.js diff --git a/packages/block-library/src/columns/index.js b/packages/block-library/src/columns/index.js index 63676d74961b90..4de9408164f514 100644 --- a/packages/block-library/src/columns/index.js +++ b/packages/block-library/src/columns/index.js @@ -12,6 +12,7 @@ import edit from './edit'; import metadata from './block.json'; import save from './save'; import variations from './variations'; +import transforms from './transforms'; const { name } = metadata; @@ -84,4 +85,5 @@ export const settings = { deprecated, edit, save, + transforms, }; diff --git a/packages/block-library/src/columns/transforms.js b/packages/block-library/src/columns/transforms.js new file mode 100644 index 00000000000000..fc3e512a7d19f8 --- /dev/null +++ b/packages/block-library/src/columns/transforms.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { + createBlock, + createBlocksFromInnerBlocksTemplate, +} from '@wordpress/blocks'; + +const MAXIMUM_SELECTED_BLOCKS = 6; + +const transforms = { + from: [ + { + type: 'block', + isMultiBlock: true, + blocks: [ '*' ], + __experimentalConvert: ( blocks ) => { + const columnWidth = +( 100 / blocks.length ).toFixed( 2 ); + const innerBlocksTemplate = blocks.map( + ( { name, attributes, innerBlocks } ) => [ + 'core/column', + { width: `${ columnWidth }%` }, + [ [ name, { ...attributes }, innerBlocks ] ], + ] + ); + return createBlock( + 'core/columns', + {}, + createBlocksFromInnerBlocksTemplate( innerBlocksTemplate ) + ); + }, + isMatch: ( { length: selectedBlocksLength } ) => + selectedBlocksLength > 1 && + selectedBlocksLength <= MAXIMUM_SELECTED_BLOCKS, + }, + ], +}; + +export default transforms; diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 1148b2f6028380..32a00fd9967d02 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -205,6 +205,21 @@ _Returns_ - `Object`: Block object. +# **createBlocksFromInnerBlocksTemplate** + +Given an array of InnerBlocks templates or Block Objects, +returns an array of created Blocks from them. +It handles the case of having InnerBlocks as Blocks by +converting them to the proper format to continue recursively. + +_Parameters_ + +- _innerBlocksOrTemplate_ `Array`: Nested blocks or InnerBlocks templates. + +_Returns_ + +- `Array`: Array of Block objects. + # **doBlocksMatchTemplate** Checks whether a list of blocks matches a template by comparing the block names. diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index d87d16cafd026a..edff9421fabf3a 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -91,6 +91,34 @@ export function createBlock( name, attributes = {}, innerBlocks = [] ) { }; } +/** + * Given an array of InnerBlocks templates or Block Objects, + * returns an array of created Blocks from them. + * It handles the case of having InnerBlocks as Blocks by + * converting them to the proper format to continue recursively. + * + * @param {Array} innerBlocksOrTemplate Nested blocks or InnerBlocks templates. + * + * @return {Object[]} Array of Block objects. + */ +export function createBlocksFromInnerBlocksTemplate( innerBlocksOrTemplate ) { + return innerBlocksOrTemplate.map( ( innerBlock ) => { + const innerBlockTemplate = Array.isArray( innerBlock ) + ? innerBlock + : [ + innerBlock.name, + innerBlock.attributes, + innerBlock.innerBlocks, + ]; + const [ name, attributes, innerBlocks = [] ] = innerBlockTemplate; + return createBlock( + name, + attributes, + createBlocksFromInnerBlocksTemplate( innerBlocks ) + ); + } ); +} + /** * Given a block object, returns a copy of the block object, optionally merging * new attributes and/or replacing its inner blocks. diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index ed50a953509e56..1c3a1bfb60dffb 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -1,5 +1,6 @@ export { createBlock, + createBlocksFromInnerBlocksTemplate, cloneBlock, getPossibleBlockTransformations, switchToBlockType, From b06ff5d730a1c84f3715be958eb401a28b17546f Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 6 Oct 2020 09:17:44 +0300 Subject: [PATCH 2/5] use the new function from blocks API --- .../block-editor/src/autocompleters/block.js | 19 ++++----------- .../block-variation-picker/index.native.js | 15 +----------- .../inserter/hooks/use-block-types-state.js | 23 ++++--------------- packages/block-library/src/columns/edit.js | 19 ++++----------- 4 files changed, 15 insertions(+), 61 deletions(-) diff --git a/packages/block-editor/src/autocompleters/block.js b/packages/block-editor/src/autocompleters/block.js index 79cff960e3e71e..aa7dfa160ed738 100644 --- a/packages/block-editor/src/autocompleters/block.js +++ b/packages/block-editor/src/autocompleters/block.js @@ -1,13 +1,16 @@ /** * External dependencies */ -import { noop, map, orderBy } from 'lodash'; +import { noop, orderBy } from 'lodash'; /** * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { createBlock } from '@wordpress/blocks'; +import { + createBlock, + createBlocksFromInnerBlocksTemplate, +} from '@wordpress/blocks'; import { useMemo } from '@wordpress/element'; /** @@ -19,18 +22,6 @@ import BlockIcon from '../components/block-icon'; const SHOWN_BLOCK_TYPES = 9; -const createBlocksFromInnerBlocksTemplate = ( innerBlocksTemplate ) => { - return map( - innerBlocksTemplate, - ( [ name, attributes, innerBlocks = [] ] ) => - createBlock( - name, - attributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) - ) - ); -}; - /** @typedef {import('@wordpress/block-editor').WPEditorInserterItem} WPEditorInserterItem */ /** @typedef {import('@wordpress/components').WPCompleter} WPCompleter */ diff --git a/packages/block-editor/src/components/block-variation-picker/index.native.js b/packages/block-editor/src/components/block-variation-picker/index.native.js index 9ad927c0b4796a..eba775e09f278f 100644 --- a/packages/block-editor/src/components/block-variation-picker/index.native.js +++ b/packages/block-editor/src/components/block-variation-picker/index.native.js @@ -8,14 +8,13 @@ import { TouchableWithoutFeedback, Platform, } from 'react-native'; -import { map } from 'lodash'; /** * WordPress dependencies */ import { withSelect, useDispatch } from '@wordpress/data'; import { compose, usePreferredColorSchemeStyle } from '@wordpress/compose'; -import { createBlock } from '@wordpress/blocks'; +import { createBlocksFromInnerBlocksTemplate } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import { PanelBody, @@ -33,18 +32,6 @@ import styles from './style.scss'; const hitSlop = { top: 22, bottom: 22, left: 22, right: 22 }; -function createBlocksFromInnerBlocksTemplate( innerBlocksTemplate ) { - return map( - innerBlocksTemplate, - ( [ name, attributes, innerBlocks = [] ] ) => - createBlock( - name, - attributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) - ) - ); -} - function BlockVariationPicker( { isVisible, onClose, clientId, variations } ) { const { replaceInnerBlocks } = useDispatch( 'core/block-editor' ); const isIOS = Platform.OS === 'ios'; diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 80cd70ba3013db..0ef5983c661d65 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -1,28 +1,13 @@ -/** - * External dependencies - */ -import { map } from 'lodash'; - /** * WordPress dependencies */ import { useEffect } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; +import { + createBlock, + createBlocksFromInnerBlocksTemplate, +} from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; -// Copied over from the Columns block. It seems like it should become part of public API. -const createBlocksFromInnerBlocksTemplate = ( innerBlocksTemplate ) => { - return map( - innerBlocksTemplate, - ( [ name, attributes, innerBlocks = [] ] ) => - createBlock( - name, - attributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) - ) - ); -}; - /** * Retrieves the block types inserter state. * diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index 32a51f9ee202dd..39d537c0dedc81 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { dropRight, get, map, times } from 'lodash'; +import { dropRight, get, times } from 'lodash'; /** * WordPress dependencies @@ -19,7 +19,10 @@ import { useBlockProps, } from '@wordpress/block-editor'; import { withDispatch, useDispatch, useSelect } from '@wordpress/data'; -import { createBlock } from '@wordpress/blocks'; +import { + createBlock, + createBlocksFromInnerBlocksTemplate, +} from '@wordpress/blocks'; /** * Internal dependencies @@ -198,18 +201,6 @@ const ColumnsEditContainerWrapper = withDispatch( } ) )( ColumnsEditContainer ); -const createBlocksFromInnerBlocksTemplate = ( innerBlocksTemplate ) => { - return map( - innerBlocksTemplate, - ( [ name, attributes, innerBlocks = [] ] ) => - createBlock( - name, - attributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) - ) - ); -}; - function Placeholder( { clientId, name, setAttributes } ) { const { blockType, defaultVariation, variations } = useSelect( ( select ) => { From 9e642c353e299f687fe2b89a39907102f639bc39 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 6 Oct 2020 10:58:06 +0300 Subject: [PATCH 3/5] createBlocksFromInnerBlocksTemplate unit tests --- packages/blocks/src/api/factory.js | 4 +- packages/blocks/src/api/test/factory.js | 103 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index edff9421fabf3a..b7c61132772907 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -101,7 +101,9 @@ export function createBlock( name, attributes = {}, innerBlocks = [] ) { * * @return {Object[]} Array of Block objects. */ -export function createBlocksFromInnerBlocksTemplate( innerBlocksOrTemplate ) { +export function createBlocksFromInnerBlocksTemplate( + innerBlocksOrTemplate = [] +) { return innerBlocksOrTemplate.map( ( innerBlock ) => { const innerBlockTemplate = Array.isArray( innerBlock ) ? innerBlock diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index 8b37d9349bd218..47679b1cb8d052 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -9,6 +9,7 @@ import { noop, times } from 'lodash'; */ import { createBlock, + createBlocksFromInnerBlocksTemplate, cloneBlock, getPossibleBlockTransformations, switchToBlockType, @@ -160,6 +161,108 @@ describe( 'block factory', () => { } ); } ); + describe( 'createBlocksFromInnerBlocksTemplate', () => { + it( 'should create a block without InnerBlocks', () => { + const blockName = 'core/test-block'; + registerBlockType( blockName, { ...defaultBlockSettings } ); + const res = createBlock( + blockName, + { ...defaultBlockSettings }, + createBlocksFromInnerBlocksTemplate() + ); + expect( res ).toEqual( + expect.objectContaining( { + name: blockName, + innerBlocks: [], + } ) + ); + } ); + describe( 'create block with InnerBlocks', () => { + beforeEach( () => { + registerBlockType( 'core/test-block', { + ...defaultBlockSettings, + } ); + registerBlockType( 'core/test-other', { + ...defaultBlockSettings, + } ); + registerBlockType( 'core/test-paragraph', { + ...defaultBlockSettings, + attributes: { + content: { + type: 'string', + default: 'hello', + }, + }, + } ); + } ); + it( 'should create block with InnerBlocks from template', () => { + const res = createBlock( + 'core/test-block', + defaultBlockSettings, + createBlocksFromInnerBlocksTemplate( [ + [ 'core/test-other' ], + [ 'core/test-paragraph', { content: 'fromTemplate' } ], + [ 'core/test-paragraph' ], + ] ) + ); + expect( res.innerBlocks ).toHaveLength( 3 ); + expect( res.innerBlocks ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'core/test-other', + } ), + expect.objectContaining( { + name: 'core/test-paragraph', + attributes: { content: 'fromTemplate' }, + } ), + expect.objectContaining( { + name: 'core/test-paragraph', + attributes: { content: 'hello' }, + } ), + ] ) + ); + } ); + it( 'should create blocks with InnerBlocks template and InnerBlock objects', () => { + const nestedInnerBlocks = [ + createBlock( 'core/test-other' ), + createBlock( 'core/test-paragraph' ), + ]; + const res = createBlock( + 'core/test-block', + defaultBlockSettings, + createBlocksFromInnerBlocksTemplate( [ + [ 'core/test-other' ], + [ + 'core/test-paragraph', + { content: 'fromTemplate' }, + nestedInnerBlocks, + ], + ] ) + ); + expect( res.innerBlocks ).toHaveLength( 2 ); + expect( res.innerBlocks ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'core/test-other', + } ), + expect.objectContaining( { + name: 'core/test-paragraph', + attributes: { content: 'fromTemplate' }, + innerBlocks: expect.arrayContaining( [ + expect.objectContaining( { + name: 'core/test-other', + } ), + expect.objectContaining( { + name: 'core/test-other', + } ), + ] ), + } ), + ] ) + ); + } ); + } ); + } ); + describe( 'cloneBlock()', () => { it( 'should merge attributes into the existing block', () => { registerBlockType( 'core/test-block', { From d88b7ae94f18e7649cb3ddf1d59b621d8cf17ba4 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 6 Oct 2020 13:49:29 +0300 Subject: [PATCH 4/5] add e2e tests --- .../editor/various/block-switcher.test.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/e2e-tests/specs/editor/various/block-switcher.test.js b/packages/e2e-tests/specs/editor/various/block-switcher.test.js index f4d8e1031880af..fd62f4f95648b5 100644 --- a/packages/e2e-tests/specs/editor/various/block-switcher.test.js +++ b/packages/e2e-tests/specs/editor/various/block-switcher.test.js @@ -82,4 +82,48 @@ describe( 'Block Switcher', () => { // Verify the correct block transforms appear. expect( await getAvailableBlockTransforms() ).toHaveLength( 0 ); } ); + + describe( 'Conditional tranformation options', () => { + describe( 'Columns tranforms', () => { + it( 'Should show Columns block only if selected blocks are between limits (2-6)', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'List content' ); + await insertBlock( 'Heading' ); + await page.keyboard.type( 'I am a header' ); + await page.keyboard.down( 'Shift' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.up( 'Shift' ); + expect( await getAvailableBlockTransforms() ).toEqual( + expect.arrayContaining( [ 'Columns' ] ) + ); + } ); + it( 'Should NOT show Columns transform only if selected blocks are more than max limit(6)', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'List content' ); + await insertBlock( 'Heading' ); + await page.keyboard.type( 'I am a header' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'First paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Second paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Third paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Fourth paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Fifth paragraph' ); + await page.keyboard.down( 'Shift' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.up( 'Shift' ); + expect( await getAvailableBlockTransforms() ).not.toEqual( + expect.arrayContaining( [ 'Columns' ] ) + ); + } ); + } ); + } ); } ); From 81d58e2b8d4d9cb2cb67aceb62d2790ed165e53a Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Wed, 7 Oct 2020 11:43:45 +0300 Subject: [PATCH 5/5] change selection shortcuts in tests --- .../specs/editor/various/block-switcher.test.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/e2e-tests/specs/editor/various/block-switcher.test.js b/packages/e2e-tests/specs/editor/various/block-switcher.test.js index fd62f4f95648b5..f7214b0da1c279 100644 --- a/packages/e2e-tests/specs/editor/various/block-switcher.test.js +++ b/packages/e2e-tests/specs/editor/various/block-switcher.test.js @@ -112,14 +112,8 @@ describe( 'Block Switcher', () => { await page.keyboard.type( 'Fourth paragraph' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'Fifth paragraph' ); - await page.keyboard.down( 'Shift' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.up( 'Shift' ); + await pressKeyWithModifier( 'primary', 'a' ); + await pressKeyWithModifier( 'primary', 'a' ); expect( await getAvailableBlockTransforms() ).not.toEqual( expect.arrayContaining( [ 'Columns' ] ) );