Browse Source

SONAR-8992 Add a member to an organization

tags/6.4-RC1
Grégoire Aubert 7 years ago
parent
commit
4b2cf358d3
26 changed files with 966 additions and 36 deletions
  1. 1
    1
      server/sonar-web/src/main/js/api/organizations.js
  2. 4
    1
      server/sonar-web/src/main/js/api/users.js
  3. 10
    1
      server/sonar-web/src/main/js/apps/organizations/actions.js
  4. 6
    2
      server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
  5. 18
    5
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
  6. 11
    4
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js
  7. 10
    1
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap
  8. 110
    0
      server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js
  9. 39
    0
      server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.js
  10. 66
    0
      server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/AddMemberForm-test.js.snap
  11. 1
    1
      server/sonar-web/src/main/js/apps/users/components/UsersSearch.js
  12. 13
    0
      server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.css
  13. 102
    0
      server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js
  14. 74
    0
      server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js
  15. 48
    0
      server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js
  16. 54
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/UsersSelectSearch-test.js
  17. 48
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/UsersSelectSearchOption-test.js
  18. 53
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/UsersSelectSearchValue-test.js
  19. 131
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearch-test.js.snap
  20. 45
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap
  21. 47
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap
  22. 11
    3
      server/sonar-web/src/main/js/store/organizationsMembers/actions.js
  23. 11
    1
      server/sonar-web/src/main/js/store/organizationsMembers/reducer.js
  24. 5
    0
      server/sonar-web/src/main/js/store/users/reducer.js
  25. 44
    15
      server/sonar-web/src/main/less/components/modals.less
  26. 4
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/src/main/js/api/organizations.js View File

@@ -56,7 +56,7 @@ export const updateOrganization = (key: string, changes: {}) =>
export const deleteOrganization = (key: string) => post('/api/organizations/delete', { key });

export const searchMembers = (
data: { organizations?: string, p?: number, ps?: number, q?: string, selected?: string }
data: { organization?: string, p?: number, ps?: number, q?: string, selected?: string }
) => getJSON('/api/organizations/search_members', data);

export const addMember = (data: { login: string, organization: string }) =>

+ 4
- 1
server/sonar-web/src/main/js/api/users.js View File

@@ -40,8 +40,11 @@ export function getIdentityProviders() {
return getJSON(url);
}

