Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI for unregistered block types #8274

Merged
merged 23 commits into from
Oct 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7c46017
Add block to represent unregistered block types
brandonpayton Jul 29, 2018
168c2a6
Make this feel more like a message than a block
brandonpayton Aug 27, 2018
612e0ca
Add a Remove Block button
brandonpayton Aug 27, 2018
c5657e8
Try to clarify block sidebar
brandonpayton Aug 27, 2018
1220290
Remove unused supports flag
brandonpayton Aug 27, 2018
fd87465
Fix removal for content-free, unregistered block
brandonpayton Aug 28, 2018
a889eea
Avoid showing block settings for unregistered blocks
brandonpayton Aug 28, 2018
209cbdf
Stop outline transition flicker
brandonpayton Aug 28, 2018
48774ba
Switch back to removing via block menu
brandonpayton Sep 5, 2018
a386f5b
Disallow making missing blocks reusable
brandonpayton Sep 5, 2018
2adff87
Update deprecation target
brandonpayton Sep 5, 2018
840e4df
Additional cleanup
brandonpayton Sep 6, 2018
dda63ed
Improve naming of freeform fallback
brandonpayton Sep 6, 2018
693b5f0
Add missing reducer unit tests
brandonpayton Oct 8, 2018
9e666ad
Remove cruft left by missing block iteration
brandonpayton Oct 8, 2018
fe43ce2
Clean up deprecation docs and control flow
brandonpayton Oct 8, 2018
4dabc26
Stop using deprecated getFallbackBlockName
brandonpayton Oct 8, 2018
dec455b
Clarify missing block case in block inspector
brandonpayton Oct 8, 2018
8207914
Treat unregistered block case as a warning
brandonpayton Oct 8, 2018
2712de7
Update unit tests
brandonpayton Oct 8, 2018
bf89150
Lower deprecation version to 4.2
mcsf Oct 12, 2018
2cd0f8e
chore: Build docs
mcsf Oct 12, 2018
79222f4
Minor refactor (isFallbackBlock); wrap lines
mcsf Oct 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion docs/data/data-core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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*

Expand Down
2 changes: 2 additions & 0 deletions docs/reference/deprecated.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import '@wordpress/core-data';
import {
registerBlockType,
setDefaultBlockName,
setUnknownTypeHandlerName,
setFreeformContentHandlerName,
setUnregisteredTypeHandlerName,
} from '@wordpress/blocks';

/**
Expand All @@ -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';
Expand Down Expand Up @@ -76,6 +78,7 @@ export const registerCoreBlocks = () => {
html,
latestComments,
latestPosts,
missing,
more,
nextpage,
preformatted,
Expand All @@ -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 );
};
91 changes: 91 additions & 0 deletions packages/block-library/src/missing/index.js
Original file line number Diff line number Diff line change
@@ -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 <code>%s</code> block. You can leave this block intact, convert its content to a Custom HTML block, or remove it entirely.' ),
originalName
);
actions.push(
<Button key="convert" onClick={ convertToHTML } isLarge isPrimary>
{ __( 'Keep as HTML' ) }
</Button>
);
} else {
messageHTML = sprintf(
__( 'Your site doesn\'t include support for the <code>%s</code> block. You can leave this block intact or remove it entirely.' ),
originalName
);
}

return (
<Fragment>
<Warning actions={ actions }>
<span dangerouslySetInnerHTML={ { __html: messageHTML } } />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, dangerous is actually dangerous. Localized strings should be considered equivalent to user input for sanitization purposes.

since translations should not be considered trusted strings, be sure to sanitize the result before echoing.

https://codex.wordpress.org/I18n_for_WordPress_Developers#HTML

Related: #9846

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang, I missed this one. Yes.

Since the only markup in the string is the <code> tag, we can just drop it. I'll start a branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> #10626

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks very much for catching this, @aduth, and for alerting me to the need for care here.

@mcsf, thank you for the fix. <3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since translations should not be considered trusted strings, be sure to sanitize the result before echoing.

FWIW this isn't entirely accurate. WordPress.org treats stranslations as trusted strings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this isn't entirely accurate. WordPress.org treats stranslations as trusted strings.

Is the Codex wrong then? Or is the article not applicable for core development?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both I guess :-)

</Warning>
<div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what reason do we need the div?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't necessary. I initially misunderstood how <RawHTML> worked, but see it adds its own div. I can fix this as part of the PR mentioned above or independently.

<RawHTML>{ originalUndelimitedContent }</RawHTML>
</div>
</Fragment>
);
}

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 <RawHTML>{ attributes.originalContent }</RawHTML>;
},
};
4 changes: 4 additions & 0 deletions packages/blocks/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export {
unregisterBlockType,
setUnknownTypeHandlerName,
getUnknownTypeHandlerName,
setFreeformContentHandlerName,
getFreeformContentHandlerName,
setUnregisteredTypeHandlerName,
getUnregisteredTypeHandlerName,
setDefaultBlockName,
getDefaultBlockName,
getBlockType,
Expand Down
43 changes: 27 additions & 16 deletions packages/blocks/src/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This initially struck me as odd, but upon testing the actual behavior, it's not bad to default to Freeform if Unregistered is not present. 👍 (Although, who'd have one and not the other?)


attributes = attributes || {};

// Trim content to avoid creation of intermediary freeform segments.
innerHTML = innerHTML.trim();
const originalUndelimitedContent = innerHTML = innerHTML.trim();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too clever. I'm still not totally sure I understand why / what we're doing with this line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @aduth. The intent wasn't cleverness, but I see what you mean. We want innerHTML to be trimmed, and saving its value at that point is a separate concern.

The missing block needs the original undelimited content for determining whether the unregistered block has actual HTML content, for rendering a preview of the block HTML, and for converting to an HTML block. originalUndelimitedContent exists because this pre-existing line changes the value of innerHTML to be the delimited HTML content. The delimited content is used in saving the value of the block.

The above approach, which I am responsible for, is a bit weird IMO. It would be more straightforward to:

  1. Get rid of originalUndelimitedContent.
  2. Stop overwriting innerHTML with the delimited content.
  3. Add an originalAttributes attribute to core/missing.
  4. Update core/missing to call getCommentDelimitedContent with originalName, originalAttributes and originalContent to create the save content.

If you agree this is cleaner, I can create a follow-up PR to make the change.

Copy link
Member

@aduth aduth Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't have much of a problem with originalUndelimitedContent, I just think we could be clearer / more verbose with how we assign it / document its intent.

Also, I think with this type of assignment we're inadvertently assigning innerHTML as global outside the scope of the function:

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't have much of a problem with originalUndelimitedContent, I just think we could be clearer / more verbose with how we assign it / document its intent.

✅ thanks
I don't love originalUndelimitedContent but plan to leave it after documenting its purpose and separating the assignment.

I don't believe this assignment should be creating a global since innerHTML is declared in the function scope here.


// 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;
}

Expand All @@ -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 );
}

Expand Down
53 changes: 51 additions & 2 deletions packages/blocks/src/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 );
}

/**
Expand All @@ -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();
}

/**
Expand Down
Loading