Skip to content

Commit

Permalink
Block Templates: Check the validity of the block list against the def…
Browse files Browse the repository at this point in the history
…ined template (#5162)
  • Loading branch information
youknowriad authored Mar 19, 2018
1 parent d189d54 commit d64de0f
Show file tree
Hide file tree
Showing 16 changed files with 549 additions and 28 deletions.
4 changes: 4 additions & 0 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export {
export {
isUnmodifiedDefaultBlock,
} from './utils';
export {
doBlocksMatchTemplate,
synchronizeBlocksWithTemplate,
} from './templates';
60 changes: 60 additions & 0 deletions blocks/api/templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import { every, map } from 'lodash';

/**
* Internal dependencies
*/
import { createBlock } from './factory';

/**
* Checks whether a list of blocks matches a template by comparing the block names.
*
* @param {Array} blocks Block list.
* @param {Array} template Block template.
*
* @return {boolean} Whether the list of blocks matches a templates
*/
export function doBlocksMatchTemplate( blocks = [], template = [] ) {
return (
blocks.length === template.length &&
every( template, ( [ name, , innerBlocksTemplate ], index ) => {
const block = blocks[ index ];
return (
name === block.name &&
doBlocksMatchTemplate( block.innerBlocks, innerBlocksTemplate )
);
} )
);
}

/**
* Synchronize a block list with a block template.
*
* Synchronnizing a block list with a block template means that we loop over the blocks
* keep the block as is if it matches the block at the same position in the template
* (If it has the same name) and if doesn't match, we create a new block based on the template.
* Extra blocks not present in the template are removed.
*
* @param {Array} blocks Block list.
* @param {Array} template Block template.
*
* @return {Array} Updated Block list.
*/
export function synchronizeBlocksWithTemplate( blocks = [], template = [] ) {
return map( template, ( [ name, attributes, innerBlocksTemplate ], index ) => {
const block = blocks[ index ];

if ( block && block.name === name ) {
const innerBlocks = synchronizeBlocksWithTemplate( block.innerBlocks, innerBlocksTemplate );
return { ...block, innerBlocks };
}

return createBlock(
name,
attributes,
synchronizeBlocksWithTemplate( [], innerBlocksTemplate )
);
} );
}
177 changes: 177 additions & 0 deletions blocks/api/test/templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* External dependencies
*/
import { noop } from 'lodash';

/**
* Internal dependencies
*/
import { createBlock } from '../factory';
import { getBlockTypes, unregisterBlockType, registerBlockType } from '../registration';
import { doBlocksMatchTemplate, synchronizeBlocksWithTemplate } from '../templates';

describe( 'templates', () => {
afterEach( () => {
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );

beforeEach( () => {
registerBlockType( 'core/test-block', {
attributes: {},
save: noop,
category: 'common',
title: 'test block',
} );

registerBlockType( 'core/test-block-2', {
attributes: {},
save: noop,
category: 'common',
title: 'test block',
} );
} );

describe( 'doBlocksMatchTemplate', () => {
it( 'return true if for empty templates and blocks', () => {
expect( doBlocksMatchTemplate() ).toBe( true );
} );

it( 'return true if the template matches the blocks', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2' ],
[ 'core/test-block-2' ],
];
const blockList = [
createBlock( 'core/test-block' ),
createBlock( 'core/test-block-2' ),
createBlock( 'core/test-block-2' ),
];
expect( doBlocksMatchTemplate( blockList, template ) ).toBe( true );
} );

it( 'return true if the template matches the blocks with nested blocks', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2', {}, [
[ 'core/test-block' ],
] ],
[ 'core/test-block-2' ],
];
const blockList = [
createBlock( 'core/test-block' ),
createBlock( 'core/test-block-2', {}, [ createBlock( 'core/test-block' ) ] ),
createBlock( 'core/test-block-2' ),
];
expect( doBlocksMatchTemplate( blockList, template ) ).toBe( true );
} );

it( 'return false if the template length doesn\'t match the blocks length', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2' ],
];
const blockList = [
createBlock( 'core/test-block' ),
createBlock( 'core/test-block-2' ),
createBlock( 'core/test-block-2' ),
];
expect( doBlocksMatchTemplate( blockList, template ) ).toBe( false );
} );

it( 'return false if the nested template doesn\'t match the blocks', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2', {}, [
[ 'core/test-block' ],
] ],
[ 'core/test-block-2' ],
];
const blockList = [
createBlock( 'core/test-block' ),
createBlock( 'core/test-block-2', {}, [ createBlock( 'core/test-block-2' ) ] ),
createBlock( 'core/test-block-2' ),
];
expect( doBlocksMatchTemplate( blockList, template ) ).toBe( false );
} );
} );

describe( 'synchronizeBlocksWithTemplate', () => {
it( 'should create blocks for each template entry', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2' ],
[ 'core/test-block-2' ],
];
const blockList = [];
expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
{ name: 'core/test-block' },
{ name: 'core/test-block-2' },
{ name: 'core/test-block-2' },
] );
} );

it( 'should create nested blocks', () => {
const template = [
[ 'core/test-block', {}, [
[ 'core/test-block-2' ],
] ],
];
const blockList = [];
expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
{ name: 'core/test-block', innerBlocks: [
{ name: 'core/test-block-2' },
] },
] );
} );

it( 'should append blocks if more blocks in the template', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2' ],
[ 'core/test-block-2' ],
];

