From 8944b9c81ba94a899549a16217b7791841bfb06b Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Thu, 12 Oct 2017 01:45:33 +0100 Subject: [PATCH 1/2] Attributes: Add support for site options --- blocks/api/serializer.js | 4 +- blocks/library/index.js | 1 + blocks/library/site-description/index.js | 83 +++++++++++++++++++++++ blocks/library/site-description/index.php | 46 +++++++++++++ editor/effects.js | 30 +++++++- editor/modes/visual-editor/block.js | 16 +++++ editor/modes/visual-editor/index.js | 2 + editor/reducer.js | 10 +++ editor/selectors.js | 17 ++++- editor/site-options/index.js | 68 +++++++++++++++++++ 10 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 blocks/library/site-description/index.js create mode 100644 blocks/library/site-description/index.php create mode 100644 editor/site-options/index.js diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 9e0e38b727751a..673a00d4b3d1b9 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -102,7 +102,9 @@ export function getCommentAttributes( allAttributes, schema ) { } // Ignore values sources from content and post meta - if ( attributeSchema.source || attributeSchema.meta ) { + if ( attributeSchema.source || + attributeSchema.meta || + attributeSchema.option ) { return result; } diff --git a/blocks/library/index.js b/blocks/library/index.js index 8ae8ce98543f25..82afeea2a76083 100644 --- a/blocks/library/index.js +++ b/blocks/library/index.js @@ -22,3 +22,4 @@ import './text-columns'; import './verse'; import './video'; import './audio'; +import './site-description'; diff --git a/blocks/library/site-description/index.js b/blocks/library/site-description/index.js new file mode 100644 index 00000000000000..c26ca11e72b409 --- /dev/null +++ b/blocks/library/site-description/index.js @@ -0,0 +1,83 @@ +/** + * WordPress dependencies + */ +import { Placeholder, Spinner } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { registerBlockType } from '../../api'; +import InspectorControls from '../../inspector-controls'; +import BlockDescription from '../../block-description'; +import BlockControls from '../../block-controls'; + +registerBlockType( 'core/site-description', { + title: __( 'Site Description' ), + + icon: 'list-view', + + category: 'widgets', + + attributes: { + description: { + type: 'string', + option: 'description', + }, + shouldRenderDescription: { + type: 'boolean', + default: false, + }, + }, + + keywords: [ __( 'site tagline' ) ], + + edit( { attributes, setAttributes, focus } ) { + const { + description, + shouldRenderDescription, + } = attributes; + + if ( description === undefined ) { + return ( + + + + ); + } + + return [ + focus && , + focus && ( + + +

{ __( 'Shows your site\'s description' ) }

+
+ setAttributes( { + shouldRenderDescription: ! shouldRenderDescription, + } ) } + /> +
+ ), + setAttributes( { + description: event.target.value, + } ) } + value={ description } />, + ]; + }, + + save( { attributes } ) { + const { description, shouldRenderDescription } = attributes; + return shouldRenderDescription + ?
{ description }
+ : null; + }, +} ); diff --git a/blocks/library/site-description/index.php b/blocks/library/site-description/index.php new file mode 100644 index 00000000000000..fc37cc7cba30f7 --- /dev/null +++ b/blocks/library/site-description/index.php @@ -0,0 +1,46 @@ +%2$s', + esc_attr( $class ), + esc_html( $description ) + ); + + return $block_content; +} + +register_block_type( 'core/site-description', array( + 'attributes' => array( + /* option attribute + 'description' => array( + 'type' => 'string', + 'option' => 'description', + ), + */ + 'shouldRenderDescription' => array( + 'type' => 'boolean', + 'default' => false, + ), + ), + + 'render_callback' => 'gutenberg_render_block_core_site_description', +) ); diff --git a/editor/effects.js b/editor/effects.js index 163982fba0e503..d2834e50b71720 100644 --- a/editor/effects.js +++ b/editor/effects.js @@ -2,7 +2,7 @@ * External dependencies */ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; -import { get, uniqueId } from 'lodash'; +import { get, noop, uniqueId } from 'lodash'; /** * WordPress dependencies @@ -36,6 +36,11 @@ import { isEditedPostNew, isEditedPostSaveable, } from './selectors'; +import { + fetchSiteOptions, + getOptionUpdatedMessage, + saveSiteOptions, +} from './site-options'; const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; @@ -86,7 +91,7 @@ export default { }, REQUEST_POST_UPDATE_SUCCESS( action, store ) { const { previousPost, post } = action; - const { dispatch } = store; + const { dispatch, getState } = store; const publishStatus = [ 'publish', 'private', 'future' ]; const isPublished = publishStatus.indexOf( previousPost.status ) !== -1; @@ -113,6 +118,19 @@ export default { ) ); } + const siteOptions = getState().siteOptions; + saveSiteOptions( siteOptions ).then( ( newOptions ) => { + Object.keys( newOptions ) + .map( getOptionUpdatedMessage ) + .filter( Boolean ) + .map( ( label ) => createSuccessNotice( +

