From d276d2dfdb4a5f183e86487aae7bdae7739e975a Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 29 Jul 2018 15:27:36 -0700 Subject: [PATCH] Add UI for unregistered block types --- core-blocks/index.js | 9 ++- core-blocks/missing/editor.scss | 5 ++ core-blocks/missing/index.js | 39 +++++++++++++ core-blocks/missing/missing-block-warning.js | 61 ++++++++++++++++++++ packages/blocks/src/api/index.js | 4 ++ packages/blocks/src/api/parser.js | 42 +++++++++----- packages/blocks/src/api/registration.js | 43 ++++++++++++++ packages/blocks/src/api/serializer.js | 11 +++- packages/blocks/src/store/actions.js | 40 +++++++++++++ packages/blocks/src/store/reducer.js | 5 ++ packages/blocks/src/store/selectors.js | 22 +++++++ 11 files changed, 264 insertions(+), 17 deletions(-) create mode 100644 core-blocks/missing/editor.scss create mode 100644 core-blocks/missing/index.js create mode 100644 core-blocks/missing/missing-block-warning.js diff --git a/core-blocks/index.js b/core-blocks/index.js index a7bc40d32de63..e07fb5222b609 100644 --- a/core-blocks/index.js +++ b/core-blocks/index.js @@ -4,7 +4,8 @@ import { registerBlockType, setDefaultBlockName, - setUnknownTypeHandlerName, + setNonblockHandlerName, + setUnregisteredTypeHandlerName, } from '@wordpress/blocks'; /** @@ -31,6 +32,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'; @@ -74,6 +76,7 @@ export const registerCoreBlocks = () => { html, latestComments, latestPosts, + missing, more, nextpage, preformatted, @@ -91,5 +94,7 @@ export const registerCoreBlocks = () => { } ); setDefaultBlockName( paragraph.name ); - setUnknownTypeHandlerName( freeform.name ); + setNonblockHandlerName( freeform.name ); + // TODO: Consider renaming "Unregistered" to missing + setUnregisteredTypeHandlerName( missing.name ); }; diff --git a/core-blocks/missing/editor.scss b/core-blocks/missing/editor.scss new file mode 100644 index 0000000000000..2fa929b0edd00 --- /dev/null +++ b/core-blocks/missing/editor.scss @@ -0,0 +1,5 @@ +.editor-block-list__block[data-type="core/missing"] { + .editor-warning { + position: static; + } +} diff --git a/core-blocks/missing/index.js b/core-blocks/missing/index.js new file mode 100644 index 0000000000000..b794edb902826 --- /dev/null +++ b/core-blocks/missing/index.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { RawHTML } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import MissingBlockWarning from './missing-block-warning'; +import './editor.scss'; + +export const name = 'core/missing'; + +export const settings = { + name, + category: 'common', + title: __( 'Missing Block' ), + + supports: { + className: false, + customClassName: false, + inserter: false, + html: false, + preserveOriginalContent: true, + }, + + attributes: { + originalContent: { + type: 'string', + source: 'html', + }, + }, + + edit: MissingBlockWarning, + save( { attributes } ) { + return { attributes.originalContent }; + }, +}; diff --git a/core-blocks/missing/missing-block-warning.js b/core-blocks/missing/missing-block-warning.js new file mode 100644 index 0000000000000..719d492ae3e35 --- /dev/null +++ b/core-blocks/missing/missing-block-warning.js @@ -0,0 +1,61 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { getBlockType, createBlock } from '@wordpress/blocks'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { Warning } from '@wordpress/editor'; + +export const name = 'core/unknown'; + +export function MissingBlockWarning( { block, convertToHTML } ) { + const hasContent = !! block.originalUndelimitedContent; + const hasHTMLBlock = getBlockType( 'core/html' ); + + const actions = []; + let messageHTML; + if ( hasContent && hasHTMLBlock ) { + actions.push( + + ); + messageHTML = sprintf( + __( 'Your site doesn\'t include support for the %s block. You can leave the block intact, convert its content to a Custom HTML block, or remove it entirely.' ), + block.originalName + ); + } else { + messageHTML = sprintf( + __( 'Your site doesn\'t include support for the %s block. You can leave the block intact or remove it entirely.' ), + block.originalName + ); + } + + return ( + + + + ); +} + +export default compose( [ + withSelect( ( select, { clientId } ) => { + const { getBlock } = select( 'core/editor' ); + return { + block: getBlock( clientId ), + }; + } ), + withDispatch( ( dispatch, { block } ) => { + const { replaceBlock } = dispatch( 'core/editor' ); + return { + convertToHTML() { + replaceBlock( block.clientId, createBlock( 'core/html', { + content: block.originalUndelimitedContent, + } ) ); + }, + }; + } ), +] )( MissingBlockWarning ); + diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 4b77d4a00b788..9c32396cf5e10 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -30,6 +30,10 @@ export { unregisterBlockType, setUnknownTypeHandlerName, getUnknownTypeHandlerName, + setNonblockHandlerName, + getNonblockHandlerName, + setUnregisteredTypeHandlerName, + getUnregisteredTypeHandlerName, setDefaultBlockName, getDefaultBlockName, getDefaultBlockForPostFormat, diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index 5f2ebf531f169..7a740b10f3cc8 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -15,7 +15,12 @@ import { parse as grammarParse } from '@wordpress/block-serialization-spec-parse /** * Internal dependencies */ -import { getBlockType, getUnknownTypeHandlerName } from './registration'; +import { + getBlockType, + getUnknownTypeHandlerName, + getNonblockHandlerName, + getUnregisteredTypeHandlerName, +} from './registration'; import { createBlock } from './factory'; import { isValidBlock } from './validation'; import { getCommentDelimitedContent } from './serializer'; @@ -284,46 +289,48 @@ 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 fallbackBlock = getUnknownTypeHandlerName(); + const nonblockFallbackBlock = getNonblockHandlerName(); + const unregisteredFallbackBlock = getUnregisteredTypeHandlerName(); 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(); + let name = originalName || nonblockFallbackBlock || fallbackBlock; // 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 === nonblockFallbackBlock || name === fallbackBlock ) { 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 missing type handler. if ( name ) { innerHTML = getCommentDelimitedContent( name, attributes, innerHTML ); } - name = fallbackBlock; + name = unregisteredFallbackBlock || fallbackBlock; + attributes = {}; blockType = getBlockType( name ); } @@ -331,7 +338,10 @@ export function createBlockWithFallback( blockNode ) { innerBlocks = innerBlocks.map( createBlockWithFallback ); // Include in set only if type were determined. - if ( ! blockType || ( ! innerHTML && name === fallbackBlock ) ) { + if ( + ! blockType || + ( ! innerHTML && ( name === nonblockFallbackBlock || name === fallbackBlock ) ) + ) { return; } @@ -349,6 +359,12 @@ export function createBlockWithFallback( blockNode ) { block.isValid = isValidBlock( innerHTML, blockType, block.attributes ); } + // TODO: See if there is a better way to pass this information. + if ( name === unregisteredFallbackBlock ) { + block.originalName = originalName; + block.originalUndelimitedContent = originalUndelimitedContent; + } + // Preserve original content for future use in case the block is parsed as // invalid, or future serialization attempt results in an error. block.originalContent = innerHTML; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 62d4a7c29e127..d5166dd36ab6b 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -198,6 +198,11 @@ export function unregisterBlockType( name ) { * @param {string} name Block name. */ export function setUnknownTypeHandlerName( name ) { + deprecated( 'setUnknownTypeHandlerName', { + plugin: 'Gutenberg', + version: '3.7', + alternative: 'setNonblockHandlerName and setUnregisteredTypeHandlerName', + } ); dispatch( 'core/blocks' ).setFallbackBlockName( name ); } @@ -211,6 +216,44 @@ export function getUnknownTypeHandlerName() { return select( 'core/blocks' ).getFallbackBlockName(); } +/** + * Assigns name of block handling unknown block types. + * + * @param {string} name Block name. + */ +export function setNonblockHandlerName( name ) { + dispatch( 'core/blocks' ).setNonblockFallbackBlockName( name ); +} + +/** + * Retrieves name of block handling unknown block types, or undefined if no + * handler has been defined. + * + * @return {?string} Blog name. + */ +export function getNonblockHandlerName() { + return select( 'core/blocks' ).getNonblockFallbackBlockName(); +} + +/** + * Assigns name of block handling unknown block types. + * + * @param {string} name Block name. + */ +export function setUnregisteredTypeHandlerName( name ) { + dispatch( 'core/blocks' ).setUnregisteredFallbackBlockName( name ); +} + +/** + * Retrieves name of block handling unknown block types, or undefined if no + * handler has been defined. + * + * @return {?string} Blog name. + */ +export function getUnregisteredTypeHandlerName() { + return select( 'core/blocks' ).getUnregisteredFallbackBlockName(); +} + /** * Assigns the default block name. * diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index d68a1a495446f..176912fb5679f 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -13,7 +13,12 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ -import { getBlockType, getUnknownTypeHandlerName } from './registration'; +import { + getBlockType, + getUnknownTypeHandlerName, + getNonblockHandlerName, + getUnregisteredTypeHandlerName, +} from './registration'; import BlockContentProvider from '../block-content-provider'; /** @@ -205,7 +210,7 @@ export function getBlockContent( block ) { // otherwise have no access to its original content and content loss would // still occur. let saveContent = block.originalContent; - if ( block.isValid || block.innerBlocks.length ) { + if ( blockType && ( block.isValid || block.innerBlocks.length ) ) { try { saveContent = getSaveContent( blockType, block.attributes, block.innerBlocks ); } catch ( error ) {} @@ -261,6 +266,8 @@ export function serializeBlock( block ) { const saveAttributes = getCommentAttributes( block.attributes, blockType ); switch ( blockName ) { + case getNonblockHandlerName(): + case getUnregisteredTypeHandlerName(): case getUnknownTypeHandlerName(): return saveContent; diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 37bcaa2e5cba1..1da1a02cbd3d8 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. * @@ -53,12 +58,47 @@ export function setDefaultBlockName( name ) { * @return {Object} Action object. */ export function setFallbackBlockName( name ) { + deprecated( 'setFallbackBlockName', { + plugin: 'Gutenberg', + version: '3.7', + alternative: 'setNonblockFallbackBlockName and setUnregisteredFallbackBlockName', + } ); return { type: 'SET_FALLBACK_BLOCK_NAME', name, }; } +/** + * Returns an action object used to said the name of the block used as a fallback + * for non-block content. + * + * @param {string} name Block name. + * + * @return {Object} Action object. + */ +export function setNonblockFallbackBlockName( name ) { + return { + type: 'SET_NONBLOCK_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_UNREGISTERED_FALLBACK_BLOCK_NAME', + name, + }; +} + /** * Returns an action object used to set block categories. * diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 664520e40a205..4cb31aaaa99f4 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -71,6 +71,9 @@ export const defaultBlockName = createBlockNameSetterReducer( 'SET_DEFAULT_BLOCK export const fallbackBlockName = createBlockNameSetterReducer( 'SET_FALLBACK_BLOCK_NAME' ); +export const nonblockFallbackBlockName = createBlockNameSetterReducer( 'SET_NONBLOCK_FALLBACK_BLOCK_NAME' ); +export const unregisteredFallbackBlockName = createBlockNameSetterReducer( 'SET_UNREGISTERED_FALLBACK_BLOCK_NAME' ); + /** * Reducer managing the categories * @@ -91,5 +94,7 @@ export default combineReducers( { blockTypes, defaultBlockName, fallbackBlockName, + nonblockFallbackBlockName, + unregisteredFallbackBlockName, categories, } ); diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index bf6541ae3a195..7549532859ccc 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -63,6 +63,28 @@ export function getFallbackBlockName( state ) { return state.fallbackBlockName; } +/** + * 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 getNonblockFallbackBlockName( state ) { + return state.nonblockFallbackBlockName; +} + +/** + * 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; +} + /** * Returns an array with the child blocks of a given block. *