Skip to content

Commit

Permalink
State: Update counts state in response to post status changes
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Jun 14, 2016
1 parent 6fb3213 commit 6755e50
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 15 deletions.
1 change: 1 addition & 0 deletions client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export const POST_COUNTS_RECEIVE = 'POST_COUNTS_RECEIVE';
export const POST_COUNTS_REQUEST = 'POST_COUNTS_REQUEST';
export const POST_COUNTS_REQUEST_FAILURE = 'POST_COUNTS_REQUEST_FAILURE';
export const POST_COUNTS_REQUEST_SUCCESS = 'POST_COUNTS_REQUEST_SUCCESS';
export const POST_COUNTS_RESET_INTERNAL_STATE = 'POST_COUNTS_RESET_INTERNAL_STATE';
export const POST_DELETE = 'POST_DELETE';
export const POST_DELETE_SUCCESS = 'POST_DELETE_SUCCESS';
export const POST_DELETE_FAILURE = 'POST_DELETE_FAILURE';
Expand Down
136 changes: 121 additions & 15 deletions client/state/posts/counts/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@
*/
import { combineReducers } from 'redux';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import includes from 'lodash/includes';
import omit from 'lodash/omit';

/**
* Internal dependencies
*/
import {
CURRENT_USER_ID_SET,
POST_COUNTS_RECEIVE,
POST_COUNTS_REQUEST,
POST_COUNTS_REQUEST_SUCCESS,
POST_COUNTS_REQUEST_FAILURE,
POST_COUNTS_RESET_INTERNAL_STATE,
POST_DELETE,
POSTS_RECEIVE,
SERIALIZE,
DESERIALIZE
} from 'state/action-types';
import { isValidStateWithSchema } from 'state/utils';
import { countsSchema } from './schema';
import { createReducer } from 'state/utils';

