]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11609 Update the Issues bulkchange action
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 8 Jan 2019 10:19:29 +0000 (11:19 +0100)
committerSonarTech <sonartech@sonarsource.com>
Thu, 14 Feb 2019 19:20:57 +0000 (20:20 +0100)
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/styles.css
server/sonar-web/src/main/js/helpers/testMocks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5c6db560471b862679641ab2308a56a8a5cee552..60c699186af81bdece1b3cf005689f496cca9c9d 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import * as key from 'keymaster';
 import Helmet from 'react-helmet';
 import { keyBy, omit, union, without } from 'lodash';
-import BulkChangeModal from './BulkChangeModal';
+import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
 import ComponentBreadcrumbs from './ComponentBreadcrumbs';
 import IssuesList from './IssuesList';
 import IssuesSourceViewer from './IssuesSourceViewer';
@@ -51,11 +52,10 @@ import {
   STANDARDS,
   ReferencedRule
 } from '../utils';
+import { Alert } from '../../../components/ui/Alert';
 import { Button } from '../../../components/ui/buttons';
 import Checkbox from '../../../components/controls/Checkbox';
 import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import Dropdown from '../../../components/controls/Dropdown';
-import DropdownIcon from '../../../components/icons-components/DropdownIcon';
 import EmptySearch from '../../../components/common/EmptySearch';
 import FiltersHeader from '../../../components/common/FiltersHeader';
 import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
