Browse Source

SONAR-12244 Handle pagination in portfolio projects list properly

tags/8.0
philippe-perrin-sonarsource 4 years ago
parent
commit
e5129e41c9
15 changed files with 548 additions and 614 deletions
  1. 49
    98
      server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
  2. 32
    57
      server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx
  3. 34
    24
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap
  4. 29
    62
      server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx
  5. 28
    56
      server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Projects-test.tsx
  6. 2
    3
      server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Projects-test.tsx.snap
  7. 23
    58
      server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx
  8. 40
    55
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx
  9. 2
    3
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeProjectsForm-test.tsx.snap
  10. 22
    58
      server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx
  11. 40
    53
      server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx
  12. 26
    3
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap
  13. 67
    30
      server/sonar-web/src/main/js/components/SelectList/SelectList.tsx
  14. 81
    22
      server/sonar-web/src/main/js/components/SelectList/__tests__/SelectList-test.tsx
  15. 73
    32
      server/sonar-web/src/main/js/components/SelectList/__tests__/__snapshots__/SelectList-test.tsx.snap

+ 49
- 98
server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx View File

@@ -22,8 +22,10 @@ import { find, without } from 'lodash';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import SelectList, {
Filter,
SelectListSearchParams
} from '../../../components/SelectList/SelectList';
import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../api/user_groups';

interface Props {
@@ -32,26 +34,14 @@ interface Props {
organization: string | undefined;
}

export interface SearchParams {
name: string;
organization?: string;
page: number;
pageSize: number;
query?: string;
selected: string;
}

interface State {
lastSearchParams: SearchParams;
listHasBeenTouched: boolean;
loading: boolean;
lastSearchParams?: SelectListSearchParams;
needToReload: boolean;
users: T.UserSelected[];
usersTotalCount?: number;
selectedUsers: string[];
}

const PAGE_SIZE = 100;

export default class EditMembersModal extends React.PureComponent<Props, State> {
mounted = false;

@@ -59,16 +49,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
super(props);

this.state = {
lastSearchParams: {
name: props.group.name,
organization: props.organization,
page: 1,
pageSize: PAGE_SIZE,
query: '',
selected: Filter.Selected
},
listHasBeenTouched: false,
loading: true,
needToReload: false,
users: [],
selectedUsers: []
};
@@ -76,70 +57,41 @@ export default class EditMembersModal extends React.PureComponent<Props, State>

componentDidMount() {
this.mounted = true;
this.fetchUsers(this.state.lastSearchParams);
}

componentWillUnmount() {
this.mounted = false;
}

fetchUsers = (searchParams: SearchParams, more?: boolean) =>
fetchUsers = (searchParams: SelectListSearchParams) =>
getUsersInGroup({
...searchParams,
name: this.props.group.name,
organization: this.props.organization,
p: searchParams.page,
ps: searchParams.pageSize,
q: searchParams.query !== '' ? searchParams.query : undefined
}).then(
data => {
if (this.mounted) {
this.setState(prevState => {
const users = more ? [...prevState.users, ...data.users] : data.users;
const newSelectedUsers = data.users
.filter(user => user.selected)
.map(user => user.login);
const selectedUsers = more
? [...prevState.selectedUsers, ...newSelectedUsers]
: newSelectedUsers;

return {
lastSearchParams: searchParams,
listHasBeenTouched: false,
loading: false,
users,
usersTotalCount: data.total,
selectedUsers
};
});
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
q: searchParams.query !== '' ? searchParams.query : undefined,
selected: searchParams.filter
}).then(data => {
if (this.mounted) {
this.setState(prevState => {
const more = searchParams.page != null && searchParams.page > 1;

const users = more ? [...prevState.users, ...data.users] : data.users;
const newSelectedUsers = data.users.filter(user => user.selected).map(user => user.login);
const selectedUsers = more
? [...prevState.selectedUsers, ...newSelectedUsers]
: newSelectedUsers;

return {
needToReload: false,
lastSearchParams: searchParams,
loading: false,
users,
usersTotalCount: data.total,
selectedUsers
};
});
}
);

handleLoadMore = () =>
this.fetchUsers(
{
...this.state.lastSearchParams,
page: this.state.lastSearchParams.page + 1
},
true
);

handleReload = () =>
this.fetchUsers({
...this.state.lastSearchParams,
page: 1
});

handleSearch = (query: string, selected: Filter) =>
this.fetchUsers({
...this.state.lastSearchParams,
page: 1,
query,
selected
});

handleSelect = (login: string) =>
@@ -150,7 +102,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
}).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
listHasBeenTouched: true,
needToReload: true,
selectedUsers: [...state.selectedUsers, login]
}));
}
@@ -164,7 +116,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
}).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
listHasBeenTouched: true,
needToReload: true,
selectedUsers: without(state.selectedUsers, login)
}));
}
@@ -196,22 +148,21 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
</header>

<div className="modal-body modal-container">
<DeferredSpinner loading={this.state.loading}>
<SelectList
elements={this.state.users.map(user => user.login)}
elementsTotalCount={this.state.usersTotalCount}
needReload={
this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
}
onLoadMore={this.handleLoadMore}
onReload={this.handleReload}
onSearch={this.handleSearch}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
renderElement={this.renderElement}
selectedElements={this.state.selectedUsers}
/>
</DeferredSpinner>
<SelectList
elements={this.state.users.map(user => user.login)}
elementsTotalCount={this.state.usersTotalCount}
needToReload={
this.state.needToReload &&
this.state.lastSearchParams &&
this.state.lastSearchParams.filter !== Filter.All
}
onSearch={this.fetchUsers}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
renderElement={this.renderElement}
selectedElements={this.state.selectedUsers}
withPaging={true}
/>
</div>

<footer className="modal-foot">

+ 32
- 57
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx View File

