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