Skip to content
This repository has been archived by the owner on May 19, 2020. It is now read-only.

Create space user's dropdown form for adding other org users #1172

Merged
merged 29 commits into from
Aug 1, 2017
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e56bc8c
Add new constants to check for user role by username
rememberlenny Jul 28, 2017
4ac8ee1
Create ability to get all users in org that aren't in space
rememberlenny Jul 28, 2017
503e7f4
Create functions used to check space org role status
rememberlenny Jul 28, 2017
0bba2f7
Create the component to manage the org parent user selector
rememberlenny Jul 28, 2017
931eef7
Add the selector to the user component
rememberlenny Jul 28, 2017
9fa5cbc
Get the selector based user adding working
rememberlenny Jul 28, 2017
2461e0c
Add a component test
rememberlenny Jul 28, 2017
651e522
Remove unused store cases
rememberlenny Jul 28, 2017
9c2815e
Create check again space users
rememberlenny Jul 28, 2017
fa73119
Take care of linting
rememberlenny Jul 28, 2017
04a8972
Lint tests
rememberlenny Jul 28, 2017
1d3f87f
Fix linting
rememberlenny Jul 28, 2017
55defcd
Remove unused comment
rememberlenny Jul 28, 2017
33a9778
Rename component to be easily grokable
rememberlenny Jul 28, 2017
4947863
Pull out constants
rememberlenny Jul 28, 2017
42cb434
Rework file names and component reference
rememberlenny Jul 28, 2017
a94ab91
Get the error working on the component
rememberlenny Jul 28, 2017
06ad595
Get the working selector check
rememberlenny Jul 28, 2017
02106fb
Merge branch 'master' of github.com:18F/cg-dashboard into lkb-dropdow…
rememberlenny Jul 28, 2017
b98bf11
Save the changes on the user store check
rememberlenny Jul 28, 2017
a7813cf
Fix linting
rememberlenny Jul 28, 2017
80ca7d6
Add the selector detection
rememberlenny Aug 1, 2017
fe47005
Rename the component
rememberlenny Aug 1, 2017
38ed848
Change the function argument names
rememberlenny Aug 1, 2017
ff12ede
Rename usersSelectorDisabled
rememberlenny Aug 1, 2017
5d8b81d
Linting
rememberlenny Aug 1, 2017
57ac1d0
Re-update user action names
rememberlenny Aug 1, 2017
339846c
Fix new var name on params around addUserRoles
rememberlenny Aug 1, 2017
f655f6e
Rename classes for specificity
rememberlenny Aug 1, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions static_src/actions/user_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import UserStore from '../stores/user_store';
import OrgStore from '../stores/org_store';
import SpaceStore from '../stores/space_store';

const ORG_ENTITY = 'organization';
const SPACE_ENTITY = 'space';
const ORG_NAME = OrgStore.cfName;
// const SPACE_NAME = SpaceStore.cfName;
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this commented out?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It has a linking error because it's not used.

Copy link
Contributor

Choose a reason for hiding this comment

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

lets delete it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

