From aa1ebd60aaa3ae37eb7900a0cbbea3961a5b43b2 Mon Sep 17 00:00:00 2001 From: Marin Atanasov Date: Wed, 9 Nov 2016 17:58:24 +0200 Subject: [PATCH 1/6] State: Add actions, reducer, schema and selectors for site roles --- client/state/action-types.js | 4 ++ client/state/index.js | 2 + client/state/site-roles/README.md | 73 ++++++++++++++++++++++++++++ client/state/site-roles/actions.js | 43 ++++++++++++++++ client/state/site-roles/reducer.js | 48 ++++++++++++++++++ client/state/site-roles/schema.js | 26 ++++++++++ client/state/site-roles/selectors.js | 26 ++++++++++ 7 files changed, 222 insertions(+) create mode 100644 client/state/site-roles/README.md create mode 100644 client/state/site-roles/actions.js create mode 100644 client/state/site-roles/reducer.js create mode 100644 client/state/site-roles/schema.js create mode 100644 client/state/site-roles/selectors.js diff --git a/client/state/action-types.js b/client/state/action-types.js index ee289cfff69c7..e4920d1113f65 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -410,6 +410,10 @@ export const SITE_RECEIVE = 'SITE_RECEIVE'; export const SITE_REQUEST = 'SITE_REQUEST'; export const SITE_REQUEST_FAILURE = 'SITE_REQUEST_FAILURE'; export const SITE_REQUEST_SUCCESS = 'SITE_REQUEST_SUCCESS'; +export const SITE_ROLES_RECEIVE = 'SITE_ROLES_RECEIVE'; +export const SITE_ROLES_REQUEST = 'SITE_ROLES_REQUEST'; +export const SITE_ROLES_REQUEST_FAILURE = 'SITE_ROLES_REQUEST_FAILURE'; +export const SITE_ROLES_REQUEST_SUCCESS = 'SITE_ROLES_REQUEST_SUCCESS'; export const SITE_SETTINGS_RECEIVE = 'SITE_SETTINGS_RECEIVE'; export const SITE_SETTINGS_REQUEST = 'SITE_SETTINGS_REQUEST'; export const SITE_SETTINGS_REQUEST_FAILURE = 'SITE_SETTINGS_REQUEST_FAILURE'; diff --git a/client/state/index.js b/client/state/index.js index 0f91a9d9abf78..d5c94a674f59b 100644 --- a/client/state/index.js +++ b/client/state/index.js @@ -41,6 +41,7 @@ import sharing from './sharing/reducer'; import shortcodes from './shortcodes/reducer'; import signup from './signup/reducer'; import sites from './sites/reducer'; +import siteRoles from './site-roles/reducer'; import siteSettings from './site-settings/reducer'; import stats from './stats/reducer'; import storedCards from './stored-cards/reducer'; @@ -88,6 +89,7 @@ export const reducer = combineReducers( { shortcodes, signup, sites, + siteRoles, siteSettings, stats, storedCards, diff --git a/client/state/site-roles/README.md b/client/state/site-roles/README.md new file mode 100644 index 0000000000000..e6fa92a3327e5 --- /dev/null +++ b/client/state/site-roles/README.md @@ -0,0 +1,73 @@ +Site Roles +=============== + +A module for managing site roles. + +## Actions + +Used in combination with the Redux store instance `dispatch` function, actions can be used in manipulating the current global state. + +### `requestSiteRoles( siteId: number )` + +Get a list of supported user roles for a given site. + +```js +import { requestSiteRoles } from 'state/site-roles/actions'; + +requestSiteRoles( 12345678 ); +``` + +## Reducer + +Data from the aforementioned actions is added to the global state tree, under `siteRoles`, with the following structure: + +```js +state.siteRoles = { + requesting: { + 12345678: false, + 87654321: true + }, + items: { + 12345678: [ + { + name: 'administrator', + display_name: 'Administrator', + capabilities: { + activate_plugins: true, + edit_users: true, + manage_options: true + } + }, + { + name: 'customer', + display_name: 'Customer', + capabilities: { + read: true + } + } + ] + } +} +``` + +## Selectors are intended to assist in extracting data from the global state tree for consumption by other modules. + +#### `isRequestingSiteRoles` + +Returns true if user roles are currently fetching for the given site ID. + +```js +import { isRequestingSiteRoles } from 'state/site-roles/selectors'; + +const isRequesting = isRequestingSiteRoles( state, 12345678 ); +``` + +#### `getSiteRoles` + +Returns an array of all supported user roles for the given site ID. + +```js +import { getSiteRoles } from 'state/site-roles/selectors'; + +const siteRoles = getSiteRoles( state, 12345678 ); +``` diff --git a/client/state/site-roles/actions.js b/client/state/site-roles/actions.js new file mode 100644 index 0000000000000..1067c9c7add6b --- /dev/null +++ b/client/state/site-roles/actions.js @@ -0,0 +1,43 @@ +/** + * Internal dependencies + */ +import { pick } from 'lodash'; + +/** + * Internal dependencies + */ +import wpcom from 'lib/wp'; +import { + SITE_ROLES_RECEIVE, + SITE_ROLES_REQUEST, + SITE_ROLES_REQUEST_FAILURE, + SITE_ROLES_REQUEST_SUCCESS +} from 'state/action-types'; + +export function requestSiteRoles( siteId ) { + return ( dispatch ) => { + dispatch( { + type: SITE_ROLES_REQUEST, + siteId + } ); + + return wpcom.undocumented().site( siteId ).getRoles().then( ( { roles } ) => { + dispatch( { + type: SITE_ROLES_REQUEST_SUCCESS, + siteId + } ); + + dispatch( { + type: SITE_ROLES_RECEIVE, + siteId, + roles + } ); + } ).catch( ( error ) => { + dispatch( { + type: SITE_ROLES_REQUEST_FAILURE, + siteId, + error: pick( error, [ 'error', 'status', 'message' ] ) + } ); + } ); + }; +} diff --git a/client/state/site-roles/reducer.js b/client/state/site-roles/reducer.js new file mode 100644 index 0000000000000..5ef3993481570 --- /dev/null +++ b/client/state/site-roles/reducer.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import { siteRolesSchema } from './schema'; +import { createReducer } from 'state/utils'; +import { + SITE_ROLES_RECEIVE, + SITE_ROLES_REQUEST, + SITE_ROLES_REQUEST_FAILURE, + SITE_ROLES_REQUEST_SUCCESS +} from 'state/action-types'; + +/** + * Returns the updated requests state after an action has been dispatched. The + * state maps site ID keys to a boolean value. Each site is true if roles + * for it are being currently requested, and false otherwise. + * + * @param {Object} state Current state + * @param {Object} action Action payload + * @return {Object} Updated state + */ +export const requesting = createReducer( {}, { + [ SITE_ROLES_REQUEST ]: ( state, { siteId } ) => ( { ...state, [ siteId ]: true } ), + [ SITE_ROLES_REQUEST_SUCCESS ]: ( state, { siteId } ) => ( { ...state, [ siteId ]: false } ), + [ SITE_ROLES_REQUEST_FAILURE ]: ( state, { siteId } ) => ( { ...state, [ siteId ]: false } ) +} ); + +/** + * Returns the updated items state after an action has been dispatched. The + * state maps site ID keys to an object that contains the site roles. + * + * @param {Object} state Current state + * @param {Object} action Action payload + * @return {Object} Updated state + */ +export const items = createReducer( {}, { + [ SITE_ROLES_RECEIVE ]: ( state, { siteId, roles } ) => ( { ...state, [ siteId ]: roles } ) +}, siteRolesSchema ); + +export default combineReducers( { + requesting, + items +} ); diff --git a/client/state/site-roles/schema.js b/client/state/site-roles/schema.js new file mode 100644 index 0000000000000..32ee7c9e9d1b8 --- /dev/null +++ b/client/state/site-roles/schema.js @@ -0,0 +1,26 @@ +export const siteRolesSchema = { + type: 'object', + patternProperties: { + '^\\d+$': { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + display_name: { type: 'string' }, + capabilities: { + type: 'object', + patternProperties: { + '^.+$': { type: 'boolean' } + } + }, + }, + required: [ + 'name', + 'display_name', + 'capabilities' + ] + } + } + } +}; diff --git a/client/state/site-roles/selectors.js b/client/state/site-roles/selectors.js new file mode 100644 index 0000000000000..4a0f23408abd9 --- /dev/null +++ b/client/state/site-roles/selectors.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Returns true if currently requesting roles for the specified site ID, or + * false otherwise. + * + * @param {Object} state Global state tree + * @param {Number} siteId Site ID + * @return {Boolean} Whether that shortcode is being requested + */ +export const isRequestingSiteRoles = ( state, siteId ) => { + return get( state.siteRoles.requesting, [ siteId ], false ); +}; + +/** + * Retrieve the site roles, supported in a particular site + * @param {Object} state Global state tree + * @param {Number} siteId Site ID + * @return {Array} Site roles + */ +export const getSiteRoles = ( state, siteId ) => { + return get( state.siteRoles.items, [ siteId ] ); +}; From 0302cbeea7b82d3855f1a7196a498f8cdcb89981 Mon Sep 17 00:00:00 2001 From: Marin Atanasov Date: Wed, 9 Nov 2016 17:59:03 +0200 Subject: [PATCH 2/6] State: Add tests for site roles actions, reducer and selectors --- client/state/site-roles/test/actions.js | 113 ++++++++++++ client/state/site-roles/test/reducer.js | 201 ++++++++++++++++++++++ client/state/site-roles/test/selectors.js | 89 ++++++++++ 3 files changed, 403 insertions(+) create mode 100644 client/state/site-roles/test/actions.js create mode 100644 client/state/site-roles/test/reducer.js create mode 100644 client/state/site-roles/test/selectors.js diff --git a/client/state/site-roles/test/actions.js b/client/state/site-roles/test/actions.js new file mode 100644 index 0000000000000..d76a0627688f4 --- /dev/null +++ b/client/state/site-roles/test/actions.js @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { + SITE_ROLES_RECEIVE, + SITE_ROLES_REQUEST, + SITE_ROLES_REQUEST_FAILURE, + SITE_ROLES_REQUEST_SUCCESS +} from 'state/action-types'; +import { requestSiteRoles } from '../actions'; +import { useSandbox } from 'test/helpers/use-sinon'; +import useNock from 'test/helpers/use-nock'; + +describe( 'actions', () => { + let spy; + useSandbox( ( sandbox ) => spy = sandbox.spy() ); + + describe( '#requestSiteRoles()', () => { + describe( 'success', () => { + const roles = [ + { + name: 'administrator', + display_name: 'Administrator', + capabilities: { + activate_plugins: true, + edit_users: true, + manage_options: true + } + }, + { + name: 'customer', + display_name: 'Customer', + capabilities: { + read: true + } + } + ]; + const siteId = 12345678; + + useNock( ( nock ) => { + nock( 'https://public-api.wordpress.com:443' ) + .persist() + .get( '/rest/v1.1/sites/' + siteId + '/roles' ) + .reply( 200, { + roles + }, { + 'Content-Type': 'application/json' + } ); + } ); + + it( 'should return a request action object when called', () => { + requestSiteRoles( siteId )( spy ); + + expect( spy ).to.have.been.calledWith( { + type: SITE_ROLES_REQUEST, + siteId + } ); + } ); + + it( 'should return a receive action when request successfully completes', () => { + return requestSiteRoles( siteId )( spy ).then( () => { + expect( spy ).to.have.been.calledWith( { + type: SITE_ROLES_REQUEST_SUCCESS, + siteId + } ); + + expect( spy ).to.have.been.calledWith( { + type: SITE_ROLES_RECEIVE, + siteId, + roles + } ); + } ); + } ); + } ); + + describe( 'failure', () => { + const error = 'unauthorized'; + const message = 'User cannot view roles for specified site'; + const siteId = 87654321; + + useNock( ( nock ) => { + nock( 'https://public-api.wordpress.com:443' ) + .persist() + .get( '/rest/v1.1/sites/' + siteId + '/roles' ) + .reply( 403, { + error, + message + }, { + 'Content-Type': 'application/json' + } ); + } ); + + it( 'should return a receive action when an error occurs', () => { + return requestSiteRoles( siteId )( spy ).then( () => { + expect( spy ).to.have.been.calledWith( { + type: SITE_ROLES_REQUEST_FAILURE, + siteId, + error: { + error, + message, + status: 403 + } + } ); + } ); + } ); + } ); + } ); +} ); diff --git a/client/state/site-roles/test/reducer.js b/client/state/site-roles/test/reducer.js new file mode 100644 index 0000000000000..cf5234b691a0c --- /dev/null +++ b/client/state/site-roles/test/reducer.js @@ -0,0 +1,201 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { + SITE_ROLES_RECEIVE, + SITE_ROLES_REQUEST, + SITE_ROLES_REQUEST_FAILURE, + SITE_ROLES_REQUEST_SUCCESS, + SERIALIZE, + DESERIALIZE +} from 'state/action-types'; +import reducer, { requesting, items } from '../reducer'; +import { useSandbox } from 'test/helpers/use-sinon'; + +describe( 'reducer', () => { + useSandbox( ( sandbox ) => { + sandbox.stub( console, 'warn' ); + } ); + + it( 'should include expected keys in return value', () => { + expect( reducer( undefined, {} ) ).to.have.keys( [ + 'requesting', + 'items' + ] ); + } ); + + describe( '#requesting()', () => { + it( 'should default to an empty object', () => { + const state = requesting( undefined, {} ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should set that site ID to true if a request is initiated', () => { + const state = requesting( {}, { + type: SITE_ROLES_REQUEST, + siteId: 12345678 + } ); + + expect( state ).to.eql( { + 12345678: true + } ); + } ); + + it( 'should set that site ID to false if a request finishes succesfully', () => { + const state = requesting( {}, { + type: SITE_ROLES_REQUEST_SUCCESS, + siteId: 12345678 + } ); + + expect( state ).to.eql( { + 12345678: false + } ); + } ); + + it( 'should set that site ID to false if a request finishes unsuccesfully', () => { + const state = requesting( {}, { + type: SITE_ROLES_REQUEST_FAILURE, + siteId: 12345678 + } ); + + expect( state ).to.eql( { + 12345678: false + } ); + } ); + + it( 'should not persist state', () => { + const state = requesting( deepFreeze( { + 12345678: true + } ), { + type: SERIALIZE + } ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should not load persisted state', () => { + const state = requesting( deepFreeze( { + 12345678: true + } ), { + type: DESERIALIZE + } ); + + expect( state ).to.eql( {} ); + } ); + } ); + + describe( '#items()', () => { + const roles = [ + { + name: 'administrator', + display_name: 'Administrator', + capabilities: { + activate_plugins: true, + edit_users: true, + manage_options: true + } + }, + { + name: 'customer', + display_name: 'Customer', + capabilities: { + read: true + } + } + ]; + const altRoles = [ + { + name: 'customer', + display_name: 'Customer', + capabilities: { + read: true, + level_0: true + } + } + ]; + + it( 'should default to an empty object', () => { + const state = items( undefined, {} ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should index roles by site ID', () => { + const state = items( {}, { + type: SITE_ROLES_RECEIVE, + siteId: 12345678, + roles + } ); + + expect( state ).to.eql( { + 12345678: roles + } ); + } ); + + it( 'should override previous roles of same site ID', () => { + const state = items( deepFreeze( { + 12345678: roles, + 87654321: roles + } ), { + type: SITE_ROLES_RECEIVE, + siteId: 87654321, + roles: altRoles + } ); + + expect( state ).to.eql( { + 12345678: roles, + 87654321: altRoles + } ); + } ); + + it( 'should persist state', () => { + const state = items( deepFreeze( { + 12345678: roles, + 87654321: altRoles + } ), { + type: SERIALIZE + } ); + + expect( state ).to.eql( { + 12345678: roles, + 87654321: altRoles + } ); + } ); + + it( 'should load valid persisted state', () => { + const state = items( deepFreeze( { + 12345678: roles, + 87654321: altRoles + } ), { + type: DESERIALIZE + } ); + + expect( state ).to.eql( { + 12345678: roles, + 87654321: altRoles + } ); + } ); + + it( 'should not load invalid persisted state', () => { + const state = items( deepFreeze( { + 1234567: [ { + name: 'test', + capabilities: { + test: true + } + } ] + } ), { + type: DESERIALIZE + } ); + + expect( state ).to.eql( {} ); + } ); + } ); +} ); diff --git a/client/state/site-roles/test/selectors.js b/client/state/site-roles/test/selectors.js new file mode 100644 index 0000000000000..2b87f10153240 --- /dev/null +++ b/client/state/site-roles/test/selectors.js @@ -0,0 +1,89 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { getSiteRoles, isRequestingSiteRoles } from '../selectors'; + +describe( 'selectors', () => { + describe( '#isRequestingSiteRoles()', () => { + it( 'should return false if roles have never been fetched for that site', () => { + const isRequesting = isRequestingSiteRoles( { + siteRoles: { + requesting: { + 87654321: true + } + } + }, 12345678 ); + + expect( isRequesting ).to.be.false; + } ); + + it( 'should return false if roles are not being fetched for that site', () => { + const isRequesting = isRequestingSiteRoles( { + siteRoles: { + requesting: { + 12345678: false + } + } + }, 12345678 ); + + expect( isRequesting ).to.be.false; + } ); + + it( 'should return true if the roles are being fetched for that site', () => { + const isRequesting = isRequestingSiteRoles( { + siteRoles: { + requesting: { + 12345678: true + } + } + }, 12345678 ); + + expect( isRequesting ).to.be.true; + } ); + } ); + + describe( '#getSiteRoles()', () => { + const roles = [ + { + name: 'administrator', + display_name: 'Administrator', + capabilities: { + activate_plugins: true, + edit_users: true, + manage_options: true + } + }, + { + name: 'customer', + display_name: 'Customer', + capabilities: { + read: true + } + } + ]; + const state = { + siteRoles: { + items: { + 12345678: roles + } + } + }; + + it( 'should return the roles for the site ID', () => { + const siteRoles = getSiteRoles( state, 12345678 ); + + expect( siteRoles ).to.eql( roles ); + } ); + + it( 'should return undefined if there is no such site', () => { + const siteRoles = getSiteRoles( state, 87654321 ); + + expect( siteRoles ).to.be.undefined; + } ); + } ); +} ); From 3945534191a6232b4afa1defa4c0fe118c1f9d58 Mon Sep 17 00:00:00 2001 From: Marin Atanasov Date: Wed, 9 Nov 2016 17:59:34 +0200 Subject: [PATCH 3/6] Components: Add QuerySiteRoles query component --- .../data/query-site-roles/README.md | 38 ++++++++++++++ .../data/query-site-roles/index.jsx | 50 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 client/components/data/query-site-roles/README.md create mode 100644 client/components/data/query-site-roles/index.jsx diff --git a/client/components/data/query-site-roles/README.md b/client/components/data/query-site-roles/README.md new file mode 100644 index 0000000000000..d7635c8a2bba5 --- /dev/null +++ b/client/components/data/query-site-roles/README.md @@ -0,0 +1,38 @@ +Query Site Roles +================ + +`` is a React component used in managing network requests for site roles. + +## Usage + +Render the component, passing `siteId`. It does not accept any children, nor does it render any elements 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 QuerySiteRoles from 'components/data/query-site-roles'; +import MySiteRolesListItem from './list-item'; + +export default function MySiteRolesList( { siteRoles } ) { + return ( +
+ + { siteRoles.map( ( role ) => { + return ( + + ); + } } +
+ ); +} +``` + +## Props + +### `siteId` + + + + +
TypeNumber
RequiredYes
+ +The site ID for which site roles should be requested. diff --git a/client/components/data/query-site-roles/index.jsx b/client/components/data/query-site-roles/index.jsx new file mode 100644 index 0000000000000..875a7ef17cf5a --- /dev/null +++ b/client/components/data/query-site-roles/index.jsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; + +/** + * Internal dependencies + */ +import { isRequestingSiteRoles } from 'state/site-roles/selectors'; +import { requestSiteRoles } from 'state/site-roles/actions'; + +class QuerySiteRoles extends Component { + static propTypes = { + siteId: PropTypes.number.isRequired, + requestingSiteRoles: PropTypes.bool, + requestSiteRoles: PropTypes.func + }; + + componentWillMount() { + this.request( this.props ); + } + + componentWillReceiveProps( nextProps ) { + if ( this.props.siteId !== nextProps.siteId ) { + this.request( nextProps ); + } + } + + request( props ) { + if ( props.requestingSiteRoles ) { + return; + } + + props.requestSiteRoles( props.siteId ); + } + + render() { + return null; + } +} + +export default connect( + ( state, ownProps ) => { + return { + requestingSiteRoles: isRequestingSiteRoles( state, ownProps.siteId ) + }; + }, + { requestSiteRoles } +)( QuerySiteRoles ); From cce17393019edcfce1e3290eaffce29f4d12d889 Mon Sep 17 00:00:00 2001 From: Marin Atanasov Date: Wed, 9 Nov 2016 18:02:50 +0200 Subject: [PATCH 4/6] People: Reduxify the RoleSelect component --- client/my-sites/people/role-select/index.jsx | 195 +++++++------------ 1 file changed, 72 insertions(+), 123 deletions(-) diff --git a/client/my-sites/people/role-select/index.jsx b/client/my-sites/people/role-select/index.jsx index fb0f0916dd79a..8c88b13211acd 100644 --- a/client/my-sites/people/role-select/index.jsx +++ b/client/my-sites/people/role-select/index.jsx @@ -2,138 +2,87 @@ * External dependencies */ import React from 'react'; -import debugFactory from 'debug'; import omit from 'lodash/omit'; -import map from 'lodash/map'; +import { connect } from 'react-redux'; +import { localize } from 'i18n-calypso'; /** * Internal dependencies */ -import RolesStore from 'lib/site-roles/store'; -import RolesActions from 'lib/site-roles/actions'; import FormFieldset from 'components/forms/form-fieldset'; import FormLabel from 'components/forms/form-label'; import FormSelect from 'components/forms/form-select'; import FormSettingExplanation from 'components/forms/form-setting-explanation'; -import sitesList from 'lib/sites-list'; - -/** - * Module variables - */ -const debug = debugFactory( 'calypso:my-sites:people:role-select' ); -const sites = sitesList(); - -export default React.createClass( { - displayName: 'RoleSelect', - - getInitialState() { - return ( { - roles: this.props.siteId ? RolesStore.getRoles( this.props.siteId ) : {} - } ); - }, - - componentWillMount() { - if ( this.props.includeFollower ) { - this.refreshRoles(); - } - }, - - componentDidMount() { - RolesStore.on( 'change', this.refreshRoles ); - this.fetchRoles(); - }, - - componentWillUnmount() { - RolesStore.removeListener( 'change', this.refreshRoles ); - }, - - componentWillReceiveProps( nextProps ) { - const siteId = nextProps.siteId || this.props.siteId; - this.fetchRoles( siteId ); - this.refreshRoles( nextProps ); - }, - - getWPCOMFollowerRole( siteId ) { - siteId = siteId ? siteId : this.props.siteId; - - const site = sites.getSite( siteId ), - displayName = site.is_private - ? this.translate( 'Viewer', { context: 'Role that is displayed in a select' } ) - : this.translate( 'Follower', { context: 'Role that is displayed in a select' } ); - - return { - follower: { - display_name: displayName, - name: 'follower' - } - }; - }, - - refreshRoles( nextProps ) { - const siteId = nextProps && nextProps.siteId ? nextProps.siteId : this.props.siteId; - - if ( siteId ) { - let siteRoles = RolesStore.getRoles( siteId ); - - if ( this.props.includeFollower ) { - siteRoles = Object.assign( {}, this.getWPCOMFollowerRole(), siteRoles ); - } - - this.setState( { - roles: siteRoles - } ); - } - }, - - fetchRoles( siteId = this.props.siteId ) { - if ( ! siteId ) { - debug( 'siteId not set' ); - return; - } - - if ( Object.keys( RolesStore.getRoles( siteId ) ).length ) { - debug( 'initial fetch not necessary' ); - return; - } - - debug( 'Fetching roles for ' + siteId ); +import QuerySites from 'components/data/query-sites'; +import QuerySiteRoles from 'components/data/query-site-roles'; +import { getSite } from 'state/sites/selectors'; +import { getSiteRoles } from 'state/site-roles/selectors'; + +const getWPCOMFollowerRole = ( { site, translate } ) => { + const displayName = site.is_private + ? translate( 'Viewer', { context: 'Role that is displayed in a select' } ) + : translate( 'Follower', { context: 'Role that is displayed in a select' } ); + + return { + display_name: displayName, + name: 'follower' + }; +}; + +const RoleSelect = ( props ) => { + const { siteRoles, site, includeFollower, siteId, id, explanation, translate } = props; + const roles = siteRoles && siteRoles.slice( 0 ); + const omitProps = [ + 'site', + 'key', + 'siteId', + 'includeFollower', + 'explanation', + 'siteRoles', + 'dispatch', + 'moment', + 'numberFormat', + 'translate' + ]; + + if ( site && roles && includeFollower ) { + roles.push( getWPCOMFollowerRole( props ) ); + } - // defer fetch requests to avoid dispatcher conflicts - setTimeout( function() { - const fetching = RolesStore.isFetching( siteId ); - if ( fetching ) { - return; + return ( + + { siteId && + && + } - RolesActions.fetch( siteId ); - }, 0 ); - }, - - render() { - const roleKeys = Object.keys( this.state.roles ); - return ( - - - { this.translate( 'Role', { - context: 'Text that is displayed in a label of a form.' - } ) } - - - { - map( this.state.roles, ( roleObject, key ) => { - return ( - - ); - } ) - } - - { this.props.explanation && - - { this.props.explanation } - + + { translate( 'Role', { + context: 'Text that is displayed in a label of a form.' + } ) } + + + { + roles && roles.map( ( role ) => { + return ( + + ); + } ) } - - ); - } -} ); + + { explanation && + + { explanation } + + } + + ); +}; + +export default connect( + ( state, ownProps ) => ( { + site: getSite( state, ownProps.siteId ), + siteRoles: getSiteRoles( state, ownProps.siteId ), + } ) +)( localize( RoleSelect ) ); From 90c9f5deaa6b1b9ca3f01d762517ebe5e7b078e1 Mon Sep 17 00:00:00 2001 From: Marin Atanasov Date: Wed, 9 Nov 2016 18:05:58 +0200 Subject: [PATCH 5/6] Remove lib/site-roles module --- client/lib/site-roles/README.md | 4 - client/lib/site-roles/actions.js | 33 ----- client/lib/site-roles/store.js | 68 ---------- client/lib/site-roles/test/fixtures/roles.js | 128 ------------------ client/lib/site-roles/test/fixtures/site.js | 31 ----- .../lib/site-roles/test/mocks/lib/actions.js | 11 -- client/lib/site-roles/test/store.js | 52 ------- 7 files changed, 327 deletions(-) delete mode 100644 client/lib/site-roles/README.md delete mode 100644 client/lib/site-roles/actions.js delete mode 100644 client/lib/site-roles/store.js delete mode 100644 client/lib/site-roles/test/fixtures/roles.js delete mode 100644 client/lib/site-roles/test/fixtures/site.js delete mode 100644 client/lib/site-roles/test/mocks/lib/actions.js delete mode 100644 client/lib/site-roles/test/store.js diff --git a/client/lib/site-roles/README.md b/client/lib/site-roles/README.md deleted file mode 100644 index 0dffeb031517d..0000000000000 --- a/client/lib/site-roles/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Site Roles -====== - -This module is a [flux](https://facebook.github.io/flux/docs/overview.html#content) approach to retrieving and storing roles, and is intended to be used with people management. diff --git a/client/lib/site-roles/actions.js b/client/lib/site-roles/actions.js deleted file mode 100644 index 891691e1b8742..0000000000000 --- a/client/lib/site-roles/actions.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * External dependencies - */ -var debug = require( 'debug' )( 'calypso:roles:actions' ); - -/** - * Internal dependencies - */ -var Dispatcher = require( 'dispatcher' ), - wpcom = require( 'lib/wp' ); - -var RolesActions = { - fetch: function( siteId ) { - debug( 'Fetch site roles', siteId ); - - Dispatcher.handleViewAction( { - type: 'FETCHING_ROLES', - siteId: siteId - } ); - - wpcom.undocumented().site( siteId ).getRoles( function( error, data ) { - Dispatcher.handleServerAction( { - type: 'RECEIVE_ROLES', - action: 'RECEIVE_ROLES', - siteId: siteId, - data: data, - error: error - } ); - } ); - } -}; - -module.exports = RolesActions; diff --git a/client/lib/site-roles/store.js b/client/lib/site-roles/store.js deleted file mode 100644 index 51aa6b4673209..0000000000000 --- a/client/lib/site-roles/store.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * External dependencies - */ -var debug = require( 'debug' )( 'calypso:viewers:store' ), - clone = require( 'lodash/clone' ); - -/** - * Internal dependencies - */ -var Dispatcher = require( 'dispatcher' ), - emitter = require( 'lib/mixins/emitter' ); - -var _fetchingRoles = {}, - _rolesBySite = {}; - -var RolesStore = { - isFetching: function( siteId ) { - return _fetchingRoles[ siteId ]; - }, - - getRoles: function( siteId ) { - if ( ! _rolesBySite[ siteId ] ) { - return {}; - } - - return _rolesBySite[ siteId ]; - }, - - emitChange: function() { - this.emit( 'change' ); - } -}; - -function updateRoles( siteId, roles ) { - _fetchingRoles[ siteId ] = false; - - if ( ! _rolesBySite[ siteId ] ) { - _rolesBySite[ siteId ] = {}; - } - - roles.forEach( function( role ) { - _rolesBySite[ siteId ][ role.name ] = clone( role ); - } ); -} - -RolesStore.dispatchToken = Dispatcher.register( function( payload ) { - var action = payload.action; - debug( 'register event Type', action.type, payload ); - - switch ( action.type ) { - case 'FETCHING_ROLES': - _fetchingRoles[ action.siteId ] = true; - RolesStore.emitChange(); - break; - case 'RECEIVE_ROLES': - _fetchingRoles[ action.siteId ] = false; - // Don't update roles if there was an error - if ( ! action.error ) { - updateRoles( action.siteId, action.data.roles ); - } - RolesStore.emitChange(); - break; - } -} ); - -emitter( RolesStore ); - -module.exports = RolesStore; diff --git a/client/lib/site-roles/test/fixtures/roles.js b/client/lib/site-roles/test/fixtures/roles.js deleted file mode 100644 index f0b97fd88c1b9..0000000000000 --- a/client/lib/site-roles/test/fixtures/roles.js +++ /dev/null @@ -1,128 +0,0 @@ -module.exports = { - roles: [ - { - name: 'administrator', - capabilities: { - switch_themes: true, - edit_themes: true, - edit_theme_options: true, - activate_plugins: true, - edit_plugins: true, - edit_users: true, - edit_files: true, - manage_options: true, - moderate_comments: true, - manage_categories: true, - manage_links: true, - upload_files: true, - import: true, - edit_posts: true, - edit_others_posts: true, - edit_published_posts: true, - publish_posts: true, - edit_pages: true, - read: true, - level_10: true, - level_9: true, - level_8: true, - level_7: true, - level_6: true, - level_5: true, - level_4: true, - level_3: true, - level_2: true, - level_1: true, - level_0: true, - list_users: true, - edit_others_pages: true, - edit_published_pages: true, - publish_pages: true, - delete_pages: true, - delete_others_pages: true, - delete_published_pages: true, - delete_posts: true, - delete_others_posts: true, - delete_published_posts: true, - delete_private_posts: true, - edit_private_posts: true, - read_private_posts: true, - delete_private_pages: true, - edit_private_pages: true, - read_private_pages: true, - delete_users: true, - create_users: true, - remove_users: true, - add_users: true, - promote_users: true, - export: true - }, - display_name: 'Administrator' - }, - { - name: 'editor', - capabilities: { - moderate_comments: true, - manage_categories: true, - manage_links: true, - upload_files: true, - edit_posts: true, - edit_others_posts: true, - edit_published_posts: true, - publish_posts: true, - edit_pages: true, - read: true, - level_7: true, - level_6: true, - level_5: true, - level_4: true, - level_3: true, - level_2: true, - level_1: true, - level_0: true, - edit_others_pages: true, - edit_published_pages: true, - publish_pages: true, - delete_pages: true, - delete_others_pages: true, - delete_published_pages: true, - delete_posts: true, - delete_others_posts: true, - delete_published_posts: true, - delete_private_posts: true, - edit_private_posts: true, - read_private_posts: true, - delete_private_pages: true, - edit_private_pages: true, - read_private_pages: true - }, - display_name: 'Editor' - }, - { - name: 'author', - capabilities: { - upload_files: true, - edit_posts: true, - edit_published_posts: true, - publish_posts: true, - read: true, - level_2: true, - level_1: true, - level_0: true, - delete_posts: true, - delete_published_posts: true - }, - display_name: 'Author' - }, - { - name: 'contributor', - capabilities: { - edit_posts: true, - read: true, - level_1: true, - level_0: true, - delete_posts: true - }, - display_name: 'Contributor' - } - ] -}; diff --git a/client/lib/site-roles/test/fixtures/site.js b/client/lib/site-roles/test/fixtures/site.js deleted file mode 100644 index e93f189aae4fc..0000000000000 --- a/client/lib/site-roles/test/fixtures/site.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - ID: 77203074, - URL: 'http://example.com', - canUpdateFiles: true, - capabilities: {}, - description: 'Just another WordPress site', - domain: 'example.com', - hasJetpackProtect: true, - hasMinimumJetpackVersion: true, - icon: {}, - is_following: false, - is_private: false, - jetpack: true, - lang: 'en', - logo: {}, - meta: {}, - name: 'Site 1', - options: {}, - plan: 1, - plugins: [], - post_count: 13, - single_user_site: true, - slug: 'example.com', - subscribers_count: 0, - title: 'Site 1', - update: {}, - jp_version: '3.6', - user_can_manage: true, - visible: true, - wpcom_url: 'example.com' -}; diff --git a/client/lib/site-roles/test/mocks/lib/actions.js b/client/lib/site-roles/test/mocks/lib/actions.js deleted file mode 100644 index 8855bb0a06fce..0000000000000 --- a/client/lib/site-roles/test/mocks/lib/actions.js +++ /dev/null @@ -1,11 +0,0 @@ -var site = require( '../../fixtures/site' ), - mockRoles = require( '../../fixtures/roles' ); - -module.exports = { - fetchedRoles: { - type: 'RECEIVE_ROLES', - siteId: site.ID, - data: mockRoles, - error: null - } -}; diff --git a/client/lib/site-roles/test/store.js b/client/lib/site-roles/test/store.js deleted file mode 100644 index 6e647ef66ae44..0000000000000 --- a/client/lib/site-roles/test/store.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * External dependencies - */ -import { assert } from 'chai'; - -/** - * Internal dependencies - */ -import useMockery from 'test/helpers/use-mockery'; - -describe( 'Viewers Store', () => { - let Dispatcher, RolesStore; - let actions, site, siteId; - - useMockery(); - - before( () => { - Dispatcher = require( 'dispatcher' ); - RolesStore = require( '../store' ); - actions = require( './mocks/lib/actions' ); - site = require( './fixtures/site' ); - siteId = site.ID; - } ); - - describe( 'Shape of store', () => { - it( 'Store should be an object', () => { - assert.isObject( RolesStore ); - } ); - - it( 'Store should have method getRoles', () => { - assert.isFunction( RolesStore.getRoles ); - } ); - } ); - - describe( 'Get Roles', () => { - it( 'Should return empty object when there are no roles', () => { - const roles = RolesStore.getRoles( siteId ); - - assert.isObject( roles ); - assert.lengthOf( Object.keys( roles ), 0, 'roles is empty' ); - } ); - - it( 'Should return an object of role objects when there are roles', () => { - Dispatcher.handleServerAction( actions.fetchedRoles ); - const roles = RolesStore.getRoles( siteId ); - - assert.isObject( roles ); - assert.notDeepEqual( Object.keys( roles ), [], 'roles is not empty' ); - assert.isObject( roles[ Object.keys( roles )[ 0 ] ] ); - } ); - } ); -} ); From 5e323012ce95d5a296331f62a554371010b5d69d Mon Sep 17 00:00:00 2001 From: Marin Atanasov Date: Fri, 11 Nov 2016 18:08:42 +0200 Subject: [PATCH 6/6] Components: Clean up RoleSelect component --- client/my-sites/people/role-select/index.jsx | 26 +++++++++----------- client/state/site-roles/actions.js | 10 ++------ client/state/site-roles/test/actions.js | 7 +----- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/client/my-sites/people/role-select/index.jsx b/client/my-sites/people/role-select/index.jsx index 8c88b13211acd..031b4cfdbd4d4 100644 --- a/client/my-sites/people/role-select/index.jsx +++ b/client/my-sites/people/role-select/index.jsx @@ -2,7 +2,7 @@ * External dependencies */ import React from 'react'; -import omit from 'lodash/omit'; +import { omit, map } from 'lodash'; import { connect } from 'react-redux'; import { localize } from 'i18n-calypso'; @@ -18,7 +18,7 @@ import QuerySiteRoles from 'components/data/query-site-roles'; import { getSite } from 'state/sites/selectors'; import { getSiteRoles } from 'state/site-roles/selectors'; -const getWPCOMFollowerRole = ( { site, translate } ) => { +const getWpcomFollowerRole = ( { site, translate } ) => { const displayName = site.is_private ? translate( 'Viewer', { context: 'Role that is displayed in a select' } ) : translate( 'Follower', { context: 'Role that is displayed in a select' } ); @@ -30,8 +30,8 @@ const getWPCOMFollowerRole = ( { site, translate } ) => { }; const RoleSelect = ( props ) => { - const { siteRoles, site, includeFollower, siteId, id, explanation, translate } = props; - const roles = siteRoles && siteRoles.slice( 0 ); + let { siteRoles } = props; + const { site, includeFollower, siteId, id, explanation, translate } = props; const omitProps = [ 'site', 'key', @@ -45,24 +45,20 @@ const RoleSelect = ( props ) => { 'translate' ]; - if ( site && roles && includeFollower ) { - roles.push( getWPCOMFollowerRole( props ) ); + if ( site && siteRoles && includeFollower ) { + siteRoles = siteRoles.concat( getWpcomFollowerRole( props ) ); } return ( - - { siteId && - && - - } + + { siteId && } + { siteId && } - { translate( 'Role', { - context: 'Text that is displayed in a label of a form.' - } ) } + { translate( 'Role' ) } { - roles && roles.map( ( role ) => { + siteRoles && map( siteRoles, ( role ) => { return (