);
}
-export function searchGates(data: {
+export function searchProjects(data: {
gateId: number;
organization?: string;
page?: number;
pageSize?: number;
query?: string;
selected?: string;
-}): Promise<{ more: boolean; results: Array<{ id: string; name: string; selected: boolean }> }> {
+}): Promise<{ paging: T.Paging; results: Array<{ id: string; name: string; selected: boolean }> }> {
return getJSON('/api/qualitygates/search', data).catch(throwGlobalError);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
import { find, without } from 'lodash';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
-import { translate } from '../../../helpers/l10n';
+import * as React from 'react';
import {
- searchGates,
associateGateWithProject,
- dissociateGateWithProject
+ dissociateGateWithProject,
+ searchProjects
} from '../../../api/quality-gates';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import { translate } from '../../../helpers/l10n';
interface Props {
canEdit?: boolean;
qualityGate: T.QualityGate;
}
+export interface SearchParams {
+ gateId: number;
+ organization?: string;
+ page: number;
+ pageSize: number;
+ query?: string;
+ selected: string;
+}
+
interface State {
+ lastSearchParams: SearchParams;
+ listHasBeenTouched: boolean;
projects: Array<{ id: string; name: string; selected: boolean }>;
+ projectsTotalCount?: number;
selectedProjects: string[];
}
+const PAGE_SIZE = 100;
+
export default class Projects extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { projects: [], selectedProjects: [] };
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ lastSearchParams: {
+ gateId: props.qualityGate.id,
+ organization: props.organization,
+ page: 1,
+ pageSize: PAGE_SIZE,
+ query: '',
+ selected: Filter.Selected
+ },
+ listHasBeenTouched: false,
+ projects: [],
+ selectedProjects: []
+ };
+ }
componentDidMount() {
this.mounted = true;
- this.handleSearch('', Filter.Selected);
+ this.fetchProjects(this.state.lastSearchParams);
}
componentWillUnmount() {
this.mounted = false;
}
- handleSearch = (query: string, selected: string) => {
- return searchGates({
- gateId: this.props.qualityGate.id,
- organization: this.props.organization,
- pageSize: 100,
- query: query !== '' ? query : undefined,
- selected
+ fetchProjects = (searchParams: SearchParams, more?: boolean) =>
+ searchProjects({
+ ...searchParams,
+ query: searchParams.query !== '' ? searchParams.query : undefined
}).then(data => {
if (this.mounted) {
- this.setState({
- projects: data.results,
- selectedProjects: data.results
+ this.setState(prevState => {
+ const projects = more ? [...prevState.projects, ...data.results] : data.results;
+ const newSelectedProjects = data.results
.filter(project => project.selected)
- .map(project => project.id)
+ .map(project => project.id);
+ const selectedProjects = more
+ ? [...prevState.selectedProjects, ...newSelectedProjects]
+ : newSelectedProjects;
+
+ return {
+ lastSearchParams: searchParams,
+ listHasBeenTouched: false,
+ projects,
+ projectsTotalCount: data.paging.total,
+ selectedProjects
+ };
});
}
});
- };
- handleSelect = (id: string) => {
- return associateGateWithProject({
+ 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,
organization: this.props.organization,
projectId: id
}).then(() => {
if (this.mounted) {
this.setState(state => ({
+ listHasBeenTouched: true,
selectedProjects: [...state.selectedProjects, id]
}));
}
});
- };
- handleUnselect = (id: string) => {
- return dissociateGateWithProject({
+ handleUnselect = (id: string) =>
+ dissociateGateWithProject({
gateId: this.props.qualityGate.id,
organization: this.props.organization,
projectId: id
- }).then(
- () => {
- if (this.mounted) {
- this.setState(state => ({
- selectedProjects: without(state.selectedProjects, id)
- }));
- }
- },
- () => {}
- );
- };
+ }).then(() => {
+ if (this.mounted) {
+ this.setState(state => ({
+ listHasBeenTouched: true,
+ selectedProjects: without(state.selectedProjects, id)
+ }));
+ }
+ });
renderElement = (id: string): React.ReactNode => {
const project = find(this.state.projects, { id });
return (
<SelectList
elements={this.state.projects.map(project => project.id)}
+ elementsTotalCount={this.state.projectsTotalCount}
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
+ }
+ onLoadMore={this.handleLoadMore}
+ onReload={this.handleReload}
onSearch={this.handleSearch}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Projects, { SearchParams } from '../Projects';
+import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
+import { mockQualityGate } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import {
+ searchProjects,
+ associateGateWithProject,
+ dissociateGateWithProject
+} from '../../../../api/quality-gates';
+
+jest.mock('../../../../api/quality-gates', () => ({
+ searchProjects: jest.fn().mockResolvedValue({
+ paging: { pageIndex: 1, pageSize: 3, total: 55 },
+ results: [
+ { id: 'test1', name: 'test1', selected: false },
+ { id: 'test2', name: 'test2', selected: false },
+ { id: 'test3', name: 'test3', selected: true }
+ ]
+ }),
+ associateGateWithProject: jest.fn().mockResolvedValue({}),
+ dissociateGateWithProject: jest.fn().mockResolvedValue({})
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper).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);
+});
+
+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({
+ page: 1,
+ query: 'foo',
+ 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(searchProjects).toHaveBeenCalledWith(
+ expect.objectContaining({
+ page: 2
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).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);
+});
+
+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);
+});
+
+function shallowRender(props: Partial<Projects['props']> = {}) {
+ return shallow<Projects>(<Projects qualityGate={mockQualityGate()} {...props} />);
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<SelectList
+ elements={
+ Array [
+ "test1",
+ "test2",
+ "test3",
+ ]
+ }
+ elementsTotalCount={55}
+ labelAll="quality_gates.projects.all"
+ labelSelected="quality_gates.projects.with"
+ labelUnselected="quality_gates.projects.without"
+ needReload={false}
+ onLoadMore={[Function]}
+ onReload={[Function]}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ readOnly={true}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "test3",
+ ]
+ }
+/>
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
import { find, without } from 'lodash';
-import Modal from '../../../components/controls/Modal';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
-import { translate } from '../../../helpers/l10n';
-import { Profile } from '../types';
+import * as React from 'react';
import {
- getProfileProjects,
associateProject,
dissociateProject,
+ getProfileProjects,
ProfileProject
} from '../../../api/quality-profiles';
+import Modal from '../../../components/controls/Modal';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import { translate } from '../../../helpers/l10n';
+import { Profile } from '../types';
interface Props {
onClose: () => void;
profile: Profile;
}
+export interface SearchParams {
+ key: string;
+ organization: string | null;
+ page: number;
+ pageSize: number;
+ query?: string;
+ selected: string;
+}
+
interface State {
+ lastSearchParams: SearchParams;
+ listHasBeenTouched: boolean;
projects: ProfileProject[];
+ projectsTotalCount?: number;
selectedProjects: string[];
}
-export default class ChangeProjectsForm extends React.PureComponent<Props> {
- container?: HTMLElement | null;
- state: State = { projects: [], selectedProjects: [] };
+const PAGE_SIZE = 100;
+
+export default class ChangeProjectsForm extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ lastSearchParams: {
+ key: props.profile.key,
+ organization: props.organization,
+ page: 1,
+ pageSize: PAGE_SIZE,
+ query: '',
+ selected: Filter.Selected
+ },
+ listHasBeenTouched: false,
+ projects: [],
+ selectedProjects: []
+ };
+ }
componentDidMount() {
- this.handleSearch('', Filter.Selected);
+ this.mounted = true;
+ this.fetchProjects(this.state.lastSearchParams);
}
- handleSearch = (query: string, selected: Filter) => {
- return getProfileProjects({
- key: this.props.profile.key,
- organization: this.props.organization,
- pageSize: 100,
- query: query !== '' ? query : undefined,
- selected
- }).then(
- data => {
- this.setState({
- projects: data.results,
- selectedProjects: data.results
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchProjects = (searchParams: SearchParams, more?: boolean) =>
+ getProfileProjects({
+ ...searchParams,
+ p: searchParams.page,
+ ps: searchParams.pageSize,
+ q: searchParams.query !== '' ? searchParams.query : undefined
+ }).then(data => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const projects = more ? [...prevState.projects, ...data.results] : data.results;
+ const newSeletedProjects = data.results
.filter(project => project.selected)
- .map(project => project.key)
+ .map(project => project.key);
+ const selectedProjects = more
+ ? [...prevState.selectedProjects, ...newSeletedProjects]
+ : newSeletedProjects;
+
+ return {
+ lastSearchParams: searchParams,
+ listHasBeenTouched: false,
+ projects,
+ projectsTotalCount: data.paging.total,
+ selectedProjects
+ };
});
+ }
+ });
+
+ handleLoadMore = () =>
+ this.fetchProjects(
+ {
+ ...this.state.lastSearchParams,
+ page: this.state.lastSearchParams.page + 1
},
- () => {}
+ true
);
- };
- handleSelect = (key: string) => {
- return associateProject(this.props.profile.key, key).then(() => {
- this.setState((state: State) => ({
- selectedProjects: [...state.selectedProjects, key]
- }));
+ handleReload = () =>
+ this.fetchProjects({
+ ...this.state.lastSearchParams,
+ page: 1
});
- };
- handleUnselect = (key: string) => {
- return dissociateProject(this.props.profile.key, key).then(() => {
- this.setState((state: State) => ({ selectedProjects: without(state.selectedProjects, key) }));
+ 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,
+ selectedProjects: [...state.selectedProjects, key]
+ }));
+ }
+ });
+
+ handleUnselect = (key: string) =>
+ dissociateProject(this.props.profile.key, key).then(() => {
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ listHasBeenTouched: true,
+ selectedProjects: without(state.selectedProjects, key)
+ }));
+ }
});
- };
handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
<SelectList
allowBulkSelection={true}
elements={this.state.projects.map(project => project.key)}
+ elementsTotalCount={this.state.projectsTotalCount}
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
+ }
+ onLoadMore={this.handleLoadMore}
+ onReload={this.handleReload}
onSearch={this.handleSearch}
onSelect={this.handleSelect}
onUnselect={this.handleUnselect}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import ChangeProjectsForm, { SearchParams } from '../ChangeProjectsForm';
+import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import {
+ getProfileProjects,
+ associateProject,
+ dissociateProject
+} from '../../../../api/quality-profiles';
+
+jest.mock('../../../../api/quality-profiles', () => ({
+ getProfileProjects: jest.fn().mockResolvedValue({
+ paging: { pageIndex: 1, pageSize: 3, total: 55 },
+ results: [
+ { id: 'test1', key: 'test1', name: 'test1', selected: false },
+ { id: 'test2', key: 'test2', name: 'test2', selected: false },
+ { id: 'test3', key: 'test3', name: 'test3', selected: true }
+ ]
+ }),
+ associateProject: jest.fn().mockResolvedValue({}),
+ dissociateProject: jest.fn().mockResolvedValue({})
+}));
+
+const profile: any = { key: 'profFile_key' };
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper).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);
+});
+
+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({
+ p: 1,
+ q: 'foo',
+ 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(getProfileProjects).toHaveBeenCalledWith(
+ expect.objectContaining({
+ p: 2
+ })
+ );
+ expect(wrapper.state().listHasBeenTouched).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);
+});
+
+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);
+});
+
+function shallowRender(props: Partial<ChangeProjectsForm['props']> = {}) {
+ return shallow<ChangeProjectsForm>(
+ <ChangeProjectsForm onClose={jest.fn()} organization={'TEST'} profile={profile} {...props} />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Modal
+ contentLabel="projects"
+ onRequestClose={[MockFunction]}
+>
+ <div
+ className="modal-head"
+ >
+ <h2>
+ projects
+ </h2>
+ </div>
+ <div
+ className="modal-body"
+ id="profile-projects"
+ >
+ <SelectList
+ allowBulkSelection={true}
+ elements={
+ Array [
+ "test1",
+ "test2",
+ "test3",
+ ]
+ }
+ elementsTotalCount={55}
+ labelAll="quality_gates.projects.all"
+ labelSelected="quality_gates.projects.with"
+ labelUnselected="quality_gates.projects.without"
+ needReload={false}
+ onLoadMore={[Function]}
+ onReload={[Function]}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "test3",
+ ]
+ }
+ />
+ </div>
+ <div
+ className="modal-foot"
+ >
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ close
+ </a>
+ </div>
+</Modal>
+`;
import * as React from 'react';
import SelectListListContainer from './SelectListListContainer';
import { translate } from '../../helpers/l10n';
-import SearchBox from '../controls/SearchBox';
+import ListFooter from '../controls/ListFooter';
import RadioToggle from '../controls/RadioToggle';
+import SearchBox from '../controls/SearchBox';
import './styles.css';
export enum Filter {
interface Props {
allowBulkSelection?: boolean;
elements: string[];
+ elementsTotalCount?: number;
disabledElements?: string[];
labelSelected?: string;
labelUnselected?: string;
labelAll?: string;
+ needReload?: boolean;
+ onLoadMore?: () => Promise<void>;
+ onReload?: () => Promise<void>;
onSearch: (query: string, tab: Filter) => Promise<void>;
onSelect: (element: string) => Promise<void>;
onUnselect: (element: string) => Promise<void>;
renderElement={this.props.renderElement}
selectedElements={this.props.selectedElements}
/>
+ {!!this.props.elementsTotalCount && this.props.onLoadMore && (
+ <ListFooter
+ count={this.props.elements.length}
+ loadMore={this.props.onLoadMore}
+ needReload={this.props.needReload}
+ reload={this.props.onReload}
+ total={this.props.elementsTotalCount}
+ />
+ )}
</div>
);
}
render() {
const { allowBulkSelection, elements, filter } = this.props;
- const filteredElements = elements.filter(element => {
- if (filter === Filter.All) {
- return true;
- }
- const isSelected = this.isSelected(element);
- return filter === Filter.Selected ? isSelected : !isSelected;
- });
return (
<div className={classNames('select-list-list-container spacer-top')}>
elements.length > 0 &&
filter === Filter.All &&
this.renderBulkSelector()}
- {filteredElements.map(element => (
+ {elements.map(element => (
<SelectListListElement
disabled={this.isDisabled(element)}
element={element}
import SelectList, { Filter } from '../SelectList';
import { waitAndUpdate } from '../../../helpers/testUtils';
-const selectList = (
- <SelectList
- elements={['foo', 'bar', 'baz']}
- onSearch={jest.fn(() => Promise.resolve())}
- onSelect={jest.fn(() => Promise.resolve())}
- onUnselect={jest.fn(() => Promise.resolve())}
- renderElement={(foo: string) => foo}
- selectedElements={['foo']}
- />
-);
-
it('should display selected elements only by default', () => {
- const wrapper = shallow<SelectList>(selectList);
+ const wrapper = shallowRender();
expect(wrapper.state().filter).toBe(Filter.Selected);
});
it('should display a loader when searching', async () => {
- const wrapper = shallow<SelectList>(selectList);
+ const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(wrapper.state().loading).toBe(false);
});
it('should display a loader when updating filter', async () => {
- const wrapper = shallow<SelectList>(selectList);
+ const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(wrapper.state().loading).toBe(false);
});
it('should cancel filter selection when search is active', async () => {
- const wrapper = shallow<SelectList>(selectList);
+ const wrapper = shallowRender();
wrapper.setState({ filter: Filter.Selected });
await waitAndUpdate(wrapper);
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});
+
+it('should display pagination element properly', () => {
+ const wrapper = shallowRender({ elementsTotalCount: 100, onLoadMore: jest.fn() });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setProps({ needReload: true, onReload: jest.fn() });
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<SelectList['props']> = {}) {
+ return shallow<SelectList>(
+ <SelectList
+ elements={['foo', 'bar', 'baz']}
+ onSearch={jest.fn(() => Promise.resolve())}
+ onSelect={jest.fn(() => Promise.resolve())}
+ onUnselect={jest.fn(() => Promise.resolve())}
+ renderElement={(foo: string) => foo}
+ selectedElements={['foo']}
+ {...props}
+ />
+ );
+}
import SelectListListContainer from '../SelectListListContainer';
import { Filter } from '../SelectList';
-const elementsContainer = (
- <SelectListListContainer
- disabledElements={[]}
- elements={['foo', 'bar', 'baz']}
- filter={Filter.All}
- onSelect={jest.fn(() => Promise.resolve())}
- onUnselect={jest.fn(() => Promise.resolve())}
- renderElement={(foo: string) => foo}
- selectedElements={['foo']}
- />
-);
-
-it('should display elements based on filters', () => {
- const wrapper = shallow(elementsContainer);
- expect(wrapper.find('SelectListListElement')).toHaveLength(3);
- expect(wrapper).toMatchSnapshot();
-
- wrapper.setProps({ filter: Filter.Unselected });
- expect(wrapper.find('SelectListListElement')).toHaveLength(2);
- expect(wrapper).toMatchSnapshot();
-
- wrapper.setProps({ filter: Filter.Selected });
- expect(wrapper.find('SelectListListElement')).toHaveLength(1);
+it('should render correctly', () => {
+ const wrapper = shallow(
+ <SelectListListContainer
+ allowBulkSelection={true}
+ disabledElements={[]}
+ elements={['foo', 'bar', 'baz']}
+ filter={Filter.All}
+ onSelect={jest.fn(() => Promise.resolve())}
+ onUnselect={jest.fn(() => Promise.resolve())}
+ renderElement={(foo: string) => foo}
+ selectedElements={['foo']}
+ />
+ );
expect(wrapper).toMatchSnapshot();
});
/>
</div>
`;
+
+exports[`should display pagination element properly 1`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+ <ListFooter
+ count={3}
+ loadMore={[MockFunction]}
+ total={100}
+ />
+</div>
+`;
+
+exports[`should display pagination element properly 2`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+ <ListFooter
+ count={3}
+ loadMore={[MockFunction]}
+ needReload={true}
+ reload={[MockFunction]}
+ total={100}
+ />
+</div>
+`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should display elements based on filters 1`] = `
+exports[`should render correctly 1`] = `
<div
className="select-list-list-container spacer-top"
>
<ul
className="menu"
>
+ <li>
+ <Checkbox
+ checked={true}
+ onCheck={[Function]}
+ thirdState={true}
+ >
+ <span
+ className="big-spacer-left"
+ >
+ bulk_change
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={10}
+ />
+ </span>
+ </Checkbox>
+ </li>
+ <li
+ className="divider"
+ />
<SelectListListElement
disabled={false}
element="foo"
</ul>
</div>
`;
-
-exports[`should display elements based on filters 2`] = `
-<div
- className="select-list-list-container spacer-top"
->
- <ul
- className="menu"
- >
- <SelectListListElement
- disabled={false}
- element="bar"
- key="bar"
- onSelect={[MockFunction]}
- onUnselect={[MockFunction]}
- renderElement={[Function]}
- selected={false}
- />
- <SelectListListElement
- disabled={false}
- element="baz"
- key="baz"
- onSelect={[MockFunction]}
- onUnselect={[MockFunction]}
- renderElement={[Function]}
- selected={false}
- />
- </ul>
-</div>
-`;
-
-exports[`should display elements based on filters 3`] = `
-<div
- className="select-list-list-container spacer-top"
->
- <ul
- className="menu"
- >
- <SelectListListElement
- disabled={false}
- element="foo"
- key="foo"
- onSelect={[MockFunction]}
- onUnselect={[MockFunction]}
- renderElement={[Function]}
- selected={true}
- />
- </ul>
-</div>
-`;
import { translate, translateWithParameters } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
-interface Props {
+export interface Props {
count: number;
className?: string;
loading?: boolean;
loadMore?: () => void;
+ needReload?: boolean;
+ reload?: () => void;
ready?: boolean;
total?: number;
}
}
};
+ const handleReload = (event: React.SyntheticEvent<HTMLElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ if (props.reload) {
+ props.reload();
+ }
+ };
+
const hasMore = props.total && props.total > props.count;
+
const loadMoreLink = (
<a className="spacer-left" href="#" onClick={handleLoadMore}>
{translate('show_more')}
</a>
);
+
+ const reloadLink = (
+ <a className="spacer-left" href="#" onClick={handleReload}>
+ {translate('reload')}
+ </a>
+ );
+
const className = classNames(
'spacer-top note text-center',
{ 'new-loading': !ready },
props.className
);
+ let link;
+
+ if (props.needReload && props.reload) {
+ link = reloadLink;
+ } else if (hasMore && props.loadMore) {
+ link = loadMoreLink;
+ }
+
return (
<footer className={className}>
{translateWithParameters(
formatMeasure(props.count, 'INT', null),
formatMeasure(props.total, 'INT', null)
)}
- {props.loadMore != null && hasMore ? loadMoreLink : null}
+ {link}
{props.loading && <DeferredSpinner className="text-bottom spacer-left position-absolute" />}
</footer>
);
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import ListFooter from '../ListFooter';
+import ListFooter, { Props } from '../ListFooter';
import { click } from '../../../helpers/testUtils';
it('should render "3 of 5 shown"', () => {
- const listFooter = shallow(<ListFooter count={3} total={5} />);
+ const listFooter = shallowRender();
expect(listFooter.text()).toContain('x_of_y_shown.3.5');
+ expect(listFooter).toMatchSnapshot();
});
it('should not render "show more"', () => {
- const listFooter = shallow(<ListFooter count={3} total={5} />);
+ const listFooter = shallowRender({ loadMore: undefined });
expect(listFooter.find('a').length).toBe(0);
});
it('should not render "show more"', () => {
- const listFooter = shallow(<ListFooter count={5} loadMore={jest.fn()} total={5} />);
+ const listFooter = shallowRender({ count: 5 });
expect(listFooter.find('a').length).toBe(0);
});
it('should "show more"', () => {
const loadMore = jest.fn();
- const listFooter = shallow(<ListFooter count={3} loadMore={loadMore} total={5} />);
+ const listFooter = shallowRender({ loadMore });
const link = listFooter.find('a');
expect(link.length).toBe(1);
click(link);
expect(loadMore).toBeCalled();
});
+it('should render "reload" properly', () => {
+ const listFooter = shallowRender({ needReload: true });
+ expect(listFooter).toMatchSnapshot();
+
+ const reload = jest.fn();
+
+ listFooter.setProps({ reload });
+ expect(listFooter).toMatchSnapshot();
+
+ const link = listFooter.find('a');
+ expect(link.length).toBe(1);
+
+ click(link);
+ expect(reload).toBeCalled();
+});
+
it('should display spinner while loading', () => {
expect(
- shallow(<ListFooter count={3} loadMore={jest.fn()} loading={true} total={10} />)
+ shallowRender({ loading: true })
.find('DeferredSpinner')
.exists()
).toBe(true);
});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(<ListFooter count={3} loadMore={jest.fn()} total={5} {...props} />);
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render "3 of 5 shown" 1`] = `
+<footer
+ className="spacer-top note text-center"
+>
+ x_of_y_shown.3.5
+ <a
+ className="spacer-left"
+ href="#"
+ onClick={[Function]}
+ >
+ show_more
+ </a>
+</footer>
+`;
+
+exports[`should render "reload" properly 1`] = `
+<footer
+ className="spacer-top note text-center"
+>
+ x_of_y_shown.3.5
+ <a
+ className="spacer-left"
+ href="#"
+ onClick={[Function]}
+ >
+ show_more
+ </a>
+</footer>
+`;
+
+exports[`should render "reload" properly 2`] = `
+<footer
+ className="spacer-top note text-center"
+>
+ x_of_y_shown.3.5
+ <a
+ className="spacer-left"
+ href="#"
+ onClick={[Function]}
+ >
+ reload
+ </a>
+</footer>
+`;