const MSG_USER_HAS_SPACE_ROLES = 'This user can\'t be removed because they still have a space ' +
'role within the organization. Please remove all space ' +
'associations before removing this user from the organization. ' +
Expand Down Expand Up @@ -291,8 +294,8 @@ const userActions = {
const noticeType = 'finish';
const currentViewedType = UserStore.currentlyViewedType;
const viewTypeNouns = Object.assign({},
{ space_users: { singular: 'space' } },
{ org_users: { singular: 'organization' } }
{ space_users: { singular: SPACE_ENTITY } },
{ org_users: { singular: ORG_ENTITY } }
);
const entity = viewTypeNouns[currentViewedType].singular;

Expand Down
42 changes: 42 additions & 0 deletions static_src/components/users.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import OrgStore from '../stores/org_store.js';
import SpaceStore from '../stores/space_store.js';
import UserList from './user_list.jsx';
import UsersInvite from './users_invite.jsx';
import UsersParentEntityUserSelector from './users_parent_entity_user_selector.jsx';
import Notification from './notification.jsx';
import UserStore from '../stores/user_store.js';
import ErrorMessage from './error_message.jsx';
Expand All @@ -33,12 +34,15 @@ function stateSetter() {
const isSaving = UserStore.isSaving;

let users = [];
let parentEntityUsers;
let currentUserAccess = false;
const inviteDisabled = UserStore.inviteDisabled();
const userParentEntityUserSelectDisabled = UserStore.userParentEntityUserSelectDisabled();
let entityGuid;

if (currentType === SPACE_NAME) {
users = UserStore.getAllInSpace(currentSpaceGuid);
parentEntityUsers = UserStore.getAllInOrgAndNotSpace(currentSpaceGuid);
entityGuid = currentSpaceGuid;
currentUserAccess = UserStore.hasRole(currentUser.guid, currentSpaceGuid,
SPACE_MANAGER);
Expand All @@ -53,6 +57,7 @@ function stateSetter() {
currentUser,
error: UserStore.getError(),
inviteDisabled,
userParentEntityUserSelectDisabled,
currentUserAccess,
currentOrgGuid,
currentSpaceGuid,
Expand All @@ -62,6 +67,7 @@ function stateSetter() {
loading: UserStore.loading,
empty: !UserStore.loading && !users.length,
users,
parentEntityUsers,
userListNotices: UserStore._userListNotification,
userListNoticeError: UserStore.getUserListNotificationError()
};
Expand Down Expand Up @@ -140,6 +146,13 @@ export default class Users extends React.Component {
return entityGuid;
}

get currentUserIsSpaceManager() {
const { currentUser } = this.state;
const { currentSpaceGuid } = SpaceStore;

return UserStore.hasRole(currentUser.guid, currentSpaceGuid, SPACE_MANAGER);
}

get currentUserIsOrgManager() {
const { currentUser } = this.state;
const { currentOrgGuid } = OrgStore;
Expand Down Expand Up @@ -186,6 +199,34 @@ export default class Users extends React.Component {
);
}

get userParentEntityUserSelector() {
if (!this.isSpace) {
return null;
}

if (!this.currentUserIsSpaceManager) {
return (
<PanelDocumentation>
ORG AND SPACE MANAGERS CAN DO THIS.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure you meant to have this text here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hahaha. No it should be :)

</PanelDocumentation>
);
}

return (
<UsersParentEntityUserSelector
userParentEntityUserSelectDisabled={ this.state.userParentEntityUserSelectDisabled }
parentEntityGuid={ this.state.currentOrgGuid }
parentEntity={ ORG_ENTITY }
currentEntityGuid={ this.entityGuid }
currentEntity={ this.entityType }
parentEntityUsers={ this.state.parentEntityUsers }
inviteEntityType={ this.entityType }
currentUserAccess={ this.state.currentUserAccess }
error={ this.state.userListNoticeError }
/>
);
}

_onChange() {
this.setState(stateSetter());
}
Expand All @@ -203,6 +244,7 @@ export default class Users extends React.Component {
<div className="test-users">
<ErrorMessage error={this.state.error} />
{ this.userInvite }
{ this.userParentEntityUserSelector }
{ this.notification }
<UserList
users={ this.state.users }
Expand Down
147 changes: 147 additions & 0 deletions static_src/components/users_parent_entity_user_selector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Renders a form that allows org users to invite new users
* to cloud.gov
*/

import PropTypes from 'prop-types';
import React from 'react';
import Action from './action.jsx';
import FormStore from '../stores/form_store';
import { Form, FormSelect } from './form';
import PanelDocumentation from './panel_documentation.jsx';
import userActions from '../actions/user_actions';
import { validateString } from '../util/validators';

const USERS_PARENT_ENTITY_USER_FORM_GUID = 'users-parent-entity-users-form';

const propTypes = {
userParentEntityUserSelectDisabled: PropTypes.bool,
currentUserAccess: PropTypes.bool,
parentEntityUsers: PropTypes.array,
error: PropTypes.object,
parentEntityGuid: PropTypes.string,
parentEntity: PropTypes.string,
currentEntityGuid: PropTypes.string,
currentEntity: PropTypes.string
};
const defaultProps = {
userParentEntityUserSelectDisabled: false,
currentUserAccess: false,
error: {}
};

function stateSetter(props) {
return {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is state needed in this component if everything is passed down from props? https://github.com/18F/cg-dashboard/blob/master/CONTRIBUTING.md#components

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Its not. This is a great document. Thanks.

userParentEntityUserSelectDisabled: props.userParentEntityUserSelectDisabled,
currentUserAccess: props.currentUserAccess,
parentEntityUsers: props.parentEntityUsers,
parentEntityGuid: props.parentEntityGuid,
parentEntity: props.parentEntity,
currentEntityGuid: props.currentEntityGuid,
currentEntity: props.currentEntity,
error: props.error
};
}

export default class UsersParentEntityUserSelector extends React.Component {
Copy link
Contributor

Choose a reason for hiding this comment

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

we might want to change the name of this. at first glance it's not that clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I totally agree. Originally it was supposed to be a module. Not it only does one thing, check org users, if user is in a space.

constructor(props) {
super(props);
FormStore.create(USERS_PARENT_ENTITY_USER_FORM_GUID);
Copy link
Contributor

Choose a reason for hiding this comment

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

Creating a form here could produce unwanted side effects e.g. if this component subscribed to the FormStore's change event. It should be called in componentDidMount


this.state = stateSetter(props);

this.validateString = validateString().bind(this);
this._onSubmitForm = this._onSubmitForm.bind(this);
}

_onSubmitForm(errs, values) {
const entityType = this.state.currentEntity;
const entityGuid = this.state.currentEntityGuid;
const apiKey = 'auditors';
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets make these constants

const roles = 'space_auditor';
if (values.userGuid) {
const userGuid = values.userGuid.value;
userActions.addUserRoles(roles, apiKey, userGuid, entityGuid, entityType);
Copy link
Contributor

Choose a reason for hiding this comment

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

i think in order to prevent confusion, we should only show org users currently without space roles. then here, upon successful submission, we should remove the user from the list so we don't see them here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was working on last night, but didn't know quite how the EventEmitter on the UserStore worked. I need to dig in a bit.

Basically, the logic to do this works.

}
}

get errorMessage() {
Copy link
Contributor

Choose a reason for hiding this comment

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

test for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy!

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this is copied from the UserInvite component, seems like a good time to extract a out module or higher-order component.

const { error } = this.props;

if (!error) return '';

const message = error.contextualMessage;

if (error.message) {
return `${message}: ${error.message}.`;
}

return message;
}

get invitationMessage() {
Copy link
Contributor

Choose a reason for hiding this comment

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

test for this

Copy link
Contributor

Choose a reason for hiding this comment

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

See above ^^

const parentEntity = this.props.parentEntity;
const currentEntity = this.props.currentEntity;

return `Invite an existing user in this ${parentEntity}` +
` to this ${currentEntity}.`;
}

get userSelector() {
Copy link
Contributor

Choose a reason for hiding this comment

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

test for this

const orgUsers = this.state.parentEntityUsers.map((user) =>
({ value: user.guid, label: user.username })
);

if (!orgUsers) {
return null;
}

return (
<FormSelect
formGuid={ USERS_PARENT_ENTITY_USER_FORM_GUID }
classes={ ['test-users_parent_entity_user_name'] }
label="Username"
name="userGuid"
options={ orgUsers }
validator={ this.validateString }
/>
);
}

render() {
const { userParentEntityUserSelectDisabled } = this.props;

if (!this.props.currentUserAccess) {
return null;
}

return (
<div className="test-users-invite">
<PanelDocumentation description>
<p>{ this.invitationMessage }</p>
</PanelDocumentation>
<Form
guid={ USERS_PARENT_ENTITY_USER_FORM_GUID }
classes={ ['users_parent_entity_user_form'] }
ref="form"
onSubmit={ this._onSubmitForm }
errorOverride={ this.errorMessage }
>
{ this.userSelector }
<Action
label="submit"
type="submit"
disabled={ userParentEntityUserSelectDisabled }
>
Add user to this { this.state.currentEntity }
</Action>
</Form>
</div>
);
}

}

UsersParentEntityUserSelector.propTypes = propTypes;

UsersParentEntityUserSelector.defaultProps = defaultProps;
15 changes: 14 additions & 1 deletion static_src/stores/user_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class UserStore extends BaseStore {
this._error = null;
this._saving = false;
this._inviteDisabled = false;
this._userParentEntityUserSelectDisabled = false;
this._userListNotification = {};
this._loading = {};
}
Expand Down Expand Up @@ -189,7 +190,7 @@ export class UserStore extends BaseStore {
this._userListNotificationError = Object.assign({}, action.err, {
contextualMessage: action.contextualMessage
});
this._inviteDisabled = false;
this._userParentEntityUserSelectDisabled = false;
this.emitChange();
break;
}
Expand Down Expand Up @@ -322,6 +323,14 @@ export class UserStore extends BaseStore {
return usersInOrg.toJS();
}

getAllInOrgAndNotSpace() {
const usersInOrg = this._data.toJS().filter((user) =>
!user.space_roles
);

return usersInOrg;
}

getError() {
return this._error;
}
Expand Down Expand Up @@ -367,6 +376,10 @@ export class UserStore extends BaseStore {
return !!roles.find((role) => wrappedRoles.includes(role));
}

userParentEntityUserSelectDisabled() {
return this._userParentEntityUserSelectDisabled;
}

inviteDisabled() {
return this._inviteDisabled;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import '../../global_setup.js';

import React from 'react';
import { shallow } from 'enzyme';
import { Form } from '../../../components/form';
import ParentUserSelector from '../../../components/users_parent_entity_user_selector.jsx';

describe('<ParentUserSelector />', function () {
const entityType = 'space';
const props = {
inviteEntityType: entityType,
currentUserAccess: true
};
let wrapper;

describe('when user does not have ability to invite other users', () => {
it('does not render <Form /> component', () => {
const noAccessProps = Object.assign({}, props, { currentUserAccess: false });
wrapper = shallow(<ParentUserSelector { ...noAccessProps } />);

expect(wrapper.find(Form).length).toEqual(0);
});
});
});
14 changes: 14 additions & 0 deletions static_src/test/unit/stores/user_store.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,19 @@ describe('UserStore', function () {
});
});

describe('getAllInOrgAndNotSpace()', function() {
it('should find all users that are in an org, without the space users', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am probably misreading this spec, but from the description, shouldn't the testUser be excluded here? They don't have a space_roles key on their object, they do have a space role in their roles.

Should the corresponding method in UserStore also check for the existence of space roles within the roles object?

var spaceGuid = 'asdfa';
var testUser = { guid: 'adfzxcv', roles: { [spaceGuid]: [ 'space_user'] } };

UserStore.push(testUser);

let actual = UserStore.getAllInOrg(spaceGuid);

expect(actual[0]).toEqual(testUser);
});
});

describe('USER_FETCH', function () {
let user;
beforeEach(function () {
Expand Down Expand Up @@ -909,6 +922,7 @@ describe('UserStore', function () {
});
});
});

describe('isAdmin()', function () {
describe('user with _currentUserIsAdmin', function () {
let user, space, org, actual;
Expand Down