@@ -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 UsersSearch from '../../users/components/UsersSearch'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { | |||
handleSearch: (query?: string) => void, | |||
total?: number | |||
}; | |||
export default class MembersListHeader extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
const { total } = this.props; | |||
return ( | |||
<div className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<UsersSearch onSearch={this.props.handleSearch} className="display-inline-block" /> | |||
{total != null && | |||
<span className="pull-right little-spacer-top"> | |||
<strong>{formatMeasure(total, 'INT')}</strong> | |||
{' '} | |||
{translate('organization.members.members')} | |||
</span>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -20,7 +20,7 @@ | |||
//@flow | |||
import React from 'react'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import RemoveMemberForm from './forms/RemoveMemberForm'; | |||
import ManageMemberGroupsForm from './forms/ManageMemberGroupsForm'; | |||
@@ -47,10 +47,13 @@ export default class MembersListItem extends React.PureComponent { | |||
<td className="thin nowrap"> | |||
<Avatar hash={member.avatar} email={member.email} size={AVATAR_SIZE} /> | |||
</td> | |||
<td className="nowrap text-middle"><strong>{member.name}</strong></td> | |||
<td className="nowrap text-middle"> | |||
<strong>{member.login}</strong> | |||
<span className="note little-spacer-left">{member.name}</span> | |||
</td> | |||
{organization.canAdmin && | |||
<td className="text-right text-middle"> | |||
{translate('organization.members.x_group(s)', formatMeasure(member.groupCount, 'INT'))} | |||
{translateWithParameters('organization.members.x_groups', formatMeasure(member.groupCount || 0, 'INT'))} | |||
</td>} | |||
{organization.canAdmin && | |||
<td className="nowrap text-middle text-right"> | |||
@@ -62,18 +65,18 @@ export default class MembersListItem extends React.PureComponent { | |||
</button> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
<li> | |||
<RemoveMemberForm | |||
<ManageMemberGroupsForm | |||
organizationGroups={this.props.organizationGroups} | |||
organization={this.props.organization} | |||
removeMember={this.props.removeMember} | |||
updateMemberGroups={this.props.updateMemberGroups} | |||
member={this.props.member} | |||
/> | |||
</li> | |||
<li role="separator" className="divider" /> | |||
<li> | |||
<ManageMemberGroupsForm | |||
organizationGroups={this.props.organizationGroups} | |||
<RemoveMemberForm | |||
organization={this.props.organization} | |||
updateMemberGroups={this.props.updateMemberGroups} | |||
removeMember={this.props.removeMember} | |||
member={this.props.member} | |||
/> | |||
</li> |
@@ -19,8 +19,6 @@ | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { | |||
loading: boolean, | |||
@@ -28,7 +26,7 @@ type Props = { | |||
children?: {} | |||
}; | |||
export default class PageHeader extends React.PureComponent { | |||
export default class MembersPageHeader extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
@@ -36,12 +34,6 @@ export default class PageHeader extends React.PureComponent { | |||
<header className="page-header"> | |||
{this.props.loading && <i className="spinner" />} | |||
{this.props.children} | |||
{this.props.total != null && | |||
<span className="page-totalcount"> | |||
<strong>{formatMeasure(this.props.total, 'INT')}</strong> | |||
{' '} | |||
{translate('organization.members.member(s)')} | |||
</span>} | |||
</header> | |||
); | |||
} |
@@ -0,0 +1,55 @@ | |||
/* | |||
* 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 Checkbox from '../../../components/controls/Checkbox'; | |||
import type { OrgGroup } from '../../../store/organizations/duck'; | |||
type Props = { | |||
group: OrgGroup, | |||
checked: boolean, | |||
onCheck: (string, boolean) => void | |||
}; | |||
export default class OrganizationGroupCheckbox extends React.PureComponent { | |||
props: Props; | |||
onCheck = (checked: boolean) => { | |||
this.props.onCheck(this.props.group.id, checked); | |||
}; | |||
toggleCheck = () => { | |||
this.props.onCheck(this.props.group.id, !this.props.checked); | |||
}; | |||
render() { | |||
return ( | |||
<li | |||
className="capitalize list-item-checkable-link" | |||
onClick={this.toggleCheck} | |||
tabIndex={0} | |||
role="listitem" | |||
> | |||
<Checkbox checked={this.props.checked} onCheck={this.onCheck} /> | |||
{' '}{this.props.group.name} | |||
</li> | |||
); | |||
} | |||
} |
@@ -19,10 +19,10 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import PageHeader from './PageHeader'; | |||
import MembersPageHeader from './MembersPageHeader'; | |||
import MembersListHeader from './MembersListHeader'; | |||
import MembersList from './MembersList'; | |||
import AddMemberForm from './forms/AddMemberForm'; | |||
import UsersSearch from '../../users/components/UsersSearch'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import type { Organization, OrgGroup } from '../../../store/organizations/duck'; | |||
import type { Member } from '../../../store/organizationsMembers/actions'; | |||
@@ -38,18 +38,19 @@ type Props = { | |||
fetchOrganizationGroups: (organizationKey: string) => void, | |||
addOrganizationMember: (organizationKey: string, member: Member) => void, | |||
removeOrganizationMember: (organizationKey: string, member: Member) => void, | |||
updateOrganizationMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void, | |||
updateOrganizationMemberGroups: ( | |||
member: Member, | |||
add: Array<string>, | |||
remove: Array<string> | |||
) => void | |||
}; | |||
export default class OrganizationMembers extends React.PureComponent { | |||
props: Props; | |||
componentDidMount() { | |||
const notLoadedYet = this.props.members.length < 1 || this.props.status.query != null; | |||
if (!this.props.loading && notLoadedYet) { | |||
this.handleSearchMembers(); | |||
} | |||
if (this.props.organizationGroups.length <= 0) { | |||
this.handleSearchMembers(); | |||
if (this.props.organization.canAdmin) { | |||
this.props.fetchOrganizationGroups(this.props.organization.key); | |||
} | |||
} | |||
@@ -74,15 +75,15 @@ export default class OrganizationMembers extends React.PureComponent { | |||
const { organization, status, members } = this.props; | |||
return ( | |||
<div className="page page-limited"> | |||
<PageHeader loading={status.loading} total={status.total}> | |||
<MembersPageHeader 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} /> | |||
</MembersPageHeader> | |||
<MembersListHeader total={status.total} handleSearch={this.handleSearchMembers} /> | |||
<MembersList | |||
members={members} | |||
organizationGroups={this.props.organizationGroups} |
@@ -0,0 +1,36 @@ | |||
/* | |||
* 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 MembersListHeader from '../MembersListHeader'; | |||
it('should render without the total', () => { | |||
const wrapper = shallow( | |||
<MembersListHeader handleSearch={jest.fn()} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render with the total', () => { | |||
const wrapper = shallow( | |||
<MembersListHeader handleSearch={jest.fn()} total={8} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -23,13 +23,13 @@ import MembersListItem from '../MembersListItem'; | |||
const organization = { key: 'foo', name: 'Foo' }; | |||
const admin = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }; | |||
const john = { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }; | |||
const john = { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af' }; | |||
it('should not render actions and groups for non admin', () => { | |||
const wrapper = shallow( | |||
<MembersListItem | |||
organization={organization} | |||
member={john} | |||
member={admin} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -44,3 +44,13 @@ it('should render actions and groups for admin', () => { | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should groups at 0 if the groupCount field is not defined (just added user)', () => { | |||
const wrapper = shallow( | |||
<MembersListItem | |||
organization={{ ...organization, canAdmin: true }} | |||
member={john} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -19,11 +19,11 @@ | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PageHeader from '../PageHeader'; | |||
import MembersPageHeader from '../MembersPageHeader'; | |||
it('should render the members page header', () => { | |||
const wrapper = shallow( | |||
<PageHeader /> | |||
<MembersPageHeader /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setProps({ loading: true }); | |||
@@ -31,15 +31,15 @@ it('should render the members page header', () => { | |||
}); | |||
it('should render the members page header with the total', () => { | |||
const wrapper = shallow(<PageHeader total="5" />); | |||
const wrapper = shallow(<MembersPageHeader total="5" />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render its children', () => { | |||
const wrapper = shallow( | |||
<PageHeader loading={true} total="5"> | |||
<MembersPageHeader loading={true} total="5"> | |||
<span>children test</span> | |||
</PageHeader> | |||
</MembersPageHeader> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 OrganizationGroupCheckbox from '../OrganizationGroupCheckbox'; | |||
const group = { | |||
id: '7', | |||
name: 'professionals', | |||
description: '', | |||
membersCount: 12 | |||
}; | |||
it('should render unchecked', () => { | |||
const wrapper = shallow( | |||
<OrganizationGroupCheckbox group={group} checked={false} onCheck={jest.fn()} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should be able to toggle check', () => { | |||
const onCheck = jest.fn((group, checked) => wrapper.setProps({ checked })); | |||
const wrapper = shallow( | |||
<OrganizationGroupCheckbox group={group} checked={true} onCheck={onCheck} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.instance().toggleCheck(); | |||
expect(onCheck.mock.calls).toMatchSnapshot(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -27,8 +27,6 @@ const members = [ | |||
{ login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 } | |||
]; | |||
const status = { total: members.length }; | |||
const fetchOrganizationMembers = jest.fn(); | |||
const fetchMoreOrganizationMembers = jest.fn(); | |||
it('should not render actions for non admin', () => { | |||
const wrapper = shallow( | |||
@@ -36,8 +34,8 @@ it('should not render actions for non admin', () => { | |||
organization={organization} | |||
members={members} | |||
status={status} | |||
fetchOrganizationMembers={fetchOrganizationMembers} | |||
fetchMoreOrganizationMembers={fetchMoreOrganizationMembers} | |||
fetchOrganizationMembers={jest.fn()} | |||
fetchMoreOrganizationMembers={jest.fn()} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -49,8 +47,8 @@ it('should render actions for admin', () => { | |||
organization={{ ...organization, canAdmin: true }} | |||
members={members} | |||
status={{ ...status, loading: true }} | |||
fetchOrganizationMembers={fetchOrganizationMembers} | |||
fetchMoreOrganizationMembers={fetchMoreOrganizationMembers} | |||
fetchOrganizationMembers={jest.fn()} | |||
fetchMoreOrganizationMembers={jest.fn()} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); |
@@ -0,0 +1,25 @@ | |||
exports[`test should render with the total 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<UsersSearch | |||
className="display-inline-block" | |||
onSearch={[Function]} /> | |||
<span | |||
className="pull-right little-spacer-top"> | |||
<strong> | |||
8 | |||
</strong> | |||
organization.members.members | |||
</span> | |||
</div> | |||
`; | |||
exports[`test should render without the total 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<UsersSearch | |||
className="display-inline-block" | |||
onSearch={[Function]} /> | |||
</div> | |||
`; |
@@ -1,4 +1,4 @@ | |||
exports[`test should not render actions and groups for non admin 1`] = ` | |||
exports[`test should groups at 0 if the groupCount field is not defined (just added user) 1`] = ` | |||
<tr> | |||
<td | |||
className="thin nowrap"> | |||
@@ -9,8 +9,92 @@ exports[`test should not render actions and groups for non admin 1`] = ` | |||
<td | |||
className="nowrap text-middle"> | |||
<strong> | |||
john | |||
</strong> | |||
<span | |||
className="note little-spacer-left"> | |||
John Doe | |||
</span> | |||
</td> | |||
<td | |||
className="text-right text-middle"> | |||
organization.members.x_groups.0 | |||
</td> | |||
<td | |||
className="nowrap text-middle text-right"> | |||
<div | |||
className="dropdown"> | |||
<button | |||
className="dropdown-toggle little-spacer-right" | |||
data-toggle="dropdown"> | |||
<i | |||
className="icon-settings" /> | |||
<i | |||
className="icon-dropdown" /> | |||
</button> | |||
<ul | |||
className="dropdown-menu dropdown-menu-right"> | |||
<li> | |||
<ManageMemberGroupsForm | |||
member={ | |||
Object { | |||
"avatar": "7daf6c79d4802916d83f6266e24850af", | |||
"login": "john", | |||
"name": "John Doe", | |||
} | |||
} | |||
organization={ | |||
Object { | |||
"canAdmin": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
</li> | |||
<li | |||
className="divider" | |||
role="separator" /> | |||
<li> | |||
<RemoveMemberForm | |||
member={ | |||
Object { | |||
"avatar": "7daf6c79d4802916d83f6266e24850af", | |||
"login": "john", | |||
"name": "John Doe", | |||
} | |||
} | |||
organization={ | |||
Object { | |||
"canAdmin": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
</li> | |||
</ul> | |||
</div> | |||
</td> | |||
</tr> | |||
`; | |||
exports[`test should not render actions and groups for non admin 1`] = ` | |||
<tr> | |||
<td | |||
className="thin nowrap"> | |||
<Connect(Avatar) | |||
hash="" | |||
size={36} /> | |||
</td> | |||
<td | |||
className="nowrap text-middle"> | |||
<strong> | |||
admin | |||
</strong> | |||
<span | |||
className="note little-spacer-left"> | |||
Admin Istrator | |||
</span> | |||
</td> | |||
</tr> | |||
`; | |||
@@ -26,12 +110,16 @@ exports[`test should render actions and groups for admin 1`] = ` | |||
<td | |||
className="nowrap text-middle"> | |||
<strong> | |||
Admin Istrator | |||
admin | |||
</strong> | |||
<span | |||
className="note little-spacer-left"> | |||
Admin Istrator | |||
</span> | |||
</td> | |||
<td | |||
className="text-right text-middle"> | |||
organization.members.x_group(s).3 | |||
organization.members.x_groups.3 | |||
</td> | |||
<td | |||
className="nowrap text-middle text-right"> | |||
@@ -49,7 +137,7 @@ exports[`test should render actions and groups for admin 1`] = ` | |||
<ul | |||
className="dropdown-menu dropdown-menu-right"> | |||
<li> | |||
<RemoveMemberForm | |||
<ManageMemberGroupsForm | |||
member={ | |||
Object { | |||
"avatar": "", | |||
@@ -70,7 +158,7 @@ exports[`test should render actions and groups for admin 1`] = ` | |||
className="divider" | |||
role="separator" /> | |||
<li> | |||
<ManageMemberGroupsForm | |||
<RemoveMemberForm | |||
member={ | |||
Object { | |||
"avatar": "", |
@@ -6,14 +6,6 @@ exports[`test should render its children 1`] = ` | |||
<span> | |||
children test | |||
</span> | |||
<span | |||
className="page-totalcount"> | |||
<strong> | |||
5 | |||
</strong> | |||
organization.members.member(s) | |||
</span> | |||
</header> | |||
`; | |||
@@ -29,14 +21,5 @@ exports[`test should render the members page header 2`] = ` | |||
exports[`test should render the members page header with the total 1`] = ` | |||
<header | |||
className="page-header"> | |||
<span | |||
className="page-totalcount"> | |||
<strong> | |||
5 | |||
</strong> | |||
organization.members.member(s) | |||
</span> | |||
</header> | |||
className="page-header" /> | |||
`; |
@@ -0,0 +1,53 @@ | |||
exports[`test should be able to toggle check 1`] = ` | |||
<li | |||
className="capitalize list-item-checkable-link" | |||
onClick={[Function]} | |||
role="listitem" | |||
tabIndex={0}> | |||
<Checkbox | |||
checked={true} | |||
onCheck={[Function]} | |||
thirdState={false} /> | |||
professionals | |||
</li> | |||
`; | |||
exports[`test should be able to toggle check 2`] = ` | |||
Array [ | |||
Array [ | |||
"7", | |||
false, | |||
], | |||
] | |||
`; | |||
exports[`test should be able to toggle check 3`] = ` | |||
<li | |||
className="capitalize list-item-checkable-link" | |||
onClick={[Function]} | |||
role="listitem" | |||
tabIndex={0}> | |||
<Checkbox | |||
checked={false} | |||
onCheck={[Function]} | |||
thirdState={false} /> | |||
professionals | |||
</li> | |||
`; | |||
exports[`test should render unchecked 1`] = ` | |||
<li | |||
className="capitalize list-item-checkable-link" | |||
onClick={[Function]} | |||
role="listitem" | |||
tabIndex={0}> | |||
<Checkbox | |||
checked={false} | |||
onCheck={[Function]} | |||
thirdState={false} /> | |||
professionals | |||
</li> | |||
`; |
@@ -1,10 +1,11 @@ | |||
exports[`test should not render actions for non admin 1`] = ` | |||
<div | |||
className="page page-limited"> | |||
<PageHeader | |||
<MembersPageHeader | |||
total={2} /> | |||
<MembersListHeader | |||
handleSearch={[Function]} | |||
total={2} /> | |||
<UsersSearch | |||
onSearch={[Function]} /> | |||
<MembersList | |||
members={ | |||
Array [ | |||
@@ -40,7 +41,7 @@ exports[`test should not render actions for non admin 1`] = ` | |||
exports[`test should render actions for admin 1`] = ` | |||
<div | |||
className="page page-limited"> | |||
<PageHeader | |||
<MembersPageHeader | |||
loading={true} | |||
total={2}> | |||
<div | |||
@@ -51,9 +52,10 @@ exports[`test should render actions for admin 1`] = ` | |||
addMember={[Function]} /> | |||
</div> | |||
</div> | |||
</PageHeader> | |||
<UsersSearch | |||
onSearch={[Function]} /> | |||
</MembersPageHeader> | |||
<MembersListHeader | |||
handleSearch={[Function]} | |||
total={2} /> | |||
<MembersList | |||
members={ | |||
Array [ |
@@ -21,9 +21,9 @@ | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import { keyBy, pickBy } from 'lodash'; | |||
import Checkbox from '../../../../components/controls/Checkbox'; | |||
import { getUserGroups } from '../../../../api/users'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import OrganizationGroupCheckbox from '../OrganizationGroupCheckbox'; | |||
import type { Member } from '../../../../store/organizationsMembers/actions'; | |||
import type { Organization, OrgGroup } from '../../../../store/organizations/duck'; | |||
@@ -47,15 +47,14 @@ export default class ManageMemberGroupsForm extends React.PureComponent { | |||
open: false | |||
}; | |||
openForm = () => { | |||
if (!this.state.userGroups) { | |||
this.loadUserGroups(); | |||
} | |||
openForm = (evt: MouseEvent) => { | |||
evt.preventDefault(); | |||
this.loadUserGroups(); | |||
this.setState({ open: true }); | |||
}; | |||
closeForm = () => { | |||
this.setState({ userGroups: undefined, open: false }); | |||
this.setState({ open: false }); | |||
}; | |||
loadUserGroups = () => { | |||
@@ -77,7 +76,7 @@ export default class ManageMemberGroupsForm extends React.PureComponent { | |||
return false; | |||
}; | |||
onCheck = (checked: boolean, groupId: string) => { | |||
onCheck = (groupId: string, checked: boolean) => { | |||
this.setState((prevState: State) => { | |||
const userGroups = prevState.userGroups || {}; | |||
const group = userGroups[groupId] || {}; | |||
@@ -93,11 +92,12 @@ export default class ManageMemberGroupsForm extends React.PureComponent { | |||
handleSubmit = (e: Object) => { | |||
e.preventDefault(); | |||
this.props.updateMemberGroups(this.props.member, | |||
this.props.updateMemberGroups( | |||
this.props.member, | |||
Object.keys(pickBy(this.state.userGroups, group => group.status === 'add')), | |||
Object.keys(pickBy(this.state.userGroups, group => group.status === 'remove')) | |||
); | |||
this.setState({ open: false }); | |||
this.closeForm(); | |||
}; | |||
renderModal() { | |||
@@ -115,19 +115,17 @@ export default class ManageMemberGroupsForm extends React.PureComponent { | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body"> | |||
<strong> | |||
{translateWithParameters('organization.members.x_groups', this.props.member.name)} | |||
{translateWithParameters('organization.members.members_groups', this.props.member.name)} | |||
</strong>{' '}{this.state.loading && <i className="spinner" />} | |||
{!this.state.loading && | |||
<ul className="list-spaced"> | |||
{this.props.organizationGroups.map(group => ( | |||
<li className="capitalize" key={group.id}> | |||
<Checkbox | |||
id={group.id} | |||
checked={this.isGroupSelected(group.id)} | |||
onCheck={this.onCheck} | |||
/> | |||
{' '}{group.name} | |||
</li> | |||
<OrganizationGroupCheckbox | |||
key={group.id} | |||
group={group} | |||
checked={this.isGroupSelected(group.id)} | |||
onCheck={this.onCheck} | |||
/> | |||
))} | |||
</ul>} | |||
</div> |
@@ -41,7 +41,8 @@ export default class RemoveMemberForm extends React.PureComponent { | |||
open: false | |||
}; | |||
openForm = () => { | |||
openForm = (evt: MouseEvent) => { | |||
evt.preventDefault(); | |||
this.setState({ open: true }); | |||
}; | |||
@@ -23,17 +23,16 @@ 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} />); | |||
const wrapper = shallow(<AddMemberForm memberLogins={memberLogins} addMember={jest.fn()} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ open: true }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should correctly handle user interactions', () => { | |||
const wrapper = mount(<AddMemberForm memberLogins={memberLogins} addMember={addMember} />); | |||
const wrapper = mount(<AddMemberForm memberLogins={memberLogins} addMember={jest.fn()} />); | |||
click(wrapper.find('button')); | |||
expect(wrapper.state('open')).toBeTruthy(); | |||
wrapper.instance().closeForm(); |
@@ -47,15 +47,14 @@ const organizationGroups = [ | |||
const userGroups = { | |||
11: { id: 11, name: 'pull-request-analysers', description: 'Technical accounts', selected: true } | |||
}; | |||
const updateMemberGroups = jest.fn(); | |||
const getMountedForm = function() { | |||
const getMountedForm = function(updateFunc = jest.fn()) { | |||
const wrapper = mount( | |||
<ManageMemberGroupsForm | |||
member={member} | |||
organization={organization} | |||
organizationGroups={organizationGroups} | |||
updateMemberGroups={updateMemberGroups} | |||
updateMemberGroups={updateFunc} | |||
/> | |||
); | |||
const instance = wrapper.instance(); | |||
@@ -71,7 +70,7 @@ it('should render and open the modal', () => { | |||
member={member} | |||
organization={organization} | |||
organizationGroups={organizationGroups} | |||
updateMemberGroups={updateMemberGroups} | |||
updateMemberGroups={jest.fn()} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -89,34 +88,34 @@ it('should correctly handle user interactions', () => { | |||
it('should correctly select the groups', () => { | |||
const form = getMountedForm(); | |||
form.instance.openForm(); | |||
form.instance.openForm(mockEvent); | |||
expect(form.instance.isGroupSelected(11)).toBeTruthy(); | |||
expect(form.instance.isGroupSelected(7)).toBeFalsy(); | |||
form.instance.onCheck(false, 11); | |||
form.instance.onCheck(true, 7); | |||
form.instance.onCheck(11, false); | |||
form.instance.onCheck(7, true); | |||
expect(form.wrapper.state('userGroups')).toMatchSnapshot(); | |||
expect(form.instance.isGroupSelected(11)).toBeFalsy(); | |||
expect(form.instance.isGroupSelected(7)).toBeTruthy(); | |||
}); | |||
it('should correctly handle the submit event and close the modal', () => { | |||
const form = getMountedForm(); | |||
form.instance.openForm(); | |||
form.instance.onCheck(false, 11); | |||
form.instance.onCheck(true, 7); | |||
const updateMemberGroups = jest.fn(); | |||
const form = getMountedForm(updateMemberGroups); | |||
form.instance.openForm(mockEvent); | |||
form.instance.onCheck(11, false); | |||
form.instance.onCheck(7, true); | |||
form.instance.handleSubmit(mockEvent); | |||
expect(updateMemberGroups.mock.calls).toMatchSnapshot(); | |||
expect(form.wrapper.state()).toMatchSnapshot(); | |||
form.instance.openForm(); | |||
expect(form.wrapper.state()).toMatchSnapshot(); | |||
}); | |||
it('should reset the selected groups when the modal is closed', () => { | |||
it('should reset the selected groups when the modal is opened', () => { | |||
const form = getMountedForm(); | |||
form.instance.openForm(); | |||
form.instance.onCheck(false, 11); | |||
form.instance.onCheck(true, 7); | |||
form.instance.openForm(mockEvent); | |||
form.instance.onCheck(11, false); | |||
form.instance.onCheck(7, true); | |||
expect(form.wrapper.state()).toMatchSnapshot(); | |||
form.instance.closeForm(); | |||
form.instance.openForm(mockEvent); | |||
expect(form.wrapper.state()).toMatchSnapshot(); | |||
}); |
@@ -23,12 +23,11 @@ import { click, mockEvent } from '../../../../../helpers/testUtils'; | |||
import RemoveMemberForm from '../RemoveMemberForm'; | |||
const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }; | |||
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} /> | |||
<RemoveMemberForm member={member} removeMember={jest.fn()} organization={organization} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ open: true }); | |||
@@ -36,6 +35,7 @@ it('should render and open the modal', () => { | |||
}); | |||
it('should correctly handle user interactions', () => { | |||
const removeMember = jest.fn(); | |||
const wrapper = mount( | |||
<RemoveMemberForm member={member} removeMember={removeMember} organization={organization} /> | |||
); |
@@ -36,25 +36,6 @@ Object { | |||
} | |||
`; | |||
exports[`test should correctly handle the submit event and close the modal 3`] = ` | |||
Object { | |||
"loading": false, | |||
"open": true, | |||
"userGroups": Object { | |||
"11": Object { | |||
"description": "Technical accounts", | |||
"id": 11, | |||
"name": "pull-request-analysers", | |||
"selected": true, | |||
"status": "remove", | |||
}, | |||
"7": Object { | |||
"status": "add", | |||
}, | |||
}, | |||
} | |||
`; | |||
exports[`test should correctly handle user interactions 1`] = ` | |||
Object { | |||
"loading": false, | |||
@@ -120,41 +101,44 @@ exports[`test should render and open the modal 2`] = ` | |||
<div | |||
className="modal-body"> | |||
<strong> | |||
organization.members.x_groups.Admin Istrator | |||
organization.members.members_groups.Admin Istrator | |||
</strong> | |||
<ul | |||
className="list-spaced"> | |||
<li | |||
className="capitalize"> | |||
<Checkbox | |||
checked={false} | |||
id="7" | |||
onCheck={[Function]} | |||
thirdState={false} /> | |||
professionals | |||
</li> | |||
<li | |||
className="capitalize"> | |||
<Checkbox | |||
checked={false} | |||
id="11" | |||
onCheck={[Function]} | |||
thirdState={false} /> | |||
pull-request-analysers | |||
</li> | |||
<li | |||
className="capitalize"> | |||
<Checkbox | |||
checked={false} | |||
id="1" | |||
onCheck={[Function]} | |||
thirdState={false} /> | |||
sonar-administrators | |||
</li> | |||
<OrganizationGroupCheckbox | |||
checked={false} | |||
group={ | |||
Object { | |||
"description": "", | |||
"id": "7", | |||
"membersCount": 12, | |||
"name": "professionals", | |||
} | |||
} | |||
onCheck={[Function]} /> | |||
<OrganizationGroupCheckbox | |||
checked={false} | |||
group={ | |||
Object { | |||
"description": "Technical accounts", | |||
"id": "11", | |||
"membersCount": 3, | |||
"name": "pull-request-analysers", | |||
} | |||
} | |||
onCheck={[Function]} /> | |||
<OrganizationGroupCheckbox | |||
checked={false} | |||
group={ | |||
Object { | |||
"description": "System administrators", | |||
"id": "1", | |||
"membersCount": 17, | |||
"name": "sonar-administrators", | |||
} | |||
} | |||
onCheck={[Function]} /> | |||
</ul> | |||
</div> | |||
<footer | |||
@@ -177,7 +161,7 @@ exports[`test should render and open the modal 2`] = ` | |||
</a> | |||
`; | |||
exports[`test should reset the selected groups when the modal is closed 1`] = ` | |||
exports[`test should reset the selected groups when the modal is opened 1`] = ` | |||
Object { | |||
"loading": false, | |||
"open": true, | |||
@@ -196,10 +180,17 @@ Object { | |||
} | |||
`; | |||
exports[`test should reset the selected groups when the modal is closed 2`] = ` | |||
exports[`test should reset the selected groups when the modal is opened 2`] = ` | |||
Object { | |||
"loading": false, | |||
"open": false, | |||
"userGroups": undefined, | |||
"open": true, | |||
"userGroups": Object { | |||
"11": Object { | |||
"description": "Technical accounts", | |||
"id": 11, | |||
"name": "pull-request-analysers", | |||
"selected": true, | |||
}, | |||
}, | |||
} | |||
`; |
@@ -24,7 +24,8 @@ import classNames from 'classnames'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
type Props = { | |||
onSearch: (query?: string) => void | |||
onSearch: (query?: string) => void, | |||
className?: string | |||
}; | |||
type State = { | |||
@@ -55,12 +56,12 @@ export default class UsersSearch extends React.PureComponent { | |||
render() { | |||
const { query } = this.state; | |||
const searchBoxClass = classNames('search-box', this.props.className); | |||
const inputClassName = classNames('search-box-input', { | |||
touched: query != null && query.length === 1 | |||
}); | |||
return ( | |||
<div className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<div className="search-box"> | |||
<div className={searchBoxClass}> | |||
<button className="search-box-submit button-clean"> | |||
<i className="icon-search" /> | |||
</button> | |||
@@ -76,7 +77,6 @@ export default class UsersSearch extends React.PureComponent { | |||
{translateWithParameters('select2.tooShort', 2)} | |||
</span> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -21,6 +21,7 @@ | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import { debounce } from 'lodash'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import UsersSelectSearchOption from './UsersSelectSearchOption'; | |||
import UsersSelectSearchValue from './UsersSelectSearchValue'; | |||
import './UsersSelectSearch.css'; | |||
@@ -54,7 +55,7 @@ export default class UsersSelectSearch extends React.PureComponent { | |||
constructor(props: Props) { | |||
super(props); | |||
this.handleSearch = debounce(this.handleSearch); | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
this.state = { searchResult: [], isLoading: false, search: '' }; | |||
} | |||
@@ -73,7 +74,8 @@ export default class UsersSelectSearch extends React.PureComponent { | |||
handleSearch = (search: string) => { | |||
this.setState({ isLoading: true, search }); | |||
this.props.searchUsers(search, Math.min(this.props.excludedUsers.length + LIST_SIZE, 500)) | |||
this.props | |||
.searchUsers(search, Math.min(this.props.excludedUsers.length + LIST_SIZE, 500)) | |||
.then(this.filterSearchResult) | |||
.then(searchResult => { | |||
this.setState({ isLoading: false, searchResult }); | |||
@@ -81,6 +83,9 @@ export default class UsersSelectSearch extends React.PureComponent { | |||
}; | |||
render() { | |||
const noResult = this.state.search.length === 1 | |||
? translateWithParameters('select2.tooShort', 2) | |||
: translate('no_results'); | |||
return ( | |||
<Select | |||
className="Select-big" | |||
@@ -92,6 +97,7 @@ export default class UsersSelectSearch extends React.PureComponent { | |||
onInputChange={this.handleSearch} | |||
value={this.props.selectedUser} | |||
placeholder="" | |||
noResultsText={noResult} | |||
labelKey="name" | |||
valueKey="login" | |||
clearable={false} |
@@ -21,21 +21,15 @@ import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import UsersSearch from '../UsersSearch'; | |||
const onSearch = jest.fn(); | |||
it('should render correctly without any search query', () => { | |||
const wrapper = shallow(<UsersSearch onSearch={onSearch} query={null} />); | |||
it('should render correctly', () => { | |||
const wrapper = shallow(<UsersSearch onSearch={jest.fn()} className="test" />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render with a search query', () => { | |||
const wrapper = shallow(<UsersSearch onSearch={onSearch} query={'foo'} />); | |||
wrapper.setState({ query: 'foo' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should display a help message when there is less than 2 characters', () => { | |||
const wrapper = shallow(<UsersSearch onSearch={onSearch} query={'a'} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ query: 'foo' }); | |||
const wrapper = shallow(<UsersSearch onSearch={jest.fn()} />); | |||
wrapper.setState({ query: 'f' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -1,99 +1,65 @@ | |||
exports[`test should display a help message when there is less than 2 characters 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<div | |||
className="search-box"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
className="search-box"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input touched" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="f" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
`; | |||
exports[`test should display a help message when there is less than 2 characters 2`] = ` | |||
exports[`test should render correctly 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<div | |||
className="search-box"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="foo" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
className="search-box test"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
`; | |||
exports[`test should render correctly without any search query 1`] = ` | |||
exports[`test should render correctly 2`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<div | |||
className="search-box"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
</div> | |||
`; | |||
exports[`test should render with a search query 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<div | |||
className="search-box"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
className="search-box test"> | |||
<button | |||
className="search-box-submit button-clean"> | |||
<i | |||
className="icon-search" /> | |||
</button> | |||
<input | |||
autoComplete="off" | |||
className="search-box-input" | |||
onChange={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="foo" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
`; |
@@ -24,7 +24,7 @@ exports[`test should render correctly 1`] = ` | |||
menuBuffer={0} | |||
menuRenderer={[Function]} | |||
multi={false} | |||
noResultsText="No results found" | |||
noResultsText="no_results" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
@@ -91,7 +91,7 @@ exports[`test should render correctly 3`] = ` | |||
menuBuffer={0} | |||
menuRenderer={[Function]} | |||
multi={false} | |||
noResultsText="No results found" | |||
noResultsText="no_results" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} |
@@ -22,8 +22,6 @@ import React from 'react'; | |||
import ListFooter from '../ListFooter'; | |||
import { click } from '../../../helpers/testUtils'; | |||
const loadMore = jest.fn(); | |||
it('should render "3 of 5 shown"', () => { | |||
const listFooter = shallow(<ListFooter count={3} total={5} />); | |||
expect(listFooter.text()).toContain('x_of_y_shown.3.5'); | |||
@@ -35,11 +33,12 @@ it('should not render "show more"', () => { | |||
}); | |||
it('should not render "show more"', () => { | |||
const listFooter = shallow(<ListFooter count={5} total={5} loadMore={loadMore} />); | |||
const listFooter = shallow(<ListFooter count={5} total={5} loadMore={jest.fn()} />); | |||
expect(listFooter.find('a').length).toBe(0); | |||
}); | |||
it('should "show more"', () => { | |||
const loadMore = jest.fn(); | |||
const listFooter = shallow(<ListFooter count={3} total={5} loadMore={loadMore} />); | |||
const link = listFooter.find('a'); | |||
expect(link.length).toBe(1); |
@@ -84,12 +84,6 @@ | |||
top: 3px; | |||
margin-left: 8px; | |||
} | |||
.page-totalcount { | |||
position: absolute; | |||
bottom: -50px; | |||
right: 0; | |||
} | |||
} | |||
.page-title { |
@@ -63,6 +63,14 @@ ol, ul { | |||
} | |||
} | |||
.list-item-checkable-link { | |||
cursor: pointer; | |||
&:focus { | |||
outline: none; | |||
} | |||
} | |||
// Definition lists | |||
@@ -1770,7 +1770,6 @@ user.password_cant_be_changed_on_external_auth=Password cannot be changed when e | |||
# USERS PAGE | |||
# | |||
#------------------------------------------------------------------------------ | |||
users.create=Create User | |||
users.add=Add user | |||
users.remove=Remove user | |||
users.search_description=Search by username, full name, or email address | |||
@@ -2821,11 +2820,12 @@ organization.url=Url | |||
organization.url.description=Url of the homepage of the organization. | |||
organization.members.page=Members | |||
organization.members.add=Add a member | |||
organization.members.x_group(s)={0} group(s) | |||
organization.members.member(s)=member(s) | |||
organization.members.x_groups={0} group(s) | |||
organization.members.members=member(s) | |||
organization.members.remove=Remove from organization's members | |||
organization.members.remove_x=Are you sure you want to remove {0} from {1}'s members ? | |||
organization.members.remove_warning_x={0} will no longer be able to : | |||
organization.members.browse_projects=Browse Projects | |||
organization.members.manage_groups=Manage groups | |||
organization.members.x_groups={0}'s groups: | |||
organization.members.members_groups={0}'s groups: | |||
organization.members.add_to_members=Add to members |