Skip to content

Commit

Permalink
Editor: Implement EntityProvider and use it to refactor custom sour…
Browse files Browse the repository at this point in the history
…ces with a backwards compatibility hook for meta sources. (#17153)

* Editor: Implement `EntityProvider` and use it to refactor custom sources with a backwards compatibility hook for meta sources.

* Core Data: Handle dynamic entity contexts using a proxy.

* Block Editor: Replace dependency on Core Data with an Editor filter.

* Core Data: Fix linting error from rebase.

* Core Data: Simplify proxy usage into a context specific proxy.

* Core Data: Clean up variable names.
  • Loading branch information
epiqueras authored Sep 23, 2019
1 parent c89e85c commit 03e034f
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 302 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/data": "file:../data",
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/url": "file:../url",
"equivalent-key-map": "^0.2.2",
Expand Down
90 changes: 90 additions & 0 deletions packages/core-data/src/entity-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* WordPress dependencies
*/
import { createContext, useContext, useCallback } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import { defaultEntities, kinds } from './entities';

const entities = {
...defaultEntities.reduce( ( acc, entity ) => {
if ( ! acc[ entity.kind ] ) {
acc[ entity.kind ] = {};
}
acc[ entity.kind ][ entity.name ] = { context: createContext() };
return acc;
}, {} ),
...kinds.reduce( ( acc, kind ) => {
acc[ kind.name ] = {};
return acc;
}, {} ),
};
const getEntity = ( kind, type ) => {
if ( ! entities[ kind ] ) {
throw new Error( `Missing entity config for kind: ${ kind }.` );
}

if ( ! entities[ kind ][ type ] ) {
entities[ kind ][ type ] = { context: createContext() };
}

return entities[ kind ][ type ];
};

/**
* Context provider component for providing
* an entity for a specific entity type.
*
* @param {Object} props The component's props.
* @param {string} props.kind The entity kind.
* @param {string} props.type The entity type.
* @param {number} props.id The entity ID.
* @param {*} props.children The children to wrap.
*
* @return {Object} The provided children, wrapped with
* the entity's context provider.
*/
export default function EntityProvider( { kind, type, id, children } ) {
const Provider = getEntity( kind, type ).context.Provider;
return <Provider value={ id }>{ children }</Provider>;
}

/**
* Hook that returns the value and a setter for the
* specified property of the nearest provided
* entity of the specified type.
*
* @param {string} kind The entity kind.
* @param {string} type The entity type.
* @param {string} prop The property name.
*
* @return {[*, Function]} A tuple where the first item is the
* property value and the second is the
* setter.
*/
export function useEntityProp( kind, type, prop ) {
const id = useContext( getEntity( kind, type ).context );

const value = useSelect(
( select ) => {
const entity = select( 'core' ).getEditedEntityRecord( kind, type, id );
return entity && entity[ prop ];
},
[ kind, type, id, prop ]
);

const { editEntityRecord } = useDispatch( 'core' );
const setValue = useCallback(
( newValue ) => {
editEntityRecord( kind, type, id, {
[ prop ]: newValue,
} );
},
[ kind, type, id, prop ]
);

return [ value, setValue ];
}
2 changes: 2 additions & 0 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ registerStore( REDUCER_KEY, {
selectors: { ...selectors, ...entitySelectors },
resolvers: { ...resolvers, ...entityResolvers },
} );

export { default as EntityProvider, useEntityProp } from './entity-provider';
28 changes: 16 additions & 12 deletions packages/editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { compose } from '@wordpress/compose';
import { Component } from '@wordpress/element';
import { withDispatch, withSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { EntityProvider } from '@wordpress/core-data';
import { BlockEditorProvider, transformStyles } from '@wordpress/block-editor';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
Expand Down Expand Up @@ -164,6 +165,7 @@ class EditorProvider extends Component {
const {
canUserUseUnfilteredHTML,
children,
post,
blocks,
resetEditorBlocks,
isReady,
Expand All @@ -185,18 +187,20 @@ class EditorProvider extends Component {
);

return (
<BlockEditorProvider
value={ blocks }
onInput={ resetEditorBlocksWithoutUndoLevel }
onChange={ resetEditorBlocks }
settings={ editorSettings }
useSubRegistry={ false }
>
{ children }
<ReusableBlocksButtons />
<ConvertToGroupButtons />
{ editorSettings.__experimentalBlockDirectory && <InserterMenuDownloadableBlocksPanel /> }
</BlockEditorProvider>
<EntityProvider kind="postType" type={ post.type } id={ post.id }>
<BlockEditorProvider
value={ blocks }
onInput={ resetEditorBlocksWithoutUndoLevel }
onChange={ resetEditorBlocks }
settings={ editorSettings }
useSubRegistry={ false }
>
{ children }
<ReusableBlocksButtons />
<ConvertToGroupButtons />
{ editorSettings.__experimentalBlockDirectory && <InserterMenuDownloadableBlocksPanel /> }
</BlockEditorProvider>
</EntityProvider>
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* WordPress dependencies
*/
import { getBlockType } from '@wordpress/blocks';
import { useEntityProp } from '@wordpress/core-data';
import { useMemo, useCallback } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';

const EMPTY_OBJECT = {};
function useMetaAttributeSource( name, _attributes, _setAttributes ) {
const { attributes: attributeTypes = EMPTY_OBJECT } =
getBlockType( name ) || EMPTY_OBJECT;
let [ attributes, setAttributes ] = [ _attributes, _setAttributes ];

if ( Object.values( attributeTypes ).some( ( type ) => type.source === 'meta' ) ) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [ meta, setMeta ] = useEntityProp( 'postType', 'post', 'meta' );

// eslint-disable-next-line react-hooks/rules-of-hooks
attributes = useMemo(
() => ( {
..._attributes,
...Object.keys( attributeTypes ).reduce( ( acc, key ) => {
if ( attributeTypes[ key ].source === 'meta' ) {
acc[ key ] = meta[ attributeTypes[ key ].meta ];
}
return acc;
}, {} ),
} ),
[ attributeTypes, meta, _attributes ]
);

// eslint-disable-next-line react-hooks/rules-of-hooks
setAttributes = useCallback(
( ...args ) => {
Object.keys( args[ 0 ] ).forEach( ( key ) => {
if ( attributeTypes[ key ].source === 'meta' ) {
setMeta( { [ attributeTypes[ key ].meta ]: args[ 0 ][ key ] } );
}
} );
return _setAttributes( ...args );
},
[ attributeTypes, setMeta, _setAttributes ]
);
}

return [ attributes, setAttributes ];
}
const withMetaAttributeSource = createHigherOrderComponent(
( BlockListBlock ) => ( { attributes, setAttributes, name, ...props } ) => {
[ attributes, setAttributes ] = useMetaAttributeSource(
name,
attributes,
setAttributes
);
return (
<BlockListBlock
attributes={ attributes }
setAttributes={ setAttributes }
name={ name }
{ ...props }
/>
);
},
'withMetaAttributeSource'
);

addFilter(
'editor.BlockListBlock',
'core/editor/custom-sources-backwards-compatibility/with-meta-attribute-source',
withMetaAttributeSource
);
1 change: 1 addition & 0 deletions packages/editor/src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/**
* Internal dependencies
*/
import './custom-sources-backwards-compatibility';
import './default-autocompleters';
Loading

0 comments on commit 03e034f

Please sign in to comment.