+ { label } +

+ ) ) + .forEach( dispatch ); + } ); + if ( get( window.history.state, 'id' ) !== post.id ) { window.history.replaceState( { id: post.id }, @@ -273,4 +291,12 @@ export default { return effects; }, + RESET_SITE_OPTIONS( _, { dispatch } ) { + fetchSiteOptions().then( ( siteOptions ) => { + dispatch( { + type: 'UPDATE_SITE_OPTIONS', + siteOptions, + } ); + } ).catch( noop ); + }, }; diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 773efc9bdb439c..1a64401db79882 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -148,12 +148,24 @@ class VisualEditorBlock extends Component { return result; }, {} ); + const optionAttributes = reduce( attributes, ( result, value, key ) => { + if ( type && has( type, [ 'attributes', key, 'option' ] ) ) { + result[ type.attributes[ key ].option ] = value; + } + + return result; + }, {} ); + if ( size( metaAttributes ) ) { this.props.onMetaChange( { ...this.props.meta, ...metaAttributes, } ); } + + if ( size( optionAttributes ) ) { + this.props.onOptionsChange( optionAttributes ); + } } maybeHover() { @@ -463,5 +475,9 @@ export default connect( onMetaChange( meta ) { dispatch( editPost( { meta } ) ); }, + + onOptionsChange( siteOptions ) { + dispatch( { type: 'UPDATE_SITE_OPTIONS', siteOptions } ); + }, } ) )( VisualEditorBlock ); diff --git a/editor/modes/visual-editor/index.js b/editor/modes/visual-editor/index.js index 6e31faff576e36..3b3b1a3f50526f 100644 --- a/editor/modes/visual-editor/index.js +++ b/editor/modes/visual-editor/index.js @@ -22,6 +22,7 @@ import WritingFlow from '../../writing-flow'; import TableOfContents from '../../table-of-contents'; import { getBlockUids, getMultiSelectedBlockUids } from '../../selectors'; import { clearSelectedBlock, multiSelect, redo, undo, removeBlocks } from '../../actions'; +import { QuerySiteOptions } from '../../site-options'; class VisualEditor extends Component { constructor() { @@ -101,6 +102,7 @@ class VisualEditor extends Component { backspace: this.deleteSelectedBlocks, del: this.deleteSelectedBlocks, } } /> + diff --git a/editor/reducer.js b/editor/reducer.js index b761af8502cfcc..39df433acd7a5d 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -524,6 +524,15 @@ export function notices( state = {}, action ) { return state; } +export function siteOptions( state = {}, action ) { + switch ( action.type ) { + case 'UPDATE_SITE_OPTIONS': + return { ...state, ...action.siteOptions }; + } + + return state; +} + export default optimist( combineReducers( { editor, currentPost, @@ -535,4 +544,5 @@ export default optimist( combineReducers( { panel, saving, notices, + siteOptions, } ) ); diff --git a/editor/selectors.js b/editor/selectors.js index bed57fdcd9efc6..3049aeaf8fad82 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -397,13 +397,23 @@ export const getBlock = createSelector( return result; }, {} ); - if ( ! Object.keys( metaAttributes ).length ) { + const optionAttributes = reduce( type.attributes, ( result, value, key ) => { + if ( value && ( 'option' in value ) ) { + result[ key ] = getSiteOption( state, value.option ); + } + + return result; + }, {} ); + + if ( ! Object.keys( metaAttributes ).length && + ! Object.keys( optionAttributes ).length ) { return block; } return { ...block, attributes: { + ...optionAttributes, ...block.attributes, ...metaAttributes, }, @@ -413,6 +423,7 @@ export const getBlock = createSelector( get( state, [ 'editor', 'blocksByUid', uid ] ), get( state, 'editor.edits.meta' ), get( state, 'currentPost.meta' ), + get( state, 'siteOptions' ), ] ); @@ -422,6 +433,10 @@ function getPostMeta( state, key ) { : get( state, [ 'currentPost', 'meta', key ] ); } +function getSiteOption( state, key ) { + return get( state, [ 'siteOptions', 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/site-options/index.js b/editor/site-options/index.js new file mode 100644 index 00000000000000..90c3f6c5e812a1 --- /dev/null +++ b/editor/site-options/index.js @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import { + differenceWith, + isEqual, + pick, + toPairs, +} from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; + +const optionsModel = new wp.api.models.Settings(); + +let callTime; +export function fetchSiteOptions() { + if ( ! callTime || Date.now() - callTime > 3000 ) { + callTime = Date.now(); + return Promise.resolve( optionsModel.fetch() ); + } + return Promise.reject(); +} + +export function saveSiteOptions( options ) { + const newKeys = getChangedKeys( options, getSiteOptions() ); + const newOptions = pick( options, newKeys ); + return optionsModel.save( newOptions ) + .then( ( result ) => pick( result, newKeys ) ); +} + +export function getOptionUpdatedMessage( option ) { + switch ( option ) { + case 'description': + return __( 'Site description updated!' ); + } +} + +function getSiteOptions() { + return optionsModel.attributes; +} + +function getChangedKeys( newOptions, oldOptions ) { + return differenceWith( toPairs( newOptions ), toPairs( oldOptions ), isEqual ) + .map( ( [ key ] ) => key ); +} + +export const QuerySiteOptions = connect( + null, + ( dispatch ) => ( { + requestSiteOptions() { + dispatch( { + type: 'RESET_SITE_OPTIONS', + } ); + }, + } ) +)( class extends Component { + componentDidMount() { + this.props.requestSiteOptions(); + } + render() { + return null; + } +} ); From 276eed65f3a1df5e2f0473ce81cd4231c9683c74 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Fri, 20 Oct 2017 17:53:30 +0100 Subject: [PATCH 2/2] [FIXME] WithAPIData: Add saveWith method --- components/higher-order/with-api-data/index.js | 13 ++++++++++++- components/higher-order/with-api-data/request.js | 11 +++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/components/higher-order/with-api-data/index.js b/components/higher-order/with-api-data/index.js index 1a5870a4175323..521acd6683d88a 100644 --- a/components/higher-order/with-api-data/index.js +++ b/components/higher-order/with-api-data/index.js @@ -11,7 +11,7 @@ import { Component } from 'element'; /** * Internal dependencies */ -import request from './request'; +import request, { encodeParams } from './request'; import { getRoute } from './routes'; export default ( mapPropsToData ) => ( WrappedComponent ) => { @@ -166,6 +166,8 @@ export default ( mapPropsToData ) => ( WrappedComponent ) => { return result; } + console.log( 'route', path, route ); + result[ propName ] = route.methods.reduce( ( stateValue, method ) => { // Add request initiater into data props const requestKey = this.getRequestKey( method ); @@ -183,6 +185,15 @@ export default ( mapPropsToData ) => ( WrappedComponent ) => { // Track path for future map skipping stateValue.path = path; + // TODO move this somewhere else and/or document + if ( method === 'PUT' ) { + stateValue.saveWith = ( data ) => + this.request( + propName, + method, + path + encodeParams( data ) ); + } + return stateValue; }, {} ); diff --git a/components/higher-order/with-api-data/request.js b/components/higher-order/with-api-data/request.js index 3e74b4937e1d1f..6a3389e622f6d2 100644 --- a/components/higher-order/with-api-data/request.js +++ b/components/higher-order/with-api-data/request.js @@ -2,7 +2,7 @@ * External dependencies */ import memoize from 'memize'; -import { mapKeys } from 'lodash'; +import { isEmpty, mapKeys, toPairs } from 'lodash'; export const getStablePath = memoize( ( path ) => { const [ base, query ] = path.split( '?' ); @@ -29,13 +29,20 @@ export const getStablePath = memoize( ( path ) => { // 'a=5&b=1&c=2' } ); +export const encodeParams = ( data ) => + isEmpty( data ) + ? '' + : '?' + toPairs( data ) + .map( ( [ key, value ] ) => `${ key }=${ encodeURI( value ) }` ) + .join( '& ' ); + /** * Response cache of path to response (object of data, headers arrays). * Optionally populated from window global for preloading. * * @type {Object} */ -export const cache = mapKeys( +export const cache = window._wpAPICache = mapKeys( window._wpAPIDataPreload, ( value, key ) => getStablePath( key ) );