From d276d2dfdb4a5f183e86487aae7bdae7739e975a Mon Sep 17 00:00:00 2001
From: Brandon Payton <brandon@happycode.net>
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 a7bc40d32de637..e07fb5222b6097 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 00000000000000..2fa929b0edd002
--- /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 00000000000000..b794edb902826a
--- /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 <RawHTML>{ attributes.originalContent }</RawHTML>;
+	},
+};
diff --git a/core-blocks/missing/missing-block-warning.js b/core-blocks/missing/missing-block-warning.js
new file mode 100644
index 00000000000000..719d492ae3e355
--- /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(
+			<Button key="convert" onClick={ convertToHTML } isLarge isPrimary>
+				{ __( 'HTML Block' ) }
+			</Button>
+		);
+		messageHTML = sprintf(
+			__( 'Your site doesn\'t include support for the <code>%s</code> 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 <code>%s</code> block. You can leave the block intact or remove it entirely.' ),
+			block.originalName
+		);
+	}
+
+	return (
+		<Warning actions={ actions }>
+			<span dangerouslySetInnerHTML={ { __html: messageHTML } } />
+		</Warning>
+	);
+}
+
+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 4b77d4a00b7883..9c32396cf5e10a 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 5f2ebf531f1696..7a740b10f3cc89 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 62d4a7c29e1271..d5166dd36ab6b4 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 d68a1a495446fe..176912fb5679f0 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 37bcaa2e5cba14..1da1a02cbd3d87 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 664520e40a2054..4cb31aaaa99f46 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 bf6541ae3a1950..7549532859cccc 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.
  *