export function searchUsers(query) {
export function searchUsers(query, pageSize) {
const url = '/api/users/search';
const data = { q: query };
if (pageSize != null) {
data.ps = pageSize;
}
return getJSON(url, data);
}

+ 10
- 1
server/sonar-web/src/main/js/apps/organizations/actions.js View File

@@ -26,6 +26,7 @@ import { getOrganizationMembersState } from '../../store/rootReducer';
import { addGlobalSuccessMessage } from '../../store/globalMessages/duck';
import { translate, translateWithParameters } from '../../helpers/l10n';
import type { Organization } from '../../store/organizations/duck';
import type { Member } from '../../store/organizationsMembers/actions';

const PAGE_SIZE = 50;

@@ -97,7 +98,7 @@ const fetchMembers = (
) => {
dispatch(membersActions.updateState(key, { loading: true }));
const data: Object = {
organizations: key,
organization: key,
ps: PAGE_SIZE
};
if (page != null) {
@@ -131,3 +132,11 @@ export const fetchMoreOrganizationMembers = (key: string, query?: string) =>
query,
getOrganizationMembersState(getState(), key).pageIndex + 1
);

export const addOrganizationMember = (key: string, member: Member) => (dispatch: Function) => {
dispatch(membersActions.addMember(key, member));
return api.addMember({ login: member.login, organization: key }).catch((error: Object) => {
onFail(dispatch)(error);
dispatch(membersActions.removeMember(key, member));
});
};

+ 6
- 2
server/sonar-web/src/main/js/apps/organizations/components/MembersList.js View File

@@ -25,7 +25,7 @@ import type { Organization } from '../../../store/organizations/duck';

type Props = {
members: Array<Member>,
organization?: Organization
organization?: Organization,
};

export default class MembersList extends React.PureComponent {
@@ -36,7 +36,11 @@ export default class MembersList extends React.PureComponent {
<table className="data zebra">
<tbody>
{this.props.members.map(member => (
<MembersListItem key={member.login} member={member} organization={this.props.organization} />
<MembersListItem
key={member.login}
member={member}
organization={this.props.organization}
/>
))}
</tbody>
</table>

+ 18
- 5
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js View File

@@ -21,6 +21,7 @@
import React from 'react';
import PageHeader from './PageHeader';
import MembersList from './MembersList';
import AddMemberForm from './forms/AddMemberForm';
import UsersSearch from '../../users/components/UsersSearch';
import ListFooter from '../../../components/controls/ListFooter';
import type { Organization } from '../../../store/organizations/duck';
@@ -28,10 +29,12 @@ import type { Member } from '../../../store/organizationsMembers/actions';

type Props = {
members: Array<Member>,
memberLogins: Array<string>,
status: { loading?: boolean, total?: number, pageIndex?: number, query?: string },
organization: Organization,
fetchOrganizationMembers: (organizationKey: string, query?: string) => void,
fetchMoreOrganizationMembers: (organizationKey: string, query?: string) => void,
addOrganizationMember: (organizationKey: string, login: Member) => void
};

export default class OrganizationMembers extends React.PureComponent {
@@ -52,17 +55,27 @@ export default class OrganizationMembers extends React.PureComponent {
this.props.fetchMoreOrganizationMembers(this.props.organization.key, this.props.status.query);
};

addMember() {
// TODO Not yet implemented
}
addMember = (member: Member) => {
this.props.addOrganizationMember(this.props.organization.key, member);
};

render() {
const { organization, status, members } = this.props;
return (
<div className="page page-limited">
<PageHeader loading={status.loading} total={status.total} />
<PageHeader loading={status.loading} total={status.total}>
{organization.canAdmin &&
<div className="page-actions">
<div className="button-group">
<AddMemberForm memberLogins={this.props.memberLogins} addMember={this.addMember} />
</div>
</div>}
</PageHeader>
<UsersSearch onSearch={this.handleSearchMembers} />
<MembersList members={members} organization={organization} />
<MembersList
members={members}
organization={organization}
/>
{status.total != null &&
<ListFooter
count={members.length}

+ 11
- 4
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js View File

@@ -25,18 +25,25 @@ import {
getUsersByLogins,
getOrganizationMembersState
} from '../../../store/rootReducer';
import { fetchOrganizationMembers, fetchMoreOrganizationMembers } from '../actions';
import {
fetchOrganizationMembers,
fetchMoreOrganizationMembers,
addOrganizationMember
} from '../actions';

const mapStateToProps = (state, ownProps) => {
const { organizationKey } = ownProps.params;
const memberLogins = getOrganizationMembersLogins(state, organizationKey);
return {
memberLogins,
members: getUsersByLogins(state, memberLogins),
organization: getOrganizationByKey(state, organizationKey),
status: getOrganizationMembersState(state, organizationKey)
};
};

export default connect(mapStateToProps, { fetchOrganizationMembers, fetchMoreOrganizationMembers })(
OrganizationMembers
);
export default connect(mapStateToProps, {
fetchOrganizationMembers,
fetchMoreOrganizationMembers,
addOrganizationMember
})(OrganizationMembers);

+ 10
- 1
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap View File

@@ -41,7 +41,16 @@ exports[`test should render actions for admin 1`] = `
className="page page-limited">
<PageHeader
loading={true}
total={2} />
total={2}>
<div
className="page-actions">
<div
className="button-group">
<AddMemberForm
addMember={[Function]} />
</div>
</div>
</PageHeader>
<UsersSearch
onSearch={[Function]} />
<MembersList

+ 110
- 0
server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js View File

@@ -0,0 +1,110 @@
/*
* 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 UsersSelectSearch from '../../../users/components/UsersSelectSearch';
import { searchUsers } from '../../../../api/users';
import { translate } from '../../../../helpers/l10n';
import type { Member } from '../../../../store/organizationsMembers/actions';

type Props = {
memberLogins: Array<string>,
addMember: (member: Member) => void
};

type State = {
open: boolean,
selectedMember?: Member
};

export default class AddMemberForm extends React.PureComponent {
props: Props;

state: State = {
open: false
};

openForm = () => {
this.setState({ open: true });
};

closeForm = () => {
this.setState({ open: false, selectedMember: undefined });
};

handleSubmit = (e: Object) => {
e.preventDefault();
if (this.state.selectedMember) {
this.props.addMember(this.state.selectedMember);
this.closeForm();
}
};

selectedMemberChange = (member: Member) => {
this.setState({ selectedMember: member });
};

renderModal() {
return (
<Modal
isOpen={true}
contentLabel="modal form"
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.closeForm}
>
<header className="modal-head">
<h2>{translate('users.add')}</h2>
</header>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="modal-large-field">
<label>{translate('users.search_description')}</label>
<UsersSelectSearch
selectedUser={this.state.selectedMember}
excludedUsers={this.props.memberLogins}
searchUsers={searchUsers}
handleValueChange={this.selectedMemberChange}
/>
</div>
</div>
<footer className="modal-foot">
<div>
<button type="submit">{translate('organization.members.add_to_members')}</button>
<button type="reset" className="button-link" onClick={this.closeForm}>
{translate('cancel')}
</button>
</div>
</footer>
</form>
</Modal>
);
}

render() {
return (
<button onClick={this.openForm}>
{translate('organization.members.add')}
{this.state.open && this.renderModal()}
</button>
);
}
}

+ 39
- 0
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.js View File

@@ -0,0 +1,39 @@
/*
* 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 AddMemberForm from '../AddMemberForm';

const memberLogins = ['admin'];
const addMember = jest.fn();

it('should render and open the modal', () => {
const wrapper = shallow(<AddMemberForm memberLogins={memberLogins} addMember={addMember} />);
expect(wrapper).toMatchSnapshot();
wrapper.setState({ open: true });
expect(wrapper).toMatchSnapshot();
});

it('should correctly handle user interactions', () => {
const wrapper = mount(<AddMemberForm memberLogins={memberLogins} addMember={addMember} />);
click(wrapper.find('button'));
expect(wrapper.state('open')).toBeTruthy();
});

+ 66
- 0
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/AddMemberForm-test.js.snap View File

@@ -0,0 +1,66 @@
exports[`test should render and open the modal 1`] = `
<button
onClick={[Function]}>
organization.members.add
</button>
`;

exports[`test should render and open the modal 2`] = `
<button
onClick={[Function]}>
organization.members.add
<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.add
</h2>
</header>
<form
onSubmit={[Function]}>
<div
className="modal-body">
<div
className="modal-large-field">
<label>
users.search_description
</label>
<UsersSelectSearch
excludedUsers={
Array [
"admin",
]
}
handleValueChange={[Function]}
searchUsers={[Function]} />
</div>
</div>
<footer
className="modal-foot">
<div>
<button
type="submit">
organization.members.add_to_members
</button>
<button
className="button-link"
onClick={[Function]}
type="reset">
cancel
</button>
</div>
</footer>
</form>
</Modal>
</button>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/users/components/UsersSearch.js View File

@@ -56,7 +56,7 @@ export default class UsersSearch extends React.PureComponent {
render() {
const { query } = this.state;
const inputClassName = classNames('search-box-input', {
touched: query != null && query !== '' && query.length < 2
touched: query != null && query.length === 1
});
return (
<div className="panel panel-vertical bordered-bottom spacer-bottom">

+ 13
- 0
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.css View File

@@ -0,0 +1,13 @@
.Select-big .Select-control {
padding-top: 4px;
padding-bottom: 4px;
}

.Select-big .Select-placeholder {
margin-top: 4px;
margin-bottom: 4px;
}

.Select-big .Select-value-label {
margin-top: 5px;
}

+ 102
- 0
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js View File

@@ -0,0 +1,102 @@
/*
* 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 Select from 'react-select';
import { debounce } from 'lodash';
import UsersSelectSearchOption from './UsersSelectSearchOption';
import UsersSelectSearchValue from './UsersSelectSearchValue';
import './UsersSelectSearch.css';

export type Option = {
login: string,
name: string,
email?: string,
avatar?: string,
groupCount?: number
};

type Props = {
selectedUser?: Option,
excludedUsers: Array<string>,
searchUsers: (string, number) => Promise<*>,
handleValueChange: (Option) => void
};

type State = {
searchResult: Array<Option>,
isLoading: boolean,
search: string
};

const LIST_SIZE = 10;

export default class UsersSelectSearch extends React.PureComponent {
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.handleSearch = debounce(this.handleSearch);
this.state = { searchResult: [], isLoading: false, search: '' };
}

componentDidMount() {
this.handleSearch(this.state.search);
}

componentWillReceiveProps(nextProps: Props) {
if (this.props.excludedUsers !== nextProps.excludedUsers) {
this.handleSearch(this.state.search);
}
}

filterSearchResult = ({ users }: { users: Array<Option> }) =>
users.filter(user => !this.props.excludedUsers.includes(user.login)).slice(0, LIST_SIZE);

handleSearch = (search: string) => {
this.setState({ isLoading: true, search });
this.props.searchUsers(search, Math.min(this.props.excludedUsers.length + LIST_SIZE, 500))
.then(this.filterSearchResult)
.then(searchResult => {
this.setState({ isLoading: false, searchResult });
});
};

render() {
return (
<Select
className="Select-big"
options={this.state.searchResult}
isLoading={this.state.isLoading}
optionComponent={UsersSelectSearchOption}
valueComponent={UsersSelectSearchValue}
onChange={this.props.handleValueChange}
onInputChange={this.handleSearch}
value={this.props.selectedUser}
placeholder=""
labelKey="name"
valueKey="login"
clearable={false}
searchable={true}
/>
);
}
}

+ 74
- 0
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js View File

@@ -0,0 +1,74 @@
/*
* 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 Avatar from '../../../components/ui/Avatar';
import type { Option } from './UsersSelectSearch';

type Props = {
option: Option,
children?: Element | Text,
className?: string,
isFocused?: boolean,
onFocus: (Option, MouseEvent) => void,
onSelect: (Option, MouseEvent) => void
};

const AVATAR_SIZE: number = 20;

export default class UsersSelectSearchOption extends React.PureComponent {
props: Props;

handleMouseDown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
};

handleMouseEnter = (event: MouseEvent) => {
this.props.onFocus(this.props.option, event);
};

handleMouseMove = (event: MouseEvent) => {
if (this.props.isFocused) {
return;
}
this.props.onFocus(this.props.option, event);
};

render() {
const user = this.props.option;
return (
<div
className={this.props.className}
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
title={user.name}
>
<div className="little-spacer-bottom little-spacer-top">
<Avatar hash={user.avatar} email={user.email} size={AVATAR_SIZE} />
<strong className="spacer-left">{user.login}</strong>
<span className="note little-spacer-left">{this.props.children}</span>
</div>
</div>
);
}
}

+ 48
- 0
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js View File

@@ -0,0 +1,48 @@
/*
* 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 Avatar from '../../../components/ui/Avatar';
import type { Option } from './UsersSelectSearch';

type Props = {
value: Option,
children?: Element | Text
};

const AVATAR_SIZE: number = 20;

export default class UsersSelectSearchValue extends React.PureComponent {
props: Props;

render() {
const user = this.props.value;
return (
<div className="Select-value" title={user ? user.name : ''}>
{user && user.login &&
<div className="Select-value-label">
<Avatar hash={user.avatar} email={user.email} size={AVATAR_SIZE} />
<strong className="spacer-left">{user.login}</strong>
<span className="note little-spacer-left">{this.props.children}</span>
</div>}
</div>
);
}
}

+ 54
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/UsersSelectSearch-test.js View File

@@ -0,0 +1,54 @@
/*
* 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 } from 'enzyme';
import UsersSelectSearch from '../UsersSelectSearch';

const selectedUser = {
login: 'admin',
name: 'Administrator',
avatar: '7daf6c79d4802916d83f6266e24850af'
};
const users = [
{ login: 'admin', name: 'Administrator', email: 'admin@admin.ch' },
{ login: 'test', name: 'Tester', email: 'tester@testing.ch' },
{ login: 'foo', name: 'Foo Bar', email: 'foo@bar.ch' }
];
const excludedUsers = ['admin'];
const onSearch = jest.fn(() => {
return Promise.resolve(users);
});
const onChange = jest.fn();

it('should render correctly', () => {
const wrapper = shallow(
<UsersSelectSearch
selectedUser={selectedUser}
excludedUsers={excludedUsers}
isLoading={false}
handleValueChange={onChange}
searchUsers={onSearch}
/>
);
expect(wrapper).toMatchSnapshot();
const searchResult = wrapper.instance().filterSearchResult({ users });
expect(searchResult).toMatchSnapshot();
expect(wrapper.setState({ searchResult })).toMatchSnapshot();
});

+ 48
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/UsersSelectSearchOption-test.js View File

@@ -0,0 +1,48 @@
/*
* 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 } from 'enzyme';
import UsersSelectSearchOption from '../UsersSelectSearchOption';

const user = {
login: 'admin',
name: 'Administrator',
avatar: '7daf6c79d4802916d83f6266e24850af'
};

const user2 = {
login: 'admin',
name: 'Administrator',
email: 'admin@admin.ch'
};

it('should render correctly without all parameters', () => {
const wrapper = shallow(
<UsersSelectSearchOption option={user}>{user.name}</UsersSelectSearchOption>
);
expect(wrapper).toMatchSnapshot();
});

it('should render correctly with email instead of hash', () => {
const wrapper = shallow(
<UsersSelectSearchOption option={user2}>{user.name}</UsersSelectSearchOption>
);
expect(wrapper).toMatchSnapshot();
});

+ 53
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/UsersSelectSearchValue-test.js View File

@@ -0,0 +1,53 @@
/*
* 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 } from 'enzyme';
import UsersSelectSearchValue from '../UsersSelectSearchValue';

const user = {
login: 'admin',
name: 'Administrator',
avatar: '7daf6c79d4802916d83f6266e24850af'
};

const user2 = {
login: 'admin',
name: 'Administrator',
email: 'admin@admin.ch'
};

it('should render correctly with a user', () => {
const wrapper = shallow(
<UsersSelectSearchValue value={user}>{user.name}</UsersSelectSearchValue>
);
expect(wrapper).toMatchSnapshot();
});

it('should render correctly with email instead of hash', () => {
const wrapper = shallow(
<UsersSelectSearchValue value={user2}>{user2.name}</UsersSelectSearchValue>
);
expect(wrapper).toMatchSnapshot();
});

it('should render correctly without value', () => {
const wrapper = shallow(<UsersSelectSearchValue />);
expect(wrapper).toMatchSnapshot();
});

+ 131
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearch-test.js.snap View File

@@ -0,0 +1,131 @@
exports[`test should render correctly 1`] = `
<Select
addLabelText="Add \"{label}\"?"
arrowRenderer={[Function]}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="Select-big"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="name"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
onInputChange={[Function]}
openAfterFocus={false}
optionComponent={[Function]}
options={Array []}
pageSize={5}
placeholder=""
required={false}
scrollMenuIntoView={true}
searchable={true}
simpleValue={false}
tabSelectsValue={true}
value={
Object {
"avatar": "7daf6c79d4802916d83f6266e24850af",
"login": "admin",
"name": "Administrator",
}
}
valueComponent={[Function]}
valueKey="login" />
`;

exports[`test should render correctly 2`] = `
Array [
Object {
"email": "tester@testing.ch",
"login": "test",
"name": "Tester",
},
Object {
"email": "foo@bar.ch",
"login": "foo",
"name": "Foo Bar",
},
]
`;

exports[`test should render correctly 3`] = `
<Select
addLabelText="Add \"{label}\"?"
arrowRenderer={[Function]}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="Select-big"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="name"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
onInputChange={[Function]}
openAfterFocus={false}
optionComponent={[Function]}
options={
Array [
Object {
"email": "tester@testing.ch",
"login": "test",
"name": "Tester",
},
Object {
"email": "foo@bar.ch",
"login": "foo",
"name": "Foo Bar",
},
]
}
pageSize={5}
placeholder=""
required={false}
scrollMenuIntoView={true}
searchable={true}
simpleValue={false}
tabSelectsValue={true}
value={
Object {
"avatar": "7daf6c79d4802916d83f6266e24850af",
"login": "admin",
"name": "Administrator",
}
}
valueComponent={[Function]}
valueKey="login" />
`;

+ 45
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap View File

@@ -0,0 +1,45 @@
exports[`test should render correctly with email instead of hash 1`] = `
<div
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseMove={[Function]}
title="Administrator">
<div
className="little-spacer-bottom little-spacer-top">
<Connect(Avatar)
email="admin@admin.ch"
size={20} />
<strong
className="spacer-left">
admin
</strong>
<span
className="note little-spacer-left">
Administrator
</span>
</div>
</div>
`;

exports[`test should render correctly without all parameters 1`] = `
<div
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseMove={[Function]}
title="Administrator">
<div
className="little-spacer-bottom little-spacer-top">
<Connect(Avatar)
hash="7daf6c79d4802916d83f6266e24850af"
size={20} />
<strong
className="spacer-left">
admin
</strong>
<span
className="note little-spacer-left">
Administrator
</span>
</div>
</div>
`;

+ 47
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap View File

@@ -0,0 +1,47 @@
exports[`test should render correctly with a user 1`] = `
<div
className="Select-value"
title="Administrator">
<div
className="Select-value-label">
<Connect(Avatar)
hash="7daf6c79d4802916d83f6266e24850af"
size={20} />
<strong
className="spacer-left">
admin
</strong>
<span
className="note little-spacer-left">
Administrator
</span>
</div>
</div>
`;

exports[`test should render correctly with email instead of hash 1`] = `
<div
className="Select-value"
title="Administrator">
<div
className="Select-value-label">
<Connect(Avatar)
email="admin@admin.ch"
size={20} />
<strong
className="spacer-left">
admin
</strong>
<span
className="note little-spacer-left">
Administrator
</span>
</div>
</div>
`;

exports[`test should render correctly without value 1`] = `
<div
className="Select-value"
title="" />
`;

+ 11
- 3
server/sonar-web/src/main/js/store/organizationsMembers/actions.js View File

@@ -21,8 +21,9 @@
export type Member = {
login: string,
name: string,
avatar: string,
groupCount: number
avatar?: string,
email?: string,
groupCount?: number
};

type MembersState = {
@@ -35,7 +36,8 @@ type MembersState = {
export const actions = {
UPDATE_STATE: 'organizations/UPDATE_STATE',
RECEIVE_MEMBERS: 'organizations/RECEIVE_MEMBERS',
RECEIVE_MORE_MEMBERS: 'organizations/RECEIVE_MORE_MEMBERS'
RECEIVE_MORE_MEMBERS: 'organizations/RECEIVE_MORE_MEMBERS',
ADD_MEMBER: 'organizations/ADD_MEMBER',
};

export const receiveMembers = (organizationKey: string, members: Array<Member>, stateChanges: MembersState) => ({
@@ -52,6 +54,12 @@ export const receiveMoreMembers = (organizationKey: string, members: Array<Membe
stateChanges
});

export const addMember = (organizationKey: string, member: Member) => ({
type: actions.ADD_MEMBER,
organization: organizationKey,
member
});

export const updateState = (
organizationKey: string,
stateChanges: MembersState

+ 11
- 1
server/sonar-web/src/main/js/store/organizationsMembers/reducer.js View File

@@ -31,6 +31,7 @@ export const getOrganizationMembersState = (state, organization) =>
organization && state[organization] ? state[organization] : {};

const organizationMembers = (state = {}, action = {}) => {
const members = state.members || [];
switch (action.type) {
case actions.UPDATE_STATE:
return { ...state, ...action.stateChanges };
@@ -40,8 +41,16 @@ const organizationMembers = (state = {}, action = {}) => {
return {
...state,
...action.stateChanges,
members: uniq((state.members || []).concat(action.members.map(member => member.login)))
members: uniq(members.concat(action.members.map(member => member.login)))
};
case actions.ADD_MEMBER: {
const withNew = [...members, action.member.login].sort();
return {
...state,
total: withNew.length,
members: withNew
};
}
default:
return state;
}
@@ -53,6 +62,7 @@ const organizationsMembers = (state = {}, action = {}) => {
case actions.UPDATE_STATE:
case actions.RECEIVE_MEMBERS:
case actions.RECEIVE_MORE_MEMBERS:
case actions.ADD_MEMBER:
return {
...state,
[action.organization]: organizationMembers(organization, action)

+ 5
- 0
server/sonar-web/src/main/js/store/users/reducer.js View File

@@ -29,6 +29,8 @@ const usersByLogin = (state = {}, action = {}) => {
case membersActions.RECEIVE_MEMBERS:
case membersActions.RECEIVE_MORE_MEMBERS:
return { ...state, ...keyBy(action.members, 'login') };
case membersActions.ADD_MEMBER:
return { ...state, [action.member.login]: action.member };
default:
return state;
}
@@ -41,6 +43,9 @@ const userLogins = (state = [], action = {}) => {
case membersActions.RECEIVE_MEMBERS:
case membersActions.RECEIVE_MORE_MEMBERS:
return uniq([...state, action.members.map(member => member.login)]);
case membersActions.ADD_MEMBER: {
return uniq([...state, action.member.login]).sort();
}
default:
return state;
}

+ 44
- 15
server/sonar-web/src/main/less/components/modals.less View File

@@ -116,6 +116,12 @@ ul.modal-head-metadata li {
padding: 5px 0 5px 130px;
}

.modal-large-field {
clear: both;
display: block;
padding: 20px 40px;
}

.modal-field label {
position: relative;
left: -140px;
@@ -142,31 +148,54 @@ ul.modal-head-metadata li {
}
}

.modal-large-field label {
display: inline-block;
padding-bottom: 15px;
font-weight: bold;
}

.readonly-field {
padding-top: 5px;
margin-left: -5px;
line-height: 1;
}

.modal-field input,
.modal-field select,
.modal-field textarea {
margin-right: 5px;
margin-bottom: 10px;
.modal-field, .modal-large-field {
input,
select,
textarea,
.Select {
margin-right: 5px;
margin-bottom: 10px;
}

input[type="radio"],
input[type="checkbox"] {
margin-top: 5px;
margin-bottom: 4px;
}
}

.modal-field input[type="radio"],
.modal-field input[type="checkbox"] {
margin-top: 5px;
margin-bottom: 4px;
.modal-field {
input[type=text],
input[type=email],
input[type=password],
textarea,
select,
.Select {
width: 250px;
}
}

.modal-field input[type=text],
.modal-field input[type=email],
.modal-field input[type=password],
.modal-field textarea,
.modal-field select {
width: 250px;
.modal-large-field {
input[type=text],
input[type=email],
input[type=password],
textarea,
select,
.Select {
width: 100%;
}
}

.modal-field-description {

+ 4
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1771,6 +1771,9 @@ user.password_cant_be_changed_on_external_auth=Password cannot be changed when e
#
#------------------------------------------------------------------------------
users.create=Create User
users.add=Add user
users.remove=Remove user
users.search_description=Search by username, full name, or email address

#------------------------------------------------------------------------------
#
@@ -2817,6 +2820,6 @@ organization.updated=Organization details have been updated.
organization.url=Url
organization.url.description=Url of the homepage of the organization.
organization.members.page=Members
organization.members.add=Add member
organization.members.add=Add a member
organization.members.x_group(s)={0} group(s)
organization.members.member(s)=member(s)

Loading…
Cancel
Save