@@ -110,7 +110,8 @@ interface Props {
 }
 
 export interface State {
-  bulkChange?: 'all' | 'selected';
+  bulkChangeModal: boolean;
+  checkAll?: boolean;
   checked: string[];
   effortTotal?: number;
   facets: { [facet: string]: Facet };
@@ -144,6 +145,7 @@ export class App extends React.PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
     this.state = {
+      bulkChangeModal: false,
       checked: [],
       facets: {},
       issues: [],
@@ -210,6 +212,7 @@ export class App extends React.PureComponent<Props, State> {
       areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
     ) {
       this.fetchFirstIssues();
+      this.setState({ checkAll: false });
     } else if (
       !this.state.openIssue &&
       (prevState.selected !== this.state.selected || prevState.openIssue)
@@ -515,7 +518,7 @@ export class App extends React.PureComponent<Props, State> {
 
     const p = paging.pageIndex + 1;
 
-    this.setState({ loadingMore: true });
+    this.setState({ checkAll: false, loadingMore: true });
     this.fetchIssuesPage(p).then(
       response => {
         if (this.mounted) {
@@ -623,6 +626,21 @@ export class App extends React.PureComponent<Props, State> {
     return Promise.resolve({ issues, paging });
   };
 
+  getButtonLabel = (checked: string[], checkAll?: boolean, paging?: T.Paging) => {
+    if (checked.length > 0) {
+      let count;
+      if (checkAll && paging) {
+        count = paging.total > MAX_PAGE_SIZE ? MAX_PAGE_SIZE : paging.total;
+      } else {
+        count = Math.min(checked.length, MAX_PAGE_SIZE);
+      }
+
+      return translateWithParameters('issues.bulk_change_X_issues', count);
+    } else {
+      return translate('bulk_change');
+    }
+  };
+
   handleFilterChange = (changes: Partial<Query>) => {
     this.setState({ loading: true });
     this.props.router.push({
@@ -739,10 +757,11 @@ export class App extends React.PureComponent<Props, State> {
             ? union(checked, [state.issues[i].key])
             : without(checked, state.issues[i].key);
         }
-        return { checked };
+        return { checkAll: false, checked };
       });
     } else {
       this.setState(state => ({
+        checkAll: false,
         lastChecked: issue,
         checked: state.checked.includes(issue)
           ? without(state.checked, issue)
@@ -757,29 +776,20 @@ export class App extends React.PureComponent<Props, State> {
     }));
   };
 
-  openBulkChange = (mode: 'all' | 'selected') => {
-    this.setState({ bulkChange: mode });
+  handleOpenBulkChange = () => {
     key.setScope('issues-bulk-change');
+    this.setState({ bulkChangeModal: true });
   };
 
-  closeBulkChange = () => {
+  handleCloseBulkChange = () => {
     key.setScope('issues');
-    this.setState({ bulkChange: undefined });
-  };
-
-  handleBulkChangeClick = () => {
-    this.openBulkChange('all');
-  };
-
-  handleBulkChangeSelectedClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.openBulkChange('selected');
+    this.setState({ bulkChangeModal: false });
   };
 
   handleBulkChangeDone = () => {
+    this.setState({ checkAll: false });
     this.fetchFirstIssues();
-    this.closeBulkChange();
+    this.handleCloseBulkChange();
   };
 
   handleReload = () => {
@@ -812,11 +822,14 @@ export class App extends React.PureComponent<Props, State> {
     this.setState(actions.selectPreviousLocation);
   };
 
-  onCheckAll = (checked: boolean) => {
+  handleCheckAll = (checked: boolean) => {
     if (checked) {
-      this.setState(state => ({ checked: state.issues.map(issue => issue.key) }));
+      this.setState(state => ({
+        checkAll: true,
+        checked: state.issues.map(issue => issue.key)
+      }));
     } else {
-      this.setState({ checked: [] });
+      this.setState({ checkAll: false, checked: [] });
     }
   };
 
@@ -834,7 +847,7 @@ export class App extends React.PureComponent<Props, State> {
 
   renderBulkChange(openIssue: T.Issue | undefined) {
     const { component, currentUser } = this.props;
-    const { bulkChange, checked, paging, issues } = this.state;
+    const { checkAll, bulkChangeModal, checked, issues, paging } = this.state;
 
     const isAllChecked = checked.length > 0 && issues.length === checked.length;
     const thirdState = checked.length > 0 && !isAllChecked;
@@ -851,45 +864,22 @@ export class App extends React.PureComponent<Props, State> {
           className="spacer-right vertical-middle"
           disabled={issues.length === 0}
           id="issues-selection"
-          onCheck={this.onCheckAll}
+          onCheck={this.handleCheckAll}
           thirdState={thirdState}
         />
-        {checked.length > 0 ? (
-          <Dropdown
-            className="display-inline-block"
-            overlay={
-              <ul className="menu">
-                <li>
-                  <a href="#" onClick={this.handleBulkChangeClick}>
-                    {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)}
-                  </a>
-                </li>
-                <li>
-                  <a href="#" onClick={this.handleBulkChangeSelectedClick}>
-                    {translateWithParameters('issues.bulk_change_selected', checked.length)}
-                  </a>
-                </li>
-              </ul>
-            }>
-            <Button id="issues-bulk-change">
-              {translate('bulk_change')}
-              <DropdownIcon className="little-spacer-left" />
-            </Button>
-          </Dropdown>
-        ) : (
-          <Button
-            disabled={issues.length === 0}
-            id="issues-bulk-change"
-            onClick={this.handleBulkChangeClick}>
-            {translate('bulk_change')}
-          </Button>
-        )}
-        {bulkChange && (
+        <Button
+          disabled={checked.length === 0}
+          id="issues-bulk-change"
+          onClick={this.handleOpenBulkChange}>
+          {this.getButtonLabel(checked, checkAll, paging)}
+        </Button>
+
+        {bulkChangeModal && (
           <BulkChangeModal
             component={component}
             currentUser={currentUser}
-            fetchIssues={bulkChange === 'all' ? this.fetchIssues : this.getCheckedIssues}
-            onClose={this.closeBulkChange}
+            fetchIssues={checkAll ? this.fetchIssues : this.getCheckedIssues}
+            onClose={this.handleCloseBulkChange}
             onDone={this.handleBulkChangeDone}
             organization={this.props.organization}
           />
@@ -1073,7 +1063,7 @@ export class App extends React.PureComponent<Props, State> {
   }
 
   renderPage() {
-    const { loading, openIssue } = this.state;
+    const { checkAll, loading, openIssue, paging } = this.state;
     return (
       <div className="layout-page-main-inner">
         {openIssue ? (
@@ -1089,7 +1079,20 @@ export class App extends React.PureComponent<Props, State> {
             selectedLocationIndex={this.state.selectedLocationIndex}
           />
         ) : (
-          <DeferredSpinner loading={loading}>{this.renderList()}</DeferredSpinner>
+          <DeferredSpinner loading={loading}>
+            {checkAll &&
+              paging &&
+              paging.total > MAX_PAGE_SIZE && (
+                <Alert className="big-spacer-bottom" variant="warning">
+                  <FormattedMessage
+                    defaultMessage={translate('issue_bulk_change.max_issues_reached')}
+                    id="issue_bulk_change.max_issues_reached"
+                    values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
+                  />
+                </Alert>
+              )}
+            {this.renderList()}
+          </DeferredSpinner>
         )}
       </div>
     );
index 9578cbbaa56c241ee22849b70e6e4f43d6bdbbf5..361fb238886972dc6fac501b962f3e0d0463f46b 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import { pickBy, sortBy } from 'lodash';
 import { searchAssignees } from '../utils';
 import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
@@ -30,7 +31,7 @@ import Select from '../../../components/controls/Select';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import Avatar from '../../../components/ui/Avatar';
-import { SubmitButton } from '../../../components/ui/buttons';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
 import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Alert } from '../../../components/ui/Alert';
@@ -85,6 +86,8 @@ const AssigneeSelect = SearchSelect as AssigneeSelectType;
 type TagSelectType = new () => SearchSelect<TagOption>;
 const TagSelect = SearchSelect as TagSelectType;
 
+export const MAX_PAGE_SIZE = 500;
+
 export default class BulkChangeModal extends React.PureComponent<Props, State> {
   mounted = false;
 
@@ -104,13 +107,17 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
       this.loadIssues(),
       searchIssueTags({ organization: this.state.organization })
     ]).then(
-      ([issues, tags]) => {
+      ([{ issues, paging }, tags]) => {
         if (this.mounted) {
+          if (issues.length > MAX_PAGE_SIZE) {
+            issues = issues.slice(0, MAX_PAGE_SIZE);
+          }
+
           this.setState({
             initialTags: tags.map(tag => ({ label: tag, value: tag })),
-            issues: issues.issues,
+            issues,
             loading: false,
-            paging: issues.paging
+            paging
           });
         }
       },
@@ -122,7 +129,9 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     this.mounted = false;
   }
 
-  loadIssues = () => this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 });
+  loadIssues = () => {
+    return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: MAX_PAGE_SIZE });
+  };
 
   getDefaultAssignee = () => {
     const { currentUser } = this.props;
@@ -149,12 +158,6 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     return options;
   };
 
-  handleCloseClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.onClose();
-  };
-
   handleAssigneeSearch = (query: string) => {
     return searchAssignees(query, this.state.organization).then(({ results }) =>
       results.map(r => ({ avatar: r.avatar, label: r.name, value: r.login }))
@@ -250,12 +253,6 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     }));
   }
 
-  renderCancelButton = () => (
-    <a href="#" id="bulk-change-cancel" onClick={this.handleCloseClick}>
-      {translate('cancel')}
-    </a>
-  );
-
   renderLoading = () => (
     <div>
       <div className="modal-head">
@@ -266,7 +263,9 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
           <i className="spinner spinner-margin" />
         </div>
       </div>
-      <div className="modal-foot">{this.renderCancelButton()}</div>
+      <div className="modal-foot">
+        <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
+      </div>
     </div>
   );
 
@@ -496,7 +495,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
   renderForm = () => {
     const { issues, paging, submitting } = this.state;
 
-    const limitReached = paging !== undefined && paging.total > paging.pageIndex * paging.pageSize;
+    const limitReached = paging && paging.total > MAX_PAGE_SIZE;
 
     return (
       <form id="bulk-change-form" onSubmit={this.handleSubmit}>
@@ -507,7 +506,11 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
         <div className="modal-body">
           {limitReached && (
             <Alert variant="warning">
-              {translateWithParameters('issue_bulk_change.max_issues_reached', issues.length)}
+              <FormattedMessage
+                defaultMessage={translate('issue_bulk_change.max_issues_reached')}
+                id="issue_bulk_change.max_issues_reached"
+                values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
+              />
             </Alert>
           )}
 
@@ -529,7 +532,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
           <SubmitButton disabled={submitting || issues.length === 0} id="bulk-change-submit">
             {translate('apply')}
           </SubmitButton>
-          {this.renderCancelButton()}
+          <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
         </div>
       </form>
     );
index e8bcd5e0323ac155a851717b7a29e6ef426c151e..6742f296c867c5c886e0f2d0ab736a1070ced8b0 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import { App } from '../App';
+import { mockCurrentUser, mockRouter } from '../../../../helpers/testMocks';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
 
-const issues = [
+const ISSUES = [
   { key: 'foo' } as T.Issue,
   { key: 'bar' } as T.Issue,
   { key: 'third' } as T.Issue,
   { key: 'fourth' } as T.Issue
 ];
-const facets = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }];
-const paging = { pageIndex: 1, pageSize: 100, total: 4 };
+const FACETS = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }];
+const PAGING = { pageIndex: 1, pageSize: 100, total: 4 };
 
 const eventNoShiftKey = { shiftKey: false } as MouseEvent;
 const eventWithShiftKey = { shiftKey: true } as MouseEvent;
 
 const referencedComponent = { key: 'foo-key', name: 'bar', organization: 'John', uuid: 'foo-uuid' };
-const PROPS = {
-  branch: { isMain: true, name: 'master' },
-  currentUser: {
-    isLoggedIn: true,
-    avatar: 'foo',
-    email: 'forr@bar.com',
-    login: 'JohnDoe',
-    name: 'John Doe'
-  },
-  component: { breadcrumbs: [], key: 'foo', name: 'bar', organization: 'John', qualifier: 'Doe' },
-  location: { pathname: '/issues', query: {} },
-  fetchIssues: () =>
-    Promise.resolve({
-      components: [referencedComponent],
-      effortTotal: 1,
-      facets,
-      issues,
-      languages: [],
-      paging,
-      rules: [],
-      users: []
-    }),
-  onBranchesChange: () => {},
-  onSonarCloud: false,
-  organization: { key: 'foo' },
-  router: { push: jest.fn(), replace: jest.fn() },
-  userOrganizations: []
-};
 
 it('should render a list of issue', async () => {
-  const wrapper = shallow<App>(<App {...PROPS} />);
+  const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(wrapper.state().issues.length).toBe(4);
   expect(wrapper.state().referencedComponentsById).toEqual({ 'foo-uuid': referencedComponent });
@@ -73,7 +46,7 @@ it('should render a list of issue', async () => {
 });
 
 it('should be able to check/uncheck a group of issues with the Shift key', async () => {
-  const wrapper = shallow<App>(<App {...PROPS} />);
+  const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(wrapper.state().issues.length).toBe(4);
 
@@ -92,7 +65,7 @@ it('should be able to check/uncheck a group of issues with the Shift key', async
 });
 
 it('should avoid non-existing keys', async () => {
-  const wrapper = shallow<App>(<App {...PROPS} />);
+  const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(wrapper.state().issues.length).toBe(4);
 
@@ -105,7 +78,7 @@ it('should avoid non-existing keys', async () => {
 });
 
 it('should be able to uncheck all issue with global checkbox', async () => {
-  const wrapper = shallow<App>(<App {...PROPS} />);
+  const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(wrapper.state().issues.length).toBe(4);
 
@@ -114,16 +87,92 @@ it('should be able to uncheck all issue with global checkbox', async () => {
   instance.handleIssueCheck('bar', eventNoShiftKey);
   expect(wrapper.state().checked.length).toBe(2);
 
-  instance.onCheckAll(false);
+  instance.handleCheckAll(false);
   expect(wrapper.state().checked.length).toBe(0);
 });
 
 it('should be able to check all issue with global checkbox', async () => {
-  const wrapper = shallow<App>(<App {...PROPS} />);
+  const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
 
   const instance = wrapper.instance();
   expect(wrapper.state().checked.length).toBe(0);
-  instance.onCheckAll(true);
+  instance.handleCheckAll(true);
   expect(wrapper.state().checked.length).toBe(wrapper.state().issues.length);
 });
+
+it('should check all issues, even the ones that are not visible', async () => {
+  const wrapper = shallowRender({
+    fetchIssues: jest.fn().mockResolvedValue({
+      components: [referencedComponent],
+      effortTotal: 1,
+      facets: FACETS,
+      issues: ISSUES,
+      languages: [],
+      paging: { pageIndex: 1, pageSize: 100, total: 250 },
+      rules: [],
+      users: []
+    })
+  });
+  const instance = wrapper.instance();
+  await waitAndUpdate(wrapper);
+
+  // Checking all issues should show the correct count in the Bulk Change button.
+  instance.handleCheckAll(true);
+  waitAndUpdate(wrapper);
+  expect(wrapper.find('#issues-bulk-change')).toMatchSnapshot();
+});
+
+it('should check max 500 issues', async () => {
+  const wrapper = shallowRender({
+    fetchIssues: jest.fn().mockResolvedValue({
+      components: [referencedComponent],
+      effortTotal: 1,
+      facets: FACETS,
+      issues: ISSUES,
+      languages: [],
+      paging: { pageIndex: 1, pageSize: 100, total: 1000 },
+      rules: [],
+      users: []
+    })
+  });
+  const instance = wrapper.instance();
+  await waitAndUpdate(wrapper);
+
+  // Checking all issues should show 500 in the Bulk Change button, and display
+  // a warning.
+  instance.handleCheckAll(true);
+  waitAndUpdate(wrapper);
+  expect(wrapper.find('#issues-bulk-change')).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<App['props']> = {}) {
+  return shallow<App>(
+    <App
+      component={{
+        breadcrumbs: [],
+        key: 'foo',
+        name: 'bar',
+        organization: 'John',
+        qualifier: 'Doe'
+      }}
+      currentUser={mockCurrentUser()}
+      fetchIssues={jest.fn().mockResolvedValue({
+        components: [referencedComponent],
+        effortTotal: 1,
+        facets: FACETS,
+        issues: ISSUES,
+        languages: [],
+        paging: PAGING,
+        rules: [],
+        users: []
+      })}
+      location={{ pathname: '/issues', query: {} }}
+      onBranchesChange={() => {}}
+      organization={{ key: 'foo' }}
+      router={mockRouter()}
+      userOrganizations={[]}
+      {...props}
+    />
+  );
+}
index fd1ca320e2982dce7f5468252bce23b6b088909e..39494a70beb78062fc39ca460d72a5789b3ec160 100644 (file)
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import BulkChangeModal from '../BulkChangeModal';
+import BulkChangeModal, { MAX_PAGE_SIZE } from '../BulkChangeModal';
+import { mockIssue } from '../../../../helpers/testMocks';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/issues', () => ({
   searchIssueTags: () => Promise.resolve([undefined, []])
 }));
 
+jest.mock('../BulkChangeModal', () => {
+  const mock = require.requireActual('../BulkChangeModal');
+  mock.MAX_PAGE_SIZE = 1;
+  return mock;
+});
+
 it('should display error message when no issues available', async () => {
   const wrapper = getWrapper([]);
   await waitAndUpdate(wrapper);
@@ -33,36 +40,23 @@ it('should display error message when no issues available', async () => {
 });
 
 it('should display form when issues are present', async () => {
-  const wrapper = getWrapper([
-    {
-      actions: [],
-      component: 'foo',
-      componentLongName: 'foo',
-      componentQualifier: 'foo',
-      componentUuid: 'foo',
-      creationDate: 'foo',
-      key: 'foo',
-      flows: [],
-      fromHotspot: false,
-      message: 'foo',
-      organization: 'foo',
-      project: 'foo',
-      projectName: 'foo',
-      projectOrganization: 'foo',
-      projectKey: 'foo',
-      rule: 'foo',
-      ruleName: 'foo',
-      secondaryLocations: [],
-      severity: 'foo',
-      status: 'foo',
-      transitions: [],
-      type: 'BUG'
-    }
-  ]);
+  const wrapper = getWrapper([mockIssue()]);
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should display warning when too many issues are passed', async () => {
+  const issues: T.Issue[] = [];
+  for (let i = MAX_PAGE_SIZE + 1; i > 0; i--) {
+    issues.push(mockIssue());
+  }
+
+  const wrapper = getWrapper(issues);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.find('h2')).toMatchSnapshot();
+  expect(wrapper.find('Alert')).toMatchSnapshot();
+});
+
 const getWrapper = (issues: T.Issue[]) => {
   return shallow(
     <BulkChangeModal
@@ -72,9 +66,9 @@ const getWrapper = (issues: T.Issue[]) => {
         Promise.resolve({
           issues,
           paging: {
-            pageIndex: 0,
-            pageSize: 0,
-            total: 0
+            pageIndex: issues.length,
+            pageSize: issues.length,
+            total: issues.length
           }
         })
       }
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644 (file)
index 0000000..0aa0114
--- /dev/null
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should check all issues, even the ones that are not visible 1`] = `
+<Button
+  disabled={false}
+  id="issues-bulk-change"
+  onClick={[Function]}
+>
+  issues.bulk_change_X_issues.250
+</Button>
+`;
+
+exports[`should check max 500 issues 1`] = `
+<Button
+  disabled={false}
+  id="issues-bulk-change"
+  onClick={[Function]}
+>
+  issues.bulk_change_X_issues.500
+</Button>
+`;
index 6ecd67855be34074b772360fdc08e5e1e8add152..e45da7a29a6e2c7c7ba862942db2e41aa57595fd 100644 (file)
@@ -34,13 +34,11 @@ exports[`should display error message when no issues available 1`] = `
       >
         apply
       </SubmitButton>
-      <a
-        href="#"
-        id="bulk-change-cancel"
+      <ResetButtonLink
         onClick={[Function]}
       >
         cancel
-      </a>
+      </ResetButtonLink>
     </div>
   </form>
 </Modal>
@@ -90,14 +88,36 @@ exports[`should display form when issues are present 1`] = `
       >
         apply
       </SubmitButton>
-      <a
-        href="#"
-        id="bulk-change-cancel"
+      <ResetButtonLink
         onClick={[Function]}
       >
         cancel
-      </a>
+      </ResetButtonLink>
     </div>
   </form>
 </Modal>
 `;
+
+exports[`should display warning when too many issues are passed 1`] = `
+<h2>
+  issue_bulk_change.form.title.1
+</h2>
+`;
+
+exports[`should display warning when too many issues are passed 2`] = `
+<Alert
+  variant="warning"
+>
+  <FormattedMessage
+    defaultMessage="issue_bulk_change.max_issues_reached"
+    id="issue_bulk_change.max_issues_reached"
+    values={
+      Object {
+        "max": <strong>
+          1
+        </strong>,
+      }
+    }
+  />
+</Alert>
+`;
index 80de331b3279f439aad732045c77fc04c54f050b..ff12f912eccd458c17ecb70f36393ccaa1a75d56 100644 (file)
   margin-right: 2px;
 }
 
-.concise-issues-list-header,
-.concise-issues-list-header-inner {
-}
-
-.concise-issues-list-header {
-}
-
 .concise-issues-list-header-inner {
   width: 260px;
   text-align: center;
index 4a7fd47182b0b64901860276b4d4852a43b995a1..04df471571e508a89b49828fb283b98d15e0fd01 100644 (file)
@@ -71,6 +71,34 @@ export function mockEvent(overrides = {}) {
   } as any;
 }
 
+export function mockIssue(overrides = {}): T.Issue {
+  return {
+    actions: [],
+    component: 'my-component',
+    componentLongName: 'My Component',
+    componentQualifier: 'my-component',
+    componentUuid: 'uuid',
+    creationDate: 'date',
+    key: 'foo',
+    flows: [],
+    fromHotspot: false,
+    message: 'Message',
+    organization: 'foo',
+    project: 'my-project',
+    projectName: 'My Project',
+    projectOrganization: 'org',
+    projectKey: 'key',
+    rule: 'rule',
+    ruleName: 'Rule',
+    secondaryLocations: [],
+    severity: 'severity',
+    status: 'status',
+    transitions: [],
+    type: 'BUG',
+    ...overrides
+  };
+}
+
 export function mockLocation(overrides: Partial<Location> = {}): Location {
   return {
     action: 'PUSH',
index f11a0baa6a3455a98748ed7f85807a1b5de2d009..8427e8ecd1059429f38ec9f0cd5a33f8ce4402fe 100644 (file)
@@ -665,8 +665,7 @@ issue.this_issue_involves_x_code_locations=This issue involves {0} code location
 issue.from_external_rule_engine=Issue detected by an external rule engine: {0}
 issue.external_issue_description=This is external rule {0}. No details are available.
 issues.return_to_list=Return to List
-issues.bulk_change=All Issues ({0})
-issues.bulk_change_selected=Selected Issues ({0})
+issues.bulk_change_X_issues=Bulk Change {0} Issue(s)
 issues.issues=issues
 issues.to_select_issues=to select issues
 issues.to_navigate=to navigate
@@ -745,7 +744,7 @@ issues.facet.cwe=CWE
 
 issue_bulk_change.form.title=Change {0} issues
 issue_bulk_change.comment.help=This comment will be applied only to issues that will effectively be modified
-issue_bulk_change.max_issues_reached=As too many issues have been selected, only the first {0} issues will be updated.
+issue_bulk_change.max_issues_reached=There are more issues available than can be treated by a single bulk action. Your changes will only be applied to the first {max} issues.
 issue_bulk_change.x_issues={0} issues
 issue_bulk_change.no_match=There is no issue matching your filter selection