-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2248 from Automattic/add/posts-redux
Posts: Add posts Redux state handlers
- Loading branch information
Showing
9 changed files
with
593 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
REPORTER ?= spec | ||
NODE_BIN := $(shell npm bin) | ||
MOCHA ?= $(NODE_BIN)/mocha | ||
BASE_DIR := $(NODE_BIN)/../.. | ||
NODE_PATH := $(BASE_DIR)/client:$(BASE_DIR)/shared | ||
|
||
test: | ||
@NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers jsx:babel/register,js:babel/register --reporter $(REPORTER) | ||
|
||
.PHONY: test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import wpcom from 'lib/wp'; | ||
import { | ||
POSTS_RECEIVE, | ||
POSTS_REQUEST, | ||
POSTS_REQUEST_SUCCESS, | ||
POSTS_REQUEST_FAILURE | ||
} from 'state/action-types'; | ||
|
||
/** | ||
* Returns an action object to be used in signalling that a post object has | ||
* been received. | ||
* | ||
* @param {Object} post Post received | ||
* @return {Object} Action object | ||
*/ | ||
export function receivePost( post ) { | ||
return receivePosts( [ post ] ); | ||
} | ||
|
||
/** | ||
* Returns an action object to be used in signalling that post objects have | ||
* been received. | ||
* | ||
* @param {Array} posts Posts received | ||
* @return {Object} Action object | ||
*/ | ||
export function receivePosts( posts ) { | ||
return { | ||
type: POSTS_RECEIVE, | ||
posts | ||
}; | ||
} | ||
|
||
/** | ||
* Triggers a network request to fetch posts for the specified site and query. | ||
* | ||
* @param {Number} siteId Site ID | ||
* @param {String} query Post query | ||
* @return {Function} Action thunk | ||
*/ | ||
export function requestSitePosts( siteId, query = {} ) { | ||
return ( dispatch ) => { | ||
dispatch( { | ||
type: POSTS_REQUEST, | ||
siteId, | ||
query | ||
} ); | ||
|
||
return wpcom.site( siteId ).postsList( { query } ).then( ( { posts } ) => { | ||
dispatch( receivePosts( posts ) ); | ||
dispatch( { | ||
type: POSTS_REQUEST_SUCCESS, | ||
siteId, | ||
query, | ||
posts | ||
} ); | ||
} ).catch( ( error ) => { | ||
dispatch( { | ||
type: POSTS_REQUEST_FAILURE, | ||
siteId, | ||
query, | ||
error | ||
} ); | ||
} ); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { combineReducers } from 'redux'; | ||
import indexBy from 'lodash/collection/indexBy'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
POSTS_RECEIVE, | ||
POSTS_REQUEST, | ||
POSTS_REQUEST_SUCCESS, | ||
POSTS_REQUEST_FAILURE | ||
} from 'state/action-types'; | ||
|
||
/** | ||
* Tracks all known post objects, indexed by post global ID. | ||
* | ||
* @param {Object} state Current state | ||
* @param {Object} action Action payload | ||
* @return {Object} Updated state | ||
*/ | ||
export function items( state = {}, action ) { | ||
switch ( action.type ) { | ||
case POSTS_RECEIVE: | ||
state = Object.assign( {}, state, indexBy( action.posts, 'global_ID' ) ); | ||
break; | ||
} | ||
|
||
return state; | ||
} | ||
|
||
/** | ||
* Returns the updated site posts state after an action has been dispatched. | ||
* The state reflects a mapping of site ID, post ID pairing to global post ID. | ||
* | ||
* @param {Object} state Current state | ||
* @param {Object} action Action payload | ||
* @return {Object} Updated state | ||
*/ | ||
export function sitePosts( state = {}, action ) { | ||
switch ( action.type ) { | ||
case POSTS_RECEIVE: | ||
state = Object.assign( {}, state ); | ||
action.posts.forEach( ( post ) => { | ||
if ( ! state[ post.site_ID ] ) { | ||
state[ post.site_ID ] = {}; | ||
} | ||
|
||
state[ post.site_ID ][ post.ID ] = post.global_ID; | ||
} ); | ||
break; | ||
} | ||
|
||
return state; | ||
} | ||
|
||
/** | ||
* Returns the updated post query state after an action has been dispatched. | ||
* The state reflects a mapping of site ID to active queries. | ||
* | ||
* @param {Object} state Current state | ||
* @param {Object} action Action payload | ||
* @return {Object} Updated state | ||
*/ | ||
export function siteQueries( state = {}, action ) { | ||
switch ( action.type ) { | ||
case POSTS_REQUEST: | ||
case POSTS_REQUEST_SUCCESS: | ||
case POSTS_REQUEST_FAILURE: | ||
const { type, siteId, posts } = action; | ||
const query = JSON.stringify( action.query ); | ||
|
||
// Clone state and ensure that site is tracked | ||
state = Object.assign( {}, state ); | ||
if ( ! state[ siteId ] ) { | ||
state[ siteId ] = {}; | ||
} | ||
|
||
// Only the initial request should be tracked as fetching. Success | ||
// or failure types imply that fetching has completed. | ||
state[ siteId ][ query ] = { | ||
fetching: POSTS_REQUEST === type | ||
}; | ||
|
||
// When a request succeeds, map the received posts to state. | ||
if ( POSTS_REQUEST_SUCCESS === type ) { | ||
state[ siteId ][ query ].posts = posts.map( ( post ) => post.global_ID ); | ||
} | ||
break; | ||
} | ||
|
||
return state; | ||
} | ||
|
||
export default combineReducers( { | ||
items, | ||
sitePosts, | ||
siteQueries | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* Returns a post object by its global ID. | ||
* | ||
* @param {Object} state Global state tree | ||
* @param {String} globalId Post global ID | ||
* @return {Object} Post object | ||
*/ | ||
export function getPost( state, globalId ) { | ||
return state.posts.items[ globalId ]; | ||
} | ||
|
||
/** | ||
* Returns a post object by site ID, post ID pair. | ||
* | ||
* @param {Object} state Global state tree | ||
* @param {Number} siteId Site ID | ||
* @param {String} postId Post ID | ||
* @return {?Object} Post object | ||
*/ | ||
export function getSitePost( state, siteId, postId ) { | ||
const { sitePosts } = state.posts; | ||
if ( ! sitePosts[ siteId ] || ! sitePosts[ siteId ][ postId ] ) { | ||
return null; | ||
} | ||
|
||
return getPost( state, sitePosts[ siteId ][ postId ] ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import nock from 'nock'; | ||
import sinon from 'sinon'; | ||
import sinonChai from 'sinon-chai'; | ||
import Chai, { expect } from 'chai'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
POSTS_RECEIVE, | ||
POSTS_REQUEST, | ||
POSTS_REQUEST_SUCCESS, | ||
POSTS_REQUEST_FAILURE | ||
} from 'state/action-types'; | ||
import { | ||
receivePost, | ||
receivePosts, | ||
requestSitePosts | ||
} from '../actions'; | ||
|
||
describe( 'actions', () => { | ||
describe( '#receivePost()', () => { | ||
it( 'should return an action object', () => { | ||
const post = { ID: 841, title: 'Hello World' }; | ||
const action = receivePost( post ); | ||
|
||
expect( action ).to.eql( { | ||
type: POSTS_RECEIVE, | ||
posts: [ post ] | ||
} ); | ||
} ); | ||
} ); | ||
|
||
describe( '#receivePosts()', () => { | ||
it( 'should return an action object', () => { | ||
const posts = [ { ID: 841, title: 'Hello World' } ]; | ||
const action = receivePosts( posts ); | ||
|
||
expect( action ).to.eql( { | ||
type: POSTS_RECEIVE, | ||
posts | ||
} ); | ||
} ); | ||
} ); | ||
|
||
describe( '#requestSitePosts()', () => { | ||
const spy = sinon.spy(); | ||
|
||
before( () => { | ||
Chai.use( sinonChai ); | ||
|
||
nock( 'https://public-api.wordpress.com:443' ) | ||
.persist() | ||
.get( '/rest/v1.1/sites/2916284/posts' ) | ||
.reply( 200, { | ||
posts: [ | ||
{ ID: 841, title: 'Hello World' }, | ||
{ ID: 413, title: 'Ribs & Chicken' } | ||
] | ||
} ) | ||
.get( '/rest/v1.1/sites/2916284/posts' ) | ||
.query( { search: 'Hello' } ) | ||
.reply( 200, { | ||
posts: [ { ID: 841, title: 'Hello World' } ] | ||
} ) | ||
.get( '/rest/v1.1/sites/77203074/posts' ) | ||
.reply( 403, { | ||
error: 'authorization_required', | ||
message: 'User cannot access this private blog.' | ||
} ); | ||
} ); | ||
|
||
beforeEach( () => { | ||
spy.reset(); | ||
} ); | ||
|
||
after( () => { | ||
nock.restore(); | ||
} ); | ||
|
||
it( 'should dispatch fetch action when thunk triggered', () => { | ||
requestSitePosts( 2916284 )( spy ); | ||
|
||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_REQUEST, | ||
siteId: 2916284, | ||
query: {} | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch posts receive action when request completes', ( done ) => { | ||
requestSitePosts( 2916284 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_RECEIVE, | ||
posts: [ | ||
{ ID: 841, title: 'Hello World' }, | ||
{ ID: 413, title: 'Ribs & Chicken' } | ||
] | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
} ); | ||
|
||
it( 'should dispatch posts posts request success action when request completes', ( done ) => { | ||
requestSitePosts( 2916284 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_REQUEST_SUCCESS, | ||
siteId: 2916284, | ||
query: {}, | ||
posts: [ | ||
{ ID: 841, title: 'Hello World' }, | ||
{ ID: 413, title: 'Ribs & Chicken' } | ||
] | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
} ); | ||
|
||
it( 'should dispatch fail action when request fails', ( done ) => { | ||
requestSitePosts( 77203074 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_REQUEST_FAILURE, | ||
siteId: 77203074, | ||
query: {}, | ||
error: sinon.match( { message: 'User cannot access this private blog.' } ) | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
} ); | ||
} ); | ||
} ); |
Oops, something went wrong.