diff options
16 files changed, 176 insertions, 47 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/actions.js b/server/sonar-web/src/main/js/apps/issues/actions.js index 69bc8192302..610628b15ff 100644 --- a/server/sonar-web/src/main/js/apps/issues/actions.js +++ b/server/sonar-web/src/main/js/apps/issues/actions.js @@ -22,6 +22,8 @@ import type { State } from './components/App'; export const enableLocationsNavigator = (state: State) => ({ locationsNavigator: true, + selectedFlowIndex: state.selectedFlowIndex || + (state.openIssue && state.openIssue.flows.length > 0 ? 0 : null), selectedLocationIndex: state.selectedLocationIndex || 0 }); @@ -47,12 +49,13 @@ export const selectLocation = (nextIndex: ?number) => (state: State) => { }; export const selectNextLocation = (state: State) => { - const { selectedLocationIndex: index, openIssue } = state; + const { selectedFlowIndex, selectedLocationIndex: index, openIssue } = state; if (openIssue) { + const locations = selectedFlowIndex != null + ? openIssue.flows[selectedFlowIndex] + : openIssue.secondaryLocations; return { - selectedLocationIndex: index != null && openIssue.secondaryLocations.length > index + 1 - ? index + 1 - : index + selectedLocationIndex: index != null && locations.length > index + 1 ? index + 1 : index }; } }; @@ -63,3 +66,7 @@ export const selectPreviousLocation = (state: State) => { return { selectedLocationIndex: index != null && index > 0 ? index - 1 : index }; } }; + +export const selectFlow = (nextIndex: ?number) => () => { + return { selectedFlowIndex: nextIndex, selectedLocationIndex: 0 }; +}; 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 4fac9bbe264..1b7f6a9f952 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 @@ -89,6 +89,7 @@ export type State = { referencedRules: { [string]: { name: string } }, referencedUsers: { [string]: ReferencedUser }, selected?: string, + selectedFlowIndex: ?number, selectedLocationIndex: ?number }; @@ -117,6 +118,7 @@ export default class App extends React.PureComponent { referencedRules: {}, referencedUsers: {}, selected: getOpen(props.location.query), + selectedFlowIndex: null, selectedLocationIndex: null }; } @@ -137,11 +139,15 @@ export default class App extends React.PureComponent { const openIssue = this.getOpenIssue(nextProps, this.state.issues); if (openIssue != null && openIssue.key !== this.state.selected) { - this.setState({ selected: openIssue.key, selectedLocationIndex: null }); + this.setState({ + selected: openIssue.key, + selectedFlowIndex: null, + selectedLocationIndex: null + }); } if (openIssue == null) { - this.setState({ selectedLocationIndex: null }); + this.setState({ selectedFlowIndex: null, selectedLocationIndex: null }); } this.setState({ @@ -252,7 +258,11 @@ export default class App extends React.PureComponent { if (this.state.openIssue) { this.openIssue(issues[selectedIndex + 1].key); } else { - this.setState({ selected: issues[selectedIndex + 1].key, selectedLocationIndex: null }); + this.setState({ + selected: issues[selectedIndex + 1].key, + selectedFlowIndex: null, + selectedLocationIndex: null + }); } } }; @@ -264,7 +274,11 @@ export default class App extends React.PureComponent { if (this.state.openIssue) { this.openIssue(issues[selectedIndex - 1].key); } else { - this.setState({ selected: issues[selectedIndex - 1].key, selectedLocationIndex: null }); + this.setState({ + selected: issues[selectedIndex - 1].key, + selectedFlowIndex: null, + selectedLocationIndex: null + }); } } }; @@ -372,6 +386,7 @@ export default class App extends React.PureComponent { selected: issues.length > 0 ? openIssue != null ? openIssue.key : issues[0].key : undefined, + selectedFlowIndex: null, selectedLocationIndex: null }); } @@ -560,6 +575,7 @@ export default class App extends React.PureComponent { selectLocation = (index: ?number) => this.setState(actions.selectLocation(index)); selectNextLocation = () => this.setState(actions.selectNextLocation); selectPreviousLocation = () => this.setState(actions.selectPreviousLocation); + selectFlow = (index: ?number) => this.setState(actions.selectFlow(index)); renderBulkChange(openIssue: ?Issue) { const { component, currentUser } = this.props; @@ -649,9 +665,11 @@ export default class App extends React.PureComponent { /> <ConciseIssuesList issues={issues} + onFlowSelect={this.selectFlow} onIssueSelect={this.openIssue} onLocationSelect={this.selectLocation} selected={this.state.selected} + selectedFlowIndex={this.state.selectedFlowIndex} selectedLocationIndex={this.state.selectedLocationIndex} /> {paging != null && @@ -755,6 +773,7 @@ export default class App extends React.PureComponent { onIssueChange={this.handleIssueChange} onIssueSelect={this.openIssue} onLocationSelect={this.selectLocation} + selectedFlowIndex={this.state.selectedFlowIndex} selectedLocationIndex={ this.state.locationsNavigator ? this.state.selectedLocationIndex : null } diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js index dc1de955664..9c321fb007c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js @@ -29,6 +29,7 @@ type Props = {| onIssueSelect: string => void, onLocationSelect: number => void, openIssue: Issue, + selectedFlowIndex: ?number, selectedLocationIndex: ?number |}; @@ -58,9 +59,11 @@ export default class IssuesSourceViewer extends React.PureComponent { }; render() { - const { openIssue, selectedLocationIndex } = this.props; + const { openIssue, selectedFlowIndex, selectedLocationIndex } = this.props; - const locations = openIssue.secondaryLocations; + const locations = selectedFlowIndex != null + ? openIssue.flows[selectedFlowIndex] + : openIssue.flows.length > 0 ? openIssue.flows[0] : openIssue.secondaryLocations; const locationMessage = locations != null && selectedLocationIndex != null && diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js index d6c1662c2e6..5fdeec3da53 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js @@ -25,11 +25,13 @@ import type { Issue } from '../../../components/issue/types'; type Props = {| issue: Issue, + onFlowSelect: number => void, onLocationSelect: number => void, onSelect: string => void, previousIssue: ?Issue, scroll: HTMLElement => void, selected: boolean, + selectedFlowIndex: ?number, selectedLocationIndex: ?number |}; @@ -47,9 +49,11 @@ export default class ConciseIssue extends React.PureComponent { <ConciseIssueBox issue={issue} onClick={this.props.onSelect} + onFlowSelect={this.props.onFlowSelect} onLocationSelect={this.props.onLocationSelect} scroll={this.props.scroll} selected={selected} + selectedFlowIndex={selected ? this.props.selectedFlowIndex : null} selectedLocationIndex={selected ? this.props.selectedLocationIndex : null} /> </div> diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js index da7f573a062..bff17414951 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js @@ -29,9 +29,11 @@ import type { Issue } from '../../../components/issue/types'; type Props = {| issue: Issue, onClick: string => void, + onFlowSelect: number => void, onLocationSelect: number => void, scroll: HTMLElement => void, selected: boolean, + selectedFlowIndex: ?number, selectedLocationIndex: ?number |}; @@ -66,20 +68,27 @@ export default class ConciseIssueBox extends React.PureComponent { : { onClick: this.handleClick, role: 'listitem', tabIndex: 0 }; return ( - <div className={classNames('concise-issue-box', { selected })} {...clickAttributes}> + <div + className={classNames('concise-issue-box', 'clearfix', { selected })} + {...clickAttributes}> <div className="concise-issue-box-message" ref={node => (this.node = node)}> {issue.message} </div> <div className="concise-issue-box-attributes"> <TypeHelper type={issue.type} /> <SeverityHelper className="big-spacer-left" severity={issue.severity} /> - <ConciseIssueLocations issue={issue} /> + <ConciseIssueLocations + issue={issue} + onFlowSelect={this.props.onFlowSelect} + selectedFlowIndex={this.props.selectedFlowIndex} + /> </div> {selected && <ConciseIssueLocationsNavigator issue={issue} onLocationSelect={this.props.onLocationSelect} scroll={this.props.scroll} + selectedFlowIndex={this.props.selectedFlowIndex} selectedLocationIndex={this.props.selectedLocationIndex} />} </div> diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js index 0eb9223eafe..ae19b8f367f 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js @@ -25,17 +25,20 @@ import { translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; type Props = {| - count: number + count: number, + onClick?: () => void, + selected?: boolean |}; export default function ConciseIssueLocationBadge(props: Props) { return ( <Tooltip + mouseEnterDelay={0.5} overlay={translateWithParameters( 'issue.this_issue_involves_x_code_locations', formatMeasure(props.count) )}> - <LocationIndex> + <LocationIndex onClick={props.onClick} selected={props.selected}> {'+'}{props.count} </LocationIndex> </Tooltip> diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js index e3563ade3d9..cf1144a24bc 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js @@ -23,22 +23,67 @@ import ConciseIssueLocationBadge from './ConciseIssueLocationBadge'; import type { Issue } from '../../../components/issue/types'; type Props = {| - issue: Issue + issue: Issue, + onFlowSelect: number => void, + selectedFlowIndex: ?number |}; +type State = { + collapsed: boolean +}; + +const LIMIT = 3; + export default class ConciseIssueLocations extends React.PureComponent { props: Props; + state: State = { collapsed: true }; + + handleExpandClick = (event: Event) => { + event.preventDefault(); + this.setState({ collapsed: false }); + }; + + renderExpandButton() { + return ( + <a className="little-spacer-left link-no-underline" href="#" onClick={this.handleExpandClick}> + ... + </a> + ); + } render() { const { secondaryLocations, flows } = this.props.issue; - return ( - <div className="pull-right"> - {secondaryLocations.length > 0 && - <ConciseIssueLocationBadge count={secondaryLocations.length} />} + const badges = []; - {flows.map((flow, index) => <ConciseIssueLocationBadge key={index} count={flow.length} />)} - </div> - ); + if (secondaryLocations.length > 0) { + badges.push( + <ConciseIssueLocationBadge + key="-1" + count={secondaryLocations.length} + selected={this.props.selectedFlowIndex == null} + /> + ); + } + + flows.forEach((flow, index) => { + badges.push( + <ConciseIssueLocationBadge + key={index} + count={flow.length} + onClick={() => this.props.onFlowSelect(index)} + selected={index === this.props.selectedFlowIndex} + /> + ); + }); + + return this.state.collapsed + ? <div className="concise-issue-locations pull-right"> + {badges.slice(0, LIMIT)} + {badges.length > LIMIT && this.renderExpandButton()} + </div> + : <div className="concise-issue-locations spacer-top"> + {badges} + </div>; } } diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.js index 7bac7593484..ebe8aff7935 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.js @@ -26,6 +26,7 @@ type Props = {| issue: Issue, onLocationSelect: number => void, scroll: HTMLElement => void, + selectedFlowIndex: ?number, selectedLocationIndex: ?number |}; @@ -38,16 +39,20 @@ export default class ConciseIssueLocationsNavigator extends React.PureComponent }; render() { - const { selectedLocationIndex } = this.props; - const { secondaryLocations } = this.props.issue; + const { selectedFlowIndex, selectedLocationIndex } = this.props; + const { flows, secondaryLocations } = this.props.issue; - if (secondaryLocations.length === 0) { + const locations = selectedFlowIndex != null + ? flows[selectedFlowIndex] + : flows.length > 0 ? flows[0] : secondaryLocations; + + if (locations == null || locations.length === 0) { return null; } return ( <div className="spacer-top"> - {secondaryLocations.map((location, index) => ( + {locations.map((location, index) => ( <ConciseIssueLocationsNavigatorLocation key={index} index={index} diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.js index e76a1ccdcc0..099e7804d2c 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.js @@ -54,7 +54,10 @@ export default class ConciseIssueLocationsNavigatorLocation extends React.PureCo render() { return ( <div className="little-spacer-top" ref={node => (this.node = node)}> - <a className="link-no-underline" href="#" onClick={this.handleClick}> + <a + className="consice-issue-locations-navigator-location" + href="#" + onClick={this.handleClick}> <LocationIndex selected={this.props.selected}> {this.props.index + 1} </LocationIndex> diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js index 8eae0760686..18e9018a6f5 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js @@ -25,9 +25,11 @@ import type { Issue } from '../../../components/issue/types'; type Props = {| issues: Array<Issue>, + onFlowSelect: number => void, onIssueSelect: string => void, onLocationSelect: number => void, selected?: string, + selectedFlowIndex: ?number, selectedLocationIndex: ?number |}; @@ -48,11 +50,13 @@ export default class ConciseIssuesList extends React.PureComponent { <ConciseIssue key={issue.key} issue={issue} + onFlowSelect={this.props.onFlowSelect} onLocationSelect={this.props.onLocationSelect} onSelect={this.props.onIssueSelect} previousIssue={index > 0 ? this.props.issues[index - 1] : null} scroll={this.handleScroll} selected={issue.key === this.props.selected} + selectedFlowIndex={this.props.selectedFlowIndex} selectedLocationIndex={this.props.selectedLocationIndex} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap index 1d0309673f0..1413fcc82bf 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap @@ -5,6 +5,7 @@ exports[`test should render 1`] = ` issue={Object {}} onClick={[Function]} selected={false} + selectedFlowIndex={null} selectedLocationIndex={null} /> </div> `; diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap index f131a618c5b..9eaefa3a503 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap @@ -1,5 +1,6 @@ exports[`test should render 1`] = ` <Tooltip + mouseEnterDelay={0.5} overlay="issue.this_issue_involves_x_code_locations.7" placement="bottom"> <LocationIndex diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap index 073f3d922c1..90378b33f25 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap @@ -1,27 +1,36 @@ exports[`test should render one flow 1`] = ` <div - className="pull-right"> + className="concise-issue-locations pull-right"> <ConciseIssueLocationBadge - count={3} /> + count={3} + onClick={[Function]} + selected={false} /> </div> `; exports[`test should render secondary locations 1`] = ` <div - className="pull-right"> + className="concise-issue-locations pull-right"> <ConciseIssueLocationBadge - count={3} /> + count={3} + selected={true} /> </div> `; exports[`test should render several flows 1`] = ` <div - className="pull-right"> + className="concise-issue-locations pull-right"> <ConciseIssueLocationBadge - count={3} /> + count={3} + onClick={[Function]} + selected={false} /> <ConciseIssueLocationBadge - count={2} /> + count={2} + onClick={[Function]} + selected={false} /> <ConciseIssueLocationBadge - count={3} /> + count={3} + onClick={[Function]} + selected={false} /> </div> `; 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 6fbcb9b4e73..d035084daef 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -113,4 +113,20 @@ .concise-issue-box:not(.selected) .location-index { background-color: #ccc; +} + +.concise-issue-locations { + margin-right: -4px; + margin-bottom: -4px; +} + +.concise-issue-locations .location-index { + margin-right: 4px; + margin-bottom: 4px; +} + +.consice-issue-locations-navigator-location { + display: flex; + align-items: flex-start; + border: none; }
\ No newline at end of file diff --git a/server/sonar-web/src/main/js/components/common/LocationIndex.js b/server/sonar-web/src/main/js/components/common/LocationIndex.js index 03b74ee0c84..ecc4027b124 100644 --- a/server/sonar-web/src/main/js/components/common/LocationIndex.js +++ b/server/sonar-web/src/main/js/components/common/LocationIndex.js @@ -29,19 +29,13 @@ type Props = { }; export default function LocationIndex(props: Props) { - const clickAttributes = props.onClick - ? { - onClick: props.onClick, - role: 'button', - tabIndex: 0 - } - : {}; + const { children, onClick, selected, ...other } = props; + const clickAttributes = onClick ? { onClick, role: 'button', tabIndex: 0 } : {}; + // put {...others} because Tooltip sets some event handlers return ( - <div - className={classNames('location-index', { selected: props.selected })} - {...clickAttributes}> - {props.children} + <div className={classNames('location-index', { selected })} {...clickAttributes} {...other}> + {children} </div> ); } diff --git a/server/sonar-web/src/main/js/helpers/issues.js b/server/sonar-web/src/main/js/helpers/issues.js index 804b410abf0..92603e52e17 100644 --- a/server/sonar-web/src/main/js/helpers/issues.js +++ b/server/sonar-web/src/main/js/helpers/issues.js @@ -108,6 +108,12 @@ const ensureTextRange = (issue: RawIssue) => { : {}; }; +const reverseLocations = (locations: Array<*>) => { + const x = [...locations]; + x.reverse(); + return x; +}; + const splitFlows = ( issue: RawIssue // $FlowFixMe textRange is not null @@ -121,7 +127,7 @@ const splitFlows = ( return onlySecondaryLocations ? { secondaryLocations: flatten(parsedFlows), flows: [] } - : { secondaryLocations: [], flows: parsedFlows }; + : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) }; }; export const parseIssueFromResponse = ( |