const block1 = createBlock( 'core/test-block' );
const block2 = createBlock( 'core/test-block-2' );
const blockList = [ block1, block2 ];
expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
block1,
block2,
{ name: 'core/test-block-2' },
] );
} );

it( 'should replace blocks if not matching blocks are found', () => {
const template = [
[ 'core/test-block' ],
[ 'core/test-block-2' ],
[ 'core/test-block-2' ],
];

const block1 = createBlock( 'core/test-block' );
const block2 = createBlock( 'core/test-block' );
const blockList = [ block1, block2 ];
expect( synchronizeBlocksWithTemplate( blockList, template ) ).toMatchObject( [
block1,
{ name: 'core/test-block-2' },
{ name: 'core/test-block-2' },
] );
} );

it( 'should remove blocks if extra blocks are found', () => {
const template = [
[ 'core/test-block' ],
];

const block1 = createBlock( 'core/test-block' );
const block2 = createBlock( 'core/test-block' );
const blockList = [ block1, block2 ];
expect( synchronizeBlocksWithTemplate( blockList, template ) ).toEqual( [
block1,
] );
} );
} );
} );
8 changes: 4 additions & 4 deletions components/notice/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import { __ } from '@wordpress/i18n';
*/
import './style.scss';

function Notice( { status, content, onRemove = noop, isDismissible = true } ) {
const className = classnames( 'notice notice-alt notice-' + status, {
function Notice( { className, status, children, onRemove = noop, isDismissible = true } ) {
const classNames = classnames( className, 'notice notice-alt notice-' + status, {
'is-dismissible': isDismissible,
} );
return (
<div className={ className }>
{ isString( content ) ? <p>{ content }</p> : content }
<div className={ classNames }>
{ isString( children ) ? <p>{ children }</p> : children }
{ isDismissible && (
<button className="notice-dismiss" type="button" onClick={ onRemove }>
<span className="screen-reader-text">{ __( 'Dismiss this notice' ) }</span>
Expand Down
9 changes: 6 additions & 3 deletions components/notice/list.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
/**
* External depednencies
*/
import { noop } from 'lodash';
import { noop, omit } from 'lodash';

/**
* Internal dependencies
*/
import Notice from './';

function NoticeList( { notices, onRemove = noop } ) {
function NoticeList( { notices, onRemove = noop, children } ) {
const removeNotice = ( id ) => () => onRemove( id );

return (
<div className="components-notice-list">
{ children }
{ [ ...notices ].reverse().map( ( notice ) => (
<Notice { ...notice } key={ notice.id } onRemove={ removeNotice( notice.id ) } />
<Notice { ...omit( notice, 'content' ) } key={ notice.id } onRemove={ removeNotice( notice.id ) }>
{ notice.content }
</Notice>
) ) }
</div>
);
Expand Down
11 changes: 10 additions & 1 deletion editor/components/editor-notices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@ import { NoticeList } from '@wordpress/components';
*/
import { removeNotice } from '../../store/actions';
import { getNotices } from '../../store/selectors';
import TemplateValidationNotice from '../template-validation-notice';

function EditorNotices( props ) {
return (
<NoticeList { ...props }>
<TemplateValidationNotice />
</NoticeList>
);
}

export default connect(
( state ) => ( {
notices: getNotices( state ),
} ),
{ onRemove: removeNotice }
)( NoticeList );
)( EditorNotices );
7 changes: 5 additions & 2 deletions editor/components/post-text-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { parse } from '@wordpress/blocks';
*/
import './style.scss';
import { getEditedPostContent } from '../../store/selectors';
import { editPost, resetBlocks } from '../../store/actions';
import { editPost, resetBlocks, checkTemplateValidity } from '../../store/actions';

class PostTextEditor extends Component {
constructor( props ) {
Expand Down Expand Up @@ -68,7 +68,10 @@ export default connect(
return editPost( { content } );
},
onPersist( content ) {
return resetBlocks( parse( content ) );
return [
resetBlocks( parse( content ) ),
checkTemplateValidity(),
];
},
}
)( PostTextEditor );
50 changes: 50 additions & 0 deletions editor/components/template-validation-notice/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';

/**
* WordPress dependencies
*/
import { Notice, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import './style.scss';
import { isValidTemplate } from '../../store/selectors';
import { setTemplateValidity, synchronizeTemplate } from '../../store/actions';

function TemplateValidationNotice( { isValid, ...props } ) {
if ( isValid ) {
return null;
}

const confirmSynchronization = () => {
// eslint-disable-next-line no-alert
if ( window.confirm( __( 'Resetting the template may result in loss of content, do you want to continue?' ) ) ) {
props.synchronizeTemplate();
}
};

return (
<Notice className="editor-template-validation-notice" isDismissible={ false } status="warning">
<p>{ __( 'The content of your post doesn\'t match the template assigned to your post type.' ) }</p>
<div>
<Button className="button" onClick={ props.resetTemplateValidity }>{ __( 'Keep it as is' ) }</Button>
<Button onClick={ confirmSynchronization } isPrimary>{ __( 'Reset the template' ) }</Button>
</div>
</Notice>
);
}

export default connect(
( state ) => ( {
isValid: isValidTemplate( state ),
} ),
{
resetTemplateValidity: () => setTemplateValidity( true ),
synchronizeTemplate,
}
)( TemplateValidationNotice );
9 changes: 9 additions & 0 deletions editor/components/template-validation-notice/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.editor-template-validation-notice {
display: flex;
justify-content: space-between;
align-items: center;

.components-button {
margin-left: 5px;
}
}
Loading

0 comments on commit d64de0f

Please sign in to comment.