]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15488 Fix pagination if total is undefined and page size is known
authorWouter Admiraal <45544358+wouter-admiraal-sonarsource@users.noreply.github.com>
Fri, 15 Oct 2021 14:09:42 +0000 (16:09 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 19 Oct 2021 20:03:29 +0000 (20:03 +0000)
Co-authored-by: Guillaume Peoch <guillaume.peoch@sonarsource.com>
server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap
server/sonar-web/src/main/js/components/controls/ListFooter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ListFooter-test.tsx.snap [new file with mode: 0644]

index d87c95f825c4d72b985a3af2df8a2e0d32025f1c..200aab387da713a8aaec347f79e410f343b024ce 100644 (file)
@@ -21,7 +21,6 @@ import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { Link } from 'react-router';
 import { Button } from 'sonar-ui-common/components/controls/buttons';
-import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
 import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
 import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
 import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon';
@@ -30,6 +29,7 @@ import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
 import { Alert } from 'sonar-ui-common/components/ui/Alert';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import ListFooter from '../../../components/controls/ListFooter';
 import { getProjectUrl } from '../../../helpers/urls';
 import { GitlabProject } from '../../../types/alm-integration';
 import { ComponentQualifier } from '../../../types/component';
@@ -159,6 +159,7 @@ export default function GitlabProjectSelectionForm(props: GitlabProjectSelection
         count={projects.length}
         loadMore={props.onLoadMore}
         loading={loadingMore}
+        pageSize={projectsPaging.pageSize}
         total={projectsPaging.total}
       />
     </div>
index 027ef23dc6d0db21e3e9986d0f525f8bc159cd51..eed9b379d0fa6f10c30830d2e3abe4cf32576bcd 100644 (file)
@@ -20,8 +20,8 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { Button } from 'sonar-ui-common/components/controls/buttons';
-import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
 import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
+import ListFooter from '../../../../components/controls/ListFooter';
 import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations';
 import GitlabProjectSelectionForm, {
   GitlabProjectSelectionFormProps
index b416707e214a31067606c9c9da551566d31477e1..894952f34de89e13d5865a52bdbab69c6752c580 100644 (file)
@@ -137,6 +137,7 @@ exports[`should render correctly: importing 1`] = `
     count={2}
     loadMore={[MockFunction]}
     loading={false}
+    pageSize={30}
     total={2}
   />
 </div>
@@ -194,6 +195,7 @@ exports[`should render correctly: no projects when searching 1`] = `
     count={0}
     loadMore={[MockFunction]}
     loading={false}
+    pageSize={30}
     total={0}
   />
 </div>
@@ -336,6 +338,7 @@ exports[`should render correctly: projects 1`] = `
     count={2}
     loadMore={[MockFunction]}
     loading={false}
+    pageSize={30}
     total={2}
   />
 </div>
diff --git a/server/sonar-web/src/main/js/components/controls/ListFooter.tsx b/server/sonar-web/src/main/js/components/controls/ListFooter.tsx
new file mode 100644 (file)
index 0000000..0b775fb
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 classNames from 'classnames';
+import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { formatMeasure } from 'sonar-ui-common/helpers/measures';
+
+export interface ListFooterProps {
+  count: number;
+  className?: string;
+  loading?: boolean;
+  loadMore?: () => void;
+  needReload?: boolean;
+  pageSize?: number;
+  reload?: () => void;
+  ready?: boolean;
+  total?: number;
+}
+
+export default function ListFooter(props: ListFooterProps) {
+  const { className, count, loading, needReload, total, pageSize, ready = true } = props;
+
+  let hasMore = false;
+  if (total !== undefined) {
+    hasMore = total > count;
+  } else if (pageSize !== undefined) {
+    hasMore = count % pageSize === 0;
+  }
+
+  let button;
+  if (needReload && props.reload) {
+    button = (
+      <Button className="spacer-left" data-test="reload" disabled={loading} onClick={props.reload}>
+        {translate('reload')}
+      </Button>
+    );
+  } else if (hasMore && props.loadMore) {
+    button = (
+      <Button
+        className="spacer-left"
+        disabled={loading}
+        data-test="show-more"
+        onClick={props.loadMore}>
+        {translate('show_more')}
+      </Button>
+    );
+  }
+
+  return (
+    <footer
+      className={classNames('spacer-top note text-center', { 'new-loading': !ready }, className)}>
+      {total !== undefined
+        ? translateWithParameters(
+            'x_of_y_shown',
+            formatMeasure(count, 'INT', null),
+            formatMeasure(total, 'INT', null)
+          )
+        : translateWithParameters('x_show', formatMeasure(count, 'INT', null))}
+      {button}
+      {loading && <DeferredSpinner className="text-bottom spacer-left position-absolute" />}
+    </footer>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.tsx
new file mode 100644 (file)
index 0000000..6b9928d
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { Button } from 'sonar-ui-common/components/controls/buttons';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import ListFooter, { ListFooterProps } from '../ListFooter';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+  expect(shallowRender({ needReload: true, reload: jest.fn() })).toMatchSnapshot('reload');
+  expect(shallowRender({ loading: true, needReload: true, reload: jest.fn() })).toMatchSnapshot(
+    'reload, loading'
+  );
+  expect(shallowRender({ loadMore: undefined })).toMatchSnapshot(
+    'empty if no loadMore nor reload props'
+  );
+  expect(shallowRender({ count: 5 })).toMatchSnapshot('empty if everything is loaded');
+  expect(shallowRender({ total: undefined })).toMatchSnapshot('total undefined');
+  expect(shallowRender({ total: undefined, count: 60, pageSize: 30 })).toMatchSnapshot(
+    'force show load more button if count % pageSize is 0, and total is undefined'
+  );
+});
+
+it.each([
+  [undefined, 60, 30, true],
+  [undefined, 45, 30, false],
+  [undefined, 60, undefined, false],
+  [60, 60, 30, false]
+])(
+  'handle showing load more button based on total, count and pageSize',
+  (total, count, pageSize, expected) => {
+    const wrapper = shallowRender({ total, count, pageSize });
+    expect(wrapper.find(Button).exists()).toBe(expected);
+  }
+);
+
+it('should properly call loadMore', () => {
+  const loadMore = jest.fn();
+  const wrapper = shallowRender({ loadMore });
+  click(wrapper.find(Button));
+  expect(loadMore).toBeCalled();
+});
+
+it('should properly call reload', () => {
+  const reload = jest.fn();
+  const wrapper = shallowRender({ needReload: true, reload });
+  click(wrapper.find(Button));
+  expect(reload).toBeCalled();
+});
+
+function shallowRender(props: Partial<ListFooterProps> = {}) {
+  return shallow<ListFooterProps>(
+    <ListFooter count={3} loadMore={jest.fn()} total={5} {...props} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ListFooter-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ListFooter-test.tsx.snap
new file mode 100644 (file)
index 0000000..1ef2198
--- /dev/null
@@ -0,0 +1,108 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="show-more"
+    onClick={[MockFunction]}
+  >
+    show_more
+  </Button>
+</footer>
+`;
+
+exports[`should render correctly: empty if everything is loaded 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.5.5
+</footer>
+`;
+
+exports[`should render correctly: empty if no loadMore nor reload props 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+</footer>
+`;
+
+exports[`should render correctly: force show load more button if count % pageSize is 0, and total is undefined 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_show.60
+  <Button
+    className="spacer-left"
+    data-test="show-more"
+    onClick={[MockFunction]}
+  >
+    show_more
+  </Button>
+</footer>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="show-more"
+    disabled={true}
+    onClick={[MockFunction]}
+  >
+    show_more
+  </Button>
+  <DeferredSpinner
+    className="text-bottom spacer-left position-absolute"
+  />
+</footer>
+`;
+
+exports[`should render correctly: reload 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="reload"
+    onClick={[MockFunction]}
+  >
+    reload
+  </Button>
+</footer>
+`;
+
+exports[`should render correctly: reload, loading 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="reload"
+    disabled={true}
+    onClick={[MockFunction]}
+  >
+    reload
+  </Button>
+  <DeferredSpinner
+    className="text-bottom spacer-left position-absolute"
+  />
+</footer>
+`;
+
+exports[`should render correctly: total undefined 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_show.3
+</footer>
+`;