From 35effa4f4a54b51a6e246647ff666ec965dc8c24 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Tue, 2 Feb 2016 23:05:21 +0100 Subject: [PATCH 1/5] wpcom-undocumented: Fix two debug strings --- client/lib/wpcom-undocumented/lib/undocumented.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lib/wpcom-undocumented/lib/undocumented.js b/client/lib/wpcom-undocumented/lib/undocumented.js index d221b57207ed7..9ba152bb09083 100644 --- a/client/lib/wpcom-undocumented/lib/undocumented.js +++ b/client/lib/wpcom-undocumented/lib/undocumented.js @@ -1470,12 +1470,12 @@ Undocumented.prototype.themes = function( site, query, fn ) { }; Undocumented.prototype.activeTheme = function( siteId, fn ) { - debug( '/site/:site_id/themes/mine' ); + debug( '/sites/:site_id/themes/mine' ); this.wpcom.req.get( { path: '/sites/' + siteId + '/themes/mine' }, fn ); }; Undocumented.prototype.activateTheme = function( theme, siteId, fn ) { - debug( '/site/:site_id/themes/mine' ); + debug( '/sites/:site_id/themes/mine' ); this.wpcom.req.post( { path: '/sites/' + siteId + '/themes/mine', body: { theme: theme.id } From 3fe5d9880188f8521a853d7ce4a3c213987966a6 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Thu, 4 Feb 2016 20:42:23 +0100 Subject: [PATCH 2/5] current-theme/test: Move from external to internal deps --- client/state/themes/current-theme/reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/state/themes/current-theme/reducer.js b/client/state/themes/current-theme/reducer.js index e6607b54b2300..bfe6182f880c1 100644 --- a/client/state/themes/current-theme/reducer.js +++ b/client/state/themes/current-theme/reducer.js @@ -2,12 +2,12 @@ * External dependencies */ import { fromJS } from 'immutable'; -import { DESERIALIZE, SERIALIZE } from 'state/action-types'; /** * Internal dependencies */ import ActionTypes from '../action-types'; +import { DESERIALIZE, SERIALIZE } from 'state/action-types'; export const initialState = fromJS( { isActivating: false, From 86aacf79d862c2c892d7ab22343584e1b77e8ea6 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Tue, 2 Feb 2016 22:38:10 +0100 Subject: [PATCH 3/5] Themes: Add theme-details Redux subtree Use the REST API v1.1's /theme endpoint for now. --- .../wpcom-undocumented/lib/undocumented.js | 8 ++ client/state/themes/README.md | 4 + client/state/themes/action-types.js | 1 + client/state/themes/actions.js | 19 +++++ client/state/themes/reducer.js | 2 + client/state/themes/theme-details/Makefile | 13 +++ client/state/themes/theme-details/reducer.js | 26 ++++++ .../state/themes/theme-details/selectors.js | 3 + .../themes/theme-details/test/reducer.js | 80 +++++++++++++++++++ .../themes/theme-details/test/selectors.js | 29 +++++++ 10 files changed, 185 insertions(+) create mode 100644 client/state/themes/theme-details/Makefile create mode 100644 client/state/themes/theme-details/reducer.js create mode 100644 client/state/themes/theme-details/selectors.js create mode 100644 client/state/themes/theme-details/test/reducer.js create mode 100644 client/state/themes/theme-details/test/selectors.js diff --git a/client/lib/wpcom-undocumented/lib/undocumented.js b/client/lib/wpcom-undocumented/lib/undocumented.js index 9ba152bb09083..237e1c6feb090 100644 --- a/client/lib/wpcom-undocumented/lib/undocumented.js +++ b/client/lib/wpcom-undocumented/lib/undocumented.js @@ -1469,6 +1469,14 @@ Undocumented.prototype.themes = function( site, query, fn ) { }, fn ); }; +Undocumented.prototype.themeDetails = function( themeId, fn ) { + debug( '/themes/:theme_id' ); + this.wpcom.req.get( { + apiVersion: '1.1', + path: '/themes/' + themeId + }, fn ); +}; + Undocumented.prototype.activeTheme = function( siteId, fn ) { debug( '/sites/:site_id/themes/mine' ); this.wpcom.req.get( { path: '/sites/' + siteId + '/themes/mine' }, fn ); diff --git a/client/state/themes/README.md b/client/state/themes/README.md index bcba101f86648..ca00a890e28db 100644 --- a/client/state/themes/README.md +++ b/client/state/themes/README.md @@ -7,6 +7,10 @@ Contains reducers, selectors, and action creators for themes, and the theme show Manages data concerning each site's currently selected theme. +### theme-details/ + +Provides details for a theme given its ID. + ### themes-last-query/ Tracks the last themes query. diff --git a/client/state/themes/action-types.js b/client/state/themes/action-types.js index f52f1154ae1af..928cbc21c64c1 100644 --- a/client/state/themes/action-types.js +++ b/client/state/themes/action-types.js @@ -10,6 +10,7 @@ export default keyMirror( { RECEIVE_THEMES_SERVER_ERROR: null, INCREMENT_THEMES_PAGE: null, RECEIVE_CURRENT_THEME: null, + RECEIVE_THEME_DETAILS: null, PREVIEW_THEME: null, PURCHASE_THEME: null, ACTIVATE_THEME: null, diff --git a/client/state/themes/actions.js b/client/state/themes/actions.js index c3cd05cb4caee..130fda65ac3ab 100644 --- a/client/state/themes/actions.js +++ b/client/state/themes/actions.js @@ -75,6 +75,25 @@ export function fetchCurrentTheme( site ) { }; } +export function fetchThemeDetails( id ) { + return dispatch => { + const callback = ( error, data ) => { + debug( 'Received theme details', data ); + if ( error ) { + dispatch( receiveServerError( error ) ); + } else { + dispatch( { + type: ActionTypes.RECEIVE_THEME_DETAILS, + themeId: data.id, + themeName: data.name, + themeAuthor: data.author + } ); + } + }; + wpcom.undocumented().themeDetails( id, callback ); + } +} + export function receiveServerError( error ) { return { type: ActionTypes.RECEIVE_THEMES_SERVER_ERROR, diff --git a/client/state/themes/reducer.js b/client/state/themes/reducer.js index cee35c3cce9e4..a99f344a22371 100644 --- a/client/state/themes/reducer.js +++ b/client/state/themes/reducer.js @@ -7,12 +7,14 @@ import { combineReducers } from 'redux'; * Internal dependencies */ import themes from './themes/reducer'; +import themeDetails from './theme-details/reducer'; import themesList from './themes-list/reducer'; import themesLastQuery from './themes-last-query/reducer'; import currentTheme from './current-theme/reducer'; export default combineReducers( { themes, + themeDetails, themesList, themesLastQuery, currentTheme diff --git a/client/state/themes/theme-details/Makefile b/client/state/themes/theme-details/Makefile new file mode 100644 index 0000000000000..265320e42e9d0 --- /dev/null +++ b/client/state/themes/theme-details/Makefile @@ -0,0 +1,13 @@ +UI ?= bdd +REPORTER ?= spec +COMPILERS ?= js:babel/register +NODE_BIN := $(shell npm bin) +MOCHA ?= $(NODE_BIN)/mocha +BASE_DIR := $(NODE_BIN)/../.. +NODE_PATH := test:$(BASE_DIR)/client + +# In order to simply stub modules, add test to the NODE_PATH +test: + @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers $(COMPILERS) --reporter $(REPORTER) --ui $(UI) + +.PHONY: test diff --git a/client/state/themes/theme-details/reducer.js b/client/state/themes/theme-details/reducer.js new file mode 100644 index 0000000000000..16391aea65f2b --- /dev/null +++ b/client/state/themes/theme-details/reducer.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { Map, fromJS } from 'immutable'; + +/** + * Internal dependencies + */ +import ActionTypes from '../action-types'; +import { DESERIALIZE, SERIALIZE } from '../../action-types'; + +export default ( state = Map(), action ) => { + switch ( action.type ) { + case ActionTypes.RECEIVE_THEME_DETAILS: + return state + .set( action.themeId, Map( { + name: action.themeName, + author: action.themeAuthor + } ) ) + case DESERIALIZE: + return fromJS( state ); + case SERIALIZE: + return state.toJS(); + } + return state; +}; diff --git a/client/state/themes/theme-details/selectors.js b/client/state/themes/theme-details/selectors.js new file mode 100644 index 0000000000000..65c85aba4f142 --- /dev/null +++ b/client/state/themes/theme-details/selectors.js @@ -0,0 +1,3 @@ +export function getThemeDetails( state, id ) { + return state.themes.themeDetails.get( id ).toJS(); +} diff --git a/client/state/themes/theme-details/test/reducer.js b/client/state/themes/theme-details/test/reducer.js new file mode 100644 index 0000000000000..297c5fb30d1de --- /dev/null +++ b/client/state/themes/theme-details/test/reducer.js @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import { Map, fromJS } from 'immutable'; + +/** + * Internal dependencies + */ +import { RECEIVE_THEME_DETAILS } from '../../action-types'; +import { DESERIALIZE, SERIALIZE } from '../../../action-types'; +import reducer from '../reducer'; + +describe( 'reducer', () => { + it( 'should default to an empty Immutable Map', () => { + const state = reducer( undefined, {} ); + + expect( state.toJS() ).to.be.empty; + } ); + + it( 'should set theme details for the given ID', () => { + const state = reducer( undefined, { + type: RECEIVE_THEME_DETAILS, + themeId: 'mood', + themeName: 'Mood', + themeAuthor: 'Automattic' + } ); + + expect( state.get( 'mood' ).toJS() ).to.eql( { + name: 'Mood', + author: 'Automattic' + } ); + } ); + + describe( 'persistence', () => { + const initialState = Map(); + + it( 'persists state and converts to a plain JS object', () => { + const jsObject = Object.freeze( { + mood: { + name: 'Mood', + author: 'Automattic' + } + } ); + const state = fromJS( jsObject ); + const persistedState = reducer( state, { type: SERIALIZE } ); + expect( persistedState ).to.eql( jsObject ); + } ); + it( 'loads valid persisted state and converts to immutable.js object', () => { + const jsObject = Object.freeze( { + mood: { + name: 'Mood', + author: 'Automattic' + } + } ); + const state = reducer( jsObject, { type: DESERIALIZE } ); + expect( state ).to.eql( fromJS( jsObject ) ); + } ); + + it.skip( 'should ignore loading data with invalid keys ', () => { + const jsObject = Object.freeze( { + missingKey: true, + mood: { + name: 'Mood', + author: 'Automattic' + } + } ); + const state = reducer( jsObject, { type: DESERIALIZE } ); + expect( state ).to.eql( initialState ); + } ); + + it.skip( 'should ignore loading data with invalid values ', () => { + const jsObject = Object.freeze( { + mood: 'foo', + } ); + const state = reducer( jsObject, { type: DESERIALIZE } ); + expect( state ).to.eql( initialState ); + } ); + } ); +} ); diff --git a/client/state/themes/theme-details/test/selectors.js b/client/state/themes/theme-details/test/selectors.js new file mode 100644 index 0000000000000..a1f883a9e5fdc --- /dev/null +++ b/client/state/themes/theme-details/test/selectors.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import { Map } from 'immutable'; + +/** + * Internal dependencies + */ +import { getThemeDetails } from '../selectors'; + +describe( 'selectors', () => { + describe( '#getThemeDetails()', () => { + it( 'should return details for a theme given its ID', () => { + const details = getThemeDetails( { + themes: { + themeDetails: Map( { + mood: Map( { + name: 'Mood', + author: 'Automattic' + } ) + } ) + } + }, 'mood' ); + + expect( details ).to.eql( { name: 'Mood', author: 'Automattic' } ); + } ); + } ); +} ); From 0f1cbec9122d3b8bf8cfbe9ab7a7c9821434e98d Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Thu, 4 Feb 2016 21:31:43 +0100 Subject: [PATCH 4/5] client/components/theme-details: Add --- .../components/data/theme-details/README.md | 9 +++ client/components/data/theme-details/index.js | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 client/components/data/theme-details/README.md create mode 100644 client/components/data/theme-details/index.js diff --git a/client/components/data/theme-details/README.md b/client/components/data/theme-details/README.md new file mode 100644 index 0000000000000..d6d33729a7ee0 --- /dev/null +++ b/client/components/data/theme-details/README.md @@ -0,0 +1,9 @@ +ThemeDetailsData +================ + +A component to decouple the fetching of a theme's details from any rendering. + +## Usage + +A child component wrapped with `` will be passed the prop +`themeDetails`, a theme object. diff --git a/client/components/data/theme-details/index.js b/client/components/data/theme-details/index.js new file mode 100644 index 0000000000000..1ba4bc97fe6a0 --- /dev/null +++ b/client/components/data/theme-details/index.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import React from 'react'; +import { connect } from 'react-redux'; +import omit from 'lodash/object/omit'; + +/** + * Internal dependencies + */ +import { fetchThemeDetails } from 'state/themes/actions'; +import { getThemeDetails } from 'state/themes/theme-details/selectors'; + +/** + * Fetches details for a theme specified by its ID + * and passes it to the supplied child component. + */ +const ThemeDetailsData = React.createClass( { + + propTypes: { + children: React.PropTypes.element.isRequired, + id: React.PropTypes.string.isRequired, + // Connected props + name: React.PropTypes.string, + author: React.PropTypes.string, + fetchThemeDetails: React.PropTypes.func.isRequired + }, + + componentDidMount() { + this.refresh( this.props ); + }, + + componentWillReceiveProps( nextProps ) { + if ( nextProps.id && nextProps.id !== this.props.id ) { + this.refresh( nextProps ); + } + }, + + refresh( props ) { + if ( ! this.props.name && props.id ) { + this.props.fetchThemeDetails( props.id ); + } + }, + + render() { + return React.cloneElement( this.props.children, omit( this.props, 'children' ) ); + } +} ); + +export default connect( + ( state, props ) => Object.assign( {}, + props, + getThemeDetails( state, props.id ) + ), + { fetchThemeDetails } +)( ThemeDetailsData ); From 7cb99586ec6f4a80c0646f6a6e9c923e7bf8b679 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Thu, 4 Feb 2016 22:15:42 +0100 Subject: [PATCH 5/5] Data components: omit redundant bindActionCreators() --- client/components/data/current-theme/index.js | 3 +-- client/components/data/themes-list-fetcher/index.jsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/components/data/current-theme/index.js b/client/components/data/current-theme/index.js index ed8994a860a12..ad7b3fd2cc9d7 100644 --- a/client/components/data/current-theme/index.js +++ b/client/components/data/current-theme/index.js @@ -2,7 +2,6 @@ * External dependencies */ import React from 'react'; -import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import omit from 'lodash/object/omit'; @@ -65,5 +64,5 @@ export default connect( currentTheme: getCurrentTheme( state, props.site.ID ) } ), - bindActionCreators.bind( null, { fetchCurrentTheme } ) + { fetchCurrentTheme } )( CurrentThemeData ); diff --git a/client/components/data/themes-list-fetcher/index.jsx b/client/components/data/themes-list-fetcher/index.jsx index f735fdbe7f96a..bb3836e3ed17a 100644 --- a/client/components/data/themes-list-fetcher/index.jsx +++ b/client/components/data/themes-list-fetcher/index.jsx @@ -5,7 +5,6 @@ import React from 'react'; import omit from 'lodash/object/omit'; import once from 'lodash/function/once'; import filter from 'lodash/collection/filter'; -import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; /** @@ -148,5 +147,5 @@ export default connect( } } ), - bindActionCreators.bind( null, { query, fetchNextPage } ) + { query, fetchNextPage } )( ThemesListFetcher );