@@ -20,10 +20,13 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import EditMembersModal, { SearchParams } from '../EditMembersModal';
import EditMembersModal from '../EditMembersModal';
import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
import { getUsersInGroup, addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups';

const organization = 'orga';
const group = { id: 1, name: 'foo', membersCount: 1 };

jest.mock('../../../../api/user_groups', () => ({
getUsersInGroup: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 10, total: 1 },
@@ -45,98 +48,70 @@ beforeEach(() => {

it('should render modal properly', async () => {
const wrapper = shallowRender();
wrapper
.find(SelectList)
.props()
.onSearch({
query: '',
filter: Filter.Selected,
page: 1,
pageSize: 100
});
await waitAndUpdate(wrapper);
expect(wrapper.state().needToReload).toBe(false);

expect(wrapper.instance().mounted).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(getUsersInGroup).toHaveBeenCalledWith(
expect.objectContaining({
p: 1
})
);

wrapper.setState({ listHasBeenTouched: true });
expect(wrapper.find(SelectList).props().needReload).toBe(true);

wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
expect(wrapper.find(SelectList).props().needReload).toBe(false);
});

it('should handle reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleReload();
expect(getUsersInGroup).toHaveBeenCalledWith(
expect.objectContaining({
p: 1
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});
expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();

it('should handle search reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSearch('foo', Filter.Selected);
expect(getUsersInGroup).toHaveBeenCalledWith(
expect.objectContaining({
name: group.name,
organization,
p: 1,
q: 'foo',
ps: 100,
q: undefined,
selected: Filter.Selected
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});

it('should handle load more properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleLoadMore();
expect(getUsersInGroup).toHaveBeenCalledWith(
expect.objectContaining({
p: 2
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should handle selection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSelect('toto');
await waitAndUpdate(wrapper);

expect(addUserToGroup).toHaveBeenCalledWith(
expect.objectContaining({
name: group.name,
organization,
login: 'toto'
})
);
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

it('should handle deselection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleUnselect('tata');

await waitAndUpdate(wrapper);
expect(removeUserFromGroup).toHaveBeenCalledWith(
expect.objectContaining({
name: group.name,
organization,
login: 'tata'
})
);
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

function shallowRender(props: Partial<EditMembersModal['props']> = {}) {
return shallow<EditMembersModal>(
<EditMembersModal
group={{ id: 1, name: 'foo', membersCount: 1 }}
onClose={jest.fn()}
organization="bar"
{...props}
/>
<EditMembersModal group={group} onClose={jest.fn()} organization={organization} {...props} />
);
}

+ 34
- 24
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap View File

@@ -15,30 +15,24 @@ exports[`should render modal properly 1`] = `
<div
className="modal-body modal-container"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<SelectList
elements={
Array [
"foo",
]
}
needReload={false}
onLoadMore={[Function]}
onReload={[Function]}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
renderElement={[Function]}
selectedElements={
Array [
"foo",
]
}
/>
</DeferredSpinner>
<SelectList
elements={
Array [
"foo",
]
}
needToReload={false}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
renderElement={[Function]}
selectedElements={
Array [
"foo",
]
}
withPaging={true}
/>
</div>
<footer
className="modal-foot"
@@ -51,3 +45,19 @@ exports[`should render modal properly 1`] = `
</footer>
</Modal>
`;

exports[`should render modal properly 2`] = `
<div
className="select-list-list-item"
>
test1
</div>
`;

exports[`should render modal properly 3`] = `
<div
className="select-list-list-item"
>
test_foo
</div>
`;

+ 29
- 62
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx View File

@@ -20,7 +20,10 @@
import * as React from 'react';
import { find, without } from 'lodash';
import { translate } from 'sonar-ui-common/helpers/l10n';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import SelectList, {
Filter,
SelectListSearchParams
} from '../../../components/SelectList/SelectList';
import {
associateGateWithProject,
dissociateGateWithProject,
@@ -33,25 +36,14 @@ interface Props {
qualityGate: T.QualityGate;
}

export interface SearchParams {
gateId: number;
organization?: string;
page: number;
pageSize: number;
query?: string;
selected: string;
}

interface State {
lastSearchParams: SearchParams;
listHasBeenTouched: boolean;
needToReload: boolean;
lastSearchParams?: SelectListSearchParams;
projects: Array<{ id: string; key: string; name: string; selected: boolean }>;
projectsTotalCount?: number;
selectedProjects: string[];
}

const PAGE_SIZE = 100;

export default class Projects extends React.PureComponent<Props, State> {
mounted = false;

@@ -59,15 +51,7 @@ export default class Projects extends React.PureComponent<Props, State> {
super(props);

this.state = {
lastSearchParams: {
gateId: props.qualityGate.id,
organization: props.organization,
page: 1,
pageSize: PAGE_SIZE,
query: '',
selected: Filter.Selected
},
listHasBeenTouched: false,
needToReload: false,
projects: [],
selectedProjects: []
};
@@ -75,20 +59,25 @@ export default class Projects extends React.PureComponent<Props, State> {

componentDidMount() {
this.mounted = true;
this.fetchProjects(this.state.lastSearchParams);
}

componentWillUnmount() {
this.mounted = false;
}

fetchProjects = (searchParams: SearchParams, more?: boolean) =>
fetchProjects = (searchParams: SelectListSearchParams) =>
searchProjects({
...searchParams,
query: searchParams.query !== '' ? searchParams.query : undefined
gateId: this.props.qualityGate.id,
organization: this.props.organization,
page: searchParams.page,
pageSize: searchParams.pageSize,
query: searchParams.query !== '' ? searchParams.query : undefined,
selected: searchParams.filter
}).then(data => {
if (this.mounted) {
this.setState(prevState => {
const more = searchParams.page != null && searchParams.page > 1;

const projects = more ? [...prevState.projects, ...data.results] : data.results;
const newSelectedProjects = data.results
.filter(project => project.selected)
@@ -99,7 +88,7 @@ export default class Projects extends React.PureComponent<Props, State> {

return {
lastSearchParams: searchParams,
listHasBeenTouched: false,
needToReload: false,
projects,
projectsTotalCount: data.paging.total,
selectedProjects
@@ -108,29 +97,6 @@ export default class Projects extends React.PureComponent<Props, State> {
}
});

handleLoadMore = () =>
this.fetchProjects(
{
...this.state.lastSearchParams,
page: this.state.lastSearchParams.page + 1
},
true
);

handleReload = () =>
this.fetchProjects({
...this.state.lastSearchParams,
page: 1
});

handleSearch = (query: string, selected: string) =>
this.fetchProjects({
...this.state.lastSearchParams,
page: 1,
query,
selected
});

handleSelect = (id: string) =>
associateGateWithProject({
gateId: this.props.qualityGate.id,
@@ -138,9 +104,9 @@ export default class Projects extends React.PureComponent<Props, State> {
projectId: id
}).then(() => {
if (this.mounted) {
this.setState(state => ({
listHasBeenTouched: true,
selectedProjects: [...state.selectedProjects, id]
this.setState(prevState => ({
needToReload: true,
selectedProjects: [...prevState.selectedProjects, id]
}));
}
});
@@ -152,9 +118,9 @@ export default class Projects extends React.PureComponent<Props, State> {
projectId: id
}).then(() => {
if (this.mounted) {
this.setState(state => ({
listHasBeenTouched: true,
selectedProjects: without(state.selectedProjects, id)
this.setState(prevState => ({
needToReload: true,
selectedProjects: without(prevState.selectedProjects, id)
}));
}
});
@@ -184,17 +150,18 @@ export default class Projects extends React.PureComponent<Props, State> {
labelAll={translate('quality_gates.projects.all')}
labelSelected={translate('quality_gates.projects.with')}
labelUnselected={translate('quality_gates.projects.without')}
needReload={
this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
needToReload={
this.state.needToReload &&
this.state.lastSearchParams &&
this.state.lastSearchParams.filter !== Filter.All
}
onLoadMore={this.handleLoadMore}
onReload={this.handleReload}
onSearch={this.handleSearch}
onSearch={this.fetchProjects}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
readOnly={!this.props.canEdit}
renderElement={this.renderElement}
selectedElements={this.state.selectedProjects}
withPaging={true}
/>
);
}

+ 28
- 56
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Projects-test.tsx View File

@@ -20,7 +20,7 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import Projects, { SearchParams } from '../Projects';
import Projects from '../Projects';
import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
import { mockQualityGate } from '../../../../helpers/testMocks';
import {
@@ -29,6 +29,9 @@ import {
dissociateGateWithProject
} from '../../../../api/quality-gates';

const qualityGate = mockQualityGate();
const organization = 'TEST';

jest.mock('../../../../api/quality-gates', () => ({
searchProjects: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 3, total: 55 },
@@ -48,97 +51,66 @@ beforeEach(() => {

it('should render correctly', async () => {
const wrapper = shallowRender();
wrapper
.find(SelectList)
.props()
.onSearch({
query: '',
filter: Filter.Selected,
page: 1,
pageSize: 100
});
await waitAndUpdate(wrapper);
expect(wrapper.instance().mounted).toBe(true);

expect(wrapper.instance().mounted).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
expect(searchProjects).toHaveBeenCalledWith(
expect.objectContaining({
page: 1
})
);

wrapper.setState({ listHasBeenTouched: true });
expect(wrapper.find(SelectList).props().needReload).toBe(true);

wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
expect(wrapper.find(SelectList).props().needReload).toBe(false);

wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should handle reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleReload();
expect(searchProjects).toHaveBeenCalledWith(
expect.objectContaining({
page: 1
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});

it('should handle search reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSearch('foo', Filter.Selected);
expect(searchProjects).toHaveBeenCalledWith(
expect.objectContaining({
gateId: qualityGate.id,
organization,
page: 1,
query: 'foo',
pageSize: 100,
query: undefined,
selected: Filter.Selected
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});
expect(wrapper.state().needToReload).toBe(false);

it('should handle load more properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleLoadMore();
expect(searchProjects).toHaveBeenCalledWith(
expect.objectContaining({
page: 2
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should handle selection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSelect('toto');
await waitAndUpdate(wrapper);

expect(associateGateWithProject).toHaveBeenCalledWith(
expect.objectContaining({
projectId: 'toto'
})
);
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

it('should handle deselection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleUnselect('tata');
await waitAndUpdate(wrapper);

expect(dissociateGateWithProject).toHaveBeenCalledWith(
expect.objectContaining({
projectId: 'tata'
})
);
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

function shallowRender(props: Partial<Projects['props']> = {}) {
return shallow<Projects>(<Projects qualityGate={mockQualityGate()} {...props} />);
return shallow<Projects>(
<Projects organization={organization} qualityGate={qualityGate} {...props} />
);
}

+ 2
- 3
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Projects-test.tsx.snap View File

@@ -13,9 +13,7 @@ exports[`should render correctly 1`] = `
labelAll="quality_gates.projects.all"
labelSelected="quality_gates.projects.with"
labelUnselected="quality_gates.projects.without"
needReload={false}
onLoadMore={[Function]}
onReload={[Function]}
needToReload={false}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
@@ -26,6 +24,7 @@ exports[`should render correctly 1`] = `
"test3",
]
}
withPaging={true}
/>
`;


+ 23
- 58
server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx View File

@@ -21,7 +21,10 @@ import * as React from 'react';
import { find, without } from 'lodash';
import { translate } from 'sonar-ui-common/helpers/l10n';
import Modal from 'sonar-ui-common/components/controls/Modal';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import SelectList, {
Filter,
SelectListSearchParams
} from '../../../components/SelectList/SelectList';
import { Profile } from '../types';
import {
associateProject,
@@ -36,25 +39,14 @@ interface Props {
profile: Profile;
}

export interface SearchParams {
key: string;
organization: string | null;
page: number;
pageSize: number;
query?: string;
selected: string;
}

interface State {
lastSearchParams: SearchParams;
listHasBeenTouched: boolean;
needToReload: boolean;
lastSearchParams?: SelectListSearchParams;
projects: ProfileProject[];
projectsTotalCount?: number;
selectedProjects: string[];
}

const PAGE_SIZE = 100;

export default class ChangeProjectsForm extends React.PureComponent<Props, State> {
mounted = false;

@@ -62,15 +54,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
super(props);

this.state = {
lastSearchParams: {
key: props.profile.key,
organization: props.organization,
page: 1,
pageSize: PAGE_SIZE,
query: '',
selected: Filter.Selected
},
listHasBeenTouched: false,
needToReload: false,
projects: [],
selectedProjects: []
};
@@ -78,22 +62,25 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State

componentDidMount() {
this.mounted = true;
this.fetchProjects(this.state.lastSearchParams);
}

componentWillUnmount() {
this.mounted = false;
}

fetchProjects = (searchParams: SearchParams, more?: boolean) =>
fetchProjects = (searchParams: SelectListSearchParams) =>
getProfileProjects({
...searchParams,
key: this.props.profile.key,
organization: this.props.organization,
p: searchParams.page,
ps: searchParams.pageSize,
q: searchParams.query !== '' ? searchParams.query : undefined
q: searchParams.query !== '' ? searchParams.query : undefined,
selected: searchParams.filter
}).then(data => {
if (this.mounted) {
this.setState(prevState => {
const more = searchParams.page != null && searchParams.page > 1;

const projects = more ? [...prevState.projects, ...data.results] : data.results;
const newSeletedProjects = data.results
.filter(project => project.selected)
@@ -104,7 +91,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State

return {
lastSearchParams: searchParams,
listHasBeenTouched: false,
needToReload: false,
projects,
projectsTotalCount: data.paging.total,
selectedProjects
@@ -113,34 +100,11 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
}
});

handleLoadMore = () =>
this.fetchProjects(
{
...this.state.lastSearchParams,
page: this.state.lastSearchParams.page + 1
},
true
);

handleReload = () =>
this.fetchProjects({
...this.state.lastSearchParams,
page: 1
});

handleSearch = (query: string, selected: Filter) =>
this.fetchProjects({
...this.state.lastSearchParams,
page: 1,
query,
selected
});

handleSelect = (key: string) =>
associateProject(this.props.profile.key, key).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
listHasBeenTouched: true,
needToReload: true,
selectedProjects: [...state.selectedProjects, key]
}));
}
@@ -150,7 +114,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
dissociateProject(this.props.profile.key, key).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
listHasBeenTouched: true,
needToReload: true,
selectedProjects: without(state.selectedProjects, key)
}));
}
@@ -195,16 +159,17 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
labelAll={translate('quality_gates.projects.all')}
labelSelected={translate('quality_gates.projects.with')}
labelUnselected={translate('quality_gates.projects.without')}
needReload={
this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
needToReload={
this.state.needToReload &&
this.state.lastSearchParams &&
this.state.lastSearchParams.filter !== Filter.All
}
onLoadMore={this.handleLoadMore}
onReload={this.handleReload}
onSearch={this.handleSearch}
onSearch={this.fetchProjects}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
renderElement={this.renderElement}
selectedElements={this.state.selectedProjects}
withPaging={true}
/>
</div>


+ 40
- 55
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx View File

@@ -19,8 +19,8 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import ChangeProjectsForm, { SearchParams } from '../ChangeProjectsForm';
import { waitAndUpdate, click } from 'sonar-ui-common/helpers/testUtils';
import ChangeProjectsForm from '../ChangeProjectsForm';
import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
import {
getProfileProjects,
@@ -28,6 +28,9 @@ import {
dissociateProject
} from '../../../../api/quality-profiles';

const profile: any = { key: 'profFile_key' };
const organization = 'TEST';

jest.mock('../../../../api/quality-profiles', () => ({
getProfileProjects: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 3, total: 55 },
@@ -41,95 +44,77 @@ jest.mock('../../../../api/quality-profiles', () => ({
dissociateProject: jest.fn().mockResolvedValue({})
}));

const profile: any = { key: 'profFile_key' };

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', async () => {
const wrapper = shallowRender();
wrapper
.find(SelectList)
.props()
.onSearch({
query: '',
filter: Filter.Selected,
page: 1,
pageSize: 100
});
await waitAndUpdate(wrapper);
expect(wrapper.instance().mounted).toBe(true);

expect(wrapper.instance().mounted).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
expect(getProfileProjects).toHaveBeenCalled();

wrapper.setState({ listHasBeenTouched: true });
expect(wrapper.find(SelectList).props().needReload).toBe(true);

wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
expect(wrapper.find(SelectList).props().needReload).toBe(false);

wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should handle reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleReload();
expect(getProfileProjects).toHaveBeenCalledWith(
expect.objectContaining({
p: 1
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});

it('should handle search reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSearch('foo', Filter.Selected);
expect(getProfileProjects).toHaveBeenCalledWith(
expect.objectContaining({
key: profile.key,
organization,
p: 1,
q: 'foo',
ps: 100,
q: undefined,
selected: Filter.Selected
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});

it('should handle load more properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state().needToReload).toBe(false);

wrapper.instance().handleLoadMore();
expect(getProfileProjects).toHaveBeenCalledWith(
expect.objectContaining({
p: 2
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should handle selection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSelect('toto');
await waitAndUpdate(wrapper);

expect(associateProject).toHaveBeenCalledWith(profile.key, 'toto');
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

it('should handle deselection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleUnselect('tata');
await waitAndUpdate(wrapper);

expect(dissociateProject).toHaveBeenCalledWith(profile.key, 'tata');
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

it('should close modal properly', () => {
const spy = jest.fn();
const wrapper = shallowRender({ onClose: spy });
click(wrapper.find('a'));

expect(spy).toHaveBeenCalled();
});

function shallowRender(props: Partial<ChangeProjectsForm['props']> = {}) {
return shallow<ChangeProjectsForm>(
<ChangeProjectsForm onClose={jest.fn()} organization="TEST" profile={profile} {...props} />
<ChangeProjectsForm
onClose={jest.fn()}
organization={organization}
profile={profile}
{...props}
/>
);
}

+ 2
- 3
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeProjectsForm-test.tsx.snap View File

@@ -29,9 +29,7 @@ exports[`should render correctly 1`] = `
labelAll="quality_gates.projects.all"
labelSelected="quality_gates.projects.with"
labelUnselected="quality_gates.projects.without"
needReload={false}
onLoadMore={[Function]}
onReload={[Function]}
needToReload={false}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
@@ -41,6 +39,7 @@ exports[`should render correctly 1`] = `
"test3",
]
}
withPaging={true}
/>
</div>
<div

+ 22
- 58
server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx View File

@@ -21,7 +21,10 @@ import * as React from 'react';
import { find, without } from 'lodash';
import { translate } from 'sonar-ui-common/helpers/l10n';
import Modal from 'sonar-ui-common/components/controls/Modal';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import SelectList, {
Filter,
SelectListSearchParams
} from '../../../components/SelectList/SelectList';
import { getUserGroups, UserGroup } from '../../../api/users';
import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';

@@ -31,25 +34,14 @@ interface Props {
user: T.User;
}

export interface SearchParams {
login: string;
organization?: string;
page: number;
pageSize: number;
query?: string;
selected: string;
}

interface State {
needToReload: boolean;
lastSearchParams?: SelectListSearchParams;
groups: UserGroup[];
groupsTotalCount?: number;
lastSearchParams: SearchParams;
listHasBeenTouched: boolean;
selectedGroups: string[];
}

const PAGE_SIZE = 100;

export default class GroupsForm extends React.PureComponent<Props, State> {
mounted = false;

@@ -57,39 +49,33 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
super(props);

this.state = {
needToReload: false,
groups: [],
lastSearchParams: {
login: props.user.login,
page: 1,
pageSize: PAGE_SIZE,
query: '',
selected: Filter.Selected
},
listHasBeenTouched: false,
selectedGroups: []
};
}

componentDidMount() {
this.mounted = true;
this.fetchUsers(this.state.lastSearchParams);
}

componentWillUnmount() {
this.mounted = false;
}

fetchUsers = (searchParams: SearchParams, more?: boolean) =>
fetchUsers = (searchParams: SelectListSearchParams) =>
getUserGroups({
login: searchParams.login,
organization: searchParams.organization !== '' ? searchParams.organization : undefined,
login: this.props.user.login,
organization: undefined,
p: searchParams.page,
ps: searchParams.pageSize,
q: searchParams.query !== '' ? searchParams.query : undefined,
selected: searchParams.selected
selected: searchParams.filter
}).then(data => {
if (this.mounted) {
this.setState(prevState => {
const more = searchParams.page != null && searchParams.page > 1;

const groups = more ? [...prevState.groups, ...data.groups] : data.groups;
const newSeletedGroups = data.groups.filter(gp => gp.selected).map(gp => gp.name);
const selectedGroups = more
@@ -98,7 +84,7 @@ export default class GroupsForm extends React.PureComponent<Props, State> {

return {
lastSearchParams: searchParams,
listHasBeenTouched: false,
needToReload: false,
groups,
groupsTotalCount: data.paging.total,
selectedGroups
@@ -107,29 +93,6 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
}
});

handleLoadMore = () =>
this.fetchUsers(
{
...this.state.lastSearchParams,
page: this.state.lastSearchParams.page + 1
},
true
);

handleReload = () =>
this.fetchUsers({
...this.state.lastSearchParams,
page: 1
});

handleSearch = (query: string, selected: Filter) =>
this.fetchUsers({
...this.state.lastSearchParams,
page: 1,
query,
selected
});

handleSelect = (name: string) =>
addUserToGroup({
name,
@@ -137,7 +100,7 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
}).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
listHasBeenTouched: true,
needToReload: true,
selectedGroups: [...state.selectedGroups, name]
}));
}
@@ -150,7 +113,7 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
}).then(() => {
if (this.mounted) {
this.setState((state: State) => ({
listHasBeenTouched: true,
needToReload: true,
selectedGroups: without(state.selectedGroups, name)
}));
}
@@ -196,16 +159,17 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
<SelectList
elements={this.state.groups.map(group => group.name)}
elementsTotalCount={this.state.groupsTotalCount}
needReload={
this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
needToReload={
this.state.needToReload &&
this.state.lastSearchParams &&
this.state.lastSearchParams.filter !== Filter.All
}
onLoadMore={this.handleLoadMore}
onReload={this.handleReload}
onSearch={this.handleSearch}
onSearch={this.fetchUsers}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
renderElement={this.renderElement}
selectedElements={this.state.selectedGroups}
withPaging={true}
/>
</div>


+ 40
- 53
server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx View File

@@ -19,13 +19,15 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import GroupsForm, { SearchParams } from '../GroupsForm';
import { waitAndUpdate, click } from 'sonar-ui-common/helpers/testUtils';
import GroupsForm from '../GroupsForm';
import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
import { getUserGroups } from '../../../../api/users';
import { addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups';
import { mockUser } from '../../../../helpers/testMocks';

const user = mockUser();

jest.mock('../../../../api/users', () => ({
getUserGroups: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 10, total: 1 },
@@ -63,93 +65,78 @@ beforeEach(() => {

it('should render correctly', async () => {
const wrapper = shallowRender();
wrapper
.find(SelectList)
.props()
.onSearch({
query: '',
filter: Filter.Selected,
page: 1,
pageSize: 100
});
await waitAndUpdate(wrapper);

expect(wrapper.instance().mounted).toBe(true);
expect(wrapper).toMatchSnapshot();
expect(getUserGroups).toHaveBeenCalledWith(
expect.objectContaining({
p: 1
})
);
expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();

wrapper.setState({ listHasBeenTouched: true });
expect(wrapper.find(SelectList).props().needReload).toBe(true);

wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
expect(wrapper.find(SelectList).props().needReload).toBe(false);
});

it('should handle reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleReload();
expect(getUserGroups).toHaveBeenCalledWith(
expect.objectContaining({
p: 1
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});

it('should handle search reload properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSearch('foo', Filter.Selected);
expect(getUserGroups).toHaveBeenCalledWith(
expect.objectContaining({
login: user.login,
organization: undefined,
p: 1,
q: 'foo',
ps: 100,
q: undefined,
selected: Filter.Selected
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
});

it('should handle load more properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state().needToReload).toBe(false);

wrapper.instance().handleLoadMore();
expect(getUserGroups).toHaveBeenCalledWith(
expect.objectContaining({
p: 2
})
);
expect(wrapper.state().listHasBeenTouched).toBe(false);
wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should handle selection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleSelect('toto');
await waitAndUpdate(wrapper);

expect(addUserToGroup).toHaveBeenCalledWith(
expect.objectContaining({
login: user.login,
name: 'toto'
})
);
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

it('should handle deselection properly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleUnselect('tata');
await waitAndUpdate(wrapper);

expect(removeUserFromGroup).toHaveBeenCalledWith(
expect.objectContaining({
login: user.login,
name: 'tata'
})
);
expect(wrapper.state().listHasBeenTouched).toBe(true);
expect(wrapper.state().needToReload).toBe(true);
});

it('should close modal properly', () => {
const spyOnClose = jest.fn();
const spyOnUpdateUsers = jest.fn();
const wrapper = shallowRender({ onClose: spyOnClose, onUpdateUsers: spyOnUpdateUsers });
click(wrapper.find('.js-modal-close'));

expect(spyOnClose).toHaveBeenCalled();
expect(spyOnUpdateUsers).toHaveBeenCalled();
});

function shallowRender(props: Partial<GroupsForm['props']> = {}) {
return shallow<GroupsForm>(
<GroupsForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={mockUser()} {...props} />
<GroupsForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={user} {...props} />
);
}

+ 26
- 3
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap View File

@@ -24,9 +24,7 @@ exports[`should render correctly 1`] = `
]
}
elementsTotalCount={1}
needReload={false}
onLoadMore={[Function]}
onReload={[Function]}
needToReload={false}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
@@ -37,6 +35,7 @@ exports[`should render correctly 1`] = `
"test2",
]
}
withPaging={true}
/>
</div>
<footer
@@ -52,3 +51,27 @@ exports[`should render correctly 1`] = `
</footer>
</Modal>
`;

exports[`should render correctly 2`] = `
<div
className="select-list-list-item"
>
<React.Fragment>
test1
<br />
<span
className="note"
>
test1
</span>
</React.Fragment>
</div>
`;

exports[`should render correctly 3`] = `
<div
className="select-list-list-item"
>
test_foo
</div>
`;

+ 67
- 30
server/sonar-web/src/main/js/components/SelectList/SelectList.tsx View File

@@ -39,55 +39,92 @@ interface Props {
labelSelected?: string;
labelUnselected?: string;
labelAll?: string;
needReload?: boolean;
onLoadMore?: () => Promise<void>;
onReload?: () => Promise<void>;
onSearch: (query: string, tab: Filter) => Promise<void>;
needToReload?: boolean;
onSearch: (searchParams: SelectListSearchParams) => Promise<void>;
onSelect: (element: string) => Promise<void>;
onUnselect: (element: string) => Promise<void>;
pageSize?: number;
readOnly?: boolean;
renderElement: (element: string) => React.ReactNode;
selectedElements: string[];
withPaging?: boolean;
}

interface State {
export interface SelectListSearchParams {
filter: Filter;
loading: boolean;
page?: number;
pageSize?: number;
query: string;
}

interface State {
lastSearchParams: SelectListSearchParams;
loading: boolean;
}

const DEFAULT_PAGE_SIZE = 100;

export default class SelectList extends React.PureComponent<Props, State> {
mounted = false;
state: State = { filter: Filter.Selected, loading: false, query: '' };

constructor(props: Props) {
super(props);

this.state = {
lastSearchParams: {
filter: Filter.Selected,
page: 1,
pageSize: props.pageSize ? props.pageSize : DEFAULT_PAGE_SIZE,
query: ''
},
loading: false
};
}

componentDidMount() {
this.mounted = true;
this.search({});
}

componentWillUnmount() {
this.mounted = false;
}

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};
getFilter = () =>
this.state.lastSearchParams.query === '' ? this.state.lastSearchParams.filter : Filter.All;

search = (searchParams: Partial<SelectListSearchParams>) =>
this.setState(
prevState => ({
loading: true,
lastSearchParams: { ...prevState.lastSearchParams, ...searchParams }
}),
() =>
this.props
.onSearch({
filter: this.getFilter(),
page: this.props.withPaging ? this.state.lastSearchParams.page : undefined,
pageSize: this.props.withPaging ? this.state.lastSearchParams.pageSize : undefined,
query: this.state.lastSearchParams.query
})
.finally(() => {
if (this.mounted) {
this.setState({ loading: false });
}
})
);

changeFilter = (filter: Filter) => this.search({ filter, page: 1 });

changeFilter = (filter: Filter) => {
this.setState({ filter, loading: true });
this.props.onSearch(this.state.query, filter).then(this.stopLoading, this.stopLoading);
};
handleQueryChange = (query: string) => this.search({ page: 1, query });

handleQueryChange = (query: string) => {
this.setState({ loading: true, query }, () => {
this.props.onSearch(query, this.getFilter()).then(this.stopLoading, this.stopLoading);
onLoadMore = () =>
this.search({
page:
this.state.lastSearchParams.page != null ? this.state.lastSearchParams.page + 1 : undefined
});
};

getFilter = () => {
return this.state.query === '' ? this.state.filter : Filter.All;
};
onReload = () => this.search({ page: 1 });

render() {
const {
@@ -95,9 +132,9 @@ export default class SelectList extends React.PureComponent<Props, State> {
labelUnselected = translate('unselected'),
labelAll = translate('all')
} = this.props;
const { filter } = this.state;
const { filter } = this.state.lastSearchParams;

const disabled = this.state.query !== '';
const disabled = this.state.lastSearchParams.query !== '';

return (
<div className="select-list">
@@ -118,7 +155,7 @@ export default class SelectList extends React.PureComponent<Props, State> {
loading={this.state.loading}
onChange={this.handleQueryChange}
placeholder={translate('search_verb')}
value={this.state.query}
value={this.state.lastSearchParams.query}
/>
</div>
<SelectListListContainer
@@ -132,12 +169,12 @@ export default class SelectList extends React.PureComponent<Props, State> {
renderElement={this.props.renderElement}
selectedElements={this.props.selectedElements}
/>
{!!this.props.elementsTotalCount && this.props.onLoadMore && (
{!!this.props.elementsTotalCount && (
<ListFooter
count={this.props.elements.length}
loadMore={this.props.onLoadMore}
needReload={this.props.needReload}
reload={this.props.onReload}
loadMore={this.onLoadMore}
needReload={this.props.needToReload}
reload={this.onReload}
total={this.props.elementsTotalCount}
/>
)}

+ 81
- 22
server/sonar-web/src/main/js/components/SelectList/__tests__/SelectList-test.tsx View File

@@ -22,67 +22,126 @@ import { shallow } from 'enzyme';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import SelectList, { Filter } from '../SelectList';

it('should display selected elements only by default', () => {
const wrapper = shallowRender();
expect(wrapper.state().filter).toBe(Filter.Selected);
});
const elements = ['foo', 'bar', 'baz'];
const selectedElements = [elements[0]];
const disabledElements = [elements[1]];

it('should display a loader when searching', async () => {
it('should display properly with basics features', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(wrapper.state().loading).toBe(false);
await waitAndUpdate(wrapper);
expect(wrapper.instance().mounted).toBe(true);

wrapper.instance().handleQueryChange('');
expect(wrapper.state().loading).toBe(true);
expect(wrapper).toMatchSnapshot();

wrapper.instance().componentWillUnmount();
expect(wrapper.instance().mounted).toBe(false);
});

it('should display properly with advanced features', async () => {
const wrapper = shallowRender({
allowBulkSelection: true,
elementsTotalCount: 125,
pageSize: 10,
readOnly: true,
withPaging: true
});
await waitAndUpdate(wrapper);
expect(wrapper.state().loading).toBe(false);

expect(wrapper).toMatchSnapshot();
});

it('should display a loader when updating filter', async () => {
it('should display a loader when searching', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper.state().loading).toBe(false);

wrapper.instance().changeFilter(Filter.Unselected);
wrapper.instance().search({});
expect(wrapper.state().loading).toBe(true);
expect(wrapper).toMatchSnapshot();

await waitAndUpdate(wrapper);
expect(wrapper.state().filter).toBe(Filter.Unselected);
expect(wrapper.state().loading).toBe(false);
});

it('should cancel filter selection when search is active', async () => {
const wrapper = shallowRender();
const spy = jest.fn().mockResolvedValue({});
const wrapper = shallowRender({ onSearch: spy });
wrapper.instance().changeFilter(Filter.Unselected);
await waitAndUpdate(wrapper);

expect(spy).toHaveBeenCalledWith({
query: '',
filter: Filter.Unselected,
page: undefined,
pageSize: undefined
});
expect(wrapper).toMatchSnapshot();

const query = 'test';
wrapper.instance().handleQueryChange(query);
expect(spy).toHaveBeenCalledWith({
query,
filter: Filter.All,
page: undefined,
pageSize: undefined
});

wrapper.setState({ filter: Filter.Selected });
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();

wrapper.setState({ query: 'foobar' });
wrapper.instance().handleQueryChange('');
expect(spy).toHaveBeenCalledWith({
query: '',
filter: Filter.Unselected,
page: undefined,
pageSize: undefined
});

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

it('should display pagination element properly', () => {
const wrapper = shallowRender({ elementsTotalCount: 100, onLoadMore: jest.fn() });
it('should display pagination element properly and call search method with correct parameters', () => {
const spy = jest.fn().mockResolvedValue({});
const wrapper = shallowRender({ elementsTotalCount: 100, onSearch: spy, withPaging: true });
expect(wrapper).toMatchSnapshot();
expect(spy).toHaveBeenCalledWith({
query: '',
filter: Filter.Selected,
page: 1,
pageSize: 100
}); // Basic default call

wrapper.instance().onLoadMore();
expect(spy).toHaveBeenCalledWith({
query: '',
filter: Filter.Selected,
page: 2,
pageSize: 100
}); // Load more call

wrapper.instance().onReload();
expect(spy).toHaveBeenCalledWith({
query: '',
filter: Filter.Selected,
page: 1,
pageSize: 100
}); // Reload call

wrapper.setProps({ needReload: true, onReload: jest.fn() });
wrapper.setProps({ needToReload: true });
expect(wrapper).toMatchSnapshot();
});

function shallowRender(props: Partial<SelectList['props']> = {}) {
return shallow<SelectList>(
<SelectList
elements={['foo', 'bar', 'baz']}
disabledElements={disabledElements}
elements={elements}
onSearch={jest.fn(() => Promise.resolve())}
onSelect={jest.fn(() => Promise.resolve())}
onUnselect={jest.fn(() => Promise.resolve())}
renderElement={(foo: string) => foo}
selectedElements={['foo']}
selectedElements={selectedElements}
{...props}
/>
);

+ 73
- 32
server/sonar-web/src/main/js/components/SelectList/__tests__/__snapshots__/SelectList-test.tsx.snap View File

@@ -31,7 +31,7 @@ exports[`should cancel filter selection when search is active 1`] = `
},
]
}
value="selected"
value="deselected"
/>
<SearchBox
autoFocus={true}
@@ -42,7 +42,11 @@ exports[`should cancel filter selection when search is active 1`] = `
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -50,7 +54,7 @@ exports[`should cancel filter selection when search is active 1`] = `
"baz",
]
}
filter="selected"
filter="deselected"
onSelect={[MockFunction]}
onUnselect={[MockFunction]}
renderElement={[Function]}
@@ -94,18 +98,22 @@ exports[`should cancel filter selection when search is active 2`] = `
},
]
}
value="selected"
value="deselected"
/>
<SearchBox
autoFocus={true}
loading={false}
onChange={[Function]}
placeholder="search_verb"
value="foobar"
value="test"
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -126,7 +134,7 @@ exports[`should cancel filter selection when search is active 2`] = `
</div>
`;

exports[`should display a loader when searching 1`] = `
exports[`should cancel filter selection when search is active 3`] = `
<div
className="select-list"
>
@@ -157,7 +165,7 @@ exports[`should display a loader when searching 1`] = `
},
]
}
value="selected"
value="deselected"
/>
<SearchBox
autoFocus={true}
@@ -168,7 +176,11 @@ exports[`should display a loader when searching 1`] = `
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -176,7 +188,7 @@ exports[`should display a loader when searching 1`] = `
"baz",
]
}
filter="selected"
filter="deselected"
onSelect={[MockFunction]}
onUnselect={[MockFunction]}
renderElement={[Function]}
@@ -189,7 +201,7 @@ exports[`should display a loader when searching 1`] = `
</div>
`;

exports[`should display a loader when searching 2`] = `
exports[`should display a loader when searching 1`] = `
<div
className="select-list"
>
@@ -231,7 +243,11 @@ exports[`should display a loader when searching 2`] = `
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -252,7 +268,7 @@ exports[`should display a loader when searching 2`] = `
</div>
`;

exports[`should display a loader when updating filter 1`] = `
exports[`should display pagination element properly and call search method with correct parameters 1`] = `
<div
className="select-list"
>
@@ -287,14 +303,18 @@ exports[`should display a loader when updating filter 1`] = `
/>
<SearchBox
autoFocus={true}
loading={false}
loading={true}
onChange={[Function]}
placeholder="search_verb"
value=""
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -312,10 +332,16 @@ exports[`should display a loader when updating filter 1`] = `
]
}
/>
<ListFooter
count={3}
loadMore={[Function]}
reload={[Function]}
total={100}
/>
</div>
`;

exports[`should display a loader when updating filter 2`] = `
exports[`should display pagination element properly and call search method with correct parameters 2`] = `
<div
className="select-list"
>
@@ -346,7 +372,7 @@ exports[`should display a loader when updating filter 2`] = `
},
]
}
value="deselected"
value="selected"
/>
<SearchBox
autoFocus={true}
@@ -357,7 +383,11 @@ exports[`should display a loader when updating filter 2`] = `
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -365,7 +395,7 @@ exports[`should display a loader when updating filter 2`] = `
"baz",
]
}
filter="deselected"
filter="selected"
onSelect={[MockFunction]}
onUnselect={[MockFunction]}
renderElement={[Function]}
@@ -375,10 +405,17 @@ exports[`should display a loader when updating filter 2`] = `
]
}
/>
<ListFooter
count={3}
loadMore={[Function]}
needReload={true}
reload={[Function]}
total={100}
/>
</div>
`;

exports[`should display pagination element properly 1`] = `
exports[`should display properly with advanced features 1`] = `
<div
className="select-list"
>
@@ -420,7 +457,12 @@ exports[`should display pagination element properly 1`] = `
/>
</div>
<SelectListListContainer
disabledElements={Array []}
allowBulkSelection={true}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -431,6 +473,7 @@ exports[`should display pagination element properly 1`] = `
filter="selected"
onSelect={[MockFunction]}
onUnselect={[MockFunction]}
readOnly={true}
renderElement={[Function]}
selectedElements={
Array [
@@ -440,13 +483,14 @@ exports[`should display pagination element properly 1`] = `
/>
<ListFooter
count={3}
loadMore={[MockFunction]}
total={100}
loadMore={[Function]}
reload={[Function]}
total={125}
/>
</div>
`;

exports[`should display pagination element properly 2`] = `
exports[`should display properly with basics features 1`] = `
<div
className="select-list"
>
@@ -488,7 +532,11 @@ exports[`should display pagination element properly 2`] = `
/>
</div>
<SelectListListContainer
disabledElements={Array []}
disabledElements={
Array [
"bar",
]
}
elements={
Array [
"foo",
@@ -506,12 +554,5 @@ exports[`should display pagination element properly 2`] = `
]
}
/>
<ListFooter
count={3}
loadMore={[MockFunction]}
needReload={true}
reload={[MockFunction]}
total={100}
/>
</div>
`;

Loading…
Cancel
Save