Просмотр исходного кода

Fix code review requests

tags/6.4-RC1
Grégoire Aubert 7 лет назад
Родитель
Сommit
f94f052f7e
30 измененных файлов: 581 добавлений и 285 удалений
  1. 48
    0
      server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js
  2. 11
    8
      server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
  3. 1
    9
      server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js
  4. 55
    0
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js
  5. 12
    11
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js
  6. 36
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListHeader-test.js
  7. 12
    2
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js
  8. 5
    5
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js
  9. 47
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationGroupCheckbox-test.js
  10. 4
    6
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js
  11. 25
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap
  12. 93
    5
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
  13. 1
    18
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap
  14. 53
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationGroupCheckbox-test.js.snap
  15. 9
    7
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap
  16. 16
    18
      server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js
  17. 2
    1
      server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js
  18. 2
    3
      server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.js
  19. 16
    17
      server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js
  20. 2
    2
      server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js
  21. 45
    54
      server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap
  22. 4
    4
      server/sonar-web/src/main/js/apps/users/components/UsersSearch.js
  23. 8
    2
      server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js
  24. 5
    11
      server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js
  25. 53
    87
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap
  26. 2
    2
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearch-test.js.snap
  27. 2
    3
      server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.js
  28. 0
    6
      server/sonar-web/src/main/less/components/page.less
  29. 8
    0
      server/sonar-web/src/main/less/init/lists.less
  30. 4
    4
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 48
- 0
server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js Просмотреть файл

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

+ 11
- 8
server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js Просмотреть файл

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

server/sonar-web/src/main/js/apps/organizations/components/PageHeader.js → server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.js Просмотреть файл

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

+ 55
- 0
server/sonar-web/src/main/js/apps/organizations/components/OrganizationGroupCheckbox.js Просмотреть файл

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

+ 12
- 11
server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.js Просмотреть файл

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

+ 36
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListHeader-test.js Просмотреть файл

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

+ 12
- 2
server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.js Просмотреть файл

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

server/sonar-web/src/main/js/apps/organizations/components/__tests__/PageHeader-test.js → server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.js Просмотреть файл

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

+ 47
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationGroupCheckbox-test.js Просмотреть файл

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

+ 4
- 6
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.js Просмотреть файл

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

+ 25
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap Просмотреть файл

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

+ 93
- 5
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap Просмотреть файл

@@ -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": "",

server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/PageHeader-test.js.snap → server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.js.snap Просмотреть файл

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

+ 53
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationGroupCheckbox-test.js.snap Просмотреть файл

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

+ 9
- 7
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.js.snap Просмотреть файл

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

+ 16
- 18
server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.js Просмотреть файл

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

+ 2
- 1
server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.js Просмотреть файл

@@ -41,7 +41,8 @@ export default class RemoveMemberForm extends React.PureComponent {
open: false
};

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


+ 2
- 3
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.js Просмотреть файл

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

+ 16
- 17
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.js Просмотреть файл

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

+ 2
- 2
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.js Просмотреть файл

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

+ 45
- 54
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.js.snap Просмотреть файл

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

+ 4
- 4
server/sonar-web/src/main/js/apps/users/components/UsersSearch.js Просмотреть файл

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

+ 8
- 2
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js Просмотреть файл

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

+ 5
- 11
server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js Просмотреть файл

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

+ 53
- 87
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap Просмотреть файл

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

+ 2
- 2
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearch-test.js.snap Просмотреть файл

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

+ 2
- 3
server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.js Просмотреть файл

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

+ 0
- 6
server/sonar-web/src/main/less/components/page.less Просмотреть файл

@@ -84,12 +84,6 @@
top: 3px;
margin-left: 8px;
}

.page-totalcount {
position: absolute;
bottom: -50px;
right: 0;
}
}

.page-title {

+ 8
- 0
server/sonar-web/src/main/less/init/lists.less Просмотреть файл

@@ -63,6 +63,14 @@ ol, ul {
}
}

.list-item-checkable-link {
cursor: pointer;

&:focus {
outline: none;
}
}


// Definition lists


+ 4
- 4
sonar-core/src/main/resources/org/sonar/l10n/core.properties Просмотреть файл

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

Загрузка…
Отмена
Сохранить