aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorPascal Mugnier <pascal.mugnier@sonarsource.com>2018-03-08 09:30:47 +0100
committerGitHub <noreply@github.com>2018-03-08 09:30:47 +0100
commitd334ffbc6ab7bec1ad6a4c32df2007f6367ad03d (patch)
tree101089cba9ad1d67fe8ed349dbe8586cdb809936 /server
parent89562bc1246dd24f82e624ed6a319e5f42169980 (diff)
downloadsonarqube-d334ffbc6ab7bec1ad6a4c32df2007f6367ad03d.tar.gz
sonarqube-d334ffbc6ab7bec1ad6a4c32df2007f6367ad03d.zip
SONAR-7449 Selecting multiple issues with shift+click (#3127)
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.js40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesList.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ListItem.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js94
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.d.ts2
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.js6
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueView.js4
7 files changed, 135 insertions, 15 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js
index edd359159ad..02c1a0e1e47 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.js
@@ -21,7 +21,7 @@
import React from 'react';
import Helmet from 'react-helmet';
import key from 'keymaster';
-import { keyBy, without } from 'lodash';
+import { keyBy, union, without } from 'lodash';
import PropTypes from 'prop-types';
import PageActions from './PageActions';
import MyIssuesFilter from './MyIssuesFilter';
@@ -83,6 +83,7 @@ export type Props = {
/*::
export type State = {
bulkChange: 'all' | 'selected' | null,
+ lastChecked: null | string,
checked: Array<string>,
facets: { [string]: Facet },
issues: Array<Issue>,
@@ -122,6 +123,7 @@ export default class App extends React.PureComponent {
super(props);
this.state = {
bulkChange: null,
+ lastChecked: null,
checked: [],
facets: {},
issues: [],
@@ -652,12 +654,36 @@ export default class App extends React.PureComponent {
});
};
- handleIssueCheck = (issue /*: string */) => {
- this.setState(state => ({
- checked: state.checked.includes(issue)
- ? without(state.checked, issue)
- : [...state.checked, issue]
- }));
+ handleIssueCheck = (issue /*: string */, event /*: Event */) => {
+ // Selecting multiple issues with shift+click
+ const { lastChecked } = this.state;
+ if (event.shiftKey && lastChecked !== null) {
+ this.setState(state => {
+ const issueKeys = state.issues.map(issue => issue.key);
+ const currentIssueIndex = issueKeys.indexOf(issue);
+ const lastSelectedIndex = issueKeys.indexOf(lastChecked);
+ const shouldCheck = state.checked.includes(lastChecked);
+ let { checked } = state;
+ if (currentIssueIndex < 0) {
+ return null;
+ }
+ const start = Math.min(currentIssueIndex, lastSelectedIndex);
+ const end = Math.max(currentIssueIndex, lastSelectedIndex);
+ for (let i = start; i < end + 1; i++) {
+ checked = shouldCheck
+ ? union(checked, [state.issues[i].key])
+ : without(checked, state.issues[i].key);
+ }
+ return { checked };
+ });
+ } else {
+ this.setState(state => ({
+ lastChecked: issue,
+ checked: state.checked.includes(issue)
+ ? without(state.checked, issue)
+ : [...state.checked, issue]
+ }));
+ }
};
handleIssueChange = (issue /*: Issue */) => {
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
index 7689dd712a2..30506292162 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
@@ -31,7 +31,7 @@ type Props = {|
issues: Array<Issue>,
onFilterChange: (changes: {}) => void,
onIssueChange: Issue => void,
- onIssueCheck?: string => void,
+ onIssueCheck?: (issue: string, event: Event) => void,
onIssueClick: string => void,
onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
openPopup: ?{ issue: string, name: string},
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
index 86301d6e2bd..28c6ae69760 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
@@ -31,7 +31,7 @@ type Props = {|
component?: Component,
issue: IssueType,
onChange: IssueType => void,
- onCheck?: string => void,
+ onCheck?: (issue: string, event: Event) => void,
onClick: string => void,
onFilterChange: (changes: {}) => void,
onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void,
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js
new file mode 100644
index 00000000000..7cf87139121
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+// @flow
+import * as React from 'react';
+import { shallow, mount } from 'enzyme';
+import App from '../App';
+import { shallowWithIntl, waitAndUpdate } from '../../../../helpers/testUtils';
+
+const replace = jest.fn();
+const issues = [{ key: 'foo' }, { key: 'bar' }, { key: 'third' }, { key: 'fourth' }];
+const facets = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }];
+const paging = [{ pageIndex: 1, pageSize: 100, total: 4 }];
+
+const eventNoShiftKey = { shiftKey: false };
+const eventWithShiftKey = { shiftKey: true };
+
+const PROPS = {
+ branch: { isMain: true, name: 'master' },
+ currentUser: {
+ isLoggedIn: true,
+ avatar: 'foo',
+ email: 'forr@bar.com',
+ login: 'JohnDoe',
+ name: 'John Doe'
+ },
+ component: { key: 'foo', name: 'bar', organization: 'John', qualifier: 'Doe' },
+ location: { pathname: '/issues', query: {} },
+ fetchIssues: () => Promise.resolve({ facets, issues, paging }),
+ onBranchesChange: () => {},
+ onSonarCloud: false,
+ organization: { key: 'foo' }
+};
+
+it('should render a list of issue', async () => {
+ const wrapper = shallowWithIntl(<App {...PROPS} />, {
+ context: { router: { replace } }
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().issues.length).toBe(4);
+});
+
+it('should be able to check/uncheck a group of issues with the Shift key', async () => {
+ const wrapper = shallowWithIntl(<App {...PROPS} />, {
+ context: { router: { replace } }
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().issues.length).toBe(4);
+
+ wrapper.instance().handleIssueCheck('foo', eventNoShiftKey);
+ expect(wrapper.state().checked.length).toBe(1);
+
+ wrapper.instance().handleIssueCheck('fourth', eventWithShiftKey);
+ expect(wrapper.state().checked.length).toBe(4);
+
+ wrapper.instance().handleIssueCheck('third', eventNoShiftKey);
+ expect(wrapper.state().checked.length).toBe(3);
+
+ wrapper.instance().handleIssueCheck('foo', eventWithShiftKey);
+ expect(wrapper.state().checked.length).toBe(1);
+});
+
+it('should avoid non-existing keys', async () => {
+ const wrapper = shallowWithIntl(<App {...PROPS} />, {
+ context: { router: { replace } }
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().issues.length).toBe(4);
+
+ wrapper.instance().handleIssueCheck('foo', eventNoShiftKey);
+ expect(wrapper.state().checked.length).toBe(1);
+
+ wrapper.instance().handleIssueCheck('non-existing-key', eventWithShiftKey);
+ expect(wrapper.state().checked.length).toBe(1);
+});
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.d.ts b/server/sonar-web/src/main/js/components/issue/Issue.d.ts
index 5abf041951b..6930914f170 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.d.ts
+++ b/server/sonar-web/src/main/js/components/issue/Issue.d.ts
@@ -27,7 +27,7 @@ interface IssueProps {
displayLocationsLink?: boolean;
issue: IssueType;
onChange: (issue: IssueType) => void;
- onCheck?: (issueKey: string) => void;
+ onCheck?: (issueKey: string, event: Event) => void;
onClick: (issueKey: string) => void;
onFilter?: (property: string, issue: IssueType) => void;
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js
index 0100ba16b5b..47bf09dab76 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.js
+++ b/server/sonar-web/src/main/js/components/issue/Issue.js
@@ -35,7 +35,7 @@ type Props = {|
displayLocationsLink?: boolean;
issue: IssueType,
onChange: IssueType => void,
- onCheck?: string => void,
+ onCheck?: (issue: string, event: Event) => void,
onClick: string => void,
onFilter?: (property: string, issue: IssueType) => void,
onPopupToggle: (issue: string, popupName: string, open: ?boolean) => void,
@@ -108,9 +108,9 @@ export default class Issue extends React.PureComponent {
this.togglePopup('edit-tags');
return false;
});
- key('space', 'issues', () => {
+ key('space', 'issues', (event /*: Event*/) => {
if (this.props.onCheck) {
- this.props.onCheck(this.props.issue.key);
+ this.props.onCheck(this.props.issue.key, event);
return false;
}
return undefined;
diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js
index d6f3ab8335e..b7a658e77ba 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueView.js
+++ b/server/sonar-web/src/main/js/components/issue/IssueView.js
@@ -37,7 +37,7 @@ type Props = {|
issue: Issue,
onAssign: string => void,
onChange: Issue => void,
- onCheck?: string => void,
+ onCheck?: (issue: string, event: Event) => void,
onClick: string => void,
onFail: Error => void,
onFilter?: (property: string, issue: Issue) => void,
@@ -53,7 +53,7 @@ export default class IssueView extends React.PureComponent {
event.preventDefault();
event.stopPropagation();
if (this.props.onCheck) {
- this.props.onCheck(this.props.issue.key);
+ this.props.onCheck(this.props.issue.key, event);
}
};