]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12244 Handle pagination in portfolio projects list properly
authorphilippe-perrin-sonarsource <philippe.perrin@sonarsource.com>
Tue, 2 Jul 2019 11:53:28 +0000 (13:53 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 18 Jul 2019 18:21:12 +0000 (20:21 +0200)
15 files changed:
server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Projects-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeProjectsForm-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap
server/sonar-web/src/main/js/components/SelectList/SelectList.tsx
server/sonar-web/src/main/js/components/SelectList/__tests__/SelectList-test.tsx
server/sonar-web/src/main/js/components/SelectList/__tests__/__snapshots__/SelectList-test.tsx.snap

index 712206e016f2669b3e3cf8b65af6ed2012e90fde..5bfeb5e20bfc6e7b290c1c16fbddbc239384b1af 100644 (file)
@@ -22,8 +22,10 @@ import { find, without } from 'lodash';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
 import Modal from 'sonar-ui-common/components/controls/Modal';
-import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import SelectList, {
+  Filter,
+  SelectListSearchParams
+} from '../../../components/SelectList/SelectList';
 import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../api/user_groups';
 
 interface Props {
@@ -32,26 +34,14 @@ interface Props {
   organization: string | undefined;
 }
 
-export interface SearchParams {
-  name: string;
-  organization?: string;
-  page: number;
-  pageSize: number;
-  query?: string;
-  selected: string;
-}
-
 interface State {
-  lastSearchParams: SearchParams;
-  listHasBeenTouched: boolean;
-  loading: boolean;
+  lastSearchParams?: SelectListSearchParams;
+  needToReload: boolean;
   users: T.UserSelected[];
   usersTotalCount?: number;
   selectedUsers: string[];
 }
 
-const PAGE_SIZE = 100;
-
 export default class EditMembersModal extends React.PureComponent<Props, State> {
   mounted = false;
 
@@ -59,16 +49,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
     super(props);
 
     this.state = {
-      lastSearchParams: {
-        name: props.group.name,
-        organization: props.organization,
-        page: 1,
-        pageSize: PAGE_SIZE,
-        query: '',
-        selected: Filter.Selected
-      },
-      listHasBeenTouched: false,
-      loading: true,
+      needToReload: false,
       users: [],
       selectedUsers: []
     };
@@ -76,70 +57,41 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
 
   componentDidMount() {
     this.mounted = true;
-    this.fetchUsers(this.state.lastSearchParams);
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
-  fetchUsers = (searchParams: SearchParams, more?: boolean) =>
+  fetchUsers = (searchParams: SelectListSearchParams) =>
     getUsersInGroup({
-      ...searchParams,
+      name: this.props.group.name,
+      organization: this.props.organization,
       p: searchParams.page,
       ps: searchParams.pageSize,
-      q: searchParams.query !== '' ? searchParams.query : undefined
-    }).then(
-      data => {
-        if (this.mounted) {
-          this.setState(prevState => {
-            const users = more ? [...prevState.users, ...data.users] : data.users;
-            const newSelectedUsers = data.users
-              .filter(user => user.selected)
-              .map(user => user.login);
-            const selectedUsers = more
-              ? [...prevState.selectedUsers, ...newSelectedUsers]
-              : newSelectedUsers;
-
-            return {
-              lastSearchParams: searchParams,
-              listHasBeenTouched: false,
-              loading: false,
-              users,
-              usersTotalCount: data.total,
-              selectedUsers
-            };
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
+      q: searchParams.query !== '' ? searchParams.query : undefined,
+      selected: searchParams.filter
+    }).then(data => {
+      if (this.mounted) {
+        this.setState(prevState => {
+          const more = searchParams.page != null && searchParams.page > 1;
+
+          const users = more ? [...prevState.users, ...data.users] : data.users;
+          const newSelectedUsers = data.users.filter(user => user.selected).map(user => user.login);
+          const selectedUsers = more
+            ? [...prevState.selectedUsers, ...newSelectedUsers]
+            : newSelectedUsers;
+
+          return {
+            needToReload: false,
+            lastSearchParams: searchParams,
+            loading: false,
+            users,
+            usersTotalCount: data.total,
+            selectedUsers
+          };
+        });
       }
-    );
-
-  handleLoadMore = () =>
-    this.fetchUsers(
-      {
-        ...this.state.lastSearchParams,
-        page: this.state.lastSearchParams.page + 1
-      },
-      true
-    );
-
-  handleReload = () =>
-    this.fetchUsers({
-      ...this.state.lastSearchParams,
-      page: 1
-    });
-
-  handleSearch = (query: string, selected: Filter) =>
-    this.fetchUsers({
-      ...this.state.lastSearchParams,
-      page: 1,
-      query,
-      selected
     });
 
   handleSelect = (login: string) =>
@@ -150,7 +102,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
     }).then(() => {
       if (this.mounted) {
         this.setState((state: State) => ({
-          listHasBeenTouched: true,
+          needToReload: true,
           selectedUsers: [...state.selectedUsers, login]
         }));
       }
@@ -164,7 +116,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
     }).then(() => {
       if (this.mounted) {
         this.setState((state: State) => ({
-          listHasBeenTouched: true,
+          needToReload: true,
           selectedUsers: without(state.selectedUsers, login)
         }));
       }
@@ -196,22 +148,21 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
         </header>
 
         <div className="modal-body modal-container">
-          <DeferredSpinner loading={this.state.loading}>
-            <SelectList
-              elements={this.state.users.map(user => user.login)}
-              elementsTotalCount={this.state.usersTotalCount}
-              needReload={
-                this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
-              }
-              onLoadMore={this.handleLoadMore}
-              onReload={this.handleReload}
-              onSearch={this.handleSearch}
-              onSelect={this.handleSelect}
-              onUnselect={this.handleUnselect}
-              renderElement={this.renderElement}
-              selectedElements={this.state.selectedUsers}
-            />
-          </DeferredSpinner>
+          <SelectList
+            elements={this.state.users.map(user => user.login)}
+            elementsTotalCount={this.state.usersTotalCount}
+            needToReload={
+              this.state.needToReload &&
+              this.state.lastSearchParams &&
+              this.state.lastSearchParams.filter !== Filter.All
+            }
+            onSearch={this.fetchUsers}
+            onSelect={this.handleSelect}
+            onUnselect={this.handleUnselect}
+            renderElement={this.renderElement}
+            selectedElements={this.state.selectedUsers}
+            withPaging={true}
+          />
         </div>
 
         <footer className="modal-foot">
index e0d134373c4d4c43de6d095cc3b9d2108597458e..ed9e065877c41ff1ead2ef1ebcb6377603f8b55f 100644 (file)
 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} />
   );
 }
index faf0598e59b37af17de88d84ee24f686a11f3e7e..874619ef9ea413718bb90a4db54395eb34f3b48c 100644 (file)
@@ -15,30 +15,24 @@ exports[`should render modal properly 1`] = `
   <div
     className="modal-body modal-container"
   >
-    <DeferredSpinner
-      loading={false}
-      timeout={100}
-    >
-      <SelectList
-        elements={
-          Array [
-            "foo",
-          ]
-        }
-        needReload={false}
-        onLoadMore={[Function]}
-        onReload={[Function]}
-        onSearch={[Function]}
-        onSelect={[Function]}
-        onUnselect={[Function]}
-        renderElement={[Function]}
-        selectedElements={
-          Array [
-            "foo",
-          ]
-        }
-      />
-    </DeferredSpinner>
+    <SelectList
+      elements={
+        Array [
+          "foo",
+        ]
+      }
+      needToReload={false}
+      onSearch={[Function]}
+      onSelect={[Function]}
+      onUnselect={[Function]}
+      renderElement={[Function]}
+      selectedElements={
+        Array [
+          "foo",
+        ]
+      }
+      withPaging={true}
+    />
   </div>
   <footer
     className="modal-foot"
@@ -51,3 +45,19 @@ exports[`should render modal properly 1`] = `
   </footer>
 </Modal>
 `;
+
+exports[`should render modal properly 2`] = `
+<div
+  className="select-list-list-item"
+>
+  test1
+</div>
+`;
+
+exports[`should render modal properly 3`] = `
+<div
+  className="select-list-list-item"
+>
+  test_foo
+</div>
+`;
index f1bd32dd9e94565269da1879248a1dcc08458e55..a64bf9ece871c0f09a26b2f7d7807c0564a27b6b 100644 (file)
 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}
       />
     );
   }
index 555c8859ede7f2b6c1e991a23f4ca686ea4a41d8..572808e4466a94dcf7f3b60236c0b887ff7164ba 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import Projects, { SearchParams } from '../Projects';
+import Projects from '../Projects';
 import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
 import { mockQualityGate } from '../../../../helpers/testMocks';
 import {
@@ -29,6 +29,9 @@ import {
   dissociateGateWithProject
 } from '../../../../api/quality-gates';
 
+const qualityGate = mockQualityGate();
+const organization = 'TEST';
+
 jest.mock('../../../../api/quality-gates', () => ({
   searchProjects: jest.fn().mockResolvedValue({
     paging: { pageIndex: 1, pageSize: 3, total: 55 },
@@ -48,97 +51,66 @@ beforeEach(() => {
 
 it('should render correctly', async () => {
   const wrapper = shallowRender();
+  wrapper
+    .find(SelectList)
+    .props()
+    .onSearch({
+      query: '',
+      filter: Filter.Selected,
+      page: 1,
+      pageSize: 100
+    });
   await waitAndUpdate(wrapper);
-  expect(wrapper.instance().mounted).toBe(true);
 
+  expect(wrapper.instance().mounted).toBe(true);
   expect(wrapper).toMatchSnapshot();
   expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
   expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
-  expect(searchProjects).toHaveBeenCalledWith(
-    expect.objectContaining({
-      page: 1
-    })
-  );
-
-  wrapper.setState({ listHasBeenTouched: true });
-  expect(wrapper.find(SelectList).props().needReload).toBe(true);
-
-  wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
-  expect(wrapper.find(SelectList).props().needReload).toBe(false);
-
-  wrapper.instance().componentWillUnmount();
-  expect(wrapper.instance().mounted).toBe(false);
-});
-
-it('should handle reload properly', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
 
-  wrapper.instance().handleReload();
-  expect(searchProjects).toHaveBeenCalledWith(
-    expect.objectContaining({
-      page: 1
-    })
-  );
-  expect(wrapper.state().listHasBeenTouched).toBe(false);
-});
-
-it('should handle search reload properly', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  wrapper.instance().handleSearch('foo', Filter.Selected);
   expect(searchProjects).toHaveBeenCalledWith(
     expect.objectContaining({
+      gateId: qualityGate.id,
+      organization,
       page: 1,
-      query: 'foo',
+      pageSize: 100,
+      query: undefined,
       selected: Filter.Selected
     })
   );
-  expect(wrapper.state().listHasBeenTouched).toBe(false);
-});
+  expect(wrapper.state().needToReload).toBe(false);
 
-it('should handle load more properly', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  wrapper.instance().handleLoadMore();
-  expect(searchProjects).toHaveBeenCalledWith(
-    expect.objectContaining({
-      page: 2
-    })
-  );
-  expect(wrapper.state().listHasBeenTouched).toBe(false);
+  wrapper.instance().componentWillUnmount();
+  expect(wrapper.instance().mounted).toBe(false);
 });
 
 it('should handle selection properly', async () => {
   const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
   wrapper.instance().handleSelect('toto');
   await waitAndUpdate(wrapper);
+
   expect(associateGateWithProject).toHaveBeenCalledWith(
     expect.objectContaining({
       projectId: 'toto'
     })
   );
-  expect(wrapper.state().listHasBeenTouched).toBe(true);
+  expect(wrapper.state().needToReload).toBe(true);
 });
 
 it('should handle deselection properly', async () => {
   const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
   wrapper.instance().handleUnselect('tata');
   await waitAndUpdate(wrapper);
+
   expect(dissociateGateWithProject).toHaveBeenCalledWith(
     expect.objectContaining({
       projectId: 'tata'
     })
   );
-  expect(wrapper.state().listHasBeenTouched).toBe(true);
+  expect(wrapper.state().needToReload).toBe(true);
 });
 
 function shallowRender(props: Partial<Projects['props']> = {}) {
-  return shallow<Projects>(<Projects qualityGate={mockQualityGate()} {...props} />);
+  return shallow<Projects>(
+    <Projects organization={organization} qualityGate={qualityGate} {...props} />
+  );
 }
index 804961561e4d48653585907ed08b2cbaca942446..58db09fcecd1c24512cb3a995cac243707a363c3 100644 (file)
@@ -13,9 +13,7 @@ exports[`should render correctly 1`] = `
   labelAll="quality_gates.projects.all"
   labelSelected="quality_gates.projects.with"
   labelUnselected="quality_gates.projects.without"
-  needReload={false}
-  onLoadMore={[Function]}
-  onReload={[Function]}
+  needToReload={false}
   onSearch={[Function]}
   onSelect={[Function]}
   onUnselect={[Function]}
@@ -26,6 +24,7 @@ exports[`should render correctly 1`] = `
       "test3",
     ]
   }
+  withPaging={true}
 />
 `;
 
index ed132d1bd467894e72a840c12faacce2080ec325..5119c0a0b0d69256de809d9f01e521e60b74b761 100644 (file)
@@ -21,7 +21,10 @@ import * as React from 'react';
 import { find, without } from 'lodash';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import Modal from 'sonar-ui-common/components/controls/Modal';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import SelectList, {
+  Filter,
+  SelectListSearchParams
+} from '../../../components/SelectList/SelectList';
 import { Profile } from '../types';
 import {
   associateProject,
@@ -36,25 +39,14 @@ interface Props {
   profile: Profile;
 }
 
-export interface SearchParams {
-  key: string;
-  organization: string | null;
-  page: number;
-  pageSize: number;
-  query?: string;
-  selected: string;
-}
-
 interface State {
-  lastSearchParams: SearchParams;
-  listHasBeenTouched: boolean;
+  needToReload: boolean;
+  lastSearchParams?: SelectListSearchParams;
   projects: ProfileProject[];
   projectsTotalCount?: number;
   selectedProjects: string[];
 }
 
-const PAGE_SIZE = 100;
-
 export default class ChangeProjectsForm extends React.PureComponent<Props, State> {
   mounted = false;
 
@@ -62,15 +54,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
     super(props);
 
     this.state = {
-      lastSearchParams: {
-        key: props.profile.key,
-        organization: props.organization,
-        page: 1,
-        pageSize: PAGE_SIZE,
-        query: '',
-        selected: Filter.Selected
-      },
-      listHasBeenTouched: false,
+      needToReload: false,
       projects: [],
       selectedProjects: []
     };
@@ -78,22 +62,25 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
 
   componentDidMount() {
     this.mounted = true;
-    this.fetchProjects(this.state.lastSearchParams);
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
-  fetchProjects = (searchParams: SearchParams, more?: boolean) =>
+  fetchProjects = (searchParams: SelectListSearchParams) =>
     getProfileProjects({
-      ...searchParams,
+      key: this.props.profile.key,
+      organization: this.props.organization,
       p: searchParams.page,
       ps: searchParams.pageSize,
-      q: searchParams.query !== '' ? searchParams.query : undefined
+      q: searchParams.query !== '' ? searchParams.query : undefined,
+      selected: searchParams.filter
     }).then(data => {
       if (this.mounted) {
         this.setState(prevState => {
+          const more = searchParams.page != null && searchParams.page > 1;
+
           const projects = more ? [...prevState.projects, ...data.results] : data.results;
           const newSeletedProjects = data.results
             .filter(project => project.selected)
@@ -104,7 +91,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
 
           return {
             lastSearchParams: searchParams,
-            listHasBeenTouched: false,
+            needToReload: false,
             projects,
             projectsTotalCount: data.paging.total,
             selectedProjects
@@ -113,34 +100,11 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
       }
     });
 
-  handleLoadMore = () =>
-    this.fetchProjects(
-      {
-        ...this.state.lastSearchParams,
-        page: this.state.lastSearchParams.page + 1
-      },
-      true
-    );
-
-  handleReload = () =>
-    this.fetchProjects({
-      ...this.state.lastSearchParams,
-      page: 1
-    });
-
-  handleSearch = (query: string, selected: Filter) =>
-    this.fetchProjects({
-      ...this.state.lastSearchParams,
-      page: 1,
-      query,
-      selected
-    });
-
   handleSelect = (key: string) =>
     associateProject(this.props.profile.key, key).then(() => {
       if (this.mounted) {
         this.setState((state: State) => ({
-          listHasBeenTouched: true,
+          needToReload: true,
           selectedProjects: [...state.selectedProjects, key]
         }));
       }
@@ -150,7 +114,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
     dissociateProject(this.props.profile.key, key).then(() => {
       if (this.mounted) {
         this.setState((state: State) => ({
-          listHasBeenTouched: true,
+          needToReload: true,
           selectedProjects: without(state.selectedProjects, key)
         }));
       }
@@ -195,16 +159,17 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
             labelAll={translate('quality_gates.projects.all')}
             labelSelected={translate('quality_gates.projects.with')}
             labelUnselected={translate('quality_gates.projects.without')}
-            needReload={
-              this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
+            needToReload={
+              this.state.needToReload &&
+              this.state.lastSearchParams &&
+              this.state.lastSearchParams.filter !== Filter.All
             }
-            onLoadMore={this.handleLoadMore}
-            onReload={this.handleReload}
-            onSearch={this.handleSearch}
+            onSearch={this.fetchProjects}
             onSelect={this.handleSelect}
             onUnselect={this.handleUnselect}
             renderElement={this.renderElement}
             selectedElements={this.state.selectedProjects}
+            withPaging={true}
           />
         </div>
 
index 19256e1d4d612ec40725649f459af5d5b3a4de23..8bc50653e14452d6134c25beff841ef8ab49da87 100644 (file)
@@ -19,8 +19,8 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import ChangeProjectsForm, { SearchParams } from '../ChangeProjectsForm';
+import { waitAndUpdate, click } from 'sonar-ui-common/helpers/testUtils';
+import ChangeProjectsForm from '../ChangeProjectsForm';
 import SelectList, { Filter } from '../../../../components/SelectList/SelectList';
 import {
   getProfileProjects,
@@ -28,6 +28,9 @@ import {
   dissociateProject
 } from '../../../../api/quality-profiles';
 
+const profile: any = { key: 'profFile_key' };
+const organization = 'TEST';
+
 jest.mock('../../../../api/quality-profiles', () => ({
   getProfileProjects: jest.fn().mockResolvedValue({
     paging: { pageIndex: 1, pageSize: 3, total: 55 },
@@ -41,95 +44,77 @@ jest.mock('../../../../api/quality-profiles', () => ({
   dissociateProject: jest.fn().mockResolvedValue({})
 }));
 
-const profile: any = { key: 'profFile_key' };
-
 beforeEach(() => {
   jest.clearAllMocks();
 });
 
 it('should render correctly', async () => {
   const wrapper = shallowRender();
+  wrapper
+    .find(SelectList)
+    .props()
+    .onSearch({
+      query: '',
+      filter: Filter.Selected,
+      page: 1,
+      pageSize: 100
+    });
   await waitAndUpdate(wrapper);
-  expect(wrapper.instance().mounted).toBe(true);
 
+  expect(wrapper.instance().mounted).toBe(true);
   expect(wrapper).toMatchSnapshot();
   expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
   expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
-  expect(getProfileProjects).toHaveBeenCalled();
-
-  wrapper.setState({ listHasBeenTouched: true });
-  expect(wrapper.find(SelectList).props().needReload).toBe(true);
-
-  wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams });
-  expect(wrapper.find(SelectList).props().needReload).toBe(false);
-
-  wrapper.instance().componentWillUnmount();
-  expect(wrapper.instance().mounted).toBe(false);
-});
-
-it('should handle reload properly', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
 
-  wrapper.instance().handleReload();
-  expect(getProfileProjects).toHaveBeenCalledWith(
-    expect.objectContaining({
-      p: 1
-    })
-  );
-  expect(wrapper.state().listHasBeenTouched).toBe(false);
-});
-
-it('should handle search reload properly', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  wrapper.instance().handleSearch('foo', Filter.Selected);
   expect(getProfileProjects).toHaveBeenCalledWith(
     expect.objectContaining({
+      key: profile.key,
+      organization,
       p: 1,
-      q: 'foo',
+      ps: 100,
+      q: undefined,
       selected: Filter.Selected
     })
   );
-  expect(wrapper.state().listHasBeenTouched).toBe(false);
-});
-
-it('should handle load more properly', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
+  expect(wrapper.state().needToReload).toBe(false);
 
-  wrapper.instance().handleLoadMore();
-  expect(getProfileProjects).toHaveBeenCalledWith(
-    expect.objectContaining({
-      p: 2
-    })
-  );
-  expect(wrapper.state().listHasBeenTouched).toBe(false);
+  wrapper.instance().componentWillUnmount();
+  expect(wrapper.instance().mounted).toBe(false);
 });
 
 it('should handle selection properly', async () => {
   const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
   wrapper.instance().handleSelect('toto');
   await waitAndUpdate(wrapper);
+
   expect(associateProject).toHaveBeenCalledWith(profile.key, 'toto');
-  expect(wrapper.state().listHasBeenTouched).toBe(true);
+  expect(wrapper.state().needToReload).toBe(true);
 });
 
 it('should handle deselection properly', async () => {
   const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
   wrapper.instance().handleUnselect('tata');
   await waitAndUpdate(wrapper);
+
   expect(dissociateProject).toHaveBeenCalledWith(profile.key, 'tata');
-  expect(wrapper.state().listHasBeenTouched).toBe(true);
+  expect(wrapper.state().needToReload).toBe(true);
+});
+
+it('should close modal properly', () => {
+  const spy = jest.fn();
+  const wrapper = shallowRender({ onClose: spy });
+  click(wrapper.find('a'));
+
+  expect(spy).toHaveBeenCalled();
 });
 
 function shallowRender(props: Partial<ChangeProjectsForm['props']> = {}) {
   return shallow<ChangeProjectsForm>(
-    <ChangeProjectsForm onClose={jest.fn()} organization="TEST" profile={profile} {...props} />
+    <ChangeProjectsForm
+      onClose={jest.fn()}
+      organization={organization}
+      profile={profile}
+      {...props}
+    />
   );
 }
index c67712805f87245b65b578cef18ac97f226e7c70..602b4366eae4e86d4d8e52eb290bdcafec6eef0d 100644 (file)
@@ -29,9 +29,7 @@ exports[`should render correctly 1`] = `
       labelAll="quality_gates.projects.all"
       labelSelected="quality_gates.projects.with"
       labelUnselected="quality_gates.projects.without"
-      needReload={false}
-      onLoadMore={[Function]}
-      onReload={[Function]}
+      needToReload={false}
       onSearch={[Function]}
       onSelect={[Function]}
       onUnselect={[Function]}
@@ -41,6 +39,7 @@ exports[`should render correctly 1`] = `
           "test3",
         ]
       }
+      withPaging={true}
     />
   </div>
   <div
index cdf7e5eab4afd904bb1f7edaee61c0fb3f2ce5c8..697c533e387b7232ffcce19d8b65a0bd69bab867 100644 (file)
@@ -21,7 +21,10 @@ import * as React from 'react';
 import { find, without } from 'lodash';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import Modal from 'sonar-ui-common/components/controls/Modal';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import SelectList, {
+  Filter,
+  SelectListSearchParams
+} from '../../../components/SelectList/SelectList';
 import { getUserGroups, UserGroup } from '../../../api/users';
 import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
 
@@ -31,25 +34,14 @@ interface Props {
   user: T.User;
 }
 
-export interface SearchParams {
-  login: string;
-  organization?: string;
-  page: number;
-  pageSize: number;
-  query?: string;
-  selected: string;
-}
-
 interface State {
+  needToReload: boolean;
+  lastSearchParams?: SelectListSearchParams;
   groups: UserGroup[];
   groupsTotalCount?: number;
-  lastSearchParams: SearchParams;
-  listHasBeenTouched: boolean;
   selectedGroups: string[];
 }
 
-const PAGE_SIZE = 100;
-
 export default class GroupsForm extends React.PureComponent<Props, State> {
   mounted = false;
 
@@ -57,39 +49,33 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
     super(props);
 
     this.state = {
+      needToReload: false,
       groups: [],
-      lastSearchParams: {
-        login: props.user.login,
-        page: 1,
-        pageSize: PAGE_SIZE,
-        query: '',
-        selected: Filter.Selected
-      },
-      listHasBeenTouched: false,
       selectedGroups: []
     };
   }
 
   componentDidMount() {
     this.mounted = true;
-    this.fetchUsers(this.state.lastSearchParams);
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
-  fetchUsers = (searchParams: SearchParams, more?: boolean) =>
+  fetchUsers = (searchParams: SelectListSearchParams) =>
     getUserGroups({
-      login: searchParams.login,
-      organization: searchParams.organization !== '' ? searchParams.organization : undefined,
+      login: this.props.user.login,
+      organization: undefined,
       p: searchParams.page,
       ps: searchParams.pageSize,
       q: searchParams.query !== '' ? searchParams.query : undefined,
-      selected: searchParams.selected
+      selected: searchParams.filter
     }).then(data => {
       if (this.mounted) {
         this.setState(prevState => {
+          const more = searchParams.page != null && searchParams.page > 1;
+
           const groups = more ? [...prevState.groups, ...data.groups] : data.groups;
           const newSeletedGroups = data.groups.filter(gp => gp.selected).map(gp => gp.name);
           const selectedGroups = more
@@ -98,7 +84,7 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
 
           return {
             lastSearchParams: searchParams,
-            listHasBeenTouched: false,
+            needToReload: false,
             groups,
             groupsTotalCount: data.paging.total,
             selectedGroups
@@ -107,29 +93,6 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
       }
     });
 
-  handleLoadMore = () =>
-    this.fetchUsers(
-      {
-        ...this.state.lastSearchParams,
-        page: this.state.lastSearchParams.page + 1
-      },
-      true
-    );
-
-  handleReload = () =>
-    this.fetchUsers({
-      ...this.state.lastSearchParams,
-      page: 1
-    });
-
-  handleSearch = (query: string, selected: Filter) =>
-    this.fetchUsers({
-      ...this.state.lastSearchParams,
-      page: 1,
-      query,
-      selected
-    });
-
   handleSelect = (name: string) =>
     addUserToGroup({
       name,
@@ -137,7 +100,7 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
     }).then(() => {
       if (this.mounted) {
         this.setState((state: State) => ({
-          listHasBeenTouched: true,
+          needToReload: true,
           selectedGroups: [...state.selectedGroups, name]
         }));
       }
@@ -150,7 +113,7 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
     }).then(() => {
       if (this.mounted) {
         this.setState((state: State) => ({
-          listHasBeenTouched: true,
+          needToReload: true,
           selectedGroups: without(state.selectedGroups, name)
         }));
       }
@@ -196,16 +159,17 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
           <SelectList
             elements={this.state.groups.map(group => group.name)}
             elementsTotalCount={this.state.groupsTotalCount}
-            needReload={
-              this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All
+            needToReload={
+              this.state.needToReload &&
+              this.state.lastSearchParams &&
+              this.state.lastSearchParams.filter !== Filter.All
             }
-            onLoadMore={this.handleLoadMore}
-            onReload={this.handleReload}
-            onSearch={this.handleSearch}
+            onSearch={this.fetchUsers}
             onSelect={this.handleSelect}
             onUnselect={this.handleUnselect}
             renderElement={this.renderElement}
             selectedElements={this.state.selectedGroups}
+            withPaging={true}
           />
         </div>
 
index 6d9846b5efd47080c706afa50d80366b699c79cf..e6cbf910c6400dd772e283b870c5e528bc6372e1 100644 (file)
  */
 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} />
   );
 }
index 6a383259146e0cffa91d156e162e220c734ffd1e..f8a5bab167e49d2b8c931cb0e5e976b31630141f 100644 (file)
@@ -24,9 +24,7 @@ exports[`should render correctly 1`] = `
         ]
       }
       elementsTotalCount={1}
-      needReload={false}
-      onLoadMore={[Function]}
-      onReload={[Function]}
+      needToReload={false}
       onSearch={[Function]}
       onSelect={[Function]}
       onUnselect={[Function]}
@@ -37,6 +35,7 @@ exports[`should render correctly 1`] = `
           "test2",
         ]
       }
+      withPaging={true}
     />
   </div>
   <footer
@@ -52,3 +51,27 @@ exports[`should render correctly 1`] = `
   </footer>
 </Modal>
 `;
+
+exports[`should render correctly 2`] = `
+<div
+  className="select-list-list-item"
+>
+  <React.Fragment>
+    test1
+    <br />
+    <span
+      className="note"
+    >
+      test1
+    </span>
+  </React.Fragment>
+</div>
+`;
+
+exports[`should render correctly 3`] = `
+<div
+  className="select-list-list-item"
+>
+  test_foo
+</div>
+`;
index 14c2168c237431064e49023dd1972f5a2d77d595..7cafc36df69fc9bd5c155150f21ef4febd80e4b1 100644 (file)
@@ -39,55 +39,92 @@ interface Props {
   labelSelected?: string;
   labelUnselected?: string;
   labelAll?: string;
-  needReload?: boolean;
-  onLoadMore?: () => Promise<void>;
-  onReload?: () => Promise<void>;
-  onSearch: (query: string, tab: Filter) => Promise<void>;
+  needToReload?: boolean;
+  onSearch: (searchParams: SelectListSearchParams) => Promise<void>;
   onSelect: (element: string) => Promise<void>;
   onUnselect: (element: string) => Promise<void>;
+  pageSize?: number;
   readOnly?: boolean;
   renderElement: (element: string) => React.ReactNode;
   selectedElements: string[];
+  withPaging?: boolean;
 }
 
-interface State {
+export interface SelectListSearchParams {
   filter: Filter;
-  loading: boolean;
+  page?: number;
+  pageSize?: number;
   query: string;
 }
 
+interface State {
+  lastSearchParams: SelectListSearchParams;
+  loading: boolean;
+}
+
+const DEFAULT_PAGE_SIZE = 100;
+
 export default class SelectList extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = { filter: Filter.Selected, loading: false, query: '' };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      lastSearchParams: {
+        filter: Filter.Selected,
+        page: 1,
+        pageSize: props.pageSize ? props.pageSize : DEFAULT_PAGE_SIZE,
+        query: ''
+      },
+      loading: false
+    };
+  }
 
   componentDidMount() {
     this.mounted = true;
+    this.search({});
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
-  stopLoading = () => {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  };
+  getFilter = () =>
+    this.state.lastSearchParams.query === '' ? this.state.lastSearchParams.filter : Filter.All;
+
+  search = (searchParams: Partial<SelectListSearchParams>) =>
+    this.setState(
+      prevState => ({
+        loading: true,
+        lastSearchParams: { ...prevState.lastSearchParams, ...searchParams }
+      }),
+      () =>
+        this.props
+          .onSearch({
+            filter: this.getFilter(),
+            page: this.props.withPaging ? this.state.lastSearchParams.page : undefined,
+            pageSize: this.props.withPaging ? this.state.lastSearchParams.pageSize : undefined,
+            query: this.state.lastSearchParams.query
+          })
+          .finally(() => {
+            if (this.mounted) {
+              this.setState({ loading: false });
+            }
+          })
+    );
+
+  changeFilter = (filter: Filter) => this.search({ filter, page: 1 });
 
-  changeFilter = (filter: Filter) => {
-    this.setState({ filter, loading: true });
-    this.props.onSearch(this.state.query, filter).then(this.stopLoading, this.stopLoading);
-  };
+  handleQueryChange = (query: string) => this.search({ page: 1, query });
 
-  handleQueryChange = (query: string) => {
-    this.setState({ loading: true, query }, () => {
-      this.props.onSearch(query, this.getFilter()).then(this.stopLoading, this.stopLoading);
+  onLoadMore = () =>
+    this.search({
+      page:
+        this.state.lastSearchParams.page != null ? this.state.lastSearchParams.page + 1 : undefined
     });
-  };
 
-  getFilter = () => {
-    return this.state.query === '' ? this.state.filter : Filter.All;
-  };
+  onReload = () => this.search({ page: 1 });
 
   render() {
     const {
@@ -95,9 +132,9 @@ export default class SelectList extends React.PureComponent<Props, State> {
       labelUnselected = translate('unselected'),
       labelAll = translate('all')
     } = this.props;
-    const { filter } = this.state;
+    const { filter } = this.state.lastSearchParams;
 
-    const disabled = this.state.query !== '';
+    const disabled = this.state.lastSearchParams.query !== '';
 
     return (
       <div className="select-list">
@@ -118,7 +155,7 @@ export default class SelectList extends React.PureComponent<Props, State> {
             loading={this.state.loading}
             onChange={this.handleQueryChange}
             placeholder={translate('search_verb')}
-            value={this.state.query}
+            value={this.state.lastSearchParams.query}
           />
         </div>
         <SelectListListContainer
@@ -132,12 +169,12 @@ export default class SelectList extends React.PureComponent<Props, State> {
           renderElement={this.props.renderElement}
           selectedElements={this.props.selectedElements}
         />
-        {!!this.props.elementsTotalCount && this.props.onLoadMore && (
+        {!!this.props.elementsTotalCount && (
           <ListFooter
             count={this.props.elements.length}
-            loadMore={this.props.onLoadMore}
-            needReload={this.props.needReload}
-            reload={this.props.onReload}
+            loadMore={this.onLoadMore}
+            needReload={this.props.needToReload}
+            reload={this.onReload}
             total={this.props.elementsTotalCount}
           />
         )}
index 98401d0f14eb805c98edc70f6d5c87135479b41e..2d1ab5a0c9983e2135281ca45e1ee0c814880ceb 100644 (file)
@@ -22,67 +22,126 @@ import { shallow } from 'enzyme';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import SelectList, { Filter } from '../SelectList';
 
-it('should display selected elements only by default', () => {
-  const wrapper = shallowRender();
-  expect(wrapper.state().filter).toBe(Filter.Selected);
-});
+const elements = ['foo', 'bar', 'baz'];
+const selectedElements = [elements[0]];
+const disabledElements = [elements[1]];
 
-it('should display a loader when searching', async () => {
+it('should display properly with basics features', async () => {
   const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
-  expect(wrapper.state().loading).toBe(false);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.instance().mounted).toBe(true);
 
-  wrapper.instance().handleQueryChange('');
-  expect(wrapper.state().loading).toBe(true);
   expect(wrapper).toMatchSnapshot();
 
+  wrapper.instance().componentWillUnmount();
+  expect(wrapper.instance().mounted).toBe(false);
+});
+
+it('should display properly with advanced features', async () => {
+  const wrapper = shallowRender({
+    allowBulkSelection: true,
+    elementsTotalCount: 125,
+    pageSize: 10,
+    readOnly: true,
+    withPaging: true
+  });
   await waitAndUpdate(wrapper);
-  expect(wrapper.state().loading).toBe(false);
+
+  expect(wrapper).toMatchSnapshot();
 });
 
-it('should display a loader when updating filter', async () => {
+it('should display a loader when searching', async () => {
   const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().loading).toBe(false);
 
-  wrapper.instance().changeFilter(Filter.Unselected);
+  wrapper.instance().search({});
   expect(wrapper.state().loading).toBe(true);
   expect(wrapper).toMatchSnapshot();
 
   await waitAndUpdate(wrapper);
-  expect(wrapper.state().filter).toBe(Filter.Unselected);
   expect(wrapper.state().loading).toBe(false);
 });
 
 it('should cancel filter selection when search is active', async () => {
-  const wrapper = shallowRender();
+  const spy = jest.fn().mockResolvedValue({});
+  const wrapper = shallowRender({ onSearch: spy });
+  wrapper.instance().changeFilter(Filter.Unselected);
+  await waitAndUpdate(wrapper);
+
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: Filter.Unselected,
+    page: undefined,
+    pageSize: undefined
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  const query = 'test';
+  wrapper.instance().handleQueryChange(query);
+  expect(spy).toHaveBeenCalledWith({
+    query,
+    filter: Filter.All,
+    page: undefined,
+    pageSize: undefined
+  });
 
-  wrapper.setState({ filter: Filter.Selected });
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 
-  wrapper.setState({ query: 'foobar' });
+  wrapper.instance().handleQueryChange('');
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: Filter.Unselected,
+    page: undefined,
+    pageSize: undefined
+  });
+
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
-it('should display pagination element properly', () => {
-  const wrapper = shallowRender({ elementsTotalCount: 100, onLoadMore: jest.fn() });
+it('should display pagination element properly and call search method with correct parameters', () => {
+  const spy = jest.fn().mockResolvedValue({});
+  const wrapper = shallowRender({ elementsTotalCount: 100, onSearch: spy, withPaging: true });
   expect(wrapper).toMatchSnapshot();
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: Filter.Selected,
+    page: 1,
+    pageSize: 100
+  }); // Basic default call
+
+  wrapper.instance().onLoadMore();
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: Filter.Selected,
+    page: 2,
+    pageSize: 100
+  }); // Load more call
+
+  wrapper.instance().onReload();
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: Filter.Selected,
+    page: 1,
+    pageSize: 100
+  }); // Reload call
 
-  wrapper.setProps({ needReload: true, onReload: jest.fn() });
+  wrapper.setProps({ needToReload: true });
   expect(wrapper).toMatchSnapshot();
 });
 
 function shallowRender(props: Partial<SelectList['props']> = {}) {
   return shallow<SelectList>(
     <SelectList
-      elements={['foo', 'bar', 'baz']}
+      disabledElements={disabledElements}
+      elements={elements}
       onSearch={jest.fn(() => Promise.resolve())}
       onSelect={jest.fn(() => Promise.resolve())}
       onUnselect={jest.fn(() => Promise.resolve())}
       renderElement={(foo: string) => foo}
-      selectedElements={['foo']}
+      selectedElements={selectedElements}
       {...props}
     />
   );
index eb38a0a6304f3c6feef834799d2ce4243947718b..f68fa52b1bf252bbb71f1d523247aecacb234518 100644 (file)
@@ -31,7 +31,7 @@ exports[`should cancel filter selection when search is active 1`] = `
           },
         ]
       }
-      value="selected"
+      value="deselected"
     />
     <SearchBox
       autoFocus={true}
@@ -42,7 +42,11 @@ exports[`should cancel filter selection when search is active 1`] = `
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -50,7 +54,7 @@ exports[`should cancel filter selection when search is active 1`] = `
         "baz",
       ]
     }
-    filter="selected"
+    filter="deselected"
     onSelect={[MockFunction]}
     onUnselect={[MockFunction]}
     renderElement={[Function]}
@@ -94,18 +98,22 @@ exports[`should cancel filter selection when search is active 2`] = `
           },
         ]
       }
-      value="selected"
+      value="deselected"
     />
     <SearchBox
       autoFocus={true}
       loading={false}
       onChange={[Function]}
       placeholder="search_verb"
-      value="foobar"
+      value="test"
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -126,7 +134,7 @@ exports[`should cancel filter selection when search is active 2`] = `
 </div>
 `;
 
-exports[`should display a loader when searching 1`] = `
+exports[`should cancel filter selection when search is active 3`] = `
 <div
   className="select-list"
 >
@@ -157,7 +165,7 @@ exports[`should display a loader when searching 1`] = `
           },
         ]
       }
-      value="selected"
+      value="deselected"
     />
     <SearchBox
       autoFocus={true}
@@ -168,7 +176,11 @@ exports[`should display a loader when searching 1`] = `
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -176,7 +188,7 @@ exports[`should display a loader when searching 1`] = `
         "baz",
       ]
     }
-    filter="selected"
+    filter="deselected"
     onSelect={[MockFunction]}
     onUnselect={[MockFunction]}
     renderElement={[Function]}
@@ -189,7 +201,7 @@ exports[`should display a loader when searching 1`] = `
 </div>
 `;
 
-exports[`should display a loader when searching 2`] = `
+exports[`should display a loader when searching 1`] = `
 <div
   className="select-list"
 >
@@ -231,7 +243,11 @@ exports[`should display a loader when searching 2`] = `
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -252,7 +268,7 @@ exports[`should display a loader when searching 2`] = `
 </div>
 `;
 
-exports[`should display a loader when updating filter 1`] = `
+exports[`should display pagination element properly and call search method with correct parameters 1`] = `
 <div
   className="select-list"
 >
@@ -287,14 +303,18 @@ exports[`should display a loader when updating filter 1`] = `
     />
     <SearchBox
       autoFocus={true}
-      loading={false}
+      loading={true}
       onChange={[Function]}
       placeholder="search_verb"
       value=""
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -312,10 +332,16 @@ exports[`should display a loader when updating filter 1`] = `
       ]
     }
   />
+  <ListFooter
+    count={3}
+    loadMore={[Function]}
+    reload={[Function]}
+    total={100}
+  />
 </div>
 `;
 
-exports[`should display a loader when updating filter 2`] = `
+exports[`should display pagination element properly and call search method with correct parameters 2`] = `
 <div
   className="select-list"
 >
@@ -346,7 +372,7 @@ exports[`should display a loader when updating filter 2`] = `
           },
         ]
       }
-      value="deselected"
+      value="selected"
     />
     <SearchBox
       autoFocus={true}
@@ -357,7 +383,11 @@ exports[`should display a loader when updating filter 2`] = `
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -365,7 +395,7 @@ exports[`should display a loader when updating filter 2`] = `
         "baz",
       ]
     }
-    filter="deselected"
+    filter="selected"
     onSelect={[MockFunction]}
     onUnselect={[MockFunction]}
     renderElement={[Function]}
@@ -375,10 +405,17 @@ exports[`should display a loader when updating filter 2`] = `
       ]
     }
   />
+  <ListFooter
+    count={3}
+    loadMore={[Function]}
+    needReload={true}
+    reload={[Function]}
+    total={100}
+  />
 </div>
 `;
 
-exports[`should display pagination element properly 1`] = `
+exports[`should display properly with advanced features 1`] = `
 <div
   className="select-list"
 >
@@ -420,7 +457,12 @@ exports[`should display pagination element properly 1`] = `
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    allowBulkSelection={true}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -431,6 +473,7 @@ exports[`should display pagination element properly 1`] = `
     filter="selected"
     onSelect={[MockFunction]}
     onUnselect={[MockFunction]}
+    readOnly={true}
     renderElement={[Function]}
     selectedElements={
       Array [
@@ -440,13 +483,14 @@ exports[`should display pagination element properly 1`] = `
   />
   <ListFooter
     count={3}
-    loadMore={[MockFunction]}
-    total={100}
+    loadMore={[Function]}
+    reload={[Function]}
+    total={125}
   />
 </div>
 `;
 
-exports[`should display pagination element properly 2`] = `
+exports[`should display properly with basics features 1`] = `
 <div
   className="select-list"
 >
@@ -488,7 +532,11 @@ exports[`should display pagination element properly 2`] = `
     />
   </div>
   <SelectListListContainer
-    disabledElements={Array []}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
     elements={
       Array [
         "foo",
@@ -506,12 +554,5 @@ exports[`should display pagination element properly 2`] = `
       ]
     }
   />
-  <ListFooter
-    count={3}
-    loadMore={[MockFunction]}
-    needReload={true}
-    reload={[MockFunction]}
-    total={100}
-  />
 </div>
 `;