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 reusable blocks data layer #3017

Merged
merged 4 commits into from
Nov 7, 2017
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
1 change: 1 addition & 0 deletions blocks/api/categories.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const categories = [
{ slug: 'layout', title: __( 'Layout Blocks' ) },
{ slug: 'widgets', title: __( 'Widgets' ) },
{ slug: 'embed', title: __( 'Embed' ) },
{ slug: 'reusable-blocks', title: __( 'My Reusable Blocks' ) },
Copy link
Member

Choose a reason for hiding this comment

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

These should probably be allocated in another "tab" (like recent, blocks, embeds) rather than a category. The names should probably be "Saved". cc @jasmussen

];

/**
Expand Down
21 changes: 21 additions & 0 deletions blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import {
find,
} from 'lodash';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
Expand Down Expand Up @@ -116,3 +121,19 @@ export function switchToBlockType( block, name ) {
uid: index === firstSwitchedBlock ? block.uid : result.uid,
} ) );
}

/**
* Creates a new reusable block.
*
* @param {String} type The type of the block referenced by the reusable block
* @param {Object} attributes The attributes of the block referenced by the reusable block
* @return {Object} A reusable block object
*/
export function createReusableBlock( type, attributes ) {
return {
id: uuid(),
Copy link
Member

@aduth aduth Jan 29, 2018

Choose a reason for hiding this comment

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

The apparent overlap yet disconnect between a reusable block's id property and a standard block's uid property is unfortunate. I can see why we might want to call this one id, since it aligns well with how it is modeled on the server (the post's ID), but it makes me wonder whether we should then align the standard block property to id as well. I'm not strongly attached to uid as a property name.

Do you have any thoughts?

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'd be into aligning them so long as we're explicit when writing code that interacts with both types of ID, e.g:

const blockId = block.id;
const reusableBlockId = block.attributes.ref;

This would be a breaking change though, since there might be third party code that references block.uid.

name: __( 'Untitled block' ),
Copy link
Member

@aduth aduth Jan 29, 2018

Choose a reason for hiding this comment

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

Would there ever be a case where we want to create a reusable block and assign its name immediately? A bit awkward to achieve now, since name is not accepted as an argument of the function.

Copy link
Member Author

Choose a reason for hiding this comment

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

No such case was in our design so I figured you aren't gonna need it. We could add an optional argument in the future if it comes up.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should support this and have UI that focus on the name input before saving. cc @jasmussen

type,
attributes,
};
}
2 changes: 1 addition & 1 deletion blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as source from './source';

export { source };
export { createBlock, switchToBlockType } from './factory';
export { createBlock, switchToBlockType, createReusableBlock } from './factory';
export { default as parse, getSourcedAttributes } from './parser';
export { default as rawHandler } from './raw-handling';
export { default as serialize, getBlockDefaultClassname, getBlockContent } from './serializer';
Expand Down
16 changes: 15 additions & 1 deletion blocks/api/test/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { noop } from 'lodash';
/**
* Internal dependencies
*/
import { createBlock, switchToBlockType } from '../factory';
import { createBlock, switchToBlockType, createReusableBlock } from '../factory';
import { getBlockTypes, unregisterBlockType, setUnknownTypeHandlerName, registerBlockType } from '../registration';

describe( 'block factory', () => {
Expand Down Expand Up @@ -460,4 +460,18 @@ describe( 'block factory', () => {
} );
} );
} );

describe( 'createReusableBlock', () => {
it( 'should create a reusable block', () => {
const type = 'core/test-block';
const attributes = { name: 'Big Bird' };

expect( createReusableBlock( type, attributes ) ).toMatchObject( {
id: expect.stringMatching( /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ ),
name: 'Untitled block',
type,
attributes,
} );
} );
} );
} );
1 change: 1 addition & 0 deletions blocks/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ import './text-columns';
import './verse';
import './video';
import './audio';
import './reusable-block';
24 changes: 24 additions & 0 deletions blocks/library/reusable-block/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { registerBlockType } from '../../api';

registerBlockType( 'core/reusable-block', {
title: __( 'Reusable Block' ),
category: 'reusable-blocks',
isPrivate: true,

attributes: {
ref: {
type: 'string',
},
},

edit: () => <div>{ __( 'Reusable Blocks are coming soon!' ) }</div>,
save: () => null,
} );
1 change: 1 addition & 0 deletions blocks/test/fixtures/core__reusable-block.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- wp:core/reusable-block {"ref":"358b59ee-bab3-4d6f-8445-e8c6971a5605"} /-->
11 changes: 11 additions & 0 deletions blocks/test/fixtures/core__reusable-block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"uid": "_uid_0",
"name": "core/reusable-block",
"isValid": true,
"attributes": {
"ref": "358b59ee-bab3-4d6f-8445-e8c6971a5605"
},
"originalContent": ""
}
]
14 changes: 14 additions & 0 deletions blocks/test/fixtures/core__reusable-block.parsed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"blockName": "core/reusable-block",
"attrs": {
"ref": "358b59ee-bab3-4d6f-8445-e8c6971a5605"
},
"rawContent": ""
},
{
"attrs": {},
"rawContent": "\n"
}
]

1 change: 1 addition & 0 deletions blocks/test/fixtures/core__reusable-block.serialized.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- wp:reusable-block {"ref":"358b59ee-bab3-4d6f-8445-e8c6971a5605"} /-->
71 changes: 71 additions & 0 deletions editor/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,74 @@ export const createSuccessNotice = partial( createNotice, 'success' );
export const createInfoNotice = partial( createNotice, 'info' );
export const createErrorNotice = partial( createNotice, 'error' );
export const createWarningNotice = partial( createNotice, 'warning' );

