diff --git a/docs/data/data-core-blocks.md b/docs/data/data-core-blocks.md index 33092691b3eab..5f10809be4c64 100644 --- a/docs/data/data-core-blocks.md +++ b/docs/data/data-core-blocks.md @@ -59,6 +59,30 @@ Returns the name of the fallback block name. Fallback block name. +### getFreeformFallbackBlockName + +Returns the name of the block for handling non-block content. + +*Parameters* + + * state: Data state. + +*Returns* + +Name of the block for handling non-block content. + +### getUnregisteredFallbackBlockName + +Returns the name of the block for handling unregistered blocks. + +*Parameters* + + * state: Data state. + +*Returns* + +Name of the block for handling unregistered blocks. + ### getChildBlockNames Returns an array with the child blocks of a given block. @@ -159,7 +183,26 @@ Returns an action object used to set the default block name. ### setFallbackBlockName -Returns an action object used to set the fallback block name. +Returns an action object used to set the name of the block used as a fallback +for non-block content. + +*Parameters* + + * name: Block name. + +### setFreeformFallbackBlockName + +Returns an action object used to set the name of the block used as a fallback +for non-block content. + +*Parameters* + + * name: Block name. + +### setUnregisteredFallbackBlockName + +Returns an action object used to set the name of the block used as a fallback +for unregistered blocks. *Parameters* diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index d13652597953d..bdd1ef07a7053 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -13,6 +13,8 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo - Attribute type coercion has been removed. Omit the source to preserve type via serialized comment demarcation. - `mediaDetails` in object passed to `onFileChange` callback of `wp.editor.mediaUpload`. Please use `media_details` property instead. - `wp.components.CodeEditor` has been removed. Used `wp.codeEditor` directly instead. +- `wp.blocks.setUnknownTypeHandlerName` has been removed. Please use `setFreeformContentHandlerName` and `setUnregisteredTypeHandlerName` instead. +- `wp.blocks.getUnknownTypeHandlerName` has been removed. Please use `getFreeformContentHandlerName` and `getUnregisteredTypeHandlerName` instead. ## 4.1.0 diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 3c71cb52c702a..470ad4aea9feb 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -5,7 +5,8 @@ import '@wordpress/core-data'; import { registerBlockType, setDefaultBlockName, - setUnknownTypeHandlerName, + setFreeformContentHandlerName, + setUnregisteredTypeHandlerName, } from '@wordpress/blocks'; /** @@ -30,6 +31,7 @@ import * as html from './html'; import * as latestComments from './latest-comments'; import * as latestPosts from './latest-posts'; import * as list from './list'; +import * as missing from './missing'; import * as more from './more'; import * as nextpage from './nextpage'; import * as preformatted from './preformatted'; @@ -76,6 +78,7 @@ export const registerCoreBlocks = () => { html, latestComments, latestPosts, + missing, more, nextpage, preformatted, @@ -99,6 +102,7 @@ export const registerCoreBlocks = () => { setDefaultBlockName( paragraph.name ); if ( window.wp && window.wp.oldEditor ) { - setUnknownTypeHandlerName( classic.name ); + setFreeformContentHandlerName( classic.name ); } + setUnregisteredTypeHandlerName( missing.name ); }; diff --git a/packages/block-library/src/missing/index.js b/packages/block-library/src/missing/index.js new file mode 100644 index 0000000000000..53c89edbc0ba6 --- /dev/null +++ b/packages/block-library/src/missing/index.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { RawHTML, Fragment } from '@wordpress/element'; +import { Button } from '@wordpress/components'; +import { getBlockType, createBlock } from '@wordpress/blocks'; +import { withDispatch } from '@wordpress/data'; +import { Warning } from '@wordpress/editor'; + +function MissingBlockWarning( { attributes, convertToHTML } ) { + const { originalName, originalUndelimitedContent } = attributes; + const hasContent = !! originalUndelimitedContent; + const hasHTMLBlock = getBlockType( 'core/html' ); + + const actions = []; + let messageHTML; + if ( hasContent && hasHTMLBlock ) { + messageHTML = sprintf( + __( 'Your site doesn\'t include support for the %s block. You can leave this block intact, convert its content to a Custom HTML block, or remove it entirely.' ), + originalName + ); + actions.push( + + ); + } else { + messageHTML = sprintf( + __( 'Your site doesn\'t include support for the %s block. You can leave this block intact or remove it entirely.' ), + originalName + ); + } + + return ( + + + + +
+ { originalUndelimitedContent } +
+
+ ); +} + +const edit = withDispatch( ( dispatch, { clientId, attributes } ) => { + const { replaceBlock } = dispatch( 'core/editor' ); + return { + convertToHTML() { + replaceBlock( clientId, createBlock( 'core/html', { + content: attributes.originalUndelimitedContent, + } ) ); + }, + }; +} )( MissingBlockWarning ); + +export const name = 'core/missing'; + +export const settings = { + name, + category: 'common', + title: __( 'Unrecognized Block' ), + description: __( 'Your site doesn\'t include support for this block.' ), + + supports: { + className: false, + customClassName: false, + inserter: false, + html: false, + }, + + attributes: { + originalName: { + type: 'string', + }, + originalUndelimitedContent: { + type: 'string', + }, + originalContent: { + type: 'string', + source: 'html', + }, + }, + + edit, + save( { attributes } ) { + // Preserve the missing block's content. + return { attributes.originalContent }; + }, +}; diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 127b329df4fe9..53425eadfa92f 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -30,6 +30,10 @@ export { unregisterBlockType, setUnknownTypeHandlerName, getUnknownTypeHandlerName, + setFreeformContentHandlerName, + getFreeformContentHandlerName, + setUnregisteredTypeHandlerName, + getUnregisteredTypeHandlerName, setDefaultBlockName, getDefaultBlockName, getBlockType, diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index d71fdbe0ac38e..496eb6ce81cb3 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -15,7 +15,11 @@ import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ -import { getBlockType, getUnknownTypeHandlerName } from './registration'; +import { + getBlockType, + getFreeformContentHandlerName, + getUnregisteredTypeHandlerName, +} from './registration'; import { createBlock } from './factory'; import { isValidBlock } from './validation'; import { getCommentDelimitedContent } from './serializer'; @@ -394,54 +398,61 @@ export function getMigratedBlock( block ) { * @return {?Object} An initialized block object (if possible). */ export function createBlockWithFallback( blockNode ) { + const { blockName: originalName } = blockNode; let { - blockName: name, attrs: attributes, innerBlocks = [], innerHTML, } = blockNode; + const freeformContentFallbackBlock = getFreeformContentHandlerName(); + const unregisteredFallbackBlock = getUnregisteredTypeHandlerName() || freeformContentFallbackBlock; attributes = attributes || {}; // Trim content to avoid creation of intermediary freeform segments. - innerHTML = innerHTML.trim(); + const originalUndelimitedContent = innerHTML = innerHTML.trim(); - // Use type from block content, otherwise find unknown handler. - name = name || getUnknownTypeHandlerName(); + // Use type from block content if available. Otherwise, default to the + // freeform content fallback. + let name = originalName || freeformContentFallbackBlock; // Convert 'core/text' blocks in existing content to 'core/paragraph'. if ( 'core/text' === name || 'core/cover-text' === name ) { name = 'core/paragraph'; } - // Try finding the type for known block name, else fall back again. - let blockType = getBlockType( name ); - - const fallbackBlock = getUnknownTypeHandlerName(); - // Fallback content may be upgraded from classic editor expecting implicit // automatic paragraphs, so preserve them. Assumes wpautop is idempotent, // meaning there are no negative consequences to repeated autop calls. - if ( name === fallbackBlock ) { + if ( name === freeformContentFallbackBlock ) { innerHTML = autop( innerHTML ).trim(); } + // Try finding the type for known block name, else fall back again. + let blockType = getBlockType( name ); + if ( ! blockType ) { // If detected as a block which is not registered, preserve comment - // delimiters in content of unknown type handler. + // delimiters in content of unregistered type handler. if ( name ) { innerHTML = getCommentDelimitedContent( name, attributes, innerHTML ); } - name = fallbackBlock; + name = unregisteredFallbackBlock; + attributes = { originalName, originalUndelimitedContent }; blockType = getBlockType( name ); } // Coerce inner blocks from parsed form to canonical form. innerBlocks = innerBlocks.map( createBlockWithFallback ); - // Include in set only if type were determined. - if ( ! blockType || ( ! innerHTML && name === fallbackBlock ) ) { + const isFallbackBlock = ( + name === freeformContentFallbackBlock || + name === unregisteredFallbackBlock + ); + + // Include in set only if type was determined. + if ( ! blockType || ( ! innerHTML && isFallbackBlock ) ) { return; } @@ -455,7 +466,7 @@ export function createBlockWithFallback( blockNode ) { // provided there are no changes in attributes. The validation procedure thus compares the // provided source value with the serialized output before there are any modifications to // the block. When both match, the block is marked as valid. - if ( name !== fallbackBlock ) { + if ( ! isFallbackBlock ) { block.isValid = isValidBlock( innerHTML, blockType, block.attributes ); } diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 267ab2443b90d..13e62a425fc0f 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -10,6 +10,7 @@ import { get, isFunction, some } from 'lodash'; */ import { applyFilters, addFilter } from '@wordpress/hooks'; import { select, dispatch } from '@wordpress/data'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -177,7 +178,12 @@ export function unregisterBlockType( name ) { * @param {string} name Block name. */ export function setUnknownTypeHandlerName( name ) { - dispatch( 'core/blocks' ).setFallbackBlockName( name ); + deprecated( 'setUnknownTypeHandlerName', { + plugin: 'Gutenberg', + version: '4.2', + alternative: 'setFreeformContentHandlerName and setUnregisteredTypeHandlerName', + } ); + setFreeformContentHandlerName( name ); } /** @@ -187,7 +193,50 @@ export function setUnknownTypeHandlerName( name ) { * @return {?string} Blog name. */ export function getUnknownTypeHandlerName() { - return select( 'core/blocks' ).getFallbackBlockName(); + deprecated( 'getUnknownTypeHandlerName', { + plugin: 'Gutenberg', + version: '4.2', + alternative: 'getFreeformContentHandlerName and getUnregisteredTypeHandlerName', + } ); + return getFreeformContentHandlerName(); +} + +/** + * Assigns name of block for handling non-block content. + * + * @param {string} name Block name. + */ +export function setFreeformContentHandlerName( name ) { + dispatch( 'core/blocks' ).setFreeformFallbackBlockName( name ); +} + +/** + * Retrieves name of block handling non-block content, or undefined if no + * handler has been defined. + * + * @return {?string} Blog name. + */ +export function getFreeformContentHandlerName() { + return select( 'core/blocks' ).getFreeformFallbackBlockName(); +} + +/** + * Assigns name of block handling unregistered block types. + * + * @param {string} name Block name. + */ +export function setUnregisteredTypeHandlerName( name ) { + dispatch( 'core/blocks' ).setUnregisteredFallbackBlockName( name ); +} + +/** + * Retrieves name of block handling unregistered block types, or undefined if no + * handler has been defined. + * + * @return {?string} Blog name. + */ +export function getUnregisteredTypeHandlerName() { + return select( 'core/blocks' ).getUnregisteredFallbackBlockName(); } /** diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index d68a1a495446f..d54d13a0063aa 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -13,7 +13,11 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ -import { getBlockType, getUnknownTypeHandlerName } from './registration'; +import { + getBlockType, + getFreeformContentHandlerName, + getUnregisteredTypeHandlerName, +} from './registration'; import BlockContentProvider from '../block-content-provider'; /** @@ -261,7 +265,8 @@ export function serializeBlock( block ) { const saveAttributes = getCommentAttributes( block.attributes, blockType ); switch ( blockName ) { - case getUnknownTypeHandlerName(): + case getFreeformContentHandlerName(): + case getUnregisteredTypeHandlerName(): return saveContent; default: diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index b6f563bafa3a6..d8c46a857f90d 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -15,7 +15,7 @@ import { getBlockTransforms, findTransform, } from '../factory'; -import { getBlockTypes, unregisterBlockType, setUnknownTypeHandlerName, registerBlockType } from '../registration'; +import { getBlockTypes, unregisterBlockType, registerBlockType } from '../registration'; describe( 'block factory', () => { const defaultBlockSettings = { @@ -35,7 +35,6 @@ describe( 'block factory', () => { } ); afterEach( () => { - setUnknownTypeHandlerName( undefined ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index 364de6a88a84b..a23e2f995f7d0 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -28,7 +28,8 @@ import { registerBlockType, unregisterBlockType, getBlockTypes, - setUnknownTypeHandlerName, + setFreeformContentHandlerName, + setUnregisteredTypeHandlerName, } from '../registration'; import { createBlock } from '../factory'; import serialize from '../serializer'; @@ -65,7 +66,8 @@ describe( 'block parser', () => { } ); afterEach( () => { - setUnknownTypeHandlerName( undefined ); + setFreeformContentHandlerName( undefined ); + setUnregisteredTypeHandlerName( undefined ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); @@ -548,27 +550,27 @@ describe( 'block parser', () => { expect( block.attributes ).toEqual( {} ); } ); - it( 'should fall back to the unknown type handler for unknown blocks if present', () => { - registerBlockType( 'core/unknown-block', unknownBlockSettings ); - setUnknownTypeHandlerName( 'core/unknown-block' ); + it( 'should fall back to the unregistered type handler for unregistered blocks if present', () => { + registerBlockType( 'core/unregistered-block', unknownBlockSettings ); + setUnregisteredTypeHandlerName( 'core/unregistered-block' ); const block = createBlockWithFallback( { blockName: 'core/test-block', innerHTML: 'Bananas', attrs: { fruit: 'Bananas' }, } ); - expect( block.name ).toBe( 'core/unknown-block' ); + expect( block.name ).toBe( 'core/unregistered-block' ); expect( block.attributes.content ).toContain( 'wp:test-block' ); } ); - it( 'should fall back to the unknown type handler if block type not specified', () => { - registerBlockType( 'core/unknown-block', unknownBlockSettings ); - setUnknownTypeHandlerName( 'core/unknown-block' ); + it( 'should fall back to the freeform content handler if block type not specified', () => { + registerBlockType( 'core/freeform-block', unknownBlockSettings ); + setFreeformContentHandlerName( 'core/freeform-block' ); const block = createBlockWithFallback( { innerHTML: 'content', } ); - expect( block.name ).toEqual( 'core/unknown-block' ); + expect( block.name ).toEqual( 'core/freeform-block' ); expect( block.attributes ).toEqual( { content: '

content

' } ); } ); @@ -706,7 +708,8 @@ describe( 'block parser', () => { it( 'should ignore blocks with a bad namespace', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); - setUnknownTypeHandlerName( 'core/unknown-block' ); + setFreeformContentHandlerName( 'core/unknown-block' ); + setUnregisteredTypeHandlerName( 'core/unknown-block' ); const parsed = parse( '\nBananas\n' + @@ -721,7 +724,7 @@ describe( 'block parser', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/unknown-block', unknownBlockSettings ); - setUnknownTypeHandlerName( 'core/unknown-block' ); + setFreeformContentHandlerName( 'core/unknown-block' ); const parsed = parse( '\nBananas\n' + @@ -741,7 +744,7 @@ describe( 'block parser', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/unknown-block', unknownBlockSettings ); - setUnknownTypeHandlerName( 'core/unknown-block' ); + setFreeformContentHandlerName( 'core/unknown-block' ); const parsed = parse( '

Cauliflower

' + diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 2b206a25a3a9b..0db260896334d 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -16,6 +16,10 @@ import { unregisterBlockType, setUnknownTypeHandlerName, getUnknownTypeHandlerName, + setFreeformContentHandlerName, + getFreeformContentHandlerName, + setUnregisteredTypeHandlerName, + getUnregisteredTypeHandlerName, setDefaultBlockName, getDefaultBlockName, getBlockType, @@ -39,7 +43,8 @@ describe( 'blocks', () => { getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); - setUnknownTypeHandlerName( undefined ); + setFreeformContentHandlerName( undefined ); + setUnregisteredTypeHandlerName( undefined ); setDefaultBlockName( undefined ); unstable__bootstrapServerSideBlockDefinitions( {} ); } ); @@ -366,9 +371,18 @@ describe( 'blocks', () => { describe( 'setUnknownTypeHandlerName()', () => { it( 'assigns unknown type handler', () => { - setUnknownTypeHandlerName( 'core/test-block' ); - - expect( getUnknownTypeHandlerName() ).toBe( 'core/test-block' ); + try { + setUnknownTypeHandlerName( 'core/test-block' ); + + expect( getUnknownTypeHandlerName() ).toBe( 'core/test-block' ); + expect( console ).toHaveWarned(); + } finally { + // Restore undefined handler here rather than in `afterEach` because: + // - This call generates a deprecation warning. + // - Deprecation warnings become test errors unless we assert `toHaveWarned`. + // - This is too broad of an assertion to apply for all tests in the suite. + setUnknownTypeHandlerName( undefined ); + } } ); } ); @@ -378,6 +392,34 @@ describe( 'blocks', () => { } ); } ); + describe( 'setFreeformContentHandlerName()', () => { + it( 'assigns unknown type handler', () => { + setFreeformContentHandlerName( 'core/test-block' ); + + expect( getFreeformContentHandlerName() ).toBe( 'core/test-block' ); + } ); + } ); + + describe( 'getFreeformContentHandlerName()', () => { + it( 'defaults to undefined', () => { + expect( getFreeformContentHandlerName() ).toBeNull(); + } ); + } ); + + describe( 'setUnregisteredTypeHandlerName()', () => { + it( 'assigns unknown type handler', () => { + setUnregisteredTypeHandlerName( 'core/test-block' ); + + expect( getUnregisteredTypeHandlerName() ).toBe( 'core/test-block' ); + } ); + } ); + + describe( 'getUnregisteredTypeHandlerName()', () => { + it( 'defaults to undefined', () => { + expect( getUnregisteredTypeHandlerName() ).toBeNull(); + } ); + } ); + describe( 'setDefaultBlockName()', () => { it( 'assigns default block name', () => { setDefaultBlockName( 'core/test-block' ); diff --git a/packages/blocks/src/api/test/serializer.js b/packages/blocks/src/api/test/serializer.js index 994868cb19f4c..fbea4db4991de 100644 --- a/packages/blocks/src/api/test/serializer.js +++ b/packages/blocks/src/api/test/serializer.js @@ -18,7 +18,8 @@ import { getBlockTypes, registerBlockType, unregisterBlockType, - setUnknownTypeHandlerName, + setFreeformContentHandlerName, + setUnregisteredTypeHandlerName, } from '../registration'; import { createBlock } from '../'; @@ -29,7 +30,8 @@ describe( 'block serializer', () => { } ); afterEach( () => { - setUnknownTypeHandlerName( undefined ); + setFreeformContentHandlerName( undefined ); + setUnregisteredTypeHandlerName( undefined ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); @@ -196,10 +198,10 @@ describe( 'block serializer', () => { } ); describe( 'serializeBlock()', () => { - it( 'serializes the fallback block without comment delimiters', () => { - registerBlockType( 'core/unknown-block', { + it( 'serializes the freeform content fallback block without comment delimiters', () => { + registerBlockType( 'core/freeform-block', { category: 'common', - title: 'unknown block', + title: 'freeform block', attributes: { fruit: { type: 'string', @@ -207,8 +209,26 @@ describe( 'block serializer', () => { }, save: ( { attributes } ) => attributes.fruit, } ); - setUnknownTypeHandlerName( 'core/unknown-block' ); - const block = createBlock( 'core/unknown-block', { fruit: 'Bananas' } ); + setFreeformContentHandlerName( 'core/freeform-block' ); + const block = createBlock( 'core/freeform-block', { fruit: 'Bananas' } ); + + const content = serializeBlock( block ); + + expect( content ).toBe( 'Bananas' ); + } ); + it( 'serializes the unregistered fallback block without comment delimiters', () => { + registerBlockType( 'core/unregistered-block', { + category: 'common', + title: 'unregistered block', + attributes: { + fruit: { + type: 'string', + }, + }, + save: ( { attributes } ) => attributes.fruit, + } ); + setUnregisteredTypeHandlerName( 'core/unregistered-block' ); + const block = createBlock( 'core/unregistered-block', { fruit: 'Bananas' } ); const content = serializeBlock( block ); diff --git a/packages/blocks/src/api/test/validation.js b/packages/blocks/src/api/test/validation.js index 3a357bd0b6c24..0497ff378ef45 100644 --- a/packages/blocks/src/api/test/validation.js +++ b/packages/blocks/src/api/test/validation.js @@ -20,7 +20,6 @@ import { unregisterBlockType, getBlockTypes, getBlockType, - setUnknownTypeHandlerName, } from '../registration'; describe( 'validation', () => { @@ -35,7 +34,6 @@ describe( 'validation', () => { } ); afterEach( () => { - setUnknownTypeHandlerName( undefined ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 37bcaa2e5cba1..4ca964e430478 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -3,6 +3,11 @@ */ import { castArray } from 'lodash'; +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + /** * Returns an action object used in signalling that block types have been added. * @@ -46,15 +51,48 @@ export function setDefaultBlockName( name ) { } /** - * Returns an action object used to set the fallback block name. + * Returns an action object used to set the name of the block used as a fallback + * for non-block content. * * @param {string} name Block name. * * @return {Object} Action object. */ export function setFallbackBlockName( name ) { + deprecated( 'setFallbackBlockName', { + plugin: 'Gutenberg', + version: '4.2', + alternative: 'setFreeformFallbackBlockName and setUnregisteredFallbackBlockName', + } ); + return setFreeformFallbackBlockName( name ); +} + +/** + * Returns an action object used to set the name of the block used as a fallback + * for non-block content. + * + * @param {string} name Block name. + * + * @return {Object} Action object. + */ +export function setFreeformFallbackBlockName( name ) { + return { + type: 'SET_FREEFORM_FALLBACK_BLOCK_NAME', + name, + }; +} + +/** + * Returns an action object used to set the name of the block used as a fallback + * for unregistered blocks. + * + * @param {string} name Block name. + * + * @return {Object} Action object. + */ +export function setUnregisteredFallbackBlockName( name ) { return { - type: 'SET_FALLBACK_BLOCK_NAME', + type: 'SET_UNREGISTERED_FALLBACK_BLOCK_NAME', name, }; } diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 664520e40a205..e1ed669e0b217 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -69,7 +69,8 @@ export function createBlockNameSetterReducer( setActionType ) { export const defaultBlockName = createBlockNameSetterReducer( 'SET_DEFAULT_BLOCK_NAME' ); -export const fallbackBlockName = createBlockNameSetterReducer( 'SET_FALLBACK_BLOCK_NAME' ); +export const freeformFallbackBlockName = createBlockNameSetterReducer( 'SET_FREEFORM_FALLBACK_BLOCK_NAME' ); +export const unregisteredFallbackBlockName = createBlockNameSetterReducer( 'SET_UNREGISTERED_FALLBACK_BLOCK_NAME' ); /** * Reducer managing the categories @@ -90,6 +91,7 @@ export function categories( state = DEFAULT_CATEGORIES, action ) { export default combineReducers( { blockTypes, defaultBlockName, - fallbackBlockName, + freeformFallbackBlockName, + unregisteredFallbackBlockName, categories, } ); diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 92950ffe43a11..ee8005fd9be6e 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -4,6 +4,11 @@ import createSelector from 'rememo'; import { filter, get, includes, map, some } from 'lodash'; +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + /** * Returns all the available block types. * @@ -60,7 +65,34 @@ export function getDefaultBlockName( state ) { * @return {string?} Fallback block name. */ export function getFallbackBlockName( state ) { - return state.fallbackBlockName; + deprecated( 'getFallbackBlockName', { + plugin: 'Gutenberg', + version: '4.2', + alternative: 'getFreeformFallbackBlockName and getUnregisteredFallbackBlockName', + } ); + return getFreeformFallbackBlockName( state ); +} + +/** + * Returns the name of the block for handling non-block content. + * + * @param {Object} state Data state. + * + * @return {string?} Name of the block for handling non-block content. + */ +export function getFreeformFallbackBlockName( state ) { + return state.freeformFallbackBlockName; +} + +/** + * Returns the name of the block for handling unregistered blocks. + * + * @param {Object} state Data state. + * + * @return {string?} Name of the block for handling unregistered blocks. + */ +export function getUnregisteredFallbackBlockName( state ) { + return state.unregisteredFallbackBlockName; } /** diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index ee970d13c15b8..9c3e6c5d1d1dc 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -6,7 +6,14 @@ import deepFreeze from 'deep-freeze'; /** * Internal dependencies */ -import { blockTypes, categories, defaultBlockName, fallbackBlockName, DEFAULT_CATEGORIES } from '../reducer'; +import { + blockTypes, + categories, + defaultBlockName, + freeformFallbackBlockName, + unregisteredFallbackBlockName, + DEFAULT_CATEGORIES, +} from '../reducer'; describe( 'blockTypes', () => { it( 'should return an empty object as default state', () => { @@ -60,7 +67,7 @@ describe( 'defaultBlockName', () => { expect( state ).toBe( 'core/paragraph' ); } ); - it( 'should reset the fallback block name', () => { + it( 'should reset the default block name', () => { const state = defaultBlockName( 'core/code', { type: 'REMOVE_BLOCK_TYPES', names: [ 'core/code' ], @@ -70,22 +77,46 @@ describe( 'defaultBlockName', () => { } ); } ); -describe( 'fallbackBlockName', () => { +describe( 'freeformFallbackBlockName', () => { it( 'should return null as default state', () => { - expect( fallbackBlockName( undefined, {} ) ).toBeNull(); + expect( freeformFallbackBlockName( undefined, {} ) ).toBeNull(); } ); - it( 'should set the fallback block name', () => { - const state = fallbackBlockName( null, { - type: 'SET_FALLBACK_BLOCK_NAME', + it( 'should set the freeform content fallback block name', () => { + const state = freeformFallbackBlockName( null, { + type: 'SET_FREEFORM_FALLBACK_BLOCK_NAME', name: 'core/paragraph', } ); expect( state ).toBe( 'core/paragraph' ); } ); - it( 'should reset the fallback block name', () => { - const state = fallbackBlockName( 'core/code', { + it( 'should reset the freeform content fallback block name', () => { + const state = freeformFallbackBlockName( 'core/code', { + type: 'REMOVE_BLOCK_TYPES', + names: [ 'core/code' ], + } ); + + expect( state ).toBeNull(); + } ); +} ); + +describe( 'unregisteredFallbackBlockName', () => { + it( 'should return null as default state', () => { + expect( unregisteredFallbackBlockName( undefined, {} ) ).toBeNull(); + } ); + + it( 'should set the unregistered fallback block name', () => { + const state = unregisteredFallbackBlockName( null, { + type: 'SET_UNREGISTERED_FALLBACK_BLOCK_NAME', + name: 'core/paragraph', + } ); + + expect( state ).toBe( 'core/paragraph' ); + } ); + + it( 'should reset the unregistered fallback block name', () => { + const state = unregisteredFallbackBlockName( 'core/code', { type: 'REMOVE_BLOCK_TYPES', names: [ 'core/code' ], } ); diff --git a/packages/editor/src/components/block-inspector/index.js b/packages/editor/src/components/block-inspector/index.js index e2ec77d1ed00d..858d9b925aefd 100644 --- a/packages/editor/src/components/block-inspector/index.js +++ b/packages/editor/src/components/block-inspector/index.js @@ -7,7 +7,7 @@ import { isEmpty } from 'lodash'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { getBlockType } from '@wordpress/blocks'; +import { getBlockType, getUnregisteredTypeHandlerName } from '@wordpress/blocks'; import { PanelBody } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; @@ -24,7 +24,14 @@ const BlockInspector = ( { selectedBlock, blockType, count } ) => { return { __( 'Coming Soon' ) }; } - if ( ! selectedBlock ) { + const isSelectedBlockUnregistered = + !! selectedBlock && selectedBlock.name === getUnregisteredTypeHandlerName(); + + /* + * If the selected block is of an unregistered type, avoid showing it as an actual selection + * because we want the user to focus on the unregistered block warning, not block settings. + */ + if ( ! selectedBlock || isSelectedBlockUnregistered ) { return { __( 'No block selected.' ) }; } diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index c70c7faeba47a..f415d8b79ff94 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -21,6 +21,7 @@ import { getSaveElement, isReusableBlock, isUnmodifiedDefaultBlock, + getUnregisteredTypeHandlerName, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -382,6 +383,8 @@ export class BlockListBlock extends Component { // The block as rendered in the editor is composed of general block UI // (mover, toolbar, wrapper) and the display of the block content. + const isUnregisteredBlock = block.name === getUnregisteredTypeHandlerName(); + // If the block is selected and we're typing the block should not appear. // Empty paragraph blocks should always show up as unselected. const showEmptyBlockSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; @@ -404,7 +407,7 @@ export class BlockListBlock extends Component { // Generate the wrapper class names handling the different states of the block. const wrapperClassName = classnames( 'editor-block-list__block', { - 'has-warning': ! isValid || !! error, + 'has-warning': ! isValid || !! error || isUnregisteredBlock, 'is-selected': shouldAppearSelected, 'is-multi-selected': isPartOfMultiSelection, 'is-selected-parent': shouldAppearSelectedParent, diff --git a/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js b/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js index f87149710455f..c3e5bc0ffe058 100644 --- a/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js +++ b/packages/editor/src/components/block-settings-menu/block-unknown-convert-button.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { getUnknownTypeHandlerName, rawHandler, serialize } from '@wordpress/blocks'; +import { getFreeformContentHandlerName, rawHandler, serialize } from '@wordpress/blocks'; import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -17,7 +17,7 @@ export default compose( return { block, canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), - shouldRender: ( block && block.name === getUnknownTypeHandlerName() ), + shouldRender: ( block && block.name === getFreeformContentHandlerName() ), }; } ), withDispatch( ( dispatch, { block, canUserUseUnfilteredHTML } ) => ( { diff --git a/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js b/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js index b3473238ece27..0f66f6b447edb 100644 --- a/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js +++ b/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js @@ -50,7 +50,10 @@ export function ReusableBlockConvertButton( { export default compose( [ withSelect( ( select, { clientIds } ) => { const { getBlock, getReusableBlock } = select( 'core/editor' ); - const { getFallbackBlockName } = select( 'core/blocks' ); + const { + getFreeformFallbackBlockName, + getUnregisteredFallbackBlockName, + } = select( 'core/blocks' ); const blocks = map( clientIds, ( clientId ) => getBlock( clientId ) ); @@ -58,7 +61,10 @@ export default compose( [ // confusing UX, because of its similarity to the 'Convert to Blocks' button. const isVisible = ( every( blocks, ( block ) => !! block ) && - ( blocks.length !== 1 || blocks[ 0 ].name !== getFallbackBlockName() ) + ( blocks.length !== 1 || ( + blocks[ 0 ].name !== getFreeformFallbackBlockName() && + blocks[ 0 ].name !== getUnregisteredFallbackBlockName() + ) ) ); return { diff --git a/packages/editor/src/components/inner-blocks/test/index.js b/packages/editor/src/components/inner-blocks/test/index.js index 1472b8817b9cd..6bca8a8d761ea 100644 --- a/packages/editor/src/components/inner-blocks/test/index.js +++ b/packages/editor/src/components/inner-blocks/test/index.js @@ -5,7 +5,6 @@ import { createBlock, getBlockType, getBlockTypes, - setUnknownTypeHandlerName, getSaveElement, registerBlockType, serialize, @@ -20,7 +19,6 @@ import InnerBlocks from '../'; describe( 'InnerBlocks', () => { afterEach( () => { - setUnknownTypeHandlerName( undefined ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index b50e09e5f2d2b..6a38f0d77ef91 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -29,7 +29,7 @@ import { getBlockTypes, hasBlockSupport, hasChildBlocksWithInserterSupport, - getUnknownTypeHandlerName, + getFreeformContentHandlerName, isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; import { moment } from '@wordpress/date'; @@ -1441,14 +1441,14 @@ export const getEditedPostContent = createSelector( const content = serialize( blocks ); // For compatibility purposes, treat a post consisting of a single - // unknown block as legacy content and downgrade to a pre-block-editor + // freeform block as legacy content and downgrade to a pre-block-editor // removep'd content format. - const isSingleUnknownBlock = ( + const isSingleFreeformBlock = ( blocks.length === 1 && - blocks[ 0 ].name === getUnknownTypeHandlerName() + blocks[ 0 ].name === getFreeformContentHandlerName() ); - if ( isSingleUnknownBlock ) { + if ( isSingleFreeformBlock ) { return removep( content ); } diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index c089ebccf2877..26386359345b0 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -13,8 +13,8 @@ import { getBlockTypes, getDefaultBlockName, setDefaultBlockName, - getUnknownTypeHandlerName, - setUnknownTypeHandlerName, + getFreeformContentHandlerName, + setFreeformContentHandlerName, } from '@wordpress/blocks'; import { moment } from '@wordpress/date'; import { RawHTML } from '@wordpress/element'; @@ -3022,11 +3022,11 @@ describe( 'selectors', () => { } ); describe( 'getEditedPostContent', () => { - let originalDefaultBlockName, originalUnknownTypeHandlerName; + let originalDefaultBlockName, originalFreeformContentHandlerName; beforeAll( () => { originalDefaultBlockName = getDefaultBlockName(); - originalUnknownTypeHandlerName = getUnknownTypeHandlerName(); + originalFreeformContentHandlerName = getFreeformContentHandlerName(); registerBlockType( 'core/default', { category: 'common', @@ -3050,12 +3050,12 @@ describe( 'selectors', () => { save: ( { attributes } ) => { attributes.html }, } ); setDefaultBlockName( 'core/default' ); - setUnknownTypeHandlerName( 'core/unknown' ); + setFreeformContentHandlerName( 'core/unknown' ); } ); afterAll( () => { setDefaultBlockName( originalDefaultBlockName ); - setUnknownTypeHandlerName( originalUnknownTypeHandlerName ); + setFreeformContentHandlerName( originalFreeformContentHandlerName ); getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); } ); @@ -3110,7 +3110,7 @@ describe( 'selectors', () => { } ); it( 'returns removep\'d serialization of blocks for single unknown', () => { - const unknownBlock = createBlock( getUnknownTypeHandlerName(), { + const unknownBlock = createBlock( getFreeformContentHandlerName(), { html: '

foo

', } ); const state = { @@ -3134,10 +3134,10 @@ describe( 'selectors', () => { } ); it( 'returns non-removep\'d serialization of blocks for multiple unknown', () => { - const firstUnknown = createBlock( getUnknownTypeHandlerName(), { + const firstUnknown = createBlock( getFreeformContentHandlerName(), { html: '

foo

', } ); - const secondUnknown = createBlock( getUnknownTypeHandlerName(), { + const secondUnknown = createBlock( getFreeformContentHandlerName(), { html: '

bar

', } ); const state = { diff --git a/test/integration/full-content/fixtures/core__missing.html b/test/integration/full-content/fixtures/core__missing.html new file mode 100644 index 0000000000000..a29cbdea3f633 --- /dev/null +++ b/test/integration/full-content/fixtures/core__missing.html @@ -0,0 +1,6 @@ + +

Testing missing block with some

+
+ HTML content +
+ diff --git a/test/integration/full-content/fixtures/core__missing.json b/test/integration/full-content/fixtures/core__missing.json new file mode 100644 index 0000000000000..4e90cf71328ce --- /dev/null +++ b/test/integration/full-content/fixtures/core__missing.json @@ -0,0 +1,14 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/missing", + "isValid": true, + "attributes": { + "originalContent": "\n

Testing missing block with some

\n
\n\tHTML content\n
\n", + "originalName": "unregistered/example", + "originalUndelimitedContent": "

Testing missing block with some

\n
\n\tHTML content\n
" + }, + "innerBlocks": [], + "originalContent": "\n

Testing missing block with some

\n
\n\tHTML content\n
\n" + } +] diff --git a/test/integration/full-content/fixtures/core__missing.parsed.json b/test/integration/full-content/fixtures/core__missing.parsed.json new file mode 100644 index 0000000000000..3466b8cf2ac41 --- /dev/null +++ b/test/integration/full-content/fixtures/core__missing.parsed.json @@ -0,0 +1,17 @@ +[ + { + "blockName": "unregistered/example", + "attrs": { + "attr1": "One", + "attr2": "Two" + }, + "innerBlocks": [], + "innerHTML": "\n

Testing missing block with some

\n
\n\tHTML content\n
\n" + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n" + } +] diff --git a/test/integration/full-content/fixtures/core__missing.serialized.html b/test/integration/full-content/fixtures/core__missing.serialized.html new file mode 100644 index 0000000000000..a29cbdea3f633 --- /dev/null +++ b/test/integration/full-content/fixtures/core__missing.serialized.html @@ -0,0 +1,6 @@ + +

Testing missing block with some

+
+ HTML content +
+