Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invites: Add invites create validation methods #3037

Merged
merged 11 commits into from
Feb 10, 2016
22 changes: 22 additions & 0 deletions client/lib/invites/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,25 @@ export function sendInvites( siteId, usernamesOrEmails, role, message, callback
callback( error, data );
} );
}

export function createInviteValidation( siteId, usernamesOrEmails, role ) {
Dispatcher.handleViewAction( {
type: ActionTypes.CREATE_INVITE_VALIDATION,
siteId, usernamesOrEmails, role
} );
wpcom.undocumented().createInviteValidation( siteId, usernamesOrEmails, role, ( error, data ) => {
Dispatcher.handleServerAction( {
type: error ? ActionTypes.RECEIVE_CREATE_INVITE_VALIDATION_ERROR : ActionTypes.RECEIVE_CREATE_INVITE_VALIDATION_SUCCESS,
error,
siteId,
usernamesOrEmails,
role,
data
} );
if ( error ) {
analytics.tracks.recordEvent( 'calypso_invite_create_validation_failed' );
} else {
analytics.tracks.recordEvent( 'calypso_invite_create_validation_success' );
}
} );
}
5 changes: 4 additions & 1 deletion client/lib/invites/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ export const action = keyMirror( {
RECEIVE_INVITE_ACCEPTED_ERROR: null,
SENDING_INVITES: null,
RECEIVE_SENDING_INVITES_ERROR: null,
RECEIVE_SENDING_INVITES_SUCCESS: null
RECEIVE_SENDING_INVITES_SUCCESS: null,
CREATE_INVITE_VALIDATION: null,
RECEIVE_CREATE_INVITE_VALIDATION_ERROR: null,
RECEIVE_CREATE_INVITE_VALIDATION_SUCCESS: null
} );
25 changes: 25 additions & 0 deletions client/lib/invites/reducers/invites-create-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { fromJS } from 'immutable';

/**
* Internal dependencies
*/
import { action as ActionTypes } from 'lib/invites/constants';

const initialState = fromJS( {
success: {},
errors: {}
} );

const reducer = ( state = initialState, payload ) => {
const { action } = payload;
switch ( action.type ) {
case ActionTypes.RECEIVE_CREATE_INVITE_VALIDATION_SUCCESS:
return state.setIn( [ 'success', action.siteId ], action.data.success ).setIn( [ 'errors', action.siteId ], action.data.errors );
}
return state;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this matches with the API. The validation endpoint doesn't return errors for usernames or emails that don't validate. The endpoint returns an object that looks a bit like:

{
  errors: [ WP_Error objects ],
  success: [ user1, useremail@test.com ]
}


export { initialState, reducer };
12 changes: 12 additions & 0 deletions client/lib/invites/stores/invites-accept-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import { createReducerStore } from 'lib/store';
import { reducer, initialState } from 'lib/invites/reducers/invites-accept-validation';

const InvitesAcceptValidationStore = createReducerStore( reducer, initialState );

InvitesAcceptValidationStore.getInvite = ( siteId, inviteKey ) => InvitesAcceptValidationStore.get().getIn( [ 'list', siteId, inviteKey ] );
InvitesAcceptValidationStore.getInviteError = ( siteId, inviteKey ) => InvitesAcceptValidationStore.get().getIn( [ 'errors', siteId, inviteKey ] );

export default InvitesAcceptValidationStore;
12 changes: 12 additions & 0 deletions client/lib/invites/stores/invites-create-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import { createReducerStore } from 'lib/store';
import { reducer, initialState } from 'lib/invites/reducers/invites-create-validation';

const InvitesCreateValidationStore = createReducerStore( reducer, initialState );

InvitesCreateValidationStore.getSuccess = ( siteId ) => InvitesCreateValidationStore.get().getIn( [ 'success', siteId ] );
InvitesCreateValidationStore.getErrors = ( siteId ) => InvitesCreateValidationStore.get().getIn( [ 'errors', siteId ] );

export default InvitesCreateValidationStore;
12 changes: 0 additions & 12 deletions client/lib/invites/stores/invites-validation.js

This file was deleted.

51 changes: 51 additions & 0 deletions client/lib/invites/test/invites-create-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require( 'lib/react-test-env-setup' )();

const assert = require( 'chai' ).assert;

/**
* Internal dependencies
*/
const Dispatcher = require( 'dispatcher' ),
constants = require( 'lib/invites/constants' );

describe( 'Invites Create Validation Store', () => {
let InvitesCreateValidationStore;
const siteId = 123;

const validationData = {
errors: {
'test@gmail.com': {
errors: {
'form-error-username-or-email': [ 'User already has a role on your site.' ]
},
error_data: []
}
},
success: [ 'testuser', 'test2@gmail.com' ]
}

const actions = {
receiveValidaton: {
type: constants.action.RECEIVE_CREATE_INVITE_VALIDATION_SUCCESS,
siteId: siteId,
data: validationData
},
};

beforeEach( () => {
InvitesCreateValidationStore = require( 'lib/invites/stores/invites-create-validation' );
} );

describe( 'Validating invite creation', () => {
beforeEach( () => {
Dispatcher.handleServerAction( actions.receiveValidaton );
} );

it( 'Validation is not empty', () => {
const success = InvitesCreateValidationStore.getSuccess( siteId );
assert.lengthOf( success, 2 );
const errors = InvitesCreateValidationStore.getErrors( siteId );
assert.equal( errors, validationData.errors );
} );
} );
} );
8 changes: 8 additions & 0 deletions client/lib/wpcom-undocumented/lib/undocumented.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ Undocumented.prototype.sendInvites = function( siteId, usernamesOrEmails, role,
}, fn );
};

