Browse Source

SONAR-7449 Selecting multiple issues with shift+click (#3127)

tags/7.5
Pascal Mugnier 6 years ago
parent
commit
d334ffbc6a
No account linked to committer's email address

+ 33
- 7
server/sonar-web/src/main/js/apps/issues/components/App.js View File

@@ -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 */) => {

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/components/IssuesList.js View File

@@ -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},

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/components/ListItem.js View File

@@ -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,

+ 94
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js View File

@@ -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);
});

+ 1
- 1
server/sonar-web/src/main/js/components/issue/Issue.d.ts View File

@@ -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;

+ 3
- 3
server/sonar-web/src/main/js/components/issue/Issue.js View File

@@ -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;

+ 2
- 2
server/sonar-web/src/main/js/components/issue/IssueView.js View File

@@ -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);
}
};


Loading…
Cancel
Save