/**
* Returns an action object used to fetch a single reusable block or all
* reusable blocks from the REST API into the store.
*
* @param {?string} id If given, only a single reusable block with this ID will be fetched
* @return {Object} Action object
*/
export function fetchReusableBlocks( id ) {
return {
type: 'FETCH_REUSABLE_BLOCKS',
id,
};
}

/**
* Returns an action object used to insert or update a reusable block into the store.
*
* @param {Object} id The ID of the reusable block to update
* @param {Object} reusableBlock The new reusable block object. Any omitted keys are not changed
* @return {Object} Action object
*/
export function updateReusableBlock( id, reusableBlock ) {
return {
type: 'UPDATE_REUSABLE_BLOCK',
id,
reusableBlock,
};
}

/**
* Returns an action object used to save a reusable block that's in the store
* to the REST API.
*
* @param {Object} id The ID of the reusable block to save
* @return {Object} Action object
*/
export function saveReusableBlock( id ) {
return {
type: 'SAVE_REUSABLE_BLOCK',
id,
};
}

/**
* Returns an action object used to convert a reusable block into a static
* block.
*
* @param {Object} uid The ID of the block to attach
* @return {Object} Action object
*/
export function convertBlockToStatic( uid ) {
return {
type: 'CONVERT_BLOCK_TO_STATIC',
uid,
};
}

/**
* Returns an action object used to convert a static block into a reusable
* block.
*
* @param {Object} uid The ID of the block to detach
* @return {Object} Action object
*/
export function convertBlockToReusable( uid ) {
return {
type: 'CONVERT_BLOCK_TO_REUSABLE',
uid,
};
}
13 changes: 9 additions & 4 deletions editor/inserter/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class InserterMenu extends Component {
}

componentDidUpdate( prevProps, prevState ) {
const searchResults = this.searchBlocks( getBlockTypes() );
const searchResults = this.searchBlocks( this.getBlockTypes() );
// Announce the blocks search results to screen readers.
if ( !! searchResults.length ) {
this.props.debouncedSpeak( sprintf( _n(
Expand Down Expand Up @@ -100,22 +100,27 @@ export class InserterMenu extends Component {
};
}

getBlockTypes() {
// Block types that are marked as private should not appear in the inserter
return getBlockTypes().filter( ( block ) => ! block.isPrivate );
}

searchBlocks( blockTypes ) {
return searchBlocks( blockTypes, this.state.filterValue );
}

getBlocksForCurrentTab() {
// if we're searching, use everything, otherwise just get the blocks visible in this tab
if ( this.state.filterValue ) {
return getBlockTypes();
return this.getBlockTypes();
}
switch ( this.state.tab ) {
case 'recent':
return this.props.recentlyUsedBlocks;
case 'blocks':
return filter( getBlockTypes(), ( block ) => block.category !== 'embed' );
return filter( this.getBlockTypes(), ( block ) => block.category !== 'embed' );
case 'embeds':
return filter( getBlockTypes(), ( block ) => block.category === 'embed' );
return filter( this.getBlockTypes(), ( block ) => block.category === 'embed' );
}
}

Expand Down
51 changes: 51 additions & 0 deletions editor/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,56 @@ export function metaBoxes( state = defaultMetaBoxState, action ) {
}
}

export const reusableBlocks = combineReducers( {
data( state = {}, action ) {
switch ( action.type ) {
case 'FETCH_REUSABLE_BLOCKS_SUCCESS': {
return reduce( action.reusableBlocks, ( newState, reusableBlock ) => ( {
...newState,
[ reusableBlock.id ]: reusableBlock,
} ), state );
}

case 'UPDATE_REUSABLE_BLOCK': {
const { id, reusableBlock } = action;
const existingReusableBlock = state[ id ];

return {
...state,
[ id ]: {
...existingReusableBlock,
...reusableBlock,
attributes: {
...( existingReusableBlock && existingReusableBlock.attributes ),
...reusableBlock.attributes,
},
},
};
}
}

return state;
},

isSaving( state = {}, action ) {
switch ( action.type ) {
case 'SAVE_REUSABLE_BLOCK':
return {
...state,
[ action.id ]: true,
};

case 'SAVE_REUSABLE_BLOCK_SUCCESS':
case 'SAVE_REUSABLE_BLOCK_FAILURE': {
const { id } = action;
return omit( state, id );
}
}

return state;
},
} );

export default optimist( combineReducers( {
editor,
currentPost,
Expand All @@ -670,4 +720,5 @@ export default optimist( combineReducers( {
saving,
notices,
metaBoxes,
reusableBlocks,
} ) );
32 changes: 32 additions & 0 deletions editor/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,3 +1020,35 @@ export const getMostFrequentlyUsedBlocks = createSelector(
export function isFeatureActive( state, feature ) {
return !! state.preferences.features[ feature ];
}

/**
* Returns the reusable block with the given ID.
*
* @param {Object} state Global application state
* @param {String} ref The reusable block's ID
* @return {Object} The reusable block, or null if none exists
*/
export function getReusableBlock( state, ref ) {
return state.reusableBlocks.data[ ref ] || null;
}

/**
* Returns whether or not the reusable block with the given ID is being saved.
*
* @param {*} state Global application state
* @param {*} ref The reusable block's ID
* @return {Boolean} Whether or not the reusable block is being saved
*/
export function isSavingReusableBlock( state, ref ) {
return state.reusableBlocks.isSaving[ ref ] || false;
}

/**
* Returns an array of all reusable blocks.
*
* @param {Object} state Global application state
* @return {Array} An array of all reusable blocks.
*/
export function getReusableBlocks( state ) {
return Object.values( state.reusableBlocks.data );
}
Loading