@@ -0,0 +1,45 @@ | |||
/* | |||
* 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 MembersListItem from './MembersListItem'; | |||
import type { Member } from '../../../store/organizationsMembers/actions'; | |||
import type { Organization } from '../../../store/organizations/duck'; | |||
type Props = { | |||
members: Array<Member>, | |||
organization?: Organization | |||
}; | |||
export default class MembersList extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
return ( | |||
<table className="data zebra"> | |||
<tbody> | |||
{this.props.members.map(member => ( | |||
<MembersListItem key={member.login} member={member} organization={this.props.organization} /> | |||
))} | |||
</tbody> | |||
</table> | |||
); | |||
} | |||
} |
@@ -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. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import type { Member } from '../../../store/organizationsMembers/actions'; | |||
import type { Organization } from '../../../store/organizations/duck'; | |||
type Props = { | |||
member: Member, | |||
organization: Organization | |||
}; | |||
const AVATAR_SIZE: number = 36; | |||
export default class MembersListItem extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
const { member, organization } = this.props; | |||
return ( | |||
<tr> | |||
<td className="thin nowrap"> | |||
<Avatar hash={member.avatar} 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> | |||
} | |||
</tr> | |||
); | |||
} | |||
} |
@@ -0,0 +1,76 @@ | |||
/* | |||
* 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 PageHeader from './PageHeader'; | |||
import MembersList from './MembersList'; | |||
import UsersSearch from '../../users/components/UsersSearch'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import type { Organization } from '../../../store/organizations/duck'; | |||
import type { Member } from '../../../store/organizationsMembers/actions'; | |||
type Props = { | |||
members: Array<Member>, | |||
status: { loading?: boolean, total?: number, pageIndex?: number, query?: string }, | |||
organization: Organization, | |||
fetchOrganizationMembers: (organizationKey: string, query?: string) => void, | |||
fetchMoreOrganizationMembers: (organizationKey: string, query?: 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(); | |||
} | |||
} | |||
handleSearchMembers = (query?: string) => { | |||
this.props.fetchOrganizationMembers(this.props.organization.key, query); | |||
}; | |||
handleLoadMoreMembers = () => { | |||
this.props.fetchMoreOrganizationMembers(this.props.organization.key, this.props.status.query); | |||
}; | |||
addMember() { | |||
// TODO Not yet implemented | |||
} | |||
render() { | |||
const { organization, status, members } = this.props; | |||
return ( | |||
<div className="page page-limited"> | |||
<PageHeader loading={status.loading} total={status.total} /> | |||
<UsersSearch onSearch={this.handleSearchMembers} /> | |||
<MembersList members={members} organization={organization} /> | |||
{status.total != null && | |||
<ListFooter | |||
count={members.length} | |||
total={status.total} | |||
ready={!status.loading} | |||
loadMore={this.handleLoadMoreMembers} | |||
/>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
/* | |||
* 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 { connect } from 'react-redux'; | |||
import OrganizationMembers from './OrganizationMembers'; | |||
import { | |||
getOrganizationByKey, | |||
getOrganizationMembersLogins, | |||
getUsersByLogins, | |||
getOrganizationMembersState | |||
} from '../../../store/rootReducer'; | |||
import { fetchOrganizationMembers, fetchMoreOrganizationMembers } from '../actions'; | |||
const mapStateToProps = (state, ownProps) => { | |||
const { organizationKey } = ownProps.params; | |||
const memberLogins = getOrganizationMembersLogins(state, organizationKey); | |||
return { | |||
members: getUsersByLogins(state, memberLogins), | |||
organization: getOrganizationByKey(state, organizationKey), | |||
status: getOrganizationMembersState(state, organizationKey) | |||
}; | |||
}; | |||
export default connect(mapStateToProps, { fetchOrganizationMembers, fetchMoreOrganizationMembers })( | |||
OrganizationMembers | |||
); |
@@ -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 { formatMeasure } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { | |||
loading: boolean, | |||
total?: number, | |||
children?: {} | |||
}; | |||
export default class PageHeader extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
return ( | |||
<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,38 @@ | |||
/* | |||
* 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 MembersList from '../MembersList'; | |||
const organization = { key: 'foo', name: 'Foo' }; | |||
const members = [ | |||
{ login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }, | |||
{ login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 } | |||
]; | |||
it('should render a list of members of an organization', () => { | |||
const wrapper = shallow( | |||
<MembersList | |||
organization={organization} | |||
members={members} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,46 @@ | |||
/* | |||
* 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 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 }; | |||
it('should not render actions and groups for non admin', () => { | |||
const wrapper = shallow( | |||
<MembersListItem | |||
organization={organization} | |||
member={john} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render actions and groups for admin', () => { | |||
const wrapper = shallow( | |||
<MembersListItem | |||
organization={{ ...organization, canAdmin: true }} | |||
member={admin} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,57 @@ | |||
/* | |||
* 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 OrganizationMembers from '../OrganizationMembers'; | |||
const organization = { key: 'foo', name: 'Foo' }; | |||
const members = [ | |||
{ login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 }, | |||
{ 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( | |||
<OrganizationMembers | |||
organization={organization} | |||
members={members} | |||
status={status} | |||
fetchOrganizationMembers={fetchOrganizationMembers} | |||
fetchMoreOrganizationMembers={fetchMoreOrganizationMembers} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render actions for admin', () => { | |||
const wrapper = shallow( | |||
<OrganizationMembers | |||
organization={{ ...organization, canAdmin: true }} | |||
members={members} | |||
status={{ ...status, loading: true }} | |||
fetchOrganizationMembers={fetchOrganizationMembers} | |||
fetchMoreOrganizationMembers={fetchMoreOrganizationMembers} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,45 @@ | |||
/* | |||
* 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 PageHeader from '../PageHeader'; | |||
it('should render the members page header', () => { | |||
const wrapper = shallow( | |||
<PageHeader /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setProps({ loading: true }); | |||
expect(wrapper.find('.spinner')).toMatchSnapshot(); | |||
}); | |||
it('should render the members page header with the total', () => { | |||
const wrapper = shallow(<PageHeader total="5" />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render its children', () => { | |||
const wrapper = shallow( | |||
<PageHeader loading={true} total="5"> | |||
<span>children test</span> | |||
</PageHeader> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,37 @@ | |||
exports[`test should render a list of members of an organization 1`] = ` | |||
<table | |||
className="data zebra"> | |||
<tbody> | |||
<MembersListItem | |||
member={ | |||
Object { | |||
"avatar": "", | |||
"groupCount": 3, | |||
"login": "admin", | |||
"name": "Admin Istrator", | |||
} | |||
} | |||
organization={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
<MembersListItem | |||
member={ | |||
Object { | |||
"avatar": "7daf6c79d4802916d83f6266e24850af", | |||
"groupCount": 1, | |||
"login": "john", | |||
"name": "John Doe", | |||
} | |||
} | |||
organization={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
</tbody> | |||
</table> | |||
`; |
@@ -0,0 +1,37 @@ | |||
exports[`test should not render actions and groups for non admin 1`] = ` | |||
<tr> | |||
<td | |||
className="thin nowrap"> | |||
<Connect(Avatar) | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
size={36} /> | |||
</td> | |||
<td | |||
className="nowrap text-middle"> | |||
<strong> | |||
John Doe | |||
</strong> | |||
</td> | |||
</tr> | |||
`; | |||
exports[`test should render actions and groups for admin 1`] = ` | |||
<tr> | |||
<td | |||
className="thin nowrap"> | |||
<Connect(Avatar) | |||
hash="" | |||
size={36} /> | |||
</td> | |||
<td | |||
className="nowrap text-middle"> | |||
<strong> | |||
Admin Istrator | |||
</strong> | |||
</td> | |||
<td | |||
className="text-right text-middle"> | |||
organization.members.x_group(s).3 | |||
</td> | |||
</tr> | |||
`; |
@@ -0,0 +1,77 @@ | |||
exports[`test should not render actions for non admin 1`] = ` | |||
<div | |||
className="page page-limited"> | |||
<PageHeader | |||
total={2} /> | |||
<UsersSearch | |||
onSearch={[Function]} /> | |||
<MembersList | |||
members={ | |||
Array [ | |||
Object { | |||
"avatar": "", | |||
"groupCount": 3, | |||
"login": "admin", | |||
"name": "Admin Istrator", | |||
}, | |||
Object { | |||
"avatar": "7daf6c79d4802916d83f6266e24850af", | |||
"groupCount": 1, | |||
"login": "john", | |||
"name": "John Doe", | |||
}, | |||
] | |||
} | |||
organization={ | |||
Object { | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
<ListFooter | |||
count={2} | |||
loadMore={[Function]} | |||
ready={true} | |||
total={2} /> | |||
</div> | |||
`; | |||
exports[`test should render actions for admin 1`] = ` | |||
<div | |||
className="page page-limited"> | |||
<PageHeader | |||
loading={true} | |||
total={2} /> | |||
<UsersSearch | |||
onSearch={[Function]} /> | |||
<MembersList | |||
members={ | |||
Array [ | |||
Object { | |||
"avatar": "", | |||
"groupCount": 3, | |||
"login": "admin", | |||
"name": "Admin Istrator", | |||
}, | |||
Object { | |||
"avatar": "7daf6c79d4802916d83f6266e24850af", | |||
"groupCount": 1, | |||
"login": "john", | |||
"name": "John Doe", | |||
}, | |||
] | |||
} | |||
organization={ | |||
Object { | |||
"canAdmin": true, | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
<ListFooter | |||
count={2} | |||
loadMore={[Function]} | |||
ready={false} | |||
total={2} /> | |||
</div> | |||
`; |
@@ -0,0 +1,42 @@ | |||
exports[`test should render its children 1`] = ` | |||
<header | |||
className="page-header"> | |||
<i | |||
className="spinner" /> | |||
<span> | |||
children test | |||
</span> | |||
<span | |||
className="page-totalcount"> | |||
<strong> | |||
5 | |||
</strong> | |||
organization.members.member(s) | |||
</span> | |||
</header> | |||
`; | |||
exports[`test should render the members page header 1`] = ` | |||
<header | |||
className="page-header" /> | |||
`; | |||
exports[`test should render the members page header 2`] = ` | |||
<i | |||
className="spinner" /> | |||
`; | |||
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> | |||
`; |
@@ -153,6 +153,11 @@ export default class OrganizationNavigation extends React.Component { | |||
{translate('projects.page')} | |||
</Link> | |||
</li> | |||
<li> | |||
<Link to={`/organizations/${organization.key}/members`} activeClassName="active"> | |||
{translate('organization.members.page')} | |||
</Link> | |||
</li> | |||
{organization.canAdmin && this.renderAdministration()} | |||
</ul> | |||
</div> |
@@ -35,6 +35,15 @@ exports[`test admin 1`] = ` | |||
projects.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/members"> | |||
organization.members.page | |||
</Link> | |||
</li> | |||
<li | |||
className=""> | |||
<a | |||
@@ -147,6 +156,15 @@ exports[`test regular user 1`] = ` | |||
projects.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/members"> | |||
organization.members.page | |||
</Link> | |||
</li> | |||
</ul> | |||
</div> | |||
</div> | |||
@@ -190,6 +208,15 @@ exports[`test undeletable org 1`] = ` | |||
projects.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/members"> | |||
organization.members.page | |||
</Link> | |||
</li> | |||
<li | |||
className=""> | |||
<a |
@@ -23,6 +23,7 @@ import OrganizationFavoriteProjects from './components/OrganizationFavoriteProje | |||
import OrganizationAdmin from './components/OrganizationAdmin'; | |||
import OrganizationEdit from './components/OrganizationEdit'; | |||
import OrganizationGroups from './components/OrganizationGroups'; | |||
import OrganizationMembersContainer from './components/OrganizationMembersContainer'; | |||
import OrganizationPermissions from './components/OrganizationPermissions'; | |||
import OrganizationPermissionTemplates from './components/OrganizationPermissionTemplates'; | |||
import OrganizationProjectsManagement from './components/OrganizationProjectsManagement'; | |||
@@ -49,6 +50,10 @@ const routes = [ | |||
path: 'projects/favorite', | |||
component: OrganizationFavoriteProjects | |||
}, | |||
{ | |||
path: 'members', | |||
component: OrganizationMembersContainer | |||
}, | |||
{ | |||
component: OrganizationAdmin, | |||
childRoutes: [ |
@@ -0,0 +1,82 @@ | |||
/* | |||
* 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 { debounce } from 'lodash'; | |||
import classNames from 'classnames'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
type Props = { | |||
onSearch: (query?: string) => void | |||
}; | |||
type State = { | |||
query?: string | |||
}; | |||
export default class UsersSearch extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
query: '' | |||
} | |||
constructor(props: Props) { | |||
super(props); | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
} | |||
handleSearch = (query: string) => { | |||
this.props.onSearch(query); | |||
} | |||
handleInputChange = ({ target }: { target: HTMLInputElement }) => { | |||
this.setState({ query: target.value }); | |||
if (!target.value || target.value.length >= 2) { | |||
this.handleSearch(target.value); | |||
} | |||
}; | |||
render() { | |||
const { query } = this.state; | |||
const inputClassName = classNames('search-box-input', { | |||
touched: query != null && query !== '' && query.length < 2 | |||
}); | |||
return ( | |||
<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 | |||
type="search" | |||
value={query} | |||
className={inputClassName} | |||
placeholder={translate('search_verb')} | |||
onChange={this.handleInputChange} | |||
autoComplete="off" | |||
/> | |||
<span className="note spacer-left text-middle"> | |||
{translateWithParameters('select2.tooShort', 2)} | |||
</span> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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 UsersSearch from '../UsersSearch'; | |||
const onSearch = jest.fn(); | |||
it('should render correctly without any search query', () => { | |||
const wrapper = shallow(<UsersSearch onSearch={onSearch} query={null} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render with a search query', () => { | |||
const wrapper = shallow(<UsersSearch onSearch={onSearch} 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' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,99 @@ | |||
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> | |||
</div> | |||
`; | |||
exports[`test should display a help message when there is less than 2 characters 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="foo" /> | |||
<span | |||
className="note spacer-left text-middle"> | |||
select2.tooShort.2 | |||
</span> | |||
</div> | |||
</div> | |||
`; | |||
exports[`test should render correctly without any 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> | |||
</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> | |||
</div> | |||
`; |
@@ -22,6 +22,8 @@ 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'); | |||
@@ -32,8 +34,12 @@ it('should not render "show more"', () => { | |||
expect(listFooter.find('a').length).toBe(0); | |||
}); | |||
it('should not render "show more"', () => { | |||
const listFooter = shallow(<ListFooter count={5} total={5} loadMore={loadMore} />); | |||
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); |
@@ -28,6 +28,7 @@ class Avatar extends React.Component { | |||
enableGravatar: React.PropTypes.bool.isRequired, | |||
gravatarServerUrl: React.PropTypes.string.isRequired, | |||
email: React.PropTypes.string, | |||
hash: React.PropTypes.string, | |||
size: React.PropTypes.number.isRequired, | |||
className: React.PropTypes.string | |||
}; | |||
@@ -37,7 +38,7 @@ class Avatar extends React.Component { | |||
return null; | |||
} | |||
const emailHash = md5.md5((this.props.email || '').toLowerCase()).trim(); | |||
const emailHash = this.props.hash || md5.md5((this.props.email || '').toLowerCase()).trim(); | |||
const url = this.props.gravatarServerUrl | |||
.replace('{EMAIL_MD5}', emailHash) | |||
.replace('{SIZE}', this.props.size * 2); |
@@ -50,3 +50,19 @@ it('should not render', () => { | |||
); | |||
expect(avatar.is('img')).toBe(false); | |||
}); | |||
it('should be able to render with hash only', () => { | |||
const avatar = shallow( | |||
<Avatar | |||
enableGravatar={true} | |||
gravatarServerUrl={gravatarServerUrl} | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
size={30} | |||
/> | |||
); | |||
expect(avatar.is('img')).toBe(true); | |||
expect(avatar.prop('width')).toBe(30); | |||
expect(avatar.prop('height')).toBe(30); | |||
expect(avatar.prop('alt')).toBeUndefined(); | |||
expect(avatar.prop('src')).toBe('http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=60'); | |||
}); |
@@ -74,6 +74,8 @@ | |||
} | |||
.page-header { | |||
position: relative; | |||
.clearfix; | |||
margin-bottom: 20px; | |||
@@ -82,6 +84,12 @@ | |||
top: 3px; | |||
margin-left: 8px; | |||
} | |||
.page-totalcount { | |||
position: absolute; | |||
bottom: -50px; | |||
right: 0; | |||
} | |||
} | |||
.page-title { | |||
@@ -176,4 +184,4 @@ | |||
width: 260px; | |||
} | |||
} | |||
} | |||
} |
@@ -31,6 +31,15 @@ | |||
width: 250px; | |||
border: none !important; | |||
font-size: @baseFontSize; | |||
& ~ .note { | |||
opacity: 0; | |||
transition: opacity 0.3s ease; | |||
} | |||
&.touched ~ .note { | |||
opacity: 1; | |||
} | |||
} | |||
.search-box-submit { |
@@ -1765,6 +1765,12 @@ user.scm_account_already_used=The scm account '{0}' is already used by user(s) : | |||
user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts | |||
user.password_cant_be_changed_on_external_auth=Password cannot be changed when external authentication is used | |||
#------------------------------------------------------------------------------ | |||
# | |||
# USERS PAGE | |||
# | |||
#------------------------------------------------------------------------------ | |||
users.create=Create User | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -2810,3 +2816,7 @@ organization.name.description=Name of the organization (2 to 64 characters). | |||
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.x_group(s)={0} group(s) | |||
organization.members.member(s)=member(s) |