@@ -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 }; | |||
}; |
@@ -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 | |||
} |
@@ -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 && |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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>; | |||
} | |||
} |
@@ -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} |
@@ -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> |
@@ -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} | |||
/> | |||
))} |
@@ -5,6 +5,7 @@ exports[`test should render 1`] = ` | |||
issue={Object {}} | |||
onClick={[Function]} | |||
selected={false} | |||
selectedFlowIndex={null} | |||
selectedLocationIndex={null} /> | |||
</div> | |||
`; |
@@ -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 |
@@ -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> | |||
`; |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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 = ( |