Sfoglia il codice sorgente

SONAR-8990 Add the list of members of an organization

tags/6.4-RC1
Grégoire Aubert 7 anni fa
parent
commit
e4af315006
25 ha cambiato i file con 956 aggiunte e 3 eliminazioni
  1. 45
    0
      server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
  2. 54
    0
      server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
  3. 76
    0
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
  4. 42
    0
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js
  5. 48
    0
      server/sonar-web/src/main/js/apps/organizations/components/PageHeader.js
  6. 38
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.js
  7. 46
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js
  8. 57
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js
  9. 45
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/PageHeader-test.js
  10. 37
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap
  11. 37
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
  12. 77
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap
  13. 42
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/PageHeader-test.js.snap
  14. 5
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js
  15. 27
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap
  16. 5
    0
      server/sonar-web/src/main/js/apps/organizations/routes.js
  17. 82
    0
      server/sonar-web/src/main/js/apps/users/components/UsersSearch.js
  18. 41
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js
  19. 99
    0
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap
  20. 7
    1
      server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.js
  21. 2
    1
      server/sonar-web/src/main/js/components/ui/Avatar.js
  22. 16
    0
      server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js
  23. 9
    1
      server/sonar-web/src/main/less/components/page.less
  24. 9
    0
      server/sonar-web/src/main/less/components/search.less
  25. 10
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 45
- 0
server/sonar-web/src/main/js/apps/organizations/components/MembersList.js Vedi File

@@ -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>
);
}
}

+ 54
- 0
server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js Vedi 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.
*/
//@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>
);
}
}

+ 76
- 0
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js Vedi File

@@ -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>
);
}
}

+ 42
- 0
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.js Vedi File

@@ -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
);

+ 48
- 0
server/sonar-web/src/main/js/apps/organizations/components/PageHeader.js Vedi 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 { 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>
);
}
}

+ 38
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.js Vedi File

@@ -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();
});

+ 46
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js Vedi File

@@ -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();
});

+ 57
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js Vedi File

@@ -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();
});

+ 45
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/PageHeader-test.js Vedi File

@@ -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();
});

+ 37
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap Vedi File

@@ -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>
`;

+ 37
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap Vedi File

@@ -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>
`;

+ 77
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap Vedi File

@@ -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>
`;

+ 42
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/PageHeader-test.js.snap Vedi File

@@ -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>
`;

+ 5
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js Vedi File

@@ -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>

+ 27
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap Vedi File

@@ -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

+ 5
- 0
server/sonar-web/src/main/js/apps/organizations/routes.js Vedi File

@@ -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: [

+ 82
- 0
server/sonar-web/src/main/js/apps/users/components/UsersSearch.js Vedi File

@@ -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>
);
}
}

+ 41
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js Vedi File

@@ -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();
});

+ 99
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap Vedi File

@@ -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>
`;

+ 7
- 1
server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.js Vedi File

@@ -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);

+ 2
- 1
server/sonar-web/src/main/js/components/ui/Avatar.js Vedi File

@@ -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);

+ 16
- 0
server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js Vedi File

@@ -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');
});

+ 9
- 1
server/sonar-web/src/main/less/components/page.less Vedi File

@@ -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;
}
}
}
}

+ 9
- 0
server/sonar-web/src/main/less/components/search.less Vedi File

@@ -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 {

+ 10
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Vedi File

@@ -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)

Loading…
Annulla
Salva