diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-03-28 12:13:23 +0200 |
---|---|---|
committer | Grégoire Aubert <gregaubert@users.noreply.github.com> | 2017-03-31 10:29:27 +0200 |
commit | 76ad0222a481f307474b2990f264780f42cf3627 (patch) | |
tree | e23bfc78a9f43c2afd9d35baf46f54e0118fde0e /server/sonar-web/src/main/js/apps | |
parent | 4b2cf358d38ad0e1931246530d3ff6ada7467079 (diff) | |
download | sonarqube-76ad0222a481f307474b2990f264780f42cf3627.tar.gz sonarqube-76ad0222a481f307474b2990f264780f42cf3627.zip |
SONAR-8993 Remove member from organization
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
10 files changed, 304 insertions, 9 deletions
diff --git a/server/sonar-web/src/main/js/apps/organizations/actions.js b/server/sonar-web/src/main/js/apps/organizations/actions.js index f21d6bb775f..864ba1f448d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/organizations/actions.js @@ -140,3 +140,11 @@ export const addOrganizationMember = (key: string, member: Member) => (dispatch: dispatch(membersActions.removeMember(key, member)); }); }; + +export const removeOrganizationMember = (key: string, member: Member) => (dispatch: Function) => { + dispatch(membersActions.removeMember(key, member)); + return api.removeMember({ login: member.login, organization: key }).catch((error: Object) => { + onFail(dispatch)(error); + dispatch(membersActions.addMember(key, member)); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js index a5744dfd253..55997c69ba5 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersList.js @@ -26,6 +26,7 @@ import type { Organization } from '../../../store/organizations/duck'; type Props = { members: Array<Member>, organization?: Organization, + removeMember: (Member) => void, }; export default class MembersList extends React.PureComponent { @@ -40,6 +41,7 @@ export default class MembersList extends React.PureComponent { key={member.login} member={member} organization={this.props.organization} + removeMember={this.props.removeMember} /> ))} </tbody> diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js index 496d950a8e1..0925526dfda 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js @@ -22,12 +22,14 @@ import React from 'react'; import Avatar from '../../../components/ui/Avatar'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; +import RemoveMemberForm from './forms/RemoveMemberForm'; import type { Member } from '../../../store/organizationsMembers/actions'; import type { Organization } from '../../../store/organizations/duck'; type Props = { member: Member, - organization: Organization + organization: Organization, + removeMember: (Member) => void, }; const AVATAR_SIZE: number = 36; @@ -40,14 +42,32 @@ export default class MembersListItem extends React.PureComponent { return ( <tr> <td className="thin nowrap"> - <Avatar hash={member.avatar} size={AVATAR_SIZE} /> + <Avatar hash={member.avatar} email={member.email} size={AVATAR_SIZE} /> </td> <td className="nowrap text-middle"><strong>{member.name}</strong></td> {organization.canAdmin && <td className="text-right text-middle"> {translate('organization.members.x_group(s)', formatMeasure(member.groupCount, 'INT'))} - </td> - } + </td>} + {organization.canAdmin && + <td className="nowrap text-middle text-right"> + <div className="dropdown"> + <button className="dropdown-toggle little-spacer-right" data-toggle="dropdown"> + <i className="icon-edit" /> + {' '} + <i className="icon-dropdown" /> + </button> + <ul className="dropdown-menu dropdown-menu-right"> + <li> + <RemoveMemberForm + organization={this.props.organization} + removeMember={this.props.removeMember} + member={this.props.member} + /> + </li> + </ul> + </div> + </td>} </tr> ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js index cf87c5cc85e..b91fe00207d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js @@ -34,7 +34,8 @@ type Props = { organization: Organization, fetchOrganizationMembers: (organizationKey: string, query?: string) => void, fetchMoreOrganizationMembers: (organizationKey: string, query?: string) => void, - addOrganizationMember: (organizationKey: string, login: Member) => void + addOrganizationMember: (organizationKey: string, login: Member) => void, + removeOrganizationMember: (organizationKey: string, login: Member) => void }; export default class OrganizationMembers extends React.PureComponent { @@ -59,6 +60,10 @@ export default class OrganizationMembers extends React.PureComponent { this.props.addOrganizationMember(this.props.organization.key, member); }; + removeMember = (member: Member) => { + this.props.removeOrganizationMember(this.props.organization.key, member); + }; + render() { const { organization, status, members } = this.props; return ( @@ -75,6 +80,7 @@ export default class OrganizationMembers extends React.PureComponent { <MembersList members={members} organization={organization} + removeMember={this.removeMember} /> {status.total != null && <ListFooter diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js index abea8ce8552..67b07e138df 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js @@ -28,7 +28,8 @@ import { import { fetchOrganizationMembers, fetchMoreOrganizationMembers, - addOrganizationMember + addOrganizationMember, + removeOrganizationMember } from '../actions'; const mapStateToProps = (state, ownProps) => { @@ -45,5 +46,6 @@ const mapStateToProps = (state, ownProps) => { export default connect(mapStateToProps, { fetchOrganizationMembers, fetchMoreOrganizationMembers, - addOrganizationMember + addOrganizationMember, + removeOrganizationMember })(OrganizationMembers); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap index debea9c01a0..9832ec66cc2 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap @@ -33,5 +33,41 @@ exports[`test should render actions and groups for admin 1`] = ` className="text-right text-middle"> organization.members.x_group(s).3 </td> + <td + className="nowrap text-middle text-right"> + <div + className="dropdown"> + <button + className="dropdown-toggle little-spacer-right" + data-toggle="dropdown"> + <i + className="icon-edit" /> + + <i + className="icon-dropdown" /> + </button> + <ul + className="dropdown-menu dropdown-menu-right"> + <li> + <RemoveMemberForm + member={ + Object { + "avatar": "", + "groupCount": 3, + "login": "admin", + "name": "Admin Istrator", + } + } + organization={ + Object { + "canAdmin": true, + "key": "foo", + "name": "Foo", + } + } /> + </li> + </ul> + </div> + </td> </tr> `; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap index 87ab6b15a84..e5fd4a477e0 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap @@ -27,7 +27,8 @@ exports[`test should not render actions for non admin 1`] = ` "key": "foo", "name": "Foo", } - } /> + } + removeMember={[Function]} /> <ListFooter count={2} loadMore={[Function]} @@ -76,7 +77,8 @@ exports[`test should render actions for admin 1`] = ` "key": "foo", "name": "Foo", } - } /> + } + removeMember={[Function]} /> <ListFooter count={2} loadMore={[Function]} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js new file mode 100644 index 00000000000..6d4c9e59adf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import type { Member } from '../../../../store/organizationsMembers/actions'; +import type { Organization } from '../../../../store/organizations/duck'; + +type Props = { + member: Member, + organization: Organization, + removeMember: (member: Member) => void +}; + +type State = { + open: boolean +}; + +export default class RemoveMemberForm extends React.PureComponent { + props: Props; + + state: State = { + open: false + }; + + openForm = () => { + this.setState({ open: true }); + }; + + closeForm = () => { + this.setState({ open: false }); + }; + + handleSubmit = (e: Object) => { + e.preventDefault(); + this.props.removeMember(this.props.member); + this.closeForm(); + }; + + renderModal() { + return ( + <Modal + isOpen={true} + contentLabel="modal form" + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.closeForm} + > + <header className="modal-head"> + <h2>{translate('users.remove')}</h2> + </header> + <form onSubmit={this.handleSubmit}> + <div className="modal-body"> + {translateWithParameters( + 'organization.members.remove_x', + this.props.member.name, + this.props.organization.name + )} + <ul className="list-styled"> + <li>{translate('organization.members.browse_projects')}</li> + <li>{translate('projects_role.codeviewer')}</li> + <li>{translate('projects_role.issueadmin')}</li> + </ul> + </div> + <footer className="modal-foot"> + <div> + <button type="submit" className="button-red" autoFocus={true}> + {translate('remove')} + </button> + <button type="reset" className="button-link" onClick={this.closeForm}> + {translate('cancel')} + </button> + </div> + </footer> + </form> + </Modal> + ); + } + + render() { + return ( + <a onClick={this.openForm} href="#"> + {translate('organization.members.remove')} + {this.state.open && this.renderModal()} + </a> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js new file mode 100644 index 00000000000..03df0ec7df4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { click } from '../../../../../helpers/testUtils'; +import RemoveMemberForm from '../RemoveMemberForm'; + +const member = {}; +const removeMember = jest.fn(); +const organization = { name: 'MyOrg' }; + +it('should render and open the modal', () => { + const wrapper = shallow( + <RemoveMemberForm member={member} removeMember={removeMember} organization={organization} /> + ); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ open: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should correctly handle user interactions', () => { + const wrapper = mount( + <RemoveMemberForm member={member} removeMember={removeMember} organization={organization} /> + ); + click(wrapper.find('a')); + expect(wrapper.state('open')).toBeTruthy(); +}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap new file mode 100644 index 00000000000..da6b474108b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.js.snap @@ -0,0 +1,69 @@ +exports[`test should render and open the modal 1`] = ` +<a + href="#" + onClick={[Function]}> + organization.members.remove +</a> +`; + +exports[`test should render and open the modal 2`] = ` +<a + href="#" + onClick={[Function]}> + organization.members.remove + <Modal + ariaHideApp={true} + className="modal" + closeTimeoutMS={0} + contentLabel="modal form" + isOpen={true} + onRequestClose={[Function]} + overlayClassName="modal-overlay" + parentSelector={[Function]} + portalClassName="ReactModalPortal" + shouldCloseOnOverlayClick={true}> + <header + className="modal-head"> + <h2> + users.remove + </h2> + </header> + <form + onSubmit={[Function]}> + <div + className="modal-body"> + organization.members.remove_x..MyOrg + <ul + className="list-styled"> + <li> + organization.members.browse_projects + </li> + <li> + projects_role.codeviewer + </li> + <li> + projects_role.issueadmin + </li> + </ul> + </div> + <footer + className="modal-foot"> + <div> + <button + autoFocus={true} + className="button-red" + type="submit"> + remove + </button> + <button + className="button-link" + onClick={[Function]} + type="reset"> + cancel + </button> + </div> + </footer> + </form> + </Modal> +</a> +`; |