From e4fa86a23bdea91e5a299bab8c0e56fdfe9ad03f Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 3 Apr 2017 16:29:35 +0200 Subject: [PATCH 01/17] Post: Add `/revision` route + early infrastructure to load revisions: `QueryPostRevisions` --- .../data/query-post-revisions/index.jsx | 46 +++++++++++++++ client/post-editor/controller.js | 20 +++++++ client/post-editor/index.js | 1 + client/post-editor/post-revision.jsx | 57 +++++++++++++++++++ client/state/action-types.js | 4 ++ client/state/posts/actions.js | 48 ++++++++++++++++ client/state/posts/reducer.js | 19 ++++++- client/state/posts/selectors.js | 11 +++- 8 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 client/components/data/query-post-revisions/index.jsx create mode 100644 client/post-editor/post-revision.jsx diff --git a/client/components/data/query-post-revisions/index.jsx b/client/components/data/query-post-revisions/index.jsx new file mode 100644 index 00000000000000..f2f61b93e8119e --- /dev/null +++ b/client/components/data/query-post-revisions/index.jsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; + +/** + * Internal dependencies + */ +import { requestSitePostRevisions } from 'state/posts/actions'; + +class QueryPostRevisions extends Component { + componentWillMount() { + this.request( this.props ); + } + + componentWillReceiveProps( nextProps ) { + if ( this.props.siteId === nextProps.siteId && + this.props.postId === nextProps.postId ) { + return; + } + + this.request( nextProps ); + } + + request( props ) { + props.requestSitePostRevisions( props.siteId, props.postId ); + } + + render() { + return null; + } +} + +QueryPostRevisions.propTypes = { + postId: PropTypes.number, + siteId: PropTypes.number, + requestSitePostRevisions: PropTypes.func, +}; + +export default connect( + () => { + return {}; + }, + { requestSitePostRevisions } +)( QueryPostRevisions ); diff --git a/client/post-editor/controller.js b/client/post-editor/controller.js index 19d60d2773e561..12e2fce30e201f 100644 --- a/client/post-editor/controller.js +++ b/client/post-editor/controller.js @@ -22,6 +22,7 @@ import userUtils from 'lib/user/utils'; import analytics from 'lib/analytics'; import { decodeEntities } from 'lib/formatting'; import PostEditor from './post-editor'; +import PostRevision from './post-revision'; import { startEditingPost, stopEditingPost } from 'state/ui/editor/actions'; import { getSelectedSiteId } from 'state/ui/selectors'; import { getEditorPostId, getEditorPath } from 'state/ui/editor/selectors'; @@ -69,6 +70,18 @@ function renderEditor( context, postType ) { ); } +function renderRevision( context, siteId, postId ) { + ReactDom.unmountComponentAtNode( document.getElementById( 'secondary' ) ); + ReactDom.render( + React.createElement( ReduxProvider, { store: context.store }, + React.createElement( PostRevision, { + postId, siteId + } ) + ), + document.getElementById( 'primary' ) + ); +} + function maybeRedirect( context ) { const state = context.store.getState(); const siteId = getSelectedSiteId( state ); @@ -235,6 +248,13 @@ module.exports = { renderEditor( context, postType ); }, + revision: function( context ) { + const siteId = getSelectedSiteId( context.store.getState() ); + const postId = getPostID( context ); + + renderRevision( context, siteId, postId ); + }, + exitPost: function( context, next ) { const postId = getPostID( context ); const siteId = getSelectedSiteId( context.store.getState() ); diff --git a/client/post-editor/index.js b/client/post-editor/index.js index 5fc40760696394..3f2dcc41b8a1d5 100644 --- a/client/post-editor/index.js +++ b/client/post-editor/index.js @@ -14,6 +14,7 @@ module.exports = function() { page( '/post', controller.pressThis, sitesController.siteSelection, sitesController.sites ); page( '/post/new', () => page.redirect( '/post' ) ); // redirect from beep-beep-boop page( '/post/:site?/:post?', sitesController.siteSelection, sitesController.fetchJetpackSettings, controller.post ); + page( '/post/revision/:site?/:post?', sitesController.siteSelection, sitesController.fetchJetpackSettings, controller.revision ); page.exit( '/post/:site?/:post?', controller.exitPost ); page( '/page', sitesController.siteSelection, sitesController.sites ); diff --git a/client/post-editor/post-revision.jsx b/client/post-editor/post-revision.jsx new file mode 100644 index 00000000000000..beaa3ae6f11b13 --- /dev/null +++ b/client/post-editor/post-revision.jsx @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; +import React from 'react'; +import { connect } from 'react-redux'; +import { localize } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import { getPostRevisions } from 'state/posts/selectors'; +import QueryPostRevisions from 'components/data/query-post-revisions'; + +class PostRevision extends React.Component { + render() { + // FIXME: this is merely a placehodler component to test "displaying" + // stuff out of the redux store. + + return ( +
+ +

{ this.props.translate( 'Post History' ) }

+
    + { map( this.props.revisions, ( revision ) => { + // TODO: revision's title is empty, need to di + // deeper, probably some discrepancies between + // REST 1.1 / 2.0 output + + return ( +
  • +

    { revision.title }

    +

    { revision.content.rendered }

    +
  • + ); + } ) } +
+
+ ); + } +} + +PostRevision.propTypes = { + postId: React.PropTypes.number, + siteId: React.PropTypes.number, + translate: React.PropTypes.func, +}; + +export default connect( + ( state, { siteId, postId } ) => { + return { + revisions: getPostRevisions( state, siteId, postId ) + }; + } +)( localize( PostRevision ) ); diff --git a/client/state/action-types.js b/client/state/action-types.js index 94a1a84c79c8e7..1520d5f05eab2b 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -372,6 +372,10 @@ export const POST_REQUEST_SUCCESS = 'POST_REQUEST_SUCCESS'; export const POST_RESTORE = 'POST_RESTORE'; export const POST_RESTORE_FAILURE = 'POST_RESTORE_FAILURE'; export const POST_RESTORE_SUCCESS = 'POST_RESTORE_SUCCESS'; +export const POST_REVISIONS_RECEIVE = 'POST_REVISIONS_RECEIVE'; +export const POST_REVISIONS_REQUEST = 'POST_REVISIONS_REQUEST'; +export const POST_REVISIONS_REQUEST_FAILURE = 'POST_REVISIONS_REQUEST_FAILURE'; +export const POST_REVISIONS_REQUEST_SUCCESS = 'POST_REVISIONS_REQUEST_SUCCESS'; export const POST_SAVE = 'POST_SAVE'; export const POST_SAVE_FAILURE = 'POST_SAVE_FAILURE'; export const POST_SAVE_SUCCESS = 'POST_SAVE_SUCCESS'; diff --git a/client/state/posts/actions.js b/client/state/posts/actions.js index f6fe84b46ef860..755e8c247b9cb1 100644 --- a/client/state/posts/actions.js +++ b/client/state/posts/actions.js @@ -23,6 +23,10 @@ import { POST_SAVE, POST_SAVE_SUCCESS, POST_SAVE_FAILURE, + POST_REVISIONS_RECEIVE, + POST_REVISIONS_REQUEST, + POST_REVISIONS_REQUEST_FAILURE, + POST_REVISIONS_REQUEST_SUCCESS, POSTS_RECEIVE, POSTS_REQUEST, POSTS_REQUEST_SUCCESS, @@ -54,6 +58,19 @@ export function receivePosts( posts ) { }; } +export function receiveRevisions( siteId, postId, revisions ) { + // NOTE: We expect all revisions to be on the same post, thus highly + // coupling it to how the WP-API returns revisions, instead of being able + // to "receive" large (possibly unrelated) batch of revisions. + + return { + type: POST_REVISIONS_RECEIVE, + siteId, + postId, + revisions + }; +} + /** * Triggers a network request to fetch posts for the specified site and query. * @@ -133,6 +150,37 @@ export function requestSitePost( siteId, postId ) { }; } +export function requestSitePostRevisions( siteId, postId ) { + return ( dispatch ) => { + dispatch( { + type: POST_REVISIONS_REQUEST, + siteId, + postId + } ); + + const query = { + apiNamespace: 'wp/v2' + }; + + return wpcom.req.get( `/sites/${ siteId }/posts/${ postId }/revisions`, query ) + .then( ( revisions ) => { + dispatch( receiveRevisions( siteId, postId, revisions ) ); + dispatch( { + type: POST_REVISIONS_REQUEST_SUCCESS, + siteId, + postId + } ); + } ).catch( ( error ) => { + dispatch( { + type: POST_REVISIONS_REQUEST_FAILURE, + siteId, + postId, + error + } ); + } ); + }; +} + /** * Returns a function which, when invoked, triggers a network request to fetch * posts across all of the current user's sites for the specified query. diff --git a/client/state/posts/reducer.js b/client/state/posts/reducer.js index 40ed4576c78eb2..d893070f9868c5 100644 --- a/client/state/posts/reducer.js +++ b/client/state/posts/reducer.js @@ -2,7 +2,7 @@ * External dependencies */ import { combineReducers } from 'redux'; -import { get, set, omit, omitBy, isEqual, reduce, merge, findKey, mapValues, mapKeys } from 'lodash'; +import { get, set, omit, omitBy, isEqual, reduce, merge, findKey, mapValues, mapKeys, keyBy } from 'lodash'; /** * Internal dependencies @@ -20,6 +20,7 @@ import { POST_REQUEST_FAILURE, POST_RESTORE, POST_RESTORE_FAILURE, + POST_REVISIONS_RECEIVE, POST_SAVE, POST_SAVE_SUCCESS, POSTS_RECEIVE, @@ -307,6 +308,21 @@ export function edits( state = {}, action ) { return state; } +export function revisions( state = {}, action ) { + if ( action.type === POST_REVISIONS_RECEIVE ) { + const { siteId, postId } = action; + return { + ...state, + [ siteId ]: { + ...state[ siteId ], + [ postId ]: keyBy( action.revisions, 'id' ) + } + }; + } + + return state; +} + export default combineReducers( { counts, items, @@ -315,4 +331,5 @@ export default combineReducers( { queries, edits, likes, + revisions, } ); diff --git a/client/state/posts/selectors.js b/client/state/posts/selectors.js index 1cd1358c520d7e..a0d3f682fc0a47 100644 --- a/client/state/posts/selectors.js +++ b/client/state/posts/selectors.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { filter, find, has, get, includes, isEqual, omit, some } from 'lodash'; +import { filter, find, has, get, includes, isEqual, map, omit, some } from 'lodash'; import createSelector from 'lib/create-selector'; import moment from 'moment-timezone'; @@ -464,3 +464,12 @@ export function getSitePostsByTerm( state, siteId, taxonomy, termId ) { find( post.terms[ taxonomy ], postTerm => postTerm.ID === termId ); } ); } + +export const getPostRevisions = createSelector( + ( state, siteId, postId ) => { + return map( + get( state.posts.revisions, [ siteId, postId ], [] ), + normalizePostForDisplay ); + }, + ( state ) => [ state.posts.revisions ] +); From 59e3fb56f832c6e94de9190ab81771f31db1bf05 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 3 Apr 2017 17:04:02 +0200 Subject: [PATCH 02/17] Fix(tests): Add `revisions` to the expected shape of the posts redux state --- client/state/posts/test/reducer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/state/posts/test/reducer.js b/client/state/posts/test/reducer.js index 0578f7e2788172..7ebe75a0c6a9e2 100644 --- a/client/state/posts/test/reducer.js +++ b/client/state/posts/test/reducer.js @@ -52,6 +52,7 @@ describe( 'reducer', () => { 'queries', 'edits', 'likes', + 'revisions', ] ); } ); From e5400ff7b09c59e87dd3b6658b2bb13e49f0b92c Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Tue, 4 Apr 2017 14:47:27 +0200 Subject: [PATCH 03/17] Moving revisions action/reducer/selector into its own subdir under `state/posts` --- .../data/query-post-revisions/index.jsx | 2 +- client/post-editor/post-revision.jsx | 2 +- client/state/posts/actions.js | 48 ----------------- client/state/posts/reducer.js | 19 +------ client/state/posts/revisions/actions.js | 54 +++++++++++++++++++ client/state/posts/revisions/reducer.js | 26 +++++++++ client/state/posts/revisions/selectors.js | 19 +++++++ client/state/posts/selectors.js | 11 +--- 8 files changed, 104 insertions(+), 77 deletions(-) create mode 100644 client/state/posts/revisions/actions.js create mode 100644 client/state/posts/revisions/reducer.js create mode 100644 client/state/posts/revisions/selectors.js diff --git a/client/components/data/query-post-revisions/index.jsx b/client/components/data/query-post-revisions/index.jsx index f2f61b93e8119e..1e5b0e0878089f 100644 --- a/client/components/data/query-post-revisions/index.jsx +++ b/client/components/data/query-post-revisions/index.jsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; /** * Internal dependencies */ -import { requestSitePostRevisions } from 'state/posts/actions'; +import { requestSitePostRevisions } from 'state/posts/revisions/actions'; class QueryPostRevisions extends Component { componentWillMount() { diff --git a/client/post-editor/post-revision.jsx b/client/post-editor/post-revision.jsx index beaa3ae6f11b13..1aa9e7e99bab05 100644 --- a/client/post-editor/post-revision.jsx +++ b/client/post-editor/post-revision.jsx @@ -9,7 +9,7 @@ import { localize } from 'i18n-calypso'; /** * Internal dependencies */ -import { getPostRevisions } from 'state/posts/selectors'; +import { getPostRevisions } from 'state/posts/revisions/selectors'; import QueryPostRevisions from 'components/data/query-post-revisions'; class PostRevision extends React.Component { diff --git a/client/state/posts/actions.js b/client/state/posts/actions.js index 755e8c247b9cb1..f6fe84b46ef860 100644 --- a/client/state/posts/actions.js +++ b/client/state/posts/actions.js @@ -23,10 +23,6 @@ import { POST_SAVE, POST_SAVE_SUCCESS, POST_SAVE_FAILURE, - POST_REVISIONS_RECEIVE, - POST_REVISIONS_REQUEST, - POST_REVISIONS_REQUEST_FAILURE, - POST_REVISIONS_REQUEST_SUCCESS, POSTS_RECEIVE, POSTS_REQUEST, POSTS_REQUEST_SUCCESS, @@ -58,19 +54,6 @@ export function receivePosts( posts ) { }; } -export function receiveRevisions( siteId, postId, revisions ) { - // NOTE: We expect all revisions to be on the same post, thus highly - // coupling it to how the WP-API returns revisions, instead of being able - // to "receive" large (possibly unrelated) batch of revisions. - - return { - type: POST_REVISIONS_RECEIVE, - siteId, - postId, - revisions - }; -} - /** * Triggers a network request to fetch posts for the specified site and query. * @@ -150,37 +133,6 @@ export function requestSitePost( siteId, postId ) { }; } -export function requestSitePostRevisions( siteId, postId ) { - return ( dispatch ) => { - dispatch( { - type: POST_REVISIONS_REQUEST, - siteId, - postId - } ); - - const query = { - apiNamespace: 'wp/v2' - }; - - return wpcom.req.get( `/sites/${ siteId }/posts/${ postId }/revisions`, query ) - .then( ( revisions ) => { - dispatch( receiveRevisions( siteId, postId, revisions ) ); - dispatch( { - type: POST_REVISIONS_REQUEST_SUCCESS, - siteId, - postId - } ); - } ).catch( ( error ) => { - dispatch( { - type: POST_REVISIONS_REQUEST_FAILURE, - siteId, - postId, - error - } ); - } ); - }; -} - /** * Returns a function which, when invoked, triggers a network request to fetch * posts across all of the current user's sites for the specified query. diff --git a/client/state/posts/reducer.js b/client/state/posts/reducer.js index d893070f9868c5..23b0a890aa6195 100644 --- a/client/state/posts/reducer.js +++ b/client/state/posts/reducer.js @@ -2,7 +2,7 @@ * External dependencies */ import { combineReducers } from 'redux'; -import { get, set, omit, omitBy, isEqual, reduce, merge, findKey, mapValues, mapKeys, keyBy } from 'lodash'; +import { get, set, omit, omitBy, isEqual, reduce, merge, findKey, mapValues, mapKeys } from 'lodash'; /** * Internal dependencies @@ -20,7 +20,6 @@ import { POST_REQUEST_FAILURE, POST_RESTORE, POST_RESTORE_FAILURE, - POST_REVISIONS_RECEIVE, POST_SAVE, POST_SAVE_SUCCESS, POSTS_RECEIVE, @@ -32,6 +31,7 @@ import { } from 'state/action-types'; import counts from './counts/reducer'; import likes from './likes/reducer'; +import revisions from './revisions/reducer'; import { getSerializedPostsQuery, isTermsEqual, @@ -308,21 +308,6 @@ export function edits( state = {}, action ) { return state; } -export function revisions( state = {}, action ) { - if ( action.type === POST_REVISIONS_RECEIVE ) { - const { siteId, postId } = action; - return { - ...state, - [ siteId ]: { - ...state[ siteId ], - [ postId ]: keyBy( action.revisions, 'id' ) - } - }; - } - - return state; -} - export default combineReducers( { counts, items, diff --git a/client/state/posts/revisions/actions.js b/client/state/posts/revisions/actions.js new file mode 100644 index 00000000000000..03fe035cd571d9 --- /dev/null +++ b/client/state/posts/revisions/actions.js @@ -0,0 +1,54 @@ +/** + * Internal dependencies + */ +import wpcom from 'lib/wp'; +import { + POST_REVISIONS_RECEIVE, + POST_REVISIONS_REQUEST, + POST_REVISIONS_REQUEST_FAILURE, + POST_REVISIONS_REQUEST_SUCCESS, +} from 'state/action-types'; + +export function receiveRevisions( siteId, postId, revisions ) { + // NOTE: We expect all revisions to be on the same post, thus highly + // coupling it to how the WP-API returns revisions, instead of being able + // to "receive" large (possibly unrelated) batch of revisions. + + return { + type: POST_REVISIONS_RECEIVE, + siteId, + postId, + revisions + }; +} + +export function requestSitePostRevisions( siteId, postId ) { + return ( dispatch ) => { + dispatch( { + type: POST_REVISIONS_REQUEST, + siteId, + postId + } ); + + const query = { + apiNamespace: 'wp/v2' + }; + + return wpcom.req.get( `/sites/${ siteId }/posts/${ postId }/revisions`, query ) + .then( ( revisions ) => { + dispatch( receiveRevisions( siteId, postId, revisions ) ); + dispatch( { + type: POST_REVISIONS_REQUEST_SUCCESS, + siteId, + postId + } ); + } ).catch( ( error ) => { + dispatch( { + type: POST_REVISIONS_REQUEST_FAILURE, + siteId, + postId, + error + } ); + } ); + }; +} diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js new file mode 100644 index 00000000000000..ea756c60150515 --- /dev/null +++ b/client/state/posts/revisions/reducer.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { keyBy } from 'lodash'; + +/** + * Internal dependencies + */ +import { + POST_REVISIONS_RECEIVE, +} from 'state/action-types'; + +export default function revisions( state = {}, action ) { + if ( action.type === POST_REVISIONS_RECEIVE ) { + const { siteId, postId } = action; + return { + ...state, + [ siteId ]: { + ...state[ siteId ], + [ postId ]: keyBy( action.revisions, 'id' ) + } + }; + } + + return state; +} diff --git a/client/state/posts/revisions/selectors.js b/client/state/posts/revisions/selectors.js new file mode 100644 index 00000000000000..44a4c5abb9382e --- /dev/null +++ b/client/state/posts/revisions/selectors.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { get, map } from 'lodash'; +import createSelector from 'lib/create-selector'; + +/** + * Internal dependencies + */ +import { normalizePostForDisplay } from '../utils'; + +export const getPostRevisions = createSelector( + ( state, siteId, postId ) => { + return map( + get( state.posts.revisions, [ siteId, postId ], [] ), + normalizePostForDisplay ); + }, + ( state ) => [ state.posts.revisions ] +); diff --git a/client/state/posts/selectors.js b/client/state/posts/selectors.js index a0d3f682fc0a47..1cd1358c520d7e 100644 --- a/client/state/posts/selectors.js +++ b/client/state/posts/selectors.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { filter, find, has, get, includes, isEqual, map, omit, some } from 'lodash'; +import { filter, find, has, get, includes, isEqual, omit, some } from 'lodash'; import createSelector from 'lib/create-selector'; import moment from 'moment-timezone'; @@ -464,12 +464,3 @@ export function getSitePostsByTerm( state, siteId, taxonomy, termId ) { find( post.terms[ taxonomy ], postTerm => postTerm.ID === termId ); } ); } - -export const getPostRevisions = createSelector( - ( state, siteId, postId ) => { - return map( - get( state.posts.revisions, [ siteId, postId ], [] ), - normalizePostForDisplay ); - }, - ( state ) => [ state.posts.revisions ] -); From 071d11925d639b2b85a16760cfc8441a1ac13b9e Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Tue, 4 Apr 2017 16:44:56 +0200 Subject: [PATCH 04/17] Post Revisions: Normalize revisions for state --- client/post-editor/post-revision.jsx | 6 +----- client/state/posts/revisions/reducer.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/client/post-editor/post-revision.jsx b/client/post-editor/post-revision.jsx index 1aa9e7e99bab05..1f3cc7cc139493 100644 --- a/client/post-editor/post-revision.jsx +++ b/client/post-editor/post-revision.jsx @@ -25,14 +25,10 @@ class PostRevision extends React.Component {

{ this.props.translate( 'Post History' ) }

    { map( this.props.revisions, ( revision ) => { - // TODO: revision's title is empty, need to di - // deeper, probably some discrepancies between - // REST 1.1 / 2.0 output - return (
  • { revision.title }

    -

    { revision.content.rendered }

    +

    { revision.content }

  • ); } ) } diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js index ea756c60150515..113580716cd0a0 100644 --- a/client/state/posts/revisions/reducer.js +++ b/client/state/posts/revisions/reducer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { keyBy } from 'lodash'; +import { keyBy, map } from 'lodash'; /** * Internal dependencies @@ -17,10 +17,23 @@ export default function revisions( state = {}, action ) { ...state, [ siteId ]: { ...state[ siteId ], - [ postId ]: keyBy( action.revisions, 'id' ) + [ postId ]: keyBy( map( action.revisions, normalizeRevisionForState ), 'id' ) } }; } return state; } + +function normalizeRevisionForState( revision ) { + if ( ! revision ) { + return revision; + } + + return { + ...revision, + title: revision.title.rendered, + content: revision.content.rendered, + excerpt: revision.excerpt.rendered, + }; +} From af903ce9fd16260d1f9361a1d4d8742feda389ab Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Tue, 4 Apr 2017 17:39:58 +0200 Subject: [PATCH 05/17] Post Revisions: Add `requesting` to track the status of revisions requests in Redux --- client/state/posts/revisions/reducer.js | 34 ++++++++++++++++++++++- client/state/posts/revisions/selectors.js | 4 +-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js index 113580716cd0a0..50f092001b4381 100644 --- a/client/state/posts/revisions/reducer.js +++ b/client/state/posts/revisions/reducer.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import { combineReducers } from 'redux'; import { keyBy, map } from 'lodash'; /** @@ -8,9 +9,35 @@ import { keyBy, map } from 'lodash'; */ import { POST_REVISIONS_RECEIVE, + POST_REVISIONS_REQUEST, + POST_REVISIONS_REQUEST_FAILURE, + POST_REVISIONS_REQUEST_SUCCESS, + SERIALIZE, + DESERIALIZE } from 'state/action-types'; -export default function revisions( state = {}, action ) { +function requesting( state = {}, action ) { + switch ( action.type ) { + case POST_REVISIONS_REQUEST: + case POST_REVISIONS_REQUEST_FAILURE: + case POST_REVISIONS_REQUEST_SUCCESS: + return { + ...state, + [ action.siteId ]: { + ...state[ action.siteId ], + [ action.postId ]: action.type === POST_REVISIONS_REQUEST, + }, + }; + + case SERIALIZE: + case DESERIALIZE: + return {}; + } + + return state; +} + +export function revisions( state = {}, action ) { if ( action.type === POST_REVISIONS_RECEIVE ) { const { siteId, postId } = action; return { @@ -25,6 +52,11 @@ export default function revisions( state = {}, action ) { return state; } +export default combineReducers( { + requesting, + revisions, +} ); + function normalizeRevisionForState( revision ) { if ( ! revision ) { return revision; diff --git a/client/state/posts/revisions/selectors.js b/client/state/posts/revisions/selectors.js index 44a4c5abb9382e..95c5594dd5a295 100644 --- a/client/state/posts/revisions/selectors.js +++ b/client/state/posts/revisions/selectors.js @@ -12,8 +12,8 @@ import { normalizePostForDisplay } from '../utils'; export const getPostRevisions = createSelector( ( state, siteId, postId ) => { return map( - get( state.posts.revisions, [ siteId, postId ], [] ), + get( state.posts.revisions.revisions, [ siteId, postId ], [] ), normalizePostForDisplay ); }, - ( state ) => [ state.posts.revisions ] + ( state ) => [ state.posts.revisions.revisions ] ); From 40ec8e21ac40deb28726821f4f36473c03751814 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 10 Apr 2017 14:07:30 +0200 Subject: [PATCH 06/17] Post Revisions: Use `data-layer` instead of redux-thunk to fetch revisions --- .../data/query-post-revisions/index.jsx | 12 ++- client/state/data-layer/wpcom/index.js | 2 + client/state/data-layer/wpcom/posts/index.js | 6 ++ .../data-layer/wpcom/posts/revisions/index.js | 66 ++++++++++++++ client/state/posts/revisions/actions.js | 90 ++++++++++--------- 5 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 client/state/data-layer/wpcom/posts/index.js create mode 100644 client/state/data-layer/wpcom/posts/revisions/index.js diff --git a/client/components/data/query-post-revisions/index.jsx b/client/components/data/query-post-revisions/index.jsx index 1e5b0e0878089f..35ff4c3a228a89 100644 --- a/client/components/data/query-post-revisions/index.jsx +++ b/client/components/data/query-post-revisions/index.jsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; /** * Internal dependencies */ -import { requestSitePostRevisions } from 'state/posts/revisions/actions'; +import { requestPostRevisions } from 'state/posts/revisions/actions'; class QueryPostRevisions extends Component { componentWillMount() { @@ -24,7 +24,7 @@ class QueryPostRevisions extends Component { } request( props ) { - props.requestSitePostRevisions( props.siteId, props.postId ); + props.requestPostRevisions( props.siteId, props.postId ); } render() { @@ -35,12 +35,10 @@ class QueryPostRevisions extends Component { QueryPostRevisions.propTypes = { postId: PropTypes.number, siteId: PropTypes.number, - requestSitePostRevisions: PropTypes.func, + requestPostRevisions: PropTypes.func, }; export default connect( - () => { - return {}; - }, - { requestSitePostRevisions } + () => ( {} ), + { requestPostRevisions } )( QueryPostRevisions ); diff --git a/client/state/data-layer/wpcom/index.js b/client/state/data-layer/wpcom/index.js index 50e9d6dd60e3d3..44931f5bc3588b 100644 --- a/client/state/data-layer/wpcom/index.js +++ b/client/state/data-layer/wpcom/index.js @@ -4,6 +4,7 @@ import { mergeHandlers } from 'state/data-layer/utils'; import accountRecovery from './account-recovery'; import plans from './plans'; +import posts from './posts'; import read from './read'; import sites from './sites'; import timezones from './timezones'; @@ -12,6 +13,7 @@ import videos from './videos'; export const handlers = mergeHandlers( accountRecovery, plans, + posts, read, sites, timezones, diff --git a/client/state/data-layer/wpcom/posts/index.js b/client/state/data-layer/wpcom/posts/index.js new file mode 100644 index 00000000000000..8ca60b59433c55 --- /dev/null +++ b/client/state/data-layer/wpcom/posts/index.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import revisions from './revisions'; + +export default revisions; diff --git a/client/state/data-layer/wpcom/posts/revisions/index.js b/client/state/data-layer/wpcom/posts/revisions/index.js new file mode 100644 index 00000000000000..abb2b1ca067971 --- /dev/null +++ b/client/state/data-layer/wpcom/posts/revisions/index.js @@ -0,0 +1,66 @@ +/** + * Internal dependencies + */ +import { + POST_REVISIONS_REQUEST, +} from 'state/action-types'; +import { dispatchRequest } from 'state/data-layer/wpcom-http/utils'; +import { http } from 'state/data-layer/wpcom-http/actions'; +import { + receivePostRevisions, + receivePostRevisionsSuccess, + receivePostRevisionsFailure, +} from 'state/plans/actions'; + +/** + * Dispatches returned error from post revisions request + * + * @param {Function} dispatch Redux dispatcher + * @param {Object} action Redux action + * @param {String} action.siteId of the revisions + * @param {String} action.postId of the revisions + * @param {Function} next dispatches to next middleware in chain + * @param {Object} rawError from HTTP request + * @returns {Object} the dispatched action + */ +export const receiveError = ( { dispatch }, { siteId, postId }, next, rawError ) => + dispatch( receivePostRevisionsFailure( siteId, postId, rawError ) ); + +/** + * Dispatches returned post revisions + * + * @param {Function} dispatch Redux dispatcher + * @param {Object} action Redux action + * @param {String} action.siteId of the revisions + * @param {String} action.postId of the revisions + * @param {Function} next dispatches to next middleware in chain + * @param {Array} revisions raw data from post revisions API + */ +export const receiveSuccess = ( { dispatch }, { siteId, postId }, next, revisions ) => { + dispatch( receivePostRevisionsSuccess( siteId, postId ) ); + dispatch( receivePostRevisions( siteId, postId, revisions ) ); +}; + +/** + * Dispatches a request to fetch post revisions + * + * @param {Function} dispatch Redux dispatcher + * @param {Object} action Redux action + * @param {String} action.siteId of the revisions + * @param {String} action.postId of the revisions + * @returns {Object} the dispatched action + */ +export const fetchPostRevisions = ( { dispatch }, { siteId, postId } ) => + dispatch( http( { + path: `/sites/${ siteId }/posts/${ postId }/revisions`, + method: 'GET', + query: { + apiNamespace: 'wp/v2', + }, + } ) ); + +const dispatchPostRevisionsRequest = dispatchRequest( fetchPostRevisions, receiveSuccess, receiveError ); + +export default { + [ POST_REVISIONS_REQUEST ]: [ dispatchPostRevisionsRequest ] +}; diff --git a/client/state/posts/revisions/actions.js b/client/state/posts/revisions/actions.js index 03fe035cd571d9..b445354825a359 100644 --- a/client/state/posts/revisions/actions.js +++ b/client/state/posts/revisions/actions.js @@ -1,7 +1,6 @@ /** * Internal dependencies */ -import wpcom from 'lib/wp'; import { POST_REVISIONS_RECEIVE, POST_REVISIONS_REQUEST, @@ -9,46 +8,55 @@ import { POST_REVISIONS_REQUEST_SUCCESS, } from 'state/action-types'; -export function receiveRevisions( siteId, postId, revisions ) { - // NOTE: We expect all revisions to be on the same post, thus highly - // coupling it to how the WP-API returns revisions, instead of being able - // to "receive" large (possibly unrelated) batch of revisions. - - return { - type: POST_REVISIONS_RECEIVE, - siteId, - postId, - revisions - }; -} +/** + * Action creator function: POST_REVISIONS_REQUEST + * + * @param {String} siteId of the revisions + * @param {String} postId of the revisions + * @return {Object} action object + */ +export const requestPostRevisions = ( siteId, postId ) => ( { + type: POST_REVISIONS_REQUEST, + siteId, postId, +} ); -export function requestSitePostRevisions( siteId, postId ) { - return ( dispatch ) => { - dispatch( { - type: POST_REVISIONS_REQUEST, - siteId, - postId - } ); +/** + * Action creator function: POST_REVISIONS_REQUEST_SUCCESS + * + * @param {String} siteId of the revisions + * @param {String} postId of the revisions + * @return {Object} action object + */ +export const receivePostRevisionsSuccess = ( siteId, postId ) => ( { + type: POST_REVISIONS_REQUEST_SUCCESS, + siteId, postId, +} ); - const query = { - apiNamespace: 'wp/v2' - }; +/** + * Action creator function: POST_REVISIONS_REQUEST_FAILURE + * + * @param {String} siteId of the revisions + * @param {String} postId of the revisions + * @param {Object} error raw error + * @return {Object} action object + */ +export const receivePostRevisionsFailure = ( siteId, postId, error ) => ( { + type: POST_REVISIONS_REQUEST_FAILURE, + siteId, postId, error +} ); - return wpcom.req.get( `/sites/${ siteId }/posts/${ postId }/revisions`, query ) - .then( ( revisions ) => { - dispatch( receiveRevisions( siteId, postId, revisions ) ); - dispatch( { - type: POST_REVISIONS_REQUEST_SUCCESS, - siteId, - postId - } ); - } ).catch( ( error ) => { - dispatch( { - type: POST_REVISIONS_REQUEST_FAILURE, - siteId, - postId, - error - } ); - } ); - }; -} +/** + * Action creator function: POST_REVISIONS_RECEIVE + * + * @param {String} siteId of the revisions + * @param {String} postId of the revisions + * @param {Object} revisions data received + * @return {Object} action object + */ +export const receivePostRevisions = ( siteId, postId, revisions ) => ( { + // NOTE: We expect all revisions to be on the same post, thus highly + // coupling it to how the WP-API returns revisions, instead of being able + // to "receive" large (possibly unrelated) batch of revisions. + type: POST_REVISIONS_RECEIVE, + siteId, postId, revisions, +} ); From 2999c4326716487818c6e792faa0b9a643c044e8 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 17 Apr 2017 13:32:53 +0200 Subject: [PATCH 07/17] Post Revisions: Remove all presentation code for a cleaner Redux PR #12733 --- client/post-editor/controller.js | 20 ----------- client/post-editor/index.js | 1 - client/post-editor/post-revision.jsx | 53 ---------------------------- 3 files changed, 74 deletions(-) delete mode 100644 client/post-editor/post-revision.jsx diff --git a/client/post-editor/controller.js b/client/post-editor/controller.js index 12e2fce30e201f..19d60d2773e561 100644 --- a/client/post-editor/controller.js +++ b/client/post-editor/controller.js @@ -22,7 +22,6 @@ import userUtils from 'lib/user/utils'; import analytics from 'lib/analytics'; import { decodeEntities } from 'lib/formatting'; import PostEditor from './post-editor'; -import PostRevision from './post-revision'; import { startEditingPost, stopEditingPost } from 'state/ui/editor/actions'; import { getSelectedSiteId } from 'state/ui/selectors'; import { getEditorPostId, getEditorPath } from 'state/ui/editor/selectors'; @@ -70,18 +69,6 @@ function renderEditor( context, postType ) { ); } -function renderRevision( context, siteId, postId ) { - ReactDom.unmountComponentAtNode( document.getElementById( 'secondary' ) ); - ReactDom.render( - React.createElement( ReduxProvider, { store: context.store }, - React.createElement( PostRevision, { - postId, siteId - } ) - ), - document.getElementById( 'primary' ) - ); -} - function maybeRedirect( context ) { const state = context.store.getState(); const siteId = getSelectedSiteId( state ); @@ -248,13 +235,6 @@ module.exports = { renderEditor( context, postType ); }, - revision: function( context ) { - const siteId = getSelectedSiteId( context.store.getState() ); - const postId = getPostID( context ); - - renderRevision( context, siteId, postId ); - }, - exitPost: function( context, next ) { const postId = getPostID( context ); const siteId = getSelectedSiteId( context.store.getState() ); diff --git a/client/post-editor/index.js b/client/post-editor/index.js index 3f2dcc41b8a1d5..5fc40760696394 100644 --- a/client/post-editor/index.js +++ b/client/post-editor/index.js @@ -14,7 +14,6 @@ module.exports = function() { page( '/post', controller.pressThis, sitesController.siteSelection, sitesController.sites ); page( '/post/new', () => page.redirect( '/post' ) ); // redirect from beep-beep-boop page( '/post/:site?/:post?', sitesController.siteSelection, sitesController.fetchJetpackSettings, controller.post ); - page( '/post/revision/:site?/:post?', sitesController.siteSelection, sitesController.fetchJetpackSettings, controller.revision ); page.exit( '/post/:site?/:post?', controller.exitPost ); page( '/page', sitesController.siteSelection, sitesController.sites ); diff --git a/client/post-editor/post-revision.jsx b/client/post-editor/post-revision.jsx deleted file mode 100644 index 1f3cc7cc139493..00000000000000 --- a/client/post-editor/post-revision.jsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * External dependencies - */ -import { map } from 'lodash'; -import React from 'react'; -import { connect } from 'react-redux'; -import { localize } from 'i18n-calypso'; - -/** - * Internal dependencies - */ -import { getPostRevisions } from 'state/posts/revisions/selectors'; -import QueryPostRevisions from 'components/data/query-post-revisions'; - -class PostRevision extends React.Component { - render() { - // FIXME: this is merely a placehodler component to test "displaying" - // stuff out of the redux store. - - return ( -
    - -

    { this.props.translate( 'Post History' ) }

    -
      - { map( this.props.revisions, ( revision ) => { - return ( -
    • -

      { revision.title }

      -

      { revision.content }

      -
    • - ); - } ) } -
    -
    - ); - } -} - -PostRevision.propTypes = { - postId: React.PropTypes.number, - siteId: React.PropTypes.number, - translate: React.PropTypes.func, -}; - -export default connect( - ( state, { siteId, postId } ) => { - return { - revisions: getPostRevisions( state, siteId, postId ) - }; - } -)( localize( PostRevision ) ); From a6ca250a3e378dbf8cf6f13a96bfd67ac1621456 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 17 Apr 2017 22:02:06 +0200 Subject: [PATCH 08/17] Post Revisions: Tests for reducers --- client/state/posts/revisions/reducer.js | 13 +- client/state/posts/revisions/test/reducer.js | 269 +++++++++++++++++++ 2 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 client/state/posts/revisions/test/reducer.js diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js index 50f092001b4381..cbfbb76091cd99 100644 --- a/client/state/posts/revisions/reducer.js +++ b/client/state/posts/revisions/reducer.js @@ -2,7 +2,9 @@ * External dependencies */ import { combineReducers } from 'redux'; -import { keyBy, map } from 'lodash'; +import { flow, keyBy, map } from 'lodash'; +import mapValues from 'lodash/fp/mapValues'; +import pick from 'lodash/fp/pick'; /** * Internal dependencies @@ -16,7 +18,7 @@ import { DESERIALIZE } from 'state/action-types'; -function requesting( state = {}, action ) { +export function requesting( state = {}, action ) { switch ( action.type ) { case POST_REVISIONS_REQUEST: case POST_REVISIONS_REQUEST_FAILURE: @@ -64,8 +66,9 @@ function normalizeRevisionForState( revision ) { return { ...revision, - title: revision.title.rendered, - content: revision.content.rendered, - excerpt: revision.excerpt.rendered, + ...flow( + pick( [ 'title', 'content', 'excerpt' ] ), + mapValues( ( val = {} ) => val.rendered ) + )( revision ) }; } diff --git a/client/state/posts/revisions/test/reducer.js b/client/state/posts/revisions/test/reducer.js new file mode 100644 index 00000000000000..2cafae8262d7a5 --- /dev/null +++ b/client/state/posts/revisions/test/reducer.js @@ -0,0 +1,269 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { + POST_REVISIONS_RECEIVE, + POST_REVISIONS_REQUEST, + POST_REVISIONS_REQUEST_FAILURE, + POST_REVISIONS_REQUEST_SUCCESS, + DESERIALIZE, + SERIALIZE, +} from 'state/action-types'; +import reducer, { + requesting, + revisions, +} from '../reducer'; + +describe( 'reducer', () => { + it( 'should include expected keys in return value', () => { + expect( reducer( undefined, {} ) ).to.have.keys( [ + 'requesting', + 'revisions', + ] ); + } ); + + describe( '#requesting', () => { + it( 'should default to an empty object', () => { + const state = requesting( undefined, {} ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should set to `true` if a request is initiated', () => { + const state = requesting( undefined, { + type: POST_REVISIONS_REQUEST, + siteId: 12345678, + postId: 50, + } ); + + expect( state ).to.eql( { + 12345678: { + 50: true, + }, + } ); + } ); + + it( 'should set to `false` if the request is successful', () => { + const state = requesting( deepFreeze( { + 12345678: { + 50: true, + }, + } ), { + type: POST_REVISIONS_REQUEST_SUCCESS, + siteId: 12345678, + postId: 50, + } ); + + expect( state ).to.eql( { + 12345678: { + 50: false, + }, + } ); + } ); + + it( 'should set to `false` if the request fails', () => { + const state = requesting( deepFreeze( { + 12345678: { + 50: true, + }, + } ), { + type: POST_REVISIONS_REQUEST_FAILURE, + siteId: 12345678, + postId: 50, + } ); + + expect( state ).to.eql( { + 12345678: { + 50: false, + }, + } ); + } ); + + it( 'should support multiple sites', () => { + const state = requesting( deepFreeze( { + 12345678: { + 50: true, + }, + } ), { + type: POST_REVISIONS_REQUEST, + siteId: 87654321, + postId: 10, + } ); + + expect( state ).to.eql( { + 12345678: { + 50: true, + }, + 87654321: { + 10: true, + }, + } ); + } ); + + it( 'should support multiple posts', () => { + const state = requesting( deepFreeze( { + 12345678: { + 50: true, + }, + } ), { + type: POST_REVISIONS_REQUEST, + siteId: 12345678, + postId: 10, + } ); + + expect( state ).to.eql( { + 12345678: { + 50: true, + 10: true, + }, + } ); + } ); + + it( 'should not persist state', () => { + const state = requesting( deepFreeze( { + 12345678: { + 50: true, + }, + } ), { + type: SERIALIZE, + } ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should not load persisted state', () => { + const state = requesting( deepFreeze( { + 12345678: { + 50: true, + }, + } ), { + type: DESERIALIZE, + } ); + + expect( state ).to.eql( {} ); + } ); + } ); + + describe( '#revisions', () => { + it( 'should default to an empty object', () => { + const state = revisions( undefined, {} ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should normalize revisions', () => { + const state = revisions( undefined, { + type: POST_REVISIONS_RECEIVE, + siteId: 12345678, + postId: 10, + revisions: [ + { + id: 11, + title: { + rendered: 'Foo Bar', + }, + content: { + rendered: 'Lorem Ipsum dolor sit amet', + }, + excerpt: { + rendered: 'Lorem ipsum', + }, + }, + ], + } ); + + expect( state ).to.eql( { + 12345678: { + 10: { + 11: { + id: 11, + title: 'Foo Bar', + content: 'Lorem Ipsum dolor sit amet', + excerpt: 'Lorem ipsum', + }, + }, + }, + } ); + } ); + + it( 'should support multiple sites', () => { + const state = revisions( deepFreeze( { + 12345678: { + 50: { + 51: { + id: 51, + }, + }, + }, + } ), { + type: POST_REVISIONS_RECEIVE, + siteId: 87654321, + postId: 10, + revisions: [ + { + id: 11, + }, + ], + } ); + + expect( state ).to.eql( { + 12345678: { + 50: { + 51: { + id: 51, + }, + }, + }, + 87654321: { + 10: { + 11: { + id: 11, + }, + }, + }, + } ); + } ); + + it( 'should support multiple posts', () => { + const state = revisions( deepFreeze( { + 12345678: { + 50: { + 51: { + id: 51, + }, + }, + } + } ), { + type: POST_REVISIONS_RECEIVE, + siteId: 12345678, + postId: 10, + revisions: [ + { + id: 11, + }, + ], + } ); + + expect( state ).to.eql( { + 12345678: { + 50: { + 51: { + id: 51, + }, + }, + 10: { + 11: { + id: 11, + }, + }, + }, + } ); + } ); + } ); +} ); From ef55dd78538cd900647bac4e16a494ee4cbd5a6e Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Tue, 18 Apr 2017 13:32:24 +0200 Subject: [PATCH 09/17] Post Revisions: Tests for selectors --- client/state/posts/revisions/selectors.js | 8 +-- .../state/posts/revisions/test/selectors.js | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 client/state/posts/revisions/test/selectors.js diff --git a/client/state/posts/revisions/selectors.js b/client/state/posts/revisions/selectors.js index 95c5594dd5a295..7e14b7b211b466 100644 --- a/client/state/posts/revisions/selectors.js +++ b/client/state/posts/revisions/selectors.js @@ -9,11 +9,13 @@ import createSelector from 'lib/create-selector'; */ import { normalizePostForDisplay } from '../utils'; -export const getPostRevisions = createSelector( +export const getNormalizedPostRevisions = createSelector( ( state, siteId, postId ) => { - return map( + const normalizedRevisions = map( get( state.posts.revisions.revisions, [ siteId, postId ], [] ), - normalizePostForDisplay ); + normalizePostForDisplay + ); + return normalizedRevisions; }, ( state ) => [ state.posts.revisions.revisions ] ); diff --git a/client/state/posts/revisions/test/selectors.js b/client/state/posts/revisions/test/selectors.js new file mode 100644 index 00000000000000..d04217ba7b3de7 --- /dev/null +++ b/client/state/posts/revisions/test/selectors.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { + getNormalizedPostRevisions, +} from '../selectors'; + +describe( 'selectors', () => { + describe( 'getNormalizedPostRevisions', () => { + it( 'should return an empty array if there is no revision in the state for `siteId, postId`', () => { + const postRevisions = getNormalizedPostRevisions( { + posts: { + revisions: { + revisions: {}, + }, + }, + }, 12345678, 10 ); + + expect( postRevisions ).to.eql( [] ); + } ); + + it( 'should return an array of normalized post revisions', () => { + const postRevisions = getNormalizedPostRevisions( { + posts: { + revisions: { + revisions: { + 12345678: { + 10: { + 11: { + id: 11, + title: 'Badman ', + }, + }, + }, + }, + }, + }, + }, 12345678, 10 ); + + expect( postRevisions ).to.eql( [ + { + id: 11, + title: 'Badman ', + }, + ] ); + } ); + } ); +} ); From 33d10ad572112c4bbf6d35f13aa8e6c233988a85 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Tue, 18 Apr 2017 13:50:11 +0200 Subject: [PATCH 10/17] Query Post Revisions: Use `componentDidUpdate` instead of `componentWillReceiveProps` to dispatch the data request --- .../data/query-post-revisions/index.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/components/data/query-post-revisions/index.jsx b/client/components/data/query-post-revisions/index.jsx index 35ff4c3a228a89..3d70b7e83bd0c7 100644 --- a/client/components/data/query-post-revisions/index.jsx +++ b/client/components/data/query-post-revisions/index.jsx @@ -11,20 +11,22 @@ import { requestPostRevisions } from 'state/posts/revisions/actions'; class QueryPostRevisions extends Component { componentWillMount() { - this.request( this.props ); + this.request(); } - componentWillReceiveProps( nextProps ) { - if ( this.props.siteId === nextProps.siteId && - this.props.postId === nextProps.postId ) { + componentDidUpdate( prevProps ) { + if ( + this.props.siteId === prevProps.siteId && + this.props.postId === prevProps.postId + ) { return; } - this.request( nextProps ); + this.request(); } - request( props ) { - props.requestPostRevisions( props.siteId, props.postId ); + request() { + this.props.requestPostRevisions( this.props.siteId, this.props.postId ); } render() { From f04b065157d038f51ecdc3de762b1617571cb896 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Fri, 21 Apr 2017 11:29:48 +0200 Subject: [PATCH 11/17] Post Revisions: Reducers: Use Lodash's `merge` over multiple spreads --- client/state/posts/revisions/reducer.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js index cbfbb76091cd99..71c2383e20ef0d 100644 --- a/client/state/posts/revisions/reducer.js +++ b/client/state/posts/revisions/reducer.js @@ -2,7 +2,7 @@ * External dependencies */ import { combineReducers } from 'redux'; -import { flow, keyBy, map } from 'lodash'; +import { flow, keyBy, map, merge } from 'lodash'; import mapValues from 'lodash/fp/mapValues'; import pick from 'lodash/fp/pick'; @@ -23,13 +23,11 @@ export function requesting( state = {}, action ) { case POST_REVISIONS_REQUEST: case POST_REVISIONS_REQUEST_FAILURE: case POST_REVISIONS_REQUEST_SUCCESS: - return { - ...state, + return merge( {}, state, { [ action.siteId ]: { - ...state[ action.siteId ], [ action.postId ]: action.type === POST_REVISIONS_REQUEST, }, - }; + } ); case SERIALIZE: case DESERIALIZE: @@ -42,13 +40,11 @@ export function requesting( state = {}, action ) { export function revisions( state = {}, action ) { if ( action.type === POST_REVISIONS_RECEIVE ) { const { siteId, postId } = action; - return { - ...state, + return merge( {}, state, { [ siteId ]: { - ...state[ siteId ], [ postId ]: keyBy( map( action.revisions, normalizeRevisionForState ), 'id' ) } - }; + } ); } return state; From ce32197492692f0d22189915b4532fa94450767f Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Fri, 21 Apr 2017 11:38:04 +0200 Subject: [PATCH 12/17] Post Revisions: Normalize Revisions API response in the data-layer --- .../data-layer/wpcom/posts/revisions/index.js | 31 ++++- .../wpcom/posts/revisions/test/index.js | 114 ++++++++++++++++++ client/state/posts/revisions/actions.js | 2 +- client/state/posts/revisions/reducer.js | 20 +-- client/state/posts/revisions/test/reducer.js | 35 ------ 5 files changed, 146 insertions(+), 56 deletions(-) create mode 100644 client/state/data-layer/wpcom/posts/revisions/test/index.js diff --git a/client/state/data-layer/wpcom/posts/revisions/index.js b/client/state/data-layer/wpcom/posts/revisions/index.js index abb2b1ca067971..98ecbf3305b8b3 100644 --- a/client/state/data-layer/wpcom/posts/revisions/index.js +++ b/client/state/data-layer/wpcom/posts/revisions/index.js @@ -1,3 +1,10 @@ +/** + * External dependencies + */ +import { flow, map } from 'lodash'; +import mapValues from 'lodash/fp/mapValues'; +import pick from 'lodash/fp/pick'; + /** * Internal dependencies */ @@ -10,7 +17,27 @@ import { receivePostRevisions, receivePostRevisionsSuccess, receivePostRevisionsFailure, -} from 'state/plans/actions'; +} from 'state/posts/revisions/actions'; + +/** + * Normalize a WP REST API Post Revisions ressource for consumption in Calypso + * + * @param {Object} revision Raw revision from the API + * @returns {Object} the normalized revision + */ +export function normalizeRevision( revision ) { + if ( ! revision ) { + return revision; + } + + return { + ...revision, + ...flow( + pick( [ 'title', 'content', 'excerpt' ] ), + mapValues( ( val = {} ) => val.rendered ) + )( revision ) + }; +} /** * Dispatches returned error from post revisions request @@ -38,7 +65,7 @@ export const receiveError = ( { dispatch }, { siteId, postId }, next, rawError ) */ export const receiveSuccess = ( { dispatch }, { siteId, postId }, next, revisions ) => { dispatch( receivePostRevisionsSuccess( siteId, postId ) ); - dispatch( receivePostRevisions( siteId, postId, revisions ) ); + dispatch( receivePostRevisions( siteId, postId, map( revisions, normalizeRevision ) ) ); }; /** diff --git a/client/state/data-layer/wpcom/posts/revisions/test/index.js b/client/state/data-layer/wpcom/posts/revisions/test/index.js new file mode 100644 index 00000000000000..2f5247ccea0b44 --- /dev/null +++ b/client/state/data-layer/wpcom/posts/revisions/test/index.js @@ -0,0 +1,114 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import { map } from 'lodash'; +import sinon from 'sinon'; + +/** + * Internal dependencies + */ +import { + fetchPostRevisions, + normalizeRevision, + receiveSuccess, + receiveError, +} from '../'; +import { + receivePostRevisions, + receivePostRevisionsSuccess, + receivePostRevisionsFailure, + requestPostRevisions, +} from 'state/posts/revisions/actions'; +import { http } from 'state/data-layer/wpcom-http/actions'; + +const successfulPostRevisionsResponse = [ + { + author: 1, + date: '2017-04-20T12:14:40', + date_gmt: '2017-04-20T12:14:40', + id: 11, + modified: '2017-04-21T12:14:40', + modified_gmt: '2017-04-21T12:14:40', + parent: 10, + title: { + rendered: 'Sed nobis ab earum', + }, + content: { + rendered: '

    Lorem ipsum

    ', + }, + excerpt: { + rendered: '', + }, + }, +]; + +const normalizedPostRevisions = [ + { + author: 1, + date: '2017-04-20T12:14:40', + date_gmt: '2017-04-20T12:14:40', + id: 11, + modified: '2017-04-21T12:14:40', + modified_gmt: '2017-04-21T12:14:40', + parent: 10, + title: 'Sed nobis ab earum', + content: '

    Lorem ipsum

    ', + excerpt: '', + }, +]; + +describe( '#normalizeRevision', () => { + it( 'should only keep the rendered version of `title`, `content` and `excerpt`', () => { + expect( + map( successfulPostRevisionsResponse, normalizeRevision ) + ).to.eql( normalizedPostRevisions ); + } ); +} ); + +describe( '#fetchPostRevisions', () => { + it( 'should dispatch HTTP request to tag endpoint', () => { + const action = requestPostRevisions( 12345678, 10 ); + const dispatch = sinon.spy(); + const next = sinon.spy(); + + fetchPostRevisions( { dispatch }, action, next ); + + expect( dispatch ).to.have.been.calledOnce; + expect( dispatch ).to.have.been.calledWith( http( { + method: 'GET', + path: '/sites/12345678/posts/10/revisions', + query: { + apiNamespace: 'wp/v2', + }, + } ) ); + } ); +} ); + +describe( '#receiveSuccess', () => { + it( 'should normalize the revisions and dispatch `receivePostRevisions` and `receivePostRevisionsSuccess`', () => { + const action = requestPostRevisions( 12345678, 10 ); + const dispatch = sinon.spy(); + const next = sinon.spy(); + + receiveSuccess( { dispatch }, action, next, successfulPostRevisionsResponse ); + + expect( dispatch ).to.have.been.calledTwice; + expect( dispatch ).to.have.been.calledWith( receivePostRevisionsSuccess( 12345678, 10 ) ); + expect( dispatch ).to.have.been.calledWith( receivePostRevisions( 12345678, 10, normalizedPostRevisions ) ); + } ); +} ); + +describe( '#receiveError', () => { + it( 'should dispatch `receivePostRevisionsFailure`', () => { + const action = requestPostRevisions( 12345678, 10 ); + const dispatch = sinon.spy(); + const next = sinon.spy(); + const rawError = new Error( 'Foo Bar' ); + + receiveError( { dispatch }, action, next, rawError ); + + expect( dispatch ).to.have.been.calledOnce; + expect( dispatch ).to.have.been.calledWith( receivePostRevisionsFailure( 12345678, 10, rawError ) ); + } ); +} ); diff --git a/client/state/posts/revisions/actions.js b/client/state/posts/revisions/actions.js index b445354825a359..a68acd47cb247e 100644 --- a/client/state/posts/revisions/actions.js +++ b/client/state/posts/revisions/actions.js @@ -50,7 +50,7 @@ export const receivePostRevisionsFailure = ( siteId, postId, error ) => ( { * * @param {String} siteId of the revisions * @param {String} postId of the revisions - * @param {Object} revisions data received + * @param {Object} revisions already normalized * @return {Object} action object */ export const receivePostRevisions = ( siteId, postId, revisions ) => ( { diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js index 71c2383e20ef0d..1a7ae868702b34 100644 --- a/client/state/posts/revisions/reducer.js +++ b/client/state/posts/revisions/reducer.js @@ -2,9 +2,7 @@ * External dependencies */ import { combineReducers } from 'redux'; -import { flow, keyBy, map, merge } from 'lodash'; -import mapValues from 'lodash/fp/mapValues'; -import pick from 'lodash/fp/pick'; +import { keyBy, merge } from 'lodash'; /** * Internal dependencies @@ -42,7 +40,7 @@ export function revisions( state = {}, action ) { const { siteId, postId } = action; return merge( {}, state, { [ siteId ]: { - [ postId ]: keyBy( map( action.revisions, normalizeRevisionForState ), 'id' ) + [ postId ]: keyBy( action.revisions, 'id' ) } } ); } @@ -54,17 +52,3 @@ export default combineReducers( { requesting, revisions, } ); - -function normalizeRevisionForState( revision ) { - if ( ! revision ) { - return revision; - } - - return { - ...revision, - ...flow( - pick( [ 'title', 'content', 'excerpt' ] ), - mapValues( ( val = {} ) => val.rendered ) - )( revision ) - }; -} diff --git a/client/state/posts/revisions/test/reducer.js b/client/state/posts/revisions/test/reducer.js index 2cafae8262d7a5..b9710456a9669b 100644 --- a/client/state/posts/revisions/test/reducer.js +++ b/client/state/posts/revisions/test/reducer.js @@ -157,41 +157,6 @@ describe( 'reducer', () => { expect( state ).to.eql( {} ); } ); - it( 'should normalize revisions', () => { - const state = revisions( undefined, { - type: POST_REVISIONS_RECEIVE, - siteId: 12345678, - postId: 10, - revisions: [ - { - id: 11, - title: { - rendered: 'Foo Bar', - }, - content: { - rendered: 'Lorem Ipsum dolor sit amet', - }, - excerpt: { - rendered: 'Lorem ipsum', - }, - }, - ], - } ); - - expect( state ).to.eql( { - 12345678: { - 10: { - 11: { - id: 11, - title: 'Foo Bar', - content: 'Lorem Ipsum dolor sit amet', - excerpt: 'Lorem ipsum', - }, - }, - }, - } ); - } ); - it( 'should support multiple sites', () => { const state = revisions( deepFreeze( { 12345678: { From 696ef278f46135c5f4cb8092bcd1290c09281120 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Fri, 21 Apr 2017 16:19:03 +0200 Subject: [PATCH 13/17] Post Revisions: Reducers: Revert using `merge` on reducers with expectations of discarding previous content --- client/state/posts/revisions/reducer.js | 6 ++-- client/state/posts/revisions/test/reducer.js | 31 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/client/state/posts/revisions/reducer.js b/client/state/posts/revisions/reducer.js index 1a7ae868702b34..8ebd375af32fc3 100644 --- a/client/state/posts/revisions/reducer.js +++ b/client/state/posts/revisions/reducer.js @@ -38,11 +38,13 @@ export function requesting( state = {}, action ) { export function revisions( state = {}, action ) { if ( action.type === POST_REVISIONS_RECEIVE ) { const { siteId, postId } = action; - return merge( {}, state, { + return { + ...state, [ siteId ]: { + ...state[ siteId ], [ postId ]: keyBy( action.revisions, 'id' ) } - } ); + }; } return state; diff --git a/client/state/posts/revisions/test/reducer.js b/client/state/posts/revisions/test/reducer.js index b9710456a9669b..b7d3b0c8158bf9 100644 --- a/client/state/posts/revisions/test/reducer.js +++ b/client/state/posts/revisions/test/reducer.js @@ -230,5 +230,36 @@ describe( 'reducer', () => { }, } ); } ); + + it( 'should discard previous revisions for the same post', () => { + const state = revisions( deepFreeze( { + 12345678: { + 10: { + 51: { + id: 51, + }, + }, + } + } ), { + type: POST_REVISIONS_RECEIVE, + siteId: 12345678, + postId: 10, + revisions: [ + { + id: 52, + }, + ], + } ); + + expect( state ).to.eql( { + 12345678: { + 10: { + 52: { + id: 52, + }, + }, + }, + } ); + } ); } ); } ); From d96500ce68c0989b9d0a2a0f0bda48d8b8f35dd5 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 24 Apr 2017 08:41:07 +0200 Subject: [PATCH 14/17] Post Revisions: Add README for `QueryPostRevisions` component --- .../data/query-post-revisions/README.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 client/components/data/query-post-revisions/README.md diff --git a/client/components/data/query-post-revisions/README.md b/client/components/data/query-post-revisions/README.md new file mode 100644 index 00000000000000..b66095f26a2f32 --- /dev/null +++ b/client/components/data/query-post-revisions/README.md @@ -0,0 +1,46 @@ +Query Post Revisions +================ + +`` is a React component used to request post revisions data. + +## Usage + +Render the component, passing `siteId` and `postId`. It does not accept any children, nor does it render any element to the page. You can use it adjacent to other sibling components which make use of the fetched data made available through the global application state. + +```jsx +import React from 'react'; +import QueryPostRevisions from 'components/data/query-post-revisions'; + +export default function PostRevisions( { revisions } ) { + return ( +
    + +
    { revisions }
    +
    + ); +} +``` + +## Props + +### `siteId` + + + + +
    TypeNumber
    RequiredYes
    + +The site ID for which we request post revisions. + + +### `siteId` + + + + +
    TypeNumber
    RequiredYes
    + +The post ID for which we request post revisions. From 6498395307ad9743c41aa67e05f95969fb85adb7 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 24 Apr 2017 10:44:08 +0200 Subject: [PATCH 15/17] Query Post Revisions: Fix README typo: `siteId` -> `postId` --- client/components/data/query-post-revisions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/data/query-post-revisions/README.md b/client/components/data/query-post-revisions/README.md index b66095f26a2f32..84351a1a563184 100644 --- a/client/components/data/query-post-revisions/README.md +++ b/client/components/data/query-post-revisions/README.md @@ -36,7 +36,7 @@ export default function PostRevisions( { revisions } ) { The site ID for which we request post revisions. -### `siteId` +### `postId` From 8ea42a7580c3e82113b624e73740bde65adf3e91 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 24 Apr 2017 18:27:31 +0200 Subject: [PATCH 16/17] Post Revisions: Data Layer: Fix usage of `http` to make it re-dispatch the original action once the request is completed. --- client/state/data-layer/wpcom/posts/revisions/index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/state/data-layer/wpcom/posts/revisions/index.js b/client/state/data-layer/wpcom/posts/revisions/index.js index 98ecbf3305b8b3..93ec2431940791 100644 --- a/client/state/data-layer/wpcom/posts/revisions/index.js +++ b/client/state/data-layer/wpcom/posts/revisions/index.js @@ -73,18 +73,17 @@ export const receiveSuccess = ( { dispatch }, { siteId, postId }, next, revision * * @param {Function} dispatch Redux dispatcher * @param {Object} action Redux action - * @param {String} action.siteId of the revisions - * @param {String} action.postId of the revisions - * @returns {Object} the dispatched action */ -export const fetchPostRevisions = ( { dispatch }, { siteId, postId } ) => +export const fetchPostRevisions = ( { dispatch }, action ) => { + const { siteId, postId } = action; dispatch( http( { path: `/sites/${ siteId }/posts/${ postId }/revisions`, method: 'GET', query: { apiNamespace: 'wp/v2', }, - } ) ); + }, action ) ); +}; const dispatchPostRevisionsRequest = dispatchRequest( fetchPostRevisions, receiveSuccess, receiveError ); From 877d7cd906af7c18ac40d042e5f959a4943e8324 Mon Sep 17 00:00:00 2001 From: Benoit Person Date: Mon, 24 Apr 2017 18:33:12 +0200 Subject: [PATCH 17/17] Post Revisions: Fix tests broken by 8ea42a758 --- client/state/data-layer/wpcom/posts/revisions/test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/state/data-layer/wpcom/posts/revisions/test/index.js b/client/state/data-layer/wpcom/posts/revisions/test/index.js index 2f5247ccea0b44..b6b7f9b033a2bd 100644 --- a/client/state/data-layer/wpcom/posts/revisions/test/index.js +++ b/client/state/data-layer/wpcom/posts/revisions/test/index.js @@ -81,7 +81,7 @@ describe( '#fetchPostRevisions', () => { query: { apiNamespace: 'wp/v2', }, - } ) ); + }, action ) ); } ); } );
    TypeNumber