aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2019-01-08 11:19:29 +0100
committerSonarTech <sonartech@sonarsource.com>2019-02-14 20:20:57 +0100
commit92c3efb4212370af7c19514136bdbc1051148210 (patch)
tree520864fcb4f59f0abdf646fd1f9b1bb8107bbaf3 /server
parent9f4c4d01a675aeb1401c85ee6f7aad79b7659bf0 (diff)
downloadsonarqube-92c3efb4212370af7c19514136bdbc1051148210.tar.gz
sonarqube-92c3efb4212370af7c19514136bdbc1051148210.zip
SONAR-11609 Update the Issues bulkchange action
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx125
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx125
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/App-test.tsx.snap21
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css7
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts28
8 files changed, 276 insertions, 165 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index 5c6db560471..60c699186af 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -18,10 +18,11 @@
* 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>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index 9578cbbaa56..361fb238886 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
index e8bcd5e0323..6742f296c86 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
@@ -20,52 +20,25 @@
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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
index fd1ca320e29..39494a70beb 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
@@ -19,13 +19,20 @@
*/
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
index 00000000000..0aa0114a24b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
index 6ecd67855be..e45da7a29a6 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
index 80de331b327..ff12f912ecc 100644
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -25,13 +25,6 @@
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;
diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts
index 4a7fd47182b..04df471571e 100644
--- a/server/sonar-web/src/main/js/helpers/testMocks.ts
+++ b/server/sonar-web/src/main/js/helpers/testMocks.ts
@@ -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',