/**
* Returns the updated post types requesting state after an action has been
Expand Down Expand Up @@ -55,25 +62,124 @@ export function requesting( state = {}, action ) {
* @param {Object} action Action payload
* @return {Object} Updated state
*/
export function counts( state = {}, action ) {
switch ( action.type ) {
case POST_COUNTS_RECEIVE:
return merge( {}, state, {
[ action.siteId ]: {
[ action.postType ]: action.counts
}
} );
export const counts = ( () => {
let currentUserId;
let postStatuses = {};

case DESERIALIZE:
if ( isValidStateWithSchema( state, countsSchema ) ) {
return state;
/**
* Returns a serialized key to be used in tracking post status properties
*
* @param {Number} siteId Site ID
* @param {Number} postId Post ID
* @return {String} Serialized key
*/
function getPostStatusKey( siteId, postId ) {
return [ siteId, postId ].join();
}

/**
* Returns the updated post count state after transitioning a post to a new
* status.
*
* @param {Object} state Current state
* @param {Number} siteId Site ID
* @param {Number} postId Post ID
* @param {String} status Post status
* @return {Object} Updated state
*/
function transitionPostStateToStatus( state, siteId, postId, status ) {
if ( ! state[ siteId ] ) {
return state;
}

const postStatusKey = getPostStatusKey( siteId, postId );
const postStatus = postStatuses[ postStatusKey ];
if ( ! postStatus || ! state[ siteId ][ postStatus.type ] ) {
return state;
}

// Determine which count subkeys need to be updated, depending on
// whether the current user authored the post
const subKeys = [ 'all' ];
if ( postStatus.author === currentUserId ) {
subKeys.push( 'mine' );
}

const revisions = subKeys.reduce( ( memo, subKey ) => {
const subKeyCounts = state[ siteId ][ postStatus.type ][ subKey ];

memo[ subKey ] = {};

// Decrement count from the current status before transitioning
memo[ subKey ][ postStatus.status ] = ( subKeyCounts[ postStatus.status ] || 0 ) - 1;

// So long as we're not trashing an already trashed post or page,
// increment the count for the transitioned status
if ( 'trash' !== status || ( 'trash' !== postStatus.status &&
includes( [ 'post', 'page' ], postStatus.type ) ) ) {
memo[ subKey ][ status ] = ( subKeyCounts[ status ] || 0 ) + 1;
}

return {};
return memo;
}, {} );

if ( 'trash' === status && ( 'trash' === postStatus.status ||
! includes( [ 'post', 'page' ], postStatus.type ) ) ) {
// If post is permanently deleted, omit from tracked statuses
postStatuses = omit( postStatuses, postStatusKey );
} else {
// Otherwise, update object to reflect new status
postStatus.status = status;
}

return merge( {}, state, {
[ siteId ]: {
[ postStatus.type ]: revisions
}
} );
}

return state;
}
return createReducer( {}, {
[ POST_COUNTS_RESET_INTERNAL_STATE ]: ( state ) => {
currentUserId = undefined;
postStatuses = {};

return state;
},
[ CURRENT_USER_ID_SET ]: ( state, action ) => {
currentUserId = action.userId;

return state;
},
[ POSTS_RECEIVE ]: ( state, action ) => {
action.posts.forEach( ( post ) => {
const postStatusKey = getPostStatusKey( post.site_ID, post.ID );
const postStatus = postStatuses[ postStatusKey ];

// If the post is known to us and the status has changed,
// update state to reflect change
if ( postStatus && post.status !== postStatus.status ) {
state = transitionPostStateToStatus( state, post.site_ID, post.ID, post.status );
}

postStatuses[ postStatusKey ] = pick( post, 'type', 'status' );
postStatuses[ postStatusKey ].author = post.author.ID;
} );

return state;
},
[ POST_DELETE ]: ( state, action ) => {
return transitionPostStateToStatus( state, action.siteId, action.postId, 'trash' );
},
[ POST_COUNTS_RECEIVE ]: ( state, action ) => {
return merge( {}, state, {
[ action.siteId ]: {
[ action.postType ]: action.counts
}
} );
}
}, countsSchema );
} )();

export default combineReducers( {
requesting,
Expand Down
189 changes: 189 additions & 0 deletions client/state/posts/counts/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import deepFreeze from 'deep-freeze';
*/
import { useSandbox } from 'test/helpers/use-sinon';
import {
CURRENT_USER_ID_SET,
POST_COUNTS_RECEIVE,
POST_COUNTS_REQUEST,
POST_COUNTS_REQUEST_SUCCESS,
POST_COUNTS_REQUEST_FAILURE,
POST_COUNTS_RESET_INTERNAL_STATE,
POST_DELETE,
POSTS_RECEIVE,
SERIALIZE,
DESERIALIZE
} from 'state/action-types';
Expand Down Expand Up @@ -184,6 +188,10 @@ describe( 'reducer', () => {
} );

describe( '#counts()', () => {
beforeEach( () => {
counts( undefined, { type: POST_COUNTS_RESET_INTERNAL_STATE } );
} );

it( 'should default to an empty object', () => {
const state = counts( undefined, {} );

Expand Down Expand Up @@ -281,6 +289,187 @@ describe( 'reducer', () => {
} );
} );

it( 'should transition post counts to trashed when trashing', () => {
let state = counts( undefined, {
type: POSTS_RECEIVE,
posts: [
{ ID: 481, site_ID: 2916284, type: 'post', status: 'publish', author: { ID: 73705554 } }
]
} );

state = counts( state, {
type: POST_COUNTS_RECEIVE,
siteId: 2916284,
postType: 'post',
counts: {
all: { publish: 3, trash: 0 },
mine: { publish: 2, trash: 0 }
}
} );

state = counts( state, {
type: POST_DELETE,
siteId: 2916284,
postId: 481
} );

expect( state ).to.eql( {
2916284: {
post: {
all: { publish: 2, trash: 1 },
mine: { publish: 2, trash: 0 }
}
}
} );
} );

it( 'should transition mine post counts to trashed when trashing author\'s post', () => {
let state = counts( undefined, {
type: POSTS_RECEIVE,
posts: [
{ ID: 481, site_ID: 2916284, type: 'post', status: 'publish', author: { ID: 73705554 } }
]
} );

state = counts( state, {
type: CURRENT_USER_ID_SET,
userId: 73705554
} );

state = counts( state, {
type: POST_COUNTS_RECEIVE,
siteId: 2916284,
postType: 'post',
counts: {
all: { publish: 3, trash: 0 },
mine: { publish: 2, trash: 0 }
}
} );

state = counts( state, {
type: POST_DELETE,
siteId: 2916284,
postId: 481
} );

expect( state ).to.eql( {
2916284: {
post: {
all: { publish: 2, trash: 1 },
mine: { publish: 1, trash: 1 }
}
}
} );
} );

it( 'should transition trashed posts counts to the void when trashing', () => {
let state = counts( undefined, {
type: POSTS_RECEIVE,
posts: [
{ ID: 481, site_ID: 2916284, type: 'post', status: 'trash', author: { ID: 73705554 } }
]
} );

state = counts( state, {
type: CURRENT_USER_ID_SET,
userId: 73705554
} );

state = counts( state, {
type: POST_COUNTS_RECEIVE,
siteId: 2916284,
postType: 'post',
counts: {
all: { publish: 2, trash: 1 },
mine: { publish: 1, trash: 1 }
}
} );

state = counts( state, {
type: POST_DELETE,
siteId: 2916284,
postId: 481
} );

expect( state ).to.eql( {
2916284: {
post: {
all: { publish: 2, trash: 0 },
mine: { publish: 1, trash: 0 }
}
}
} );
} );

it( 'should transition non-post/page types counts to the void when trashing', () => {
let state = counts( undefined, {
type: POSTS_RECEIVE,
posts: [
{ ID: 184, site_ID: 2916284, type: 'jetpack-portfolio', status: 'publish', author: { ID: 73705554 } }
]
} );

state = counts( state, {
type: POST_COUNTS_RECEIVE,
siteId: 2916284,
postType: 'jetpack-portfolio',
counts: {
all: { publish: 3 },
mine: { publish: 2 }
}
} );

state = counts( state, {
type: POST_DELETE,
siteId: 2916284,
postId: 184
} );

expect( state ).to.eql( {
2916284: {
'jetpack-portfolio': {
all: { publish: 2 },
mine: { publish: 2 }
}
}
} );
} );

it( 'should transition an updated post\'s count to its new status when changed', () => {
let state = counts( undefined, {
type: POSTS_RECEIVE,
posts: [
{ ID: 98, site_ID: 2916284, type: 'post', status: 'draft', author: { ID: 73705554 } }
]
} );

state = counts( state, {
type: POST_COUNTS_RECEIVE,
siteId: 2916284,
postType: 'post',
counts: {
all: { publish: 3, draft: 1, trash: 0 },
mine: { publish: 2, draft: 0, trash: 0 }
}
} );

state = counts( state, {
type: POSTS_RECEIVE,
posts: [
{ ID: 98, site_ID: 2916284, type: 'post', status: 'publish', author: { ID: 73705554 } }
]
} );

expect( state ).to.eql( {
2916284: {
post: {
all: { publish: 4, draft: 0, trash: 0 },
mine: { publish: 2, draft: 0, trash: 0 }
}
}
} );
} );

it( 'should persist state', () => {
const original = deepFreeze( {
2916284: {
Expand Down

0 comments on commit 6755e50

Please sign in to comment.