diff --git a/blocks/api/parser.js b/blocks/api/parser.js index d2c58bab6757b1..edab284d70cc21 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -8,6 +8,7 @@ import { mapValues, omit } from 'lodash'; * WordPress dependencies */ import { autop } from '@wordpress/autop'; +import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies @@ -118,7 +119,15 @@ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, com break; } - return value === undefined ? attributeSchema.default : asType( value, attributeSchema.type ); + value = applyFilters( 'blocks.getBlockAttribute.source', value, attributeSchema, innerHTML, commentAttributes ); + + if ( value === undefined ) { + value = attributeSchema.default; + } else { + value = asType( value, attributeSchema.type ); + } + + return applyFilters( 'blocks.getBlockAttribute', value, attributeSchema, innerHTML, commentAttributes ); } /** diff --git a/edit-post/hooks/index.js b/edit-post/hooks/index.js new file mode 100644 index 00000000000000..ce295a2f18b29c --- /dev/null +++ b/edit-post/hooks/index.js @@ -0,0 +1,4 @@ +/** + * Internal dependencies + */ +import './meta'; diff --git a/edit-post/hooks/meta/index.js b/edit-post/hooks/meta/index.js new file mode 100644 index 00000000000000..8f9ea8c75b7e7a --- /dev/null +++ b/edit-post/hooks/meta/index.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { select } from '@wordpress/data'; + +const editor = select( 'core/editor' ); + +/** + * Filters an attribute value during parse to inject post properties from meta. + * + * @param {*} value Parsed value. + * @param {Object} schema Attribute schema. + * + * @return {*} Filtered value with meta substitute if applicable. + */ +function getMetaAttributeFromPost( value, schema ) { + if ( schema.source === 'meta' ) { + const meta = editor.getCurrentPost().meta; + if ( meta && meta.hasOwnProperty( schema.meta ) ) { + return meta[ schema.meta ]; + } + } + + return value; +} + +addFilter( 'blocks.getBlockAttribute', 'core/edit-post/meta/getMetaAttributeFromPost', getMetaAttributeFromPost ); diff --git a/edit-post/index.js b/edit-post/index.js index 3fb2777fe1bc01..6b9706ce701729 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -18,6 +18,7 @@ import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; import './assets/stylesheets/main.scss'; import Layout from './components/layout'; import store from './store'; +import './hooks'; // Configure moment globally moment.locale( dateSettings.l10n.locale ); diff --git a/editor/store/effects.js b/editor/store/effects.js index 8351e1b953a6d4..40782a15605afa 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -292,10 +292,21 @@ export default { dispatch( savePost() ); }, - SETUP_EDITOR( action ) { + SETUP_EDITOR( action, store ) { const { post, settings } = action; const effects = []; + // Dispatch must occur immediately, as subsequent block parse filtering + // may rely on post properties. + store.dispatch( resetPost( post ) ); + + // Include auto draft title in edits while not flagging post as dirty + if ( post.status === 'auto-draft' ) { + effects.push( setupNewPost( { + title: post.title.raw, + } ) ); + } + // Parse content as blocks if ( post.content.raw ) { effects.push( resetBlocks( parse( post.content.raw ) ) ); @@ -311,17 +322,6 @@ export default { effects.push( resetBlocks( blocks ) ); } - // Resetting post should occur after blocks have been reset, since it's - // the post reset that restarts history (used in dirty detection). - effects.push( resetPost( post ) ); - - // Include auto draft title in edits while not flagging post as dirty - if ( post.status === 'auto-draft' ) { - effects.push( setupNewPost( { - title: post.title.raw, - } ) ); - } - return effects; }, FETCH_REUSABLE_BLOCKS( action, store ) { diff --git a/editor/store/index.js b/editor/store/index.js index 8b9fba4192c333..9acf958d33c11f 100644 --- a/editor/store/index.js +++ b/editor/store/index.js @@ -11,6 +11,7 @@ import applyMiddlewares from './middlewares'; import { getBlockCount, getBlocks, + getCurrentPost, getEditedPostAttribute, getLastMultiSelectedBlockUid, getSelectedBlockCount, @@ -30,6 +31,7 @@ loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); registerSelectors( MODULE_KEY, { getBlockCount, getBlocks, + getCurrentPost, getEditedPostAttribute, getLastMultiSelectedBlockUid, getSelectedBlockCount, diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 641879e9ce4e0d..b69d99702928a8 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -114,12 +114,12 @@ function getFlattenedBlocks( blocks ) { export const editor = flow( [ combineReducers, - // Track undo history, starting at editor initialization. - partialRight( withHistory, { resetTypes: [ 'SETUP_NEW_POST', 'SETUP_EDITOR' ] } ), + // Track undo history, starting at editor initialization of blocks. Assumes + // one of either block resetting, new post setup occurs in initialization. + partialRight( withHistory, { resetTypes: [ 'SETUP_NEW_POST', 'RESET_BLOCKS' ] } ), - // Track whether changes exist, resetting at each post save. Relies on - // editor initialization firing post reset as an effect. - partialRight( withChangeDetection, { resetTypes: [ 'SETUP_NEW_POST', 'RESET_POST' ] } ), + // Track whether changes exist, resetting at initialization and each save. + partialRight( withChangeDetection, { resetTypes: [ 'RESET_POST', 'SETUP_NEW_POST', 'RESET_BLOCKS' ] } ), ] )( { edits( state = {}, action ) { switch ( action.type ) { diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 33b57d93410242..7d5ad21e895409 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -6,9 +6,7 @@ import { map, first, get, - has, last, - reduce, compact, find, some, @@ -422,47 +420,17 @@ export const getBlock = createSelector( return null; } - let { attributes } = block; - - // Inject custom source attribute values. - // - // TODO: Create generic external sourcing pattern, not explicitly - // targeting meta attributes. - const type = getBlockType( block.name ); - if ( type ) { - attributes = reduce( type.attributes, ( result, value, key ) => { - if ( value.source === 'meta' ) { - if ( result === attributes ) { - result = { ...result }; - } - - result[ key ] = getPostMeta( state, value.meta ); - } - - return result; - }, attributes ); - } - return { ...block, - attributes, innerBlocks: getBlocks( state, uid ), }; }, ( state, uid ) => [ get( state, [ 'editor', 'present', 'blocksByUid', uid ] ), getBlockDependantsCacheBust( state, uid ), - get( state, [ 'editor', 'present', 'edits', 'meta' ] ), - get( state, 'currentPost.meta' ), ] ); -function getPostMeta( state, key ) { - return has( state, [ 'editor', 'present', 'edits', 'meta', key ] ) ? - get( state, [ 'editor', 'present', 'edits', 'meta', key ] ) : - get( state, [ 'currentPost', 'meta', key ] ); -} - /** * Returns all block objects for the current post being edited as an array in * the order they appear in the post. diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js index dc9a25dbc75669..00b2ebf717e56b 100644 --- a/editor/store/test/effects.js +++ b/editor/store/test/effects.js @@ -46,6 +46,15 @@ jest.mock( 'uuid/v4', () => { describe( 'effects', () => { const defaultBlockSettings = { save: () => 'Saved', category: 'common', title: 'block title' }; + let mockState; + const dispatch = jest.fn(); + const store = { getState: () => mockState, dispatch }; + + beforeEach( () => { + mockState = {}; + dispatch.mockReset(); + } ); + describe( '.MERGE_BLOCKS', () => { const handler = effects.MERGE_BLOCKS; const defaultGetBlock = selectors.getBlock; @@ -71,9 +80,7 @@ describe( 'effects', () => { return blockA.uid === uid ? blockA : blockB; }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.uid, blockB.uid ), { dispatch, getState } ); + handler( mergeBlocks( blockA.uid, blockB.uid ), store ); expect( dispatch ).toHaveBeenCalledTimes( 1 ); expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken' ) ); @@ -103,9 +110,7 @@ describe( 'effects', () => { selectors.getBlock = ( state, uid ) => { return blockA.uid === uid ? blockA : blockB; }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.uid, blockB.uid ), { dispatch, getState } ); + handler( mergeBlocks( blockA.uid, blockB.uid ), store ); expect( dispatch ).toHaveBeenCalledTimes( 2 ); expect( dispatch ).toHaveBeenCalledWith( selectBlock( 'chicken', -1 ) ); @@ -141,9 +146,7 @@ describe( 'effects', () => { selectors.getBlock = ( state, uid ) => { return blockA.uid === uid ? blockA : blockB; }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.uid, blockB.uid ), { dispatch, getState } ); + handler( mergeBlocks( blockA.uid, blockB.uid ), store ); expect( dispatch ).not.toHaveBeenCalled(); } ); @@ -198,9 +201,7 @@ describe( 'effects', () => { selectors.getBlock = ( state, uid ) => { return blockA.uid === uid ? blockA : blockB; }; - const dispatch = jest.fn(); - const getState = () => ( {} ); - handler( mergeBlocks( blockA.uid, blockB.uid ), { dispatch, getState } ); + handler( mergeBlocks( blockA.uid, blockB.uid ), store ); expect( dispatch ).toHaveBeenCalledTimes( 2 ); // expect( dispatch ).toHaveBeenCalledWith( focusBlock( 'chicken', { offset: -1 } ) ); @@ -214,8 +215,6 @@ describe( 'effects', () => { describe( '.AUTOSAVE', () => { const handler = effects.AUTOSAVE; - const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; beforeAll( () => { selectors.isEditedPostSaveable = jest.spyOn( selectors, 'isEditedPostSaveable' ); @@ -225,7 +224,6 @@ describe( 'effects', () => { } ); beforeEach( () => { - dispatch.mockReset(); selectors.isEditedPostSaveable.mockReset(); selectors.isEditedPostDirty.mockReset(); selectors.isCurrentPostPublished.mockReset(); @@ -341,10 +339,9 @@ describe( 'effects', () => { } ); it( 'should dispatch meta box updates on success for dirty meta boxes', () => { - const dispatch = jest.fn(); - const store = { getState: () => ( { + mockState = { metaBoxes: { side: { isActive: true } }, - } ), dispatch }; + }; const post = getDraftPost(); @@ -355,10 +352,9 @@ describe( 'effects', () => { } ); it( 'should dispatch notices when publishing or scheduling a post', () => { - const dispatch = jest.fn(); - const store = { getState: () => ( { + mockState = { metaBoxes: { side: { isActive: true } }, - } ), dispatch }; + }; const previousPost = getDraftPost(); const post = getPublishedPost(); @@ -379,10 +375,9 @@ describe( 'effects', () => { } ); it( 'should dispatch notices when reverting a published post to a draft', () => { - const dispatch = jest.fn(); - const store = { getState: () => ( { + mockState = { metaBoxes: { side: { isActive: true } }, - } ), dispatch }; + }; const previousPost = getPublishedPost(); const post = getDraftPost(); @@ -407,10 +402,9 @@ describe( 'effects', () => { } ); it( 'should dispatch notices when just updating a published post again', () => { - const dispatch = jest.fn(); - const store = { getState: () => ( { + mockState = { metaBoxes: { side: { isActive: true } }, - } ), dispatch }; + }; const previousPost = getPublishedPost(); const post = getPublishedPost(); @@ -452,11 +446,10 @@ describe( 'effects', () => { status: 'draft', }; - const result = handler( { post, settings: {} } ); + const result = handler( { post, settings: {} }, store ); - expect( result ).toEqual( [ - resetPost( post ), - ] ); + expect( dispatch ).toHaveBeenCalledWith( resetPost( post ) ); + expect( result ).toEqual( [] ); } ); it( 'should return block reset with non-empty content', () => { @@ -472,10 +465,10 @@ describe( 'effects', () => { status: 'draft', }; - const result = handler( { post, settings: {} } ); + const result = handler( { post, settings: {} }, store ); - expect( result ).toHaveLength( 2 ); - expect( result ).toContainEqual( resetPost( post ) ); + expect( dispatch ).toHaveBeenCalledWith( resetPost( post ) ); + expect( result ).toHaveLength( 1 ); expect( result.some( ( { blocks } ) => { return blocks && blocks[ 0 ].name === 'core/test-block'; } ) ).toBe( true ); @@ -493,10 +486,10 @@ describe( 'effects', () => { status: 'auto-draft', }; - const result = handler( { post, settings: {} } ); + const result = handler( { post, settings: {} }, store ); + expect( dispatch ).toHaveBeenCalledWith( resetPost( post ) ); expect( result ).toEqual( [ - resetPost( post ), setupNewPost( { title: 'A History of Pork' } ), ] ); } ); @@ -545,9 +538,6 @@ describe( 'effects', () => { } } ); - const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; - handler( fetchReusableBlocks(), store ); return promise.then( () => { @@ -587,9 +577,6 @@ describe( 'effects', () => { } } ); - const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; - handler( fetchReusableBlocks( id ), store ); expect( modelAttributes ).toEqual( { id } ); @@ -620,9 +607,6 @@ describe( 'effects', () => { } } ); - const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; - handler( fetchReusableBlocks(), store ); return promise.catch( () => { @@ -660,10 +644,7 @@ describe( 'effects', () => { const initialState = reducer( undefined, {} ); const action = updateReusableBlock( reusableBlock.id, reusableBlock ); - const state = reducer( initialState, action ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = reducer( initialState, action ); handler( saveReusableBlock( reusableBlock.id ), store ); @@ -695,10 +676,7 @@ describe( 'effects', () => { const initialState = reducer( undefined, {} ); const action = updateReusableBlock( reusableBlock.id, reusableBlock ); - const state = reducer( initialState, action ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = reducer( initialState, action ); handler( saveReusableBlock( reusableBlock.id ), store ); @@ -738,10 +716,7 @@ describe( 'effects', () => { resetBlocks( [ associatedBlock ] ), updateReusableBlock( id, {} ), ]; - const state = actions.reduce( reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = actions.reduce( reducer, undefined ); handler( deleteReusableBlock( id ), store ); @@ -770,10 +745,7 @@ describe( 'effects', () => { } } ); - const state = reducer( undefined, updateReusableBlock( 123, {} ) ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = reducer( undefined, updateReusableBlock( 123, {} ) ); handler( deleteReusableBlock( 123 ), store ); @@ -792,10 +764,7 @@ describe( 'effects', () => { isTemporary: true, }; - const state = reducer( undefined, updateReusableBlock( -123, reusableBlock ) ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = reducer( undefined, updateReusableBlock( -123, reusableBlock ) ); handler( deleteReusableBlock( -123 ), store ); @@ -819,10 +788,7 @@ describe( 'effects', () => { updateReusableBlock( reusableBlock.id, reusableBlock ), ]; const initialState = reducer( undefined, {} ); - const state = reduce( actions, reducer, initialState ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = reduce( actions, reducer, initialState ); handler( convertBlockToStatic( staticBlock.uid ), store ); @@ -844,10 +810,7 @@ describe( 'effects', () => { } ); const initialState = reducer( undefined, {} ); - const state = reducer( initialState, resetBlocks( [ staticBlock ] ) ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; + mockState = reducer( initialState, resetBlocks( [ staticBlock ] ) ); handler( convertBlockToReusable( staticBlock.uid ), store ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 0ce5c950bbf241..ef873cb2ae8b9f 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -1327,50 +1327,6 @@ describe( 'selectors', () => { } ], } ); } ); - - it( 'should merge meta attributes for the block', () => { - registerBlockType( 'core/meta-block', { - save: ( props ) => props.attributes.text, - category: 'common', - title: 'test block', - attributes: { - foo: { - type: 'string', - source: 'meta', - meta: 'foo', - }, - }, - } ); - - const state = { - currentPost: { - meta: { - foo: 'bar', - }, - }, - editor: { - present: { - blocksByUid: { - 123: { uid: 123, name: 'core/meta-block', attributes: {} }, - }, - blockOrder: { - '': [ 123 ], - 123: [], - }, - edits: {}, - }, - }, - }; - - expect( getBlock( state, 123 ) ).toEqual( { - uid: 123, - name: 'core/meta-block', - attributes: { - foo: 'bar', - }, - innerBlocks: [], - } ); - } ); } ); describe( 'getBlocks', () => {