Undocumented.prototype.createInviteValidation = function( siteId, usernamesOrEmails, role, fn ) {
debug( '/sites/:site_id:/invites/validate query' );
this.wpcom.req.post( '/sites/' + siteId + '/invites/validate', {}, {
invitees: usernamesOrEmails,
role: role
}, fn );
};

/**
* GET/POST site settings
*
Expand Down
2 changes: 1 addition & 1 deletion client/my-sites/invites/invite-accept/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import LoggedIn from 'my-sites/invites/invite-accept-logged-in';
import LoggedOut from 'my-sites/invites/invite-accept-logged-out';
import _user from 'lib/user';
import { fetchInvite } from 'lib/invites/actions';
import InvitesStore from 'lib/invites/stores/invites-validation';
import InvitesStore from 'lib/invites/stores/invites-accept-validation';
import EmptyContent from 'components/empty-content';
import { successNotice, infoNotice } from 'state/notices/actions';
import analytics from 'analytics';
Expand Down
53 changes: 37 additions & 16 deletions client/my-sites/people/invite-people/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import Card from 'components/card';
import Main from 'components/main';
import HeaderCake from 'components/header-cake';
import CountedTextarea from 'components/forms/counted-textarea';
import { createInviteValidation } from 'lib/invites/actions';
import InvitesCreateValidationStore from 'lib/invites/stores/invites-create-validation';

/**
* Module variables
Expand All @@ -32,6 +34,14 @@ export default React.createClass( {

mixins: [ LinkedStateMixin ],

componentDidMount() {
InvitesCreateValidationStore.on( 'change', this.refreshValidation );
},

componentWillUnmount() {
InvitesCreateValidationStore.off( 'change', this.refreshValidation );
},

componentWillReceiveProps() {
this.setState( this.resetState() );
},
Expand All @@ -46,18 +56,41 @@ export default React.createClass( {
role: 'follower',
message: '',
response: false,
sendingInvites: false
sendingInvites: false,
getTokenStatus: () => {}
} );
},

onTokensChange( tokens ) {
this.setState( { usernamesOrEmails: tokens } );
const { role } = this.state;
createInviteValidation( this.props.site.ID, tokens, role );
},

onMessageChange( event ) {
this.setState( { message: event.target.value } );
},

refreshValidation() {
const errors = InvitesCreateValidationStore.getErrors( this.props.site.ID ) || [];
let success = InvitesCreateValidationStore.getSuccess( this.props.site.ID ) || [];
if ( ! success.indexOf ) {
success = Object.keys( success ).map( key => success[ key ] );
}
this.setState( {
getTokenStatus: ( value ) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we storing this function in state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we pass the same function to tokens they don't rerender, we need to create a new function every time the validation results change to see them change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, TokenField has the pureRenderMixin. Do you think we should update a key instead of creating a new function each time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be about the same, besides, the function behaviour really changes, I like that we are creating a new instance.

if ( 'string' === typeof value ) {
if ( errors[ value ] ) {
return 'is-error';
}
if ( success.indexOf( value ) > -1 ) {
return 'is-success';
}
}
}
} );
},

submitForm( event ) {
event.preventDefault();
debug( 'Submitting invite form. State: ' + JSON.stringify( this.state ) );
Expand All @@ -79,19 +112,6 @@ export default React.createClass( {
page.back( fallback );
},

getTokenStatus( value ) {
let status;
if ( 'string' === typeof value ) {
if ( -1 < value.indexOf( 'error' ) ) {
status = 'is-error';
} else if ( -1 < value.indexOf( 'success' ) ) {
status = 'is-success';
}
}

return status;
},

renderRoleExplanation() {
return (
<a target="_blank" href="http://en.support.wordpress.com/user-roles/">
Expand Down Expand Up @@ -123,7 +143,7 @@ export default React.createClass( {
<FormLabel>{ this.translate( 'Usernames or Emails' ) }</FormLabel>
<TokenField
isBorderless
tokenStatus={ this.getTokenStatus }
tokenStatus={ this.state.getTokenStatus }
value={ this.state.usernamesOrEmails }
onChange={ this.onTokensChange } />
<FormSettingExplanation>
Expand All @@ -142,7 +162,8 @@ export default React.createClass( {
siteId={ this.props.site.ID }
valueLink={ this.linkState( 'role' ) }
disabled={ this.state.sendingInvites }
explanation={ this.renderRoleExplanation() }/>
explanation={ this.renderRoleExplanation() }
/>

<FormFieldset>
<FormLabel htmlFor="message">{ this.translate( 'Custom Message' ) }</FormLabel>
Expand Down