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

Editor: Consider single unmodified default block as empty content #9808

Merged
merged 2 commits into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 54 additions & 7 deletions packages/editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ import createSelector from 'rememo';
/**
* WordPress dependencies
*/
import { serialize, getBlockType, getBlockTypes, hasBlockSupport, hasChildBlocksWithInserterSupport, getUnknownTypeHandlerName } from '@wordpress/blocks';
import {
serialize,
getBlockType,
getBlockTypes,
hasBlockSupport,
hasChildBlocksWithInserterSupport,
getUnknownTypeHandlerName,
isUnmodifiedDefaultBlock,
} from '@wordpress/blocks';
import { moment } from '@wordpress/date';
import { removep } from '@wordpress/autop';

Expand Down Expand Up @@ -349,15 +357,20 @@ export function isEditedPostSaveable( state ) {

/**
* Returns true if the edited post has content. A post has content if it has at
* least one block or otherwise has a non-empty content property assigned.
* least one saveable block or otherwise has a non-empty content property
* assigned.
*
* @param {Object} state Global application state.
*
* @return {boolean} Whether post has content.
*/
export function isEditedPostEmpty( state ) {
// While the condition of truthy content string would be sufficient for
// determining emptiness, testing saveable blocks length is a trivial
// operation by comparison. Since this function can be called frequently,
// optimize for the fast case where saveable blocks are non-empty.
return (
! getBlockCount( state ) &&
! getBlocksForSerialization( state ).length &&
! getEditedPostAttribute( state, 'content' )
);
}
Expand Down Expand Up @@ -1346,6 +1359,31 @@ export function getSuggestedPostFormat( state ) {
return null;
}

/**
* Returns a set of blocks which are to be used in consideration of the post's
* generated save content.
*
* @param {Object} state Editor state.
*
* @return {WPBlock[]} Filtered set of blocks for save.
*/
export function getBlocksForSerialization( state ) {
const blocks = getBlocks( state );

// A single unmodified default block is assumed to be equivalent to an
// empty post.
const isSingleUnmodifiedDefaultBlock = (
blocks.length === 1 &&
isUnmodifiedDefaultBlock( blocks[ 0 ] )
);

if ( isSingleUnmodifiedDefaultBlock ) {
return [];
}

return blocks;
}

/**
* Returns the content of the post being edited, preferring raw string edit
* before falling back to serialization of block state.
Expand All @@ -1361,13 +1399,22 @@ export const getEditedPostContent = createSelector(
return edits.content;
}

const blocks = getBlocks( state );
const blocks = getBlocksForSerialization( state );
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
// removep'd content format.
const isSingleUnknownBlock = (
blocks.length === 1 &&
blocks[ 0 ].name === getUnknownTypeHandlerName()
);

if ( blocks.length === 1 && blocks[ 0 ].name === getUnknownTypeHandlerName() ) {
return removep( serialize( blocks ) );
if ( isSingleUnknownBlock ) {
return removep( content );
}

return serialize( blocks );
return content;
},
( state ) => [
state.editor.present.edits.content,
Expand Down
214 changes: 208 additions & 6 deletions packages/editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@ import { filter, without } from 'lodash';
/**
* WordPress dependencies
*/
import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
import {
registerBlockType,
unregisterBlockType,
createBlock,
getBlockTypes,
getDefaultBlockName,
setDefaultBlockName,
getUnknownTypeHandlerName,
setUnknownTypeHandlerName,
} from '@wordpress/blocks';
import { moment } from '@wordpress/date';
import { RawHTML } from '@wordpress/element';

/**
* Internal dependencies
Expand Down Expand Up @@ -74,6 +84,7 @@ const {
didPostSaveRequestSucceed,
didPostSaveRequestFail,
getSuggestedPostFormat,
getEditedPostContent,
getNotices,
getReusableBlock,
isSavingReusableBlock,
Expand Down Expand Up @@ -101,6 +112,10 @@ describe( 'selectors', () => {
let cachedSelectors;

beforeAll( () => {
cachedSelectors = filter( selectors, ( selector ) => selector.clear );
} );

beforeEach( () => {
registerBlockType( 'core/block', {
save: () => null,
category: 'reusable',
Expand Down Expand Up @@ -138,14 +153,10 @@ describe( 'selectors', () => {
parent: [ 'core/test-block-b' ],
} );

cachedSelectors = filter( selectors, ( selector ) => selector.clear );
} );

beforeEach( () => {
cachedSelectors.forEach( ( { clear } ) => clear() );
} );

afterAll( () => {
afterEach( () => {
unregisterBlockType( 'core/block' );
unregisterBlockType( 'core/test-block-a' );
unregisterBlockType( 'core/test-block-b' );
Expand Down Expand Up @@ -2872,6 +2883,197 @@ describe( 'selectors', () => {
} );
} );

describe( 'getEditedPostContent', () => {
let originalDefaultBlockName, originalUnknownTypeHandlerName;

beforeAll( () => {
originalDefaultBlockName = getDefaultBlockName();
originalUnknownTypeHandlerName = getUnknownTypeHandlerName();

registerBlockType( 'core/default', {
category: 'common',
title: 'default',
attributes: {
modified: {
type: 'boolean',
default: false,
},
},
save: () => null,
} );
registerBlockType( 'core/unknown', {
category: 'common',
title: 'unknown',
attributes: {
html: {
type: 'string',
},
},
save: ( { attributes } ) => <RawHTML>{ attributes.html }</RawHTML>,
} );
setDefaultBlockName( 'core/default' );
setUnknownTypeHandlerName( 'core/unknown' );
} );

afterAll( () => {
setDefaultBlockName( originalDefaultBlockName );
setUnknownTypeHandlerName( originalUnknownTypeHandlerName );
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
} );

it( 'defers to returning an edited post attribute', () => {
const block = createBlock( 'core/block' );

const state = {
editor: {
present: {
blockOrder: {
'': [ block.clientId ],
},
blocksByClientId: {
[ block.clientId ]: block,
},
edits: {
content: 'custom edit',
},
},
},
currentPost: {},
};

const content = getEditedPostContent( state );

expect( content ).toBe( 'custom edit' );
} );

it( 'returns serialization of blocks', () => {
const block = createBlock( 'core/block' );

const state = {
editor: {
present: {
blockOrder: {
'': [ block.clientId ],
},
blocksByClientId: {
[ block.clientId ]: block,
},
edits: {},
},
},
currentPost: {},
};

const content = getEditedPostContent( state );

expect( content ).toBe( '<!-- wp:block /-->' );
} );

it( 'returns removep\'d serialization of blocks for single unknown', () => {
const unknownBlock = createBlock( getUnknownTypeHandlerName(), {
html: '<p>foo</p>',
} );
const state = {
editor: {
present: {
blockOrder: {
'': [ unknownBlock.clientId ],
},
blocksByClientId: {
[ unknownBlock.clientId ]: unknownBlock,
},
edits: {},
},
},
currentPost: {},
};

const content = getEditedPostContent( state );

expect( content ).toBe( 'foo' );
} );

it( 'returns non-removep\'d serialization of blocks for multiple unknown', () => {
const firstUnknown = createBlock( getUnknownTypeHandlerName(), {
html: '<p>foo</p>',
} );
const secondUnknown = createBlock( getUnknownTypeHandlerName(), {
html: '<p>bar</p>',
} );
const state = {
editor: {
present: {
blockOrder: {
'': [ firstUnknown.clientId, secondUnknown.clientId ],
},
blocksByClientId: {
[ firstUnknown.clientId ]: firstUnknown,
[ secondUnknown.clientId ]: secondUnknown,
},
edits: {},
},
},
currentPost: {},
};

const content = getEditedPostContent( state );

expect( content ).toBe( '<p>foo</p>\n\n<p>bar</p>' );
} );

it( 'returns empty string for single unmodified default block', () => {
const defaultBlock = createBlock( getDefaultBlockName() );
const state = {
editor: {
present: {
blockOrder: {
'': [ defaultBlock.clientId ],
},
blocksByClientId: {
[ defaultBlock.clientId ]: defaultBlock,
},
edits: {},
},
},
currentPost: {},
};

const content = getEditedPostContent( state );

expect( content ).toBe( '' );
} );

it( 'should not return empty string for modified default block', () => {
const defaultBlock = createBlock( getDefaultBlockName() );
const state = {
editor: {
present: {
blockOrder: {
'': [ defaultBlock.clientId ],
},
blocksByClientId: {
[ defaultBlock.clientId ]: {
...defaultBlock,
attributes: {
...defaultBlock.attributes,
modified: true,
},
},
},
edits: {},
},
},
currentPost: {},
};

const content = getEditedPostContent( state );

expect( content ).toBe( '<!-- wp:default {\"modified\":true} /-->' );
} );
} );

describe( 'getNotices', () => {
it( 'should return the notices array', () => {
const state = {
Expand Down
6 changes: 5 additions & 1 deletion test/e2e/specs/__snapshots__/splitting-merging.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

exports[`splitting and merging blocks should delete an empty first line 1`] = `
"<!-- wp:paragraph -->
<p></p>
<p>First</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Still Second</p>
<!-- /wp:paragraph -->"
`;

Expand Down
10 changes: 10 additions & 0 deletions test/e2e/specs/splitting-merging.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,24 @@ describe( 'splitting and merging blocks', () => {
// should remove the first.
//
// See: https://github.com/WordPress/gutenberg/issues/8388

// First paragraph
await insertBlock( 'Paragraph' );
await page.keyboard.type( 'First' );
await page.keyboard.press( 'Enter' );

// Second paragraph
await page.keyboard.down( 'Shift' );
await page.keyboard.press( 'Enter' );
await page.keyboard.up( 'Shift' );

// Delete the soft line break.
await page.keyboard.press( 'Backspace' );

// Typing at this point should occur still within the second paragraph,
// while before the regression fix it would have occurred in the first.
await page.keyboard.type( 'Still Second' );

expect( await getEditedPostContent() ).toMatchSnapshot();
} );

Expand Down