import { FormattedMessage } from 'react-intl';
import * as key from 'keymaster';
import Helmet from 'react-helmet';
-import { keyBy, omit, union, without } from 'lodash';
+import { keyBy, omit, without } from 'lodash';
import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import IssuesList from './IssuesList';
effortTotal?: number;
facets: { [facet: string]: Facet };
issues: T.Issue[];
- lastChecked?: string;
loading: boolean;
loadingFacets: { [key: string]: boolean };
loadingMore: boolean;
fetchIssuesUntil = (
p: number,
- done: (issues: T.Issue[], paging: T.Paging) => boolean
+ done: (lastIssue: T.Issue, paging: T.Paging) => boolean
): Promise<{ issues: T.Issue[]; paging: T.Paging }> => {
- return this.fetchIssuesPage(p).then(response => {
- const { issues, paging } = response;
-
- return done(issues, paging)
- ? { issues, paging }
- : this.fetchIssuesUntil(p + 1, done).then(nextResponse => {
- return {
- issues: [...issues, ...nextResponse.issues],
- paging: nextResponse.paging
- };
- });
- });
+ const recursiveFetch = (
+ p: number,
+ issues: T.Issue[]
+ ): Promise<{ issues: T.Issue[]; paging: T.Paging }> => {
+ return this.fetchIssuesPage(p)
+ .then(response => {
+ return {
+ issues: [...issues, ...response.issues],
+ paging: response.paging
+ };
+ })
+ .then(({ issues, paging }) => {
+ return done(issues[issues.length - 1], paging)
+ ? { issues, paging }
+ : recursiveFetch(p + 1, issues);
+ });
+ };
+
+ return recursiveFetch(p, []);
};
fetchMoreIssues = () => {
const isSameComponent = (issue: T.Issue) => issue.component === openIssue.component;
- const done = (issues: T.Issue[], paging: T.Paging) => {
+ const done = (lastIssue: T.Issue, paging: T.Paging) => {
if (paging.total <= paging.pageIndex * paging.pageSize) {
return true;
}
- const lastIssue = issues[issues.length - 1];
if (lastIssue.component !== openIssue.component) {
return true;
}
return lastIssue.textRange !== undefined && lastIssue.textRange.endLine > to;
};
- if (done(issues, paging)) {
+ if (done(issues[issues.length - 1], paging)) {
return Promise.resolve(issues.filter(isSameComponent));
}
});
};
- handleIssueCheck = (issue: string, event: { shiftKey?: boolean }) => {
- // Selecting multiple issues with shift+click
- const { lastChecked } = this.state;
- if (event.shiftKey && lastChecked) {
- 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 { checkAll: false, checked };
- });
- } else {
- this.setState(state => ({
- checkAll: false,
- lastChecked: issue,
- checked: state.checked.includes(issue)
- ? without(state.checked, issue)
- : [...state.checked, issue]
- }));
- }
+ handleIssueCheck = (issue: string) => {
+ this.setState(state => ({
+ checkAll: false,
+ checked: state.checked.includes(issue)
+ ? without(state.checked, issue)
+ : [...state.checked, issue]
+ }));
};
handleIssueChange = (issue: T.Issue) => {
issues: T.Issue[];
onFilterChange: (changes: Partial<Query>) => void;
onIssueChange: (issue: T.Issue) => void;
- onIssueCheck: ((issueKey: string, event: { shiftKey?: boolean }) => void) | undefined;
+ onIssueCheck: ((issueKey: string) => void) | undefined;
onIssueClick: (issueKey: string) => void;
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
openPopup: { issue: string; name: string } | undefined;
component: T.Component | undefined;
issue: T.Issue;
onChange: (issue: T.Issue) => void;
- onCheck: ((issueKey: string, event: { shiftKey?: boolean }) => void) | undefined;
+ onCheck: ((issueKey: string) => void) | undefined;
onClick: (issueKey: string) => void;
onFilterChange: (changes: Partial<Query>) => void;
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
import * as React from 'react';
import { shallow } from 'enzyme';
import { App } from '../App';
-import { mockCurrentUser, mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockCurrentUser,
+ mockRouter,
+ mockIssue,
+ mockLocation
+} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
const ISSUES = [
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' };
it('should render a list of issue', async () => {
expect(wrapper.state().referencedComponentsByKey).toEqual({ 'foo-key': referencedComponent });
});
-it('should be able to check/uncheck a group of issues with the Shift key', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().issues.length).toBe(4);
-
- const instance = wrapper.instance();
- instance.handleIssueCheck('foo', eventNoShiftKey);
- expect(wrapper.state().checked.length).toBe(1);
-
- instance.handleIssueCheck('fourth', eventWithShiftKey);
- expect(wrapper.state().checked.length).toBe(4);
-
- instance.handleIssueCheck('third', eventNoShiftKey);
- expect(wrapper.state().checked.length).toBe(3);
-
- instance.handleIssueCheck('foo', eventWithShiftKey);
- expect(wrapper.state().checked.length).toBe(1);
-});
-
-it('should avoid non-existing keys', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().issues.length).toBe(4);
-
- const instance = wrapper.instance();
- instance.handleIssueCheck('foo', eventNoShiftKey);
- expect(wrapper.state().checked.length).toBe(1);
-
- instance.handleIssueCheck('non-existing-key', eventWithShiftKey);
- expect(wrapper.state().checked.length).toBe(1);
-});
-
it('should be able to uncheck all issue with global checkbox', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state().issues.length).toBe(4);
const instance = wrapper.instance();
- instance.handleIssueCheck('foo', eventNoShiftKey);
- instance.handleIssueCheck('bar', eventNoShiftKey);
+ instance.handleIssueCheck('foo');
+ instance.handleIssueCheck('bar');
expect(wrapper.state().checked.length).toBe(2);
instance.handleCheckAll(false);
expect(wrapper.find('#issues-bulk-change')).toMatchSnapshot();
});
+it('should fetch issues for component', async () => {
+ const wrapper = shallowRender({
+ fetchIssues: fetchIssuesMockFactory(),
+ location: mockLocation({
+ query: { open: '0' }
+ })
+ });
+ const instance = wrapper.instance();
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state('issues')).toHaveLength(2);
+
+ await instance.fetchIssuesForComponent('', 0, 30);
+ expect(wrapper.state('issues')).toHaveLength(6);
+});
+
+it('should fetch issues until defined', async () => {
+ const mockDone = (_lastIssue: T.Issue, paging: T.Paging) =>
+ paging.total <= paging.pageIndex * paging.pageSize;
+
+ const wrapper = shallowRender({
+ fetchIssues: fetchIssuesMockFactory(),
+ location: mockLocation({
+ query: { open: '0' }
+ })
+ });
+ const instance = wrapper.instance();
+ await waitAndUpdate(wrapper);
+
+ const result = await instance.fetchIssuesUntil(1, mockDone);
+ expect(result.issues).toHaveLength(6);
+ expect(result.paging.pageIndex).toBe(3);
+});
+
+function fetchIssuesMockFactory(keyCount = 0, lineCount = 1) {
+ return jest.fn().mockImplementation(({ p }: any) =>
+ Promise.resolve({
+ components: [referencedComponent],
+ effortTotal: 1,
+ facets: FACETS,
+ issues: [
+ mockIssue(false, {
+ key: '' + keyCount++,
+ textRange: {
+ startLine: lineCount++,
+ endLine: lineCount,
+ startOffset: 0,
+ endOffset: 15
+ }
+ }),
+ mockIssue(false, {
+ key: '' + keyCount++,
+ textRange: {
+ startLine: lineCount++,
+ endLine: lineCount,
+ startOffset: 0,
+ endOffset: 15
+ }
+ })
+ ],
+ languages: [],
+ paging: { pageIndex: p || 1, pageSize: 2, total: 6 },
+ rules: [],
+ users: []
+ })
+ );
+}
+
function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
}
.issue-with-checkbox .issue-checkbox-container {
- display: block;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.issue-checkbox-container {
background-color: rgba(0, 0, 0, 0.05);
}
-.issue-checkbox {
- position: absolute;
- top: 50%;
- left: 50%;
- margin: -8px 0 0 -8px;
-}
-
.issue:not(.selected) .location-index {
background-color: #ccc;
}
displayLocationsLink?: boolean;
issue: T.Issue;
onChange: (issue: T.Issue) => void;
- onCheck?: (issue: string, event: { shiftKey?: boolean }) => void;
+ onCheck?: (issue: string) => void;
onClick: (issueKey: string) => void;
onFilter?: (property: string, issue: T.Issue) => void;
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
this.togglePopup('edit-tags');
return false;
});
- key('space', 'issues', (event: KeyboardEvent) => {
+ key('space', 'issues', () => {
if (this.props.onCheck) {
- this.props.onCheck(this.props.issue.key, event);
+ this.props.onCheck(this.props.issue.key);
return false;
}
return undefined;
*/
import * as React from 'react';
import classNames from 'classnames';
+import { updateIssue } from './actions';
import IssueTitleBar from './components/IssueTitleBar';
import IssueActionsBar from './components/IssueActionsBar';
import IssueCommentLine from './components/IssueCommentLine';
-import { updateIssue } from './actions';
+import Checkbox from '../controls/Checkbox';
import { deleteIssueComment, editIssueComment } from '../../api/issues';
interface Props {
issue: T.Issue;
onAssign: (login: string) => void;
onChange: (issue: T.Issue) => void;
- onCheck?: (issue: string, event: { shiftKey?: boolean }) => void;
+ onCheck?: (issue: string) => void;
onClick: (issueKey: string) => void;
onFilter?: (property: string, issue: T.Issue) => void;
selected: boolean;
}
export default class IssueView extends React.PureComponent<Props> {
- handleCheck = (event: React.MouseEvent) => {
- event.preventDefault();
+ handleCheck = (_checked: boolean) => {
if (this.props.onCheck) {
- this.props.onCheck(this.props.issue.key, event);
+ this.props.onCheck(this.props.issue.key);
}
};
</div>
)}
{hasCheckbox && (
- <a className="js-toggle issue-checkbox-container" href="#" onClick={this.handleCheck}>
- <i
- className={classNames('issue-checkbox', 'icon-checkbox', {
- 'icon-checkbox-checked': this.props.checked
- })}
+ <>
+ <Checkbox
+ checked={this.props.checked || false}
+ className="issue-checkbox-container"
+ onCheck={this.handleCheck}
/>
- </a>
+ </>
)}
</div>
);