From c5471380870c76f5d714325c29cd0964a2fb324f Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 24 Apr 2017 10:36:38 +0200 Subject: [PATCH] SONAR-9067 Display multiple flows in the issues list (#1969) --- .../src/main/js/apps/issues/actions.js | 15 +++-- .../src/main/js/apps/issues/components/App.js | 27 ++++++-- .../issues/components/IssuesSourceViewer.js | 7 ++- .../issues/conciseIssuesList/ConciseIssue.js | 4 ++ .../conciseIssuesList/ConciseIssueBox.js | 13 +++- .../ConciseIssueLocationBadge.js | 7 ++- .../ConciseIssueLocations.js | 61 ++++++++++++++++--- .../ConciseIssueLocationsNavigator.js | 13 ++-- .../ConciseIssueLocationsNavigatorLocation.js | 5 +- .../conciseIssuesList/ConciseIssuesList.js | 4 ++ .../__snapshots__/ConciseIssue-test.js.snap | 1 + .../ConciseIssueLocationBadge-test.js.snap | 1 + .../ConciseIssueLocations-test.js.snap | 25 +++++--- .../src/main/js/apps/issues/styles.css | 16 +++++ .../js/components/common/LocationIndex.js | 16 ++--- .../sonar-web/src/main/js/helpers/issues.js | 8 ++- 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 { /> {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 { 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 ( -
+
(this.node = node)}> {issue.message}
- +
{selected && }
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 ( - + {'+'}{props.count} 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 ( + + ... + + ); + } render() { const { secondaryLocations, flows } = this.props.issue; - return ( -
- {secondaryLocations.length > 0 && - } + const badges = []; - {flows.map((flow, index) => )} -
- ); + if (secondaryLocations.length > 0) { + badges.push( + + ); + } + + flows.forEach((flow, index) => { + badges.push( + this.props.onFlowSelect(index)} + selected={index === this.props.selectedFlowIndex} + /> + ); + }); + + return this.state.collapsed + ?
+ {badges.slice(0, LIMIT)} + {badges.length > LIMIT && this.renderExpandButton()} +
+ :
+ {badges} +
; } } 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 (
- {secondaryLocations.map((location, index) => ( + {locations.map((location, index) => ( (this.node = node)}> - + {this.props.index + 1} 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, + 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 { 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} />
`; 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`] = ` + className="concise-issue-locations pull-right"> + count={3} + onClick={[Function]} + selected={false} />
`; exports[`test should render secondary locations 1`] = `
+ className="concise-issue-locations pull-right"> + count={3} + selected={true} />
`; exports[`test should render several flows 1`] = `
+ className="concise-issue-locations pull-right"> + count={3} + onClick={[Function]} + selected={false} /> + count={2} + onClick={[Function]} + selected={false} /> + count={3} + onClick={[Function]} + selected={false} />
`; 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 ( -
- {props.children} +
+ {children}
); } 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 = ( -- 2.39.5