@@ -17,7 +17,6 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { getSources } from '../../../api/components'; | |||
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus'; | |||
@@ -43,12 +42,10 @@ interface Props { | |||
issuePopup?: { issue: string; name: string }; | |||
issuesByLine: T.IssuesByLine; | |||
lastSnippetGroup: boolean; | |||
linePopup?: T.LinePopup; | |||
loadDuplications: (component: string, line: T.SourceLine) => void; | |||
locations: T.FlowLocation[]; | |||
onIssueChange: (issue: T.Issue) => void; | |||
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; | |||
onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void; | |||
onLocationSelect: (index: number) => void; | |||
renderDuplicationPopup: ( | |||
component: T.SourceViewerFile, | |||
@@ -273,13 +270,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
); | |||
}; | |||
handleLinePopupToggle = (linePopup: T.LinePopup) => { | |||
this.props.onLinePopupToggle({ | |||
...linePopup, | |||
component: this.props.snippetGroup.component.key | |||
}); | |||
}; | |||
handleOpenIssues = (line: T.SourceLine) => { | |||
this.setState(state => ({ | |||
openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true } | |||
@@ -326,9 +316,10 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
<SnippetViewer | |||
branchLike={this.props.branchLike} | |||
component={this.props.snippetGroup.component} | |||
duplications={this.props.duplications} | |||
duplicationsByLine={this.props.duplicationsByLine} | |||
expandBlock={this.expandBlock} | |||
handleCloseIssues={this.handleCloseIssues} | |||
handleLinePopupToggle={this.handleLinePopupToggle} | |||
handleOpenIssues={this.handleOpenIssues} | |||
handleSymbolClick={this.handleSymbolClick} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
@@ -338,7 +329,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
issuePopup={this.props.issuePopup} | |||
issuesByLine={issuesByLine} | |||
lastSnippetOfLastGroup={lastSnippetOfLastGroup} | |||
linePopup={this.props.linePopup} | |||
loadDuplications={this.loadDuplications} | |||
locations={this.props.locations} | |||
locationsByLine={locationsByLine} | |||
@@ -354,14 +344,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
} | |||
render() { | |||
const { | |||
branchLike, | |||
duplications, | |||
issue, | |||
issuesByLine, | |||
lastSnippetGroup, | |||
snippetGroup | |||
} = this.props; | |||
const { branchLike, issue, issuesByLine, lastSnippetGroup, snippetGroup } = this.props; | |||
const { additionalLines, loading, snippets } = this.state; | |||
const locations = | |||
issue.component === snippetGroup.component.key ? locationsByLine([issue]) : {}; | |||
@@ -382,11 +365,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
isFlow ? lastSnippetGroup && snippetIndex === snippets.length - 1 : snippetIndex === 0; | |||
return ( | |||
<div | |||
className={classNames('component-source-container', { | |||
'source-duplications-expanded': duplications && duplications.length > 0 | |||
})} | |||
ref={this.rootNodeRef}> | |||
<div className="component-source-container" ref={this.rootNodeRef}> | |||
<SourceViewerHeaderSlim | |||
branchLike={branchLike} | |||
expandable={!fullyShown} |
@@ -60,7 +60,6 @@ interface State { | |||
duplications?: T.Duplication[]; | |||
duplicationsByLine: { [line: number]: number[] }; | |||
issuePopup?: { issue: string; name: string }; | |||
linePopup?: T.LinePopup & { component: string }; | |||
loading: boolean; | |||
notAccessible: boolean; | |||
} | |||
@@ -89,22 +88,18 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
this.mounted = false; | |||
} | |||
fetchDuplications = (component: string, line: T.SourceLine) => { | |||
fetchDuplications = (component: string) => { | |||
getDuplications({ | |||
key: component, | |||
...getBranchLikeQuery(this.props.branchLike) | |||
}).then( | |||
r => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
this.setState({ | |||
duplicatedFiles: r.files, | |||
duplications: r.duplications, | |||
duplicationsByLine: getDuplicationsByLine(r.duplications), | |||
linePopup: | |||
r.duplications.length === 1 | |||
? { component, index: 0, line: line.line, name: 'duplications' } | |||
: state.linePopup | |||
})); | |||
duplicationsByLine: getDuplicationsByLine(r.duplications) | |||
}); | |||
} | |||
}, | |||
() => {} | |||
@@ -119,7 +114,6 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
this.setState({ | |||
components, | |||
issuePopup: undefined, | |||
linePopup: undefined, | |||
loading: false | |||
}); | |||
if (this.props.onLoaded) { | |||
@@ -151,33 +145,6 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
}); | |||
}; | |||
handleLinePopupToggle = ({ | |||
component, | |||
index, | |||
line, | |||
name, | |||
open | |||
}: T.LinePopup & { component: string }) => { | |||
this.setState((state: State) => { | |||
const samePopup = | |||
state.linePopup !== undefined && | |||
state.linePopup.line === line && | |||
state.linePopup.name === name && | |||
state.linePopup.component === component && | |||
state.linePopup.index === index; | |||
if (open !== false && !samePopup) { | |||
return { linePopup: { component, index, line, name } }; | |||
} else if (open !== true && samePopup) { | |||
return { linePopup: undefined }; | |||
} | |||
return null; | |||
}); | |||
}; | |||
handleCloseLinePopup = () => { | |||
this.setState({ linePopup: undefined }); | |||
}; | |||
renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => { | |||
const { duplicatedFiles, duplications } = this.state; | |||
@@ -195,7 +162,6 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
branchLike={this.props.branchLike} | |||
duplicatedFiles={duplicatedFiles} | |||
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} | |||
onClose={this.handleCloseLinePopup} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
@@ -224,21 +190,13 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
} | |||
const { issue, locations } = this.props; | |||
const { components, duplications, duplicationsByLine, linePopup } = this.state; | |||
const { components, duplications, duplicationsByLine } = this.state; | |||
const issuesByComponent = issuesByComponentAndLine(this.props.issues); | |||
const locationsByComponent = groupLocationsByComponent(issue, locations, components); | |||
return ( | |||
<div> | |||
{locationsByComponent.map((snippetGroup, i) => { | |||
let componentProps = {}; | |||
if (linePopup && snippetGroup.component.key === linePopup.component) { | |||
componentProps = { | |||
duplications, | |||
duplicationsByLine, | |||
linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name } | |||
}; | |||
} | |||
return ( | |||
<SourceViewerContext.Provider | |||
// eslint-disable-next-line react/no-array-index-key | |||
@@ -246,6 +204,8 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}> | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={this.props.branchLike} | |||
duplications={duplications} | |||
duplicationsByLine={duplicationsByLine} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
issue={issue} | |||
issuePopup={this.state.issuePopup} | |||
@@ -255,12 +215,10 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
locations={snippetGroup.locations || []} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.handleIssuePopupToggle} | |||
onLinePopupToggle={this.handleLinePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
snippetGroup={snippetGroup} | |||
{...componentProps} | |||
/> | |||
</SourceViewerContext.Provider> | |||
); |
@@ -42,7 +42,6 @@ interface Props { | |||
duplicationsByLine?: { [line: number]: number[] }; | |||
expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => Promise<void>; | |||
handleCloseIssues: (line: T.SourceLine) => void; | |||
handleLinePopupToggle: (line: T.SourceLine) => void; | |||
handleOpenIssues: (line: T.SourceLine) => void; | |||
handleSymbolClick: (symbols: string[]) => void; | |||
highlightedLocationMessage: { index: number; text: string | undefined } | undefined; | |||
@@ -52,8 +51,7 @@ interface Props { | |||
issuePopup?: { issue: string; name: string }; | |||
issuesByLine: T.IssuesByLine; | |||
lastSnippetOfLastGroup: boolean; | |||
linePopup?: T.LinePopup; | |||
loadDuplications: (line: T.SourceLine) => void; | |||
loadDuplications?: (line: T.SourceLine) => void; | |||
locations: T.FlowLocation[]; | |||
locationsByLine: { [line: number]: T.LinearIssueLocation[] }; | |||
onIssueChange: (issue: T.Issue) => void; | |||
@@ -138,6 +136,7 @@ export default class SnippetViewer extends React.PureComponent<Props> { | |||
(duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || []; | |||
const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key); | |||
const noop = () => {}; | |||
return ( | |||
<Line | |||
@@ -162,15 +161,13 @@ export default class SnippetViewer extends React.PureComponent<Props> { | |||
key={line.line} | |||
last={false} | |||
line={line} | |||
linePopup={this.props.linePopup} | |||
loadDuplications={this.props.loadDuplications} | |||
loadDuplications={this.props.loadDuplications || noop} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.props.onIssuePopupToggle} | |||
onIssueSelect={() => {}} | |||
onIssueUnselect={() => {}} | |||
onIssueSelect={noop} | |||
onIssueUnselect={noop} | |||
onIssuesClose={this.props.handleCloseIssues} | |||
onIssuesOpen={this.props.handleOpenIssues} | |||
onLinePopupToggle={this.props.handleLinePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
onSymbolClick={this.props.handleSymbolClick} | |||
openIssues={this.props.openIssuesByLine[line.line]} | |||
@@ -211,7 +208,8 @@ export default class SnippetViewer extends React.PureComponent<Props> { | |||
? Math.max(0, LINES_BELOW_ISSUE - (bottomLine - lowestVisibleIssue)) | |||
: 0; | |||
const displayDuplications = snippet.some(s => !!s.duplicated); | |||
const displayDuplications = | |||
Boolean(this.props.loadDuplications) && snippet.some(s => !!s.duplicated); | |||
return ( | |||
<div className="source-viewer-code snippet" ref={this.snippetNodeRef}> |
@@ -221,12 +221,10 @@ it('should correctly handle lines actions', () => { | |||
...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56]) | |||
}; | |||
const loadDuplications = jest.fn(); | |||
const onLinePopupToggle = jest.fn(); | |||
const renderDuplicationPopup = jest.fn(); | |||
const wrapper = shallowRender({ | |||
loadDuplications, | |||
onLinePopupToggle, | |||
renderDuplicationPopup, | |||
snippetGroup | |||
}); | |||
@@ -238,12 +236,6 @@ it('should correctly handle lines actions', () => { | |||
.prop<Function>('loadDuplications')(line); | |||
expect(loadDuplications).toHaveBeenCalledWith('a', line); | |||
wrapper | |||
.find('SnippetViewer') | |||
.first() | |||
.prop<Function>('handleLinePopupToggle')({ line: 13, name: 'foo' }); | |||
expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' }); | |||
wrapper | |||
.find('SnippetViewer') | |||
.first() | |||
@@ -264,18 +256,14 @@ describe('getNodes', () => { | |||
const wrapper = mount<ComponentSourceSnippetGroupViewer>( | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={mockMainBranch()} | |||
duplications={undefined} | |||
duplicationsByLine={undefined} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetGroup={false} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
onIssueChange={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onLinePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
@@ -322,18 +310,14 @@ describe('getHeight', () => { | |||
const wrapper = mount<ComponentSourceSnippetGroupViewer>( | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={mockMainBranch()} | |||
duplications={undefined} | |||
duplicationsByLine={undefined} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetGroup={false} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
onIssueChange={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onLinePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
@@ -376,18 +360,14 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props'] | |||
return shallow<ComponentSourceSnippetGroupViewer>( | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={mockMainBranch()} | |||
duplications={undefined} | |||
duplicationsByLine={undefined} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetGroup={false} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
onIssueChange={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onLinePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} |
@@ -87,27 +87,6 @@ it('should handle issue popup', () => { | |||
expect(wrapper.state('issuePopup')).toBeUndefined(); | |||
}); | |||
it('should handle line popup', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const linePopup = { component: 'foo', index: 0, line: 16, name: 'b.tsx' }; | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(linePopup); | |||
expect(wrapper.state('linePopup')).toEqual(linePopup); | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(linePopup); | |||
expect(wrapper.state('linePopup')).toBeUndefined(); | |||
const openLinePopup = { ...linePopup, open: true }; | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')( | |||
openLinePopup | |||
); | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')( | |||
openLinePopup | |||
); | |||
expect(wrapper.state('linePopup')).toEqual(linePopup); | |||
}); | |||
it('should handle duplication popup', async () => { | |||
const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } }; | |||
const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }]; | |||
@@ -126,12 +105,6 @@ it('should handle duplication popup', async () => { | |||
expect(wrapper.state('duplicatedFiles')).toEqual(files); | |||
expect(wrapper.state('duplications')).toEqual(duplications); | |||
expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] }); | |||
expect(wrapper.state('linePopup')).toEqual({ | |||
component: 'foo', | |||
index: 0, | |||
line: 16, | |||
name: 'duplications' | |||
}); | |||
expect( | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('renderDuplicationPopup')( |
@@ -127,7 +127,6 @@ function shallowRender(props: Partial<SnippetViewer['props']> = {}) { | |||
duplicationsByLine={undefined} | |||
expandBlock={jest.fn()} | |||
handleCloseIssues={jest.fn()} | |||
handleLinePopupToggle={jest.fn()} | |||
handleOpenIssues={jest.fn()} | |||
handleSymbolClick={jest.fn()} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
@@ -136,7 +135,6 @@ function shallowRender(props: Partial<SnippetViewer['props']> = {}) { | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetOfLastGroup={false} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
locationsByLine={{}} | |||
@@ -161,7 +159,6 @@ function mountRender(props: Partial<SnippetViewer['props']> = {}) { | |||
duplicationsByLine={undefined} | |||
expandBlock={jest.fn()} | |||
handleCloseIssues={jest.fn()} | |||
handleLinePopupToggle={jest.fn()} | |||
handleOpenIssues={jest.fn()} | |||
handleSymbolClick={jest.fn()} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
@@ -170,7 +167,6 @@ function mountRender(props: Partial<SnippetViewer['props']> = {}) { | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetOfLastGroup={false} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
locationsByLine={{}} |
@@ -48,6 +48,7 @@ exports[`should render correctly 2`] = ` | |||
} | |||
> | |||
<ComponentSourceSnippetGroupViewer | |||
duplicationsByLine={Object {}} | |||
issue={ | |||
Object { | |||
"actions": Array [], | |||
@@ -169,7 +170,6 @@ exports[`should render correctly 2`] = ` | |||
} | |||
onIssueChange={[MockFunction]} | |||
onIssuePopupToggle={[Function]} | |||
onLinePopupToggle={[Function]} | |||
onLocationSelect={[MockFunction]} | |||
renderDuplicationPopup={[Function]} | |||
scroll={[MockFunction]} |
@@ -61,7 +61,6 @@ exports[`should render correctly 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
renderDuplicationPopup={[MockFunction]} | |||
@@ -110,7 +109,6 @@ exports[`should render correctly 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -172,7 +170,6 @@ exports[`should render correctly 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -271,7 +268,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
renderDuplicationPopup={[MockFunction]} | |||
@@ -320,7 +316,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -382,7 +377,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -444,7 +438,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -532,7 +525,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
renderDuplicationPopup={[MockFunction]} | |||
@@ -581,7 +573,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -643,7 +634,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -705,7 +695,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -767,7 +756,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -829,7 +817,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -891,7 +878,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -991,7 +977,6 @@ exports[`should render correctly with no SCM 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
renderDuplicationPopup={[MockFunction]} | |||
@@ -1041,7 +1026,6 @@ exports[`should render correctly with no SCM 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ | |||
@@ -1104,7 +1088,6 @@ exports[`should render correctly with no SCM 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[MockFunction]} | |||
onIssuesOpen={[MockFunction]} | |||
onLinePopupToggle={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
previousLine={ |
@@ -37,7 +37,6 @@ interface State { | |||
highlightedSymbols: string[]; | |||
lastLine?: number; | |||
loading: boolean; | |||
linePopup?: T.LinePopup & { component: string }; | |||
sourceLines: T.SourceLine[]; | |||
} | |||
@@ -143,31 +142,13 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat | |||
}); | |||
}; | |||
handleLinePopupToggle = (params: T.LinePopup & { component: string }) => { | |||
const { component, index, line, name, open } = params; | |||
this.setState((state: State) => { | |||
const samePopup = | |||
state.linePopup !== undefined && | |||
state.linePopup.line === line && | |||
state.linePopup.name === name && | |||
state.linePopup.component === component && | |||
state.linePopup.index === index; | |||
if (open !== false && !samePopup) { | |||
return { linePopup: params }; | |||
} else if (open !== true && samePopup) { | |||
return { linePopup: undefined }; | |||
} | |||
return null; | |||
}); | |||
}; | |||
handleSymbolClick = (highlightedSymbols: string[]) => { | |||
this.setState({ highlightedSymbols }); | |||
}; | |||
render() { | |||
const { branchLike, component, hotspot } = this.props; | |||
const { highlightedSymbols, lastLine, linePopup, loading, sourceLines } = this.state; | |||
const { highlightedSymbols, lastLine, loading, sourceLines } = this.state; | |||
const locations = locationsByLine([hotspot]); | |||
@@ -180,11 +161,9 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat | |||
highlightedSymbols={highlightedSymbols} | |||
hotspot={hotspot} | |||
lastLine={lastLine} | |||
linePopup={linePopup} | |||
loading={loading} | |||
locations={locations} | |||
onExpandBlock={this.handleExpansion} | |||
onLinePopupToggle={this.handleLinePopupToggle} | |||
onSymbolClick={this.handleSymbolClick} | |||
sourceLines={sourceLines} | |||
sourceViewerFile={sourceViewerFile} |
@@ -33,9 +33,7 @@ export interface HotspotSnippetContainerRendererProps { | |||
lastLine?: number; | |||
loading: boolean; | |||
locations: { [line: number]: T.LinearIssueLocation[] }; | |||
linePopup?: T.LinePopup & { component: string }; | |||
onExpandBlock: (direction: T.ExpandDirection) => Promise<void>; | |||
onLinePopupToggle: (line: T.SourceLine) => void; | |||
onSymbolClick: (symbols: string[]) => void; | |||
sourceLines: T.SourceLine[]; | |||
sourceViewerFile: T.SourceViewerFile; | |||
@@ -51,7 +49,6 @@ export default function HotspotSnippetContainerRenderer( | |||
displayProjectName, | |||
highlightedSymbols, | |||
hotspot, | |||
linePopup, | |||
loading, | |||
locations, | |||
sourceLines, | |||
@@ -79,7 +76,6 @@ export default function HotspotSnippetContainerRenderer( | |||
displaySCM={false} | |||
expandBlock={(_i, direction) => props.onExpandBlock(direction)} | |||
handleCloseIssues={noop} | |||
handleLinePopupToggle={props.onLinePopupToggle} | |||
handleOpenIssues={noop} | |||
handleSymbolClick={props.onSymbolClick} | |||
highlightedLocationMessage={undefined} | |||
@@ -88,8 +84,6 @@ export default function HotspotSnippetContainerRenderer( | |||
issue={hotspot} | |||
issuesByLine={{}} | |||
lastSnippetOfLastGroup={false} | |||
linePopup={linePopup} | |||
loadDuplications={noop} | |||
locations={[]} | |||
locationsByLine={locations} | |||
onIssueChange={noop} |
@@ -155,32 +155,6 @@ describe('Expansion', () => { | |||
}); | |||
}); | |||
it('should handle line popups', async () => { | |||
(getSources as jest.Mock).mockResolvedValueOnce( | |||
range(5, 18).map(line => mockSourceLine({ line })) | |||
); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const params = wrapper.state().sourceLines[0]; | |||
wrapper | |||
.find(HotspotSnippetContainerRenderer) | |||
.props() | |||
.onLinePopupToggle(params); | |||
expect(wrapper.state().linePopup).toEqual(params); | |||
// Close it | |||
wrapper | |||
.find(HotspotSnippetContainerRenderer) | |||
.props() | |||
.onLinePopupToggle(params); | |||
expect(wrapper.state().linePopup).toBeUndefined(); | |||
}); | |||
it('should handle symbol click', () => { | |||
const wrapper = shallowRender(); | |||
const symbols = ['symbol']; |
@@ -39,11 +39,9 @@ function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) { | |||
highlightedSymbols={[]} | |||
hotspot={mockHotspot()} | |||
lastLine={undefined} | |||
linePopup={undefined} | |||
loading={false} | |||
locations={{}} | |||
onExpandBlock={jest.fn()} | |||
onLinePopupToggle={jest.fn()} | |||
onSymbolClick={jest.fn()} | |||
sourceLines={[]} | |||
sourceViewerFile={mockSourceViewerFile()} |
@@ -124,7 +124,6 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
onExpandBlock={[Function]} | |||
onLinePopupToggle={[Function]} | |||
onSymbolClick={[Function]} | |||
sourceLines={Array []} | |||
sourceViewerFile={ |
@@ -135,7 +135,6 @@ exports[`should render correctly: with sourcelines 1`] = ` | |||
displaySCM={false} | |||
expandBlock={[Function]} | |||
handleCloseIssues={[Function]} | |||
handleLinePopupToggle={[MockFunction]} | |||
handleOpenIssues={[Function]} | |||
handleSymbolClick={[MockFunction]} | |||
highlightedSymbols={Array []} | |||
@@ -241,7 +240,6 @@ exports[`should render correctly: with sourcelines 1`] = ` | |||
} | |||
issuesByLine={Object {}} | |||
lastSnippetOfLastGroup={false} | |||
loadDuplications={[Function]} | |||
locations={Array []} | |||
locationsByLine={Object {}} | |||
onIssueChange={[Function]} |
@@ -17,7 +17,6 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import { intersection, uniqBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
@@ -96,7 +95,6 @@ export interface Props { | |||
interface State { | |||
component?: T.SourceViewerFile; | |||
displayDuplications: boolean; | |||
duplicatedFiles?: T.Dict<T.DuplicatedFile>; | |||
duplications?: T.Duplication[]; | |||
duplicationsByLine: { [line: number]: number[] }; | |||
@@ -106,7 +104,6 @@ interface State { | |||
issuePopup?: { issue: string; name: string }; | |||
issues?: T.Issue[]; | |||
issuesByLine: { [line: number]: T.Issue[] }; | |||
linePopup?: T.LinePopup; | |||
loading: boolean; | |||
loadingSourcesAfter: boolean; | |||
loadingSourcesBefore: boolean; | |||
@@ -136,7 +133,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
super(props); | |||
this.state = { | |||
displayDuplications: false, | |||
duplicationsByLine: {}, | |||
hasSourcesAfter: false, | |||
highlightedSymbols: [], | |||
@@ -245,7 +241,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
this.setState( | |||
{ | |||
component, | |||
displayDuplications: false, | |||
duplicatedFiles: undefined, | |||
duplications: undefined, | |||
duplicationsByLine: {}, | |||
@@ -254,7 +249,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
issueLocationsByLine: locationsByLine(issues), | |||
issues, | |||
issuesByLine: issuesByLine(issues), | |||
linePopup: undefined, | |||
loading: false, | |||
notAccessible: false, | |||
notExist: false, | |||
@@ -474,23 +468,18 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
); | |||
}; | |||
loadDuplications = (line: T.SourceLine) => { | |||
loadDuplications = () => { | |||
getDuplications({ | |||
key: this.props.component, | |||
...getBranchLikeQuery(this.props.branchLike) | |||
}).then( | |||
r => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
displayDuplications: true, | |||
this.setState({ | |||
duplications: r.duplications, | |||
duplicationsByLine: duplicationsByLine(r.duplications), | |||
duplicatedFiles: r.files, | |||
linePopup: | |||
r.duplications.length === 1 | |||
? { index: 0, line: line.line, name: 'duplications' } | |||
: state.linePopup | |||
})); | |||
duplicatedFiles: r.files | |||
}); | |||
} | |||
}, | |||
() => { | |||
@@ -499,26 +488,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
); | |||
}; | |||
handleLinePopupToggle = ({ index, line, name, open }: T.LinePopup) => { | |||
this.setState((state: State) => { | |||
const samePopup = | |||
state.linePopup !== undefined && | |||
state.linePopup.name === name && | |||
state.linePopup.line === line && | |||
state.linePopup.index === index; | |||
if (open !== false && !samePopup) { | |||
return { linePopup: { index, line, name } }; | |||
} else if (open !== true && samePopup) { | |||
return { linePopup: undefined }; | |||
} | |||
return null; | |||
}); | |||
}; | |||
closeLinePopup = () => { | |||
this.setState({ linePopup: undefined }); | |||
}; | |||
handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => { | |||
this.setState((state: State) => { | |||
const samePopup = | |||
@@ -595,7 +564,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
branchLike={this.props.branchLike} | |||
duplicatedFiles={duplicatedFiles} | |||
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} | |||
onClose={this.closeLinePopup} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
@@ -626,7 +594,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
issuePopup={this.state.issuePopup} | |||
issues={this.state.issues} | |||
issuesByLine={this.state.issuesByLine} | |||
linePopup={this.state.linePopup} | |||
loadDuplications={this.loadDuplications} | |||
loadSourcesAfter={this.loadSourcesAfter} | |||
loadSourcesBefore={this.loadSourcesBefore} | |||
@@ -638,7 +605,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
onIssueUnselect={this.handleIssueUnselect} | |||
onIssuesClose={this.handleCloseIssues} | |||
onIssuesOpen={this.handleOpenIssues} | |||
onLinePopupToggle={this.handleLinePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
onSymbolClick={this.handleSymbolClick} | |||
openIssuesByLine={this.state.openIssuesByLine} | |||
@@ -696,13 +662,9 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
return null; | |||
} | |||
const className = classNames('source-viewer', { | |||
'source-duplications-expanded': this.state.displayDuplications | |||
}); | |||
return ( | |||
<SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}> | |||
<div className={className} ref={node => (this.node = node)}> | |||
<div className="source-viewer" ref={node => (this.node = node)}> | |||
{this.renderHeader(this.props.branchLike, component)} | |||
{sourceRemoved && ( | |||
<Alert className="spacer-top" variant="warning"> |
@@ -59,7 +59,6 @@ interface Props { | |||
issuePopup: { issue: string; name: string } | undefined; | |||
issues: T.Issue[] | undefined; | |||
issuesByLine: { [line: number]: T.Issue[] }; | |||
linePopup: T.LinePopup | undefined; | |||
loadDuplications: (line: T.SourceLine) => void; | |||
loadingSourcesAfter: boolean; | |||
loadingSourcesBefore: boolean; | |||
@@ -71,7 +70,6 @@ interface Props { | |||
onIssueSelect: (issueKey: string) => void; | |||
onIssuesOpen: (line: T.SourceLine) => void; | |||
onIssueUnselect: () => void; | |||
onLinePopupToggle: (linePopup: T.LinePopup) => void; | |||
onLocationSelect: ((index: number) => void) | undefined; | |||
onSymbolClick: (symbols: string[]) => void; | |||
openIssuesByLine: { [line: number]: boolean }; | |||
@@ -143,7 +141,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
key={line.line} | |||
last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter} | |||
line={line} | |||
linePopup={this.props.linePopup} | |||
loadDuplications={this.props.loadDuplications} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.props.onIssuePopupToggle} | |||
@@ -151,7 +148,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
onIssueUnselect={this.props.onIssueUnselect} | |||
onIssuesClose={this.props.onIssuesClose} | |||
onIssuesOpen={this.props.onIssuesOpen} | |||
onLinePopupToggle={this.props.onLinePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
onSymbolClick={this.props.onSymbolClick} | |||
openIssues={this.props.openIssuesByLine[line.line] || false} |
@@ -153,7 +153,6 @@ exports[`should render correctly 1`] = ` | |||
onIssueUnselect={[Function]} | |||
onIssuesClose={[Function]} | |||
onIssuesOpen={[Function]} | |||
onLinePopupToggle={[Function]} | |||
onSymbolClick={[Function]} | |||
openIssuesByLine={Object {}} | |||
renderDuplicationPopup={[Function]} |
@@ -20,10 +20,8 @@ | |||
import { groupBy, sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { collapsedDirFromPath, fileFromPath } from 'sonar-ui-common/helpers/path'; | |||
import { isPullRequest } from '../../../helpers/branch-like'; | |||
@@ -36,7 +34,6 @@ interface Props { | |||
branchLike: BranchLike | undefined; | |||
duplicatedFiles?: T.Dict<T.DuplicatedFile>; | |||
inRemovedComponent: boolean; | |||
onClose: () => void; | |||
openComponent: WorkspaceContextShape['openComponent']; | |||
sourceViewerFile: T.SourceViewerFile; | |||
} | |||
@@ -65,7 +62,6 @@ export default class DuplicationPopup extends React.PureComponent<Props> { | |||
line: line ? Number(line) : undefined | |||
}); | |||
} | |||
this.props.onClose(); | |||
}; | |||
renderDuplication(file: T.DuplicatedFile, children: React.ReactNode, line?: number) { | |||
@@ -106,76 +102,74 @@ export default class DuplicationPopup extends React.PureComponent<Props> { | |||
); | |||
return ( | |||
<DropdownOverlay placement={PopupPlacement.RightTop}> | |||
<div className="source-viewer-bubble-popup abs-width-400"> | |||
{this.props.inRemovedComponent && ( | |||
<Alert variant="warning"> | |||
{translate('duplications.dups_found_on_deleted_resource')} | |||
</Alert> | |||
)} | |||
{duplications.length > 0 && ( | |||
<> | |||
<h6 className="spacer-bottom"> | |||
{translate('component_viewer.transition.duplication')} | |||
</h6> | |||
{duplications.map(duplication => ( | |||
<div className="spacer-top text-ellipsis" key={duplication.file.key}> | |||
<div className="component-name"> | |||
{this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && ( | |||
<> | |||
<div className="source-viewer-bubble-popup abs-width-400"> | |||
{this.props.inRemovedComponent && ( | |||
<Alert variant="warning"> | |||
{translate('duplications.dups_found_on_deleted_resource')} | |||
</Alert> | |||
)} | |||
{duplications.length > 0 && ( | |||
<> | |||
<h6 className="spacer-bottom"> | |||
{translate('component_viewer.transition.duplication')} | |||
</h6> | |||
{duplications.map(duplication => ( | |||
<div className="spacer-top text-ellipsis" key={duplication.file.key}> | |||
<div className="component-name"> | |||
{this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && ( | |||
<> | |||
<div className="component-name-parent"> | |||
<QualifierIcon className="little-spacer-right" qualifier="TRK" /> | |||
<Link to={getProjectUrl(duplication.file.project)}> | |||
{duplication.file.projectName} | |||
</Link> | |||
</div> | |||
{duplication.file.subProject && duplication.file.subProjectName && ( | |||
<div className="component-name-parent"> | |||
<QualifierIcon className="little-spacer-right" qualifier="TRK" /> | |||
<Link to={getProjectUrl(duplication.file.project)}> | |||
{duplication.file.projectName} | |||
</Link> | |||
<QualifierIcon className="little-spacer-right" qualifier="BRC" /> | |||
{duplication.file.subProjectName} | |||
</div> | |||
{duplication.file.subProject && duplication.file.subProjectName && ( | |||
<div className="component-name-parent"> | |||
<QualifierIcon className="little-spacer-right" qualifier="BRC" /> | |||
{duplication.file.subProjectName} | |||
</div> | |||
)} | |||
</> | |||
)} | |||
)} | |||
</> | |||
)} | |||
{duplication.file.key !== this.props.sourceViewerFile.key && ( | |||
<div className="component-name-path"> | |||
{this.renderDuplication( | |||
duplication.file, | |||
<> | |||
<span>{collapsedDirFromPath(duplication.file.name)}</span> | |||
<span className="component-name-file"> | |||
{fileFromPath(duplication.file.name)} | |||
</span> | |||
</> | |||
)} | |||
</div> | |||
)} | |||
{duplication.file.key !== this.props.sourceViewerFile.key && ( | |||
<div className="component-name-path"> | |||
<div className="component-name-path"> | |||
{'Lines: '} | |||
{duplication.blocks.map((block, index) => ( | |||
<React.Fragment key={index}> | |||
{this.renderDuplication( | |||
duplication.file, | |||
<> | |||
<span>{collapsedDirFromPath(duplication.file.name)}</span> | |||
<span className="component-name-file"> | |||
{fileFromPath(duplication.file.name)} | |||
</span> | |||
</> | |||
{block.from} | |||
{' – '} | |||
{block.from + block.size - 1} | |||
</>, | |||
block.from | |||
)} | |||
</div> | |||
)} | |||
<div className="component-name-path"> | |||
{'Lines: '} | |||
{duplication.blocks.map((block, index) => ( | |||
<React.Fragment key={index}> | |||
{this.renderDuplication( | |||
duplication.file, | |||
<> | |||
{block.from} | |||
{' – '} | |||
{block.from + block.size - 1} | |||
</>, | |||
block.from | |||
)} | |||
{index < duplication.blocks.length - 1 && ', '} | |||
</React.Fragment> | |||
))} | |||
</div> | |||
{index < duplication.blocks.length - 1 && ', '} | |||
</React.Fragment> | |||
))} | |||
</div> | |||
</div> | |||
))} | |||
</> | |||
)} | |||
</div> | |||
</DropdownOverlay> | |||
</div> | |||
))} | |||
</> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -31,6 +31,10 @@ | |||
background-color: #f5f5f5; | |||
} | |||
.source-line [role='button'] { | |||
cursor: pointer; | |||
} | |||
.source-line-highlighted .source-line-number, | |||
.source-line-highlighted:hover .source-line-number, | |||
.source-line-highlighted .source-line-issues, | |||
@@ -140,10 +144,6 @@ | |||
outline: none; | |||
} | |||
.source-meta[role='button'] { | |||
cursor: pointer; | |||
} | |||
.source-line-number { | |||
min-width: 18px; | |||
padding: 0 10px; | |||
@@ -152,10 +152,6 @@ | |||
text-align: right; | |||
} | |||
.source-line-number:before { | |||
content: attr(data-line-number); | |||
} | |||
.source-line-issues { | |||
position: relative; | |||
padding: 0 2px; | |||
@@ -189,17 +185,17 @@ | |||
display: none; | |||
} | |||
.source-duplications-expanded .source-line-duplications { | |||
display: none; | |||
.source-line-scm { | |||
padding: 0 5px; | |||
background-color: var(--barBackgroundColor); | |||
} | |||
.source-duplications-expanded .source-line-duplications-extra { | |||
display: table-cell; | |||
.source-line-scm .dropdown { | |||
display: block; | |||
} | |||
.source-line-scm { | |||
padding: 0 5px; | |||
background-color: var(--barBackgroundColor); | |||
.source-line-scm [role='button'] { | |||
height: 18px; | |||
} | |||
.source-line-scm-inner { | |||
@@ -209,19 +205,11 @@ | |||
white-space: nowrap; | |||
} | |||
.source-line-scm-inner:before { | |||
content: attr(data-author); | |||
} | |||
.source-line-bar { | |||
width: 5px; | |||
height: 18px; | |||
} | |||
.source-line-bar[role='button'] { | |||
cursor: pointer; | |||
} | |||
.source-line-bar:focus { | |||
outline: none; | |||
} |
@@ -25,7 +25,6 @@ import './Line.css'; | |||
import LineCode from './LineCode'; | |||
import LineCoverage from './LineCoverage'; | |||
import LineDuplicationBlock from './LineDuplicationBlock'; | |||
import LineDuplications from './LineDuplications'; | |||
import LineIssuesIndicator from './LineIssuesIndicator'; | |||
import LineNumber from './LineNumber'; | |||
import LineSCM from './LineSCM'; | |||
@@ -50,9 +49,7 @@ interface Props { | |||
issues: T.Issue[]; | |||
last: boolean; | |||
line: T.SourceLine; | |||
linePopup: T.LinePopup | undefined; | |||
loadDuplications: (line: T.SourceLine) => void; | |||
onLinePopupToggle: (linePopup: T.LinePopup) => void; | |||
onIssueChange: (issue: T.Issue) => void; | |||
onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void; | |||
onIssuesClose: (line: T.SourceLine) => void; | |||
@@ -73,16 +70,6 @@ interface Props { | |||
const LINE_HEIGHT = 18; | |||
export default class Line extends React.PureComponent<Props> { | |||
isPopupOpen = (name: string, index?: number) => { | |||
const { line, linePopup } = this.props; | |||
return ( | |||
linePopup !== undefined && | |||
linePopup.index === index && | |||
linePopup.line === line.line && | |||
linePopup.name === name | |||
); | |||
}; | |||
handleIssuesIndicatorClick = () => { | |||
if (this.props.openIssues) { | |||
this.props.onIssuesClose(this.props.line); | |||
@@ -99,45 +86,52 @@ export default class Line extends React.PureComponent<Props> { | |||
render() { | |||
const { | |||
branchLike, | |||
displayAllIssues, | |||
displayCoverage, | |||
displayDuplications, | |||
displayIssueLocationsCount, | |||
displayIssueLocationsLink, | |||
displayLocationMarkers, | |||
highlightedLocationMessage, | |||
displayIssues, | |||
displaySCM = true, | |||
duplications, | |||
duplicationsCount, | |||
highlighted, | |||
highlightedSymbols, | |||
issueLocations, | |||
issuePopup, | |||
line | |||
issues, | |||
last, | |||
line, | |||
openIssues, | |||
previousLine, | |||
secondaryIssueLocations, | |||
selectedIssue, | |||
verticalBuffer | |||
} = this.props; | |||
const className = classNames('source-line', { | |||
'source-line-highlighted': this.props.highlighted, | |||
'source-line-highlighted': highlighted, | |||
'source-line-filtered': line.isNew, | |||
'source-line-filtered-dark': | |||
displayCoverage && | |||
(line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'), | |||
'source-line-last': this.props.last === true | |||
'source-line-last': last === true | |||
}); | |||
const bottomPadding = this.props.verticalBuffer | |||
? this.props.verticalBuffer * LINE_HEIGHT | |||
: undefined; | |||
const bottomPadding = verticalBuffer ? verticalBuffer * LINE_HEIGHT : undefined; | |||
return ( | |||
<tr className={className} data-line-number={line.line}> | |||
<LineNumber | |||
line={line} | |||
onPopupToggle={this.props.onLinePopupToggle} | |||
popupOpen={this.isPopupOpen('line-number')} | |||
/> | |||
<LineNumber line={line} /> | |||
{displaySCM && ( | |||
<LineSCM | |||
line={line} | |||
onPopupToggle={this.props.onLinePopupToggle} | |||
popupOpen={this.isPopupOpen('scm')} | |||
previousLine={this.props.previousLine} | |||
/> | |||
)} | |||
{this.props.displayIssues && !this.props.displayAllIssues ? ( | |||
{displaySCM && <LineSCM line={line} previousLine={previousLine} />} | |||
{displayIssues && !displayAllIssues ? ( | |||
<LineIssuesIndicator | |||
issues={this.props.issues} | |||
issues={issues} | |||
issuesOpen={openIssues} | |||
line={line} | |||
onClick={this.handleIssuesIndicatorClick} | |||
/> | |||
@@ -145,34 +139,42 @@ export default class Line extends React.PureComponent<Props> { | |||
<td className="source-meta source-line-issues" /> | |||
)} | |||
{this.props.displayDuplications && ( | |||
<LineDuplications line={line} onClick={this.props.loadDuplications} /> | |||
)} | |||
{times(duplicationsCount, index => ( | |||
{displayDuplications && ( | |||
<LineDuplicationBlock | |||
duplicated={duplications.includes(index)} | |||
index={index} | |||
key={index} | |||
blocksLoaded={duplicationsCount > 0} | |||
duplicated={Boolean(line.duplicated)} | |||
index={0} | |||
key={0} | |||
line={this.props.line} | |||
onPopupToggle={this.props.onLinePopupToggle} | |||
popupOpen={this.isPopupOpen('duplications', index)} | |||
onClick={this.props.loadDuplications} | |||
renderDuplicationPopup={this.props.renderDuplicationPopup} | |||
/> | |||
))} | |||
)} | |||
{duplicationsCount > 1 && | |||
times(duplicationsCount - 1, index => ( | |||
<LineDuplicationBlock | |||
blocksLoaded={true} | |||
duplicated={duplications.includes(index + 1)} | |||
index={index + 1} | |||
key={index + 1} | |||
line={this.props.line} | |||
renderDuplicationPopup={this.props.renderDuplicationPopup} | |||
/> | |||
))} | |||
{this.props.displayCoverage && <LineCoverage line={line} />} | |||
{displayCoverage && <LineCoverage line={line} />} | |||
<LineCode | |||
branchLike={this.props.branchLike} | |||
displayIssueLocationsCount={this.props.displayIssueLocationsCount} | |||
displayIssueLocationsLink={this.props.displayIssueLocationsLink} | |||
displayLocationMarkers={this.props.displayLocationMarkers} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
highlightedSymbols={this.props.highlightedSymbols} | |||
issueLocations={this.props.issueLocations} | |||
branchLike={branchLike} | |||
displayIssueLocationsCount={displayIssueLocationsCount} | |||
displayIssueLocationsLink={displayIssueLocationsLink} | |||
displayLocationMarkers={displayLocationMarkers} | |||
highlightedLocationMessage={highlightedLocationMessage} | |||
highlightedSymbols={highlightedSymbols} | |||
issueLocations={issueLocations} | |||
issuePopup={issuePopup} | |||
issues={this.props.issues} | |||
issues={issues} | |||
line={line} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.props.onIssuePopupToggle} | |||
@@ -181,9 +183,9 @@ export default class Line extends React.PureComponent<Props> { | |||
onSymbolClick={this.props.onSymbolClick} | |||
padding={bottomPadding} | |||
scroll={this.props.scroll} | |||
secondaryIssueLocations={this.props.secondaryIssueLocations} | |||
selectedIssue={this.props.selectedIssue} | |||
showIssues={this.props.openIssues || this.props.displayAllIssues} | |||
secondaryIssueLocations={secondaryIssueLocations} | |||
selectedIssue={selectedIssue} | |||
showIssues={openIssues || displayAllIssues} | |||
/> | |||
</tr> | |||
); |
@@ -21,18 +21,20 @@ import * as React from 'react'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
interface Props { | |||
export interface LineCoverageProps { | |||
line: T.SourceLine; | |||
} | |||
export default function LineCoverage({ line }: Props) { | |||
export function LineCoverage({ line }: LineCoverageProps) { | |||
const className = | |||
'source-meta source-line-coverage' + | |||
(line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); | |||
const status = getStatusTooltip(line); | |||
return ( | |||
<td className={className} data-line-number={line.line}> | |||
<Tooltip overlay={getStatusTooltip(line)} placement="right"> | |||
<div className="source-line-bar" /> | |||
<Tooltip overlay={status} placement="right"> | |||
<div aria-label={status} className="source-line-bar" /> | |||
</Tooltip> | |||
</td> | |||
); | |||
@@ -64,3 +66,5 @@ function getStatusTooltip(line: T.SourceLine) { | |||
} | |||
return undefined; | |||
} | |||
export default React.memo(LineCoverage); |
@@ -19,72 +19,64 @@ | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import Toggler from 'sonar-ui-common/components/controls/Toggler'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
interface Props { | |||
export interface LineDuplicationBlockProps { | |||
blocksLoaded: boolean; | |||
duplicated: boolean; | |||
index: number; | |||
line: T.SourceLine; | |||
onPopupToggle: (linePopup: T.LinePopup) => void; | |||
popupOpen: boolean; | |||
onClick?: (line: T.SourceLine) => void; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
} | |||
export default class LineDuplicationBlock extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.currentTarget.blur(); | |||
this.props.onPopupToggle({ | |||
index: this.props.index, | |||
line: this.props.line.line, | |||
name: 'duplications' | |||
}); | |||
}; | |||
export function LineDuplicationBlock(props: LineDuplicationBlockProps) { | |||
const { blocksLoaded, duplicated, index, line } = props; | |||
const [dropdownOpen, setDropdownOpen] = React.useState(false); | |||
handleTogglePopup = (open: boolean) => { | |||
this.props.onPopupToggle({ | |||
index: this.props.index, | |||
line: this.props.line.line, | |||
name: 'duplications', | |||
open | |||
}); | |||
}; | |||
const className = classNames('source-meta', 'source-line-duplications', { | |||
'source-line-duplicated': duplicated | |||
}); | |||
closePopup = () => { | |||
this.handleTogglePopup(false); | |||
}; | |||
render() { | |||
const { duplicated, index, line, popupOpen } = this.props; | |||
const className = classNames('source-meta', 'source-line-duplications-extra', { | |||
'source-line-duplicated': duplicated | |||
}); | |||
return duplicated ? ( | |||
<td className={className} data-index={index} data-line-number={line.line}> | |||
<Toggler | |||
onRequestClose={this.closePopup} | |||
open={popupOpen} | |||
overlay={this.props.renderDuplicationPopup(index, line.line)}> | |||
<Tooltip | |||
overlay={popupOpen ? undefined : translate('source_viewer.tooltip.duplicated_block')} | |||
placement="right"> | |||
return duplicated ? ( | |||
<td className={className} data-index={index} data-line-number={line.line}> | |||
<Tooltip | |||
overlay={dropdownOpen ? undefined : translate('source_viewer.tooltip.duplicated_block')} | |||
placement="right"> | |||
<div> | |||
<Toggler | |||
onRequestClose={() => setDropdownOpen(false)} | |||
open={dropdownOpen} | |||
overlay={ | |||
<DropdownOverlay placement={PopupPlacement.RightTop}> | |||
{props.renderDuplicationPopup(index, line.line)} | |||
</DropdownOverlay> | |||
}> | |||
<div | |||
aria-label={translate('source_viewer.tooltip.duplicated_block')} | |||
className="source-line-bar" | |||
onClick={this.handleClick} | |||
onClick={() => { | |||
setDropdownOpen(true); | |||
if (!blocksLoaded && line.duplicated && props.onClick) { | |||
props.onClick(line); | |||
} | |||
}} | |||
role="button" | |||
tabIndex={0} | |||
/> | |||
</Tooltip> | |||
</Toggler> | |||
</td> | |||
) : ( | |||
<td className={className} data-index={index} data-line-number={line.line}> | |||
<div className="source-line-bar" /> | |||
</td> | |||
); | |||
} | |||
</Toggler> | |||
</div> | |||
</Tooltip> | |||
</td> | |||
) : ( | |||
<td className={className} data-index={index} data-line-number={line.line}> | |||
<div className="source-line-bar" /> | |||
</td> | |||
); | |||
} | |||
export default React.memo(LineDuplicationBlock); |
@@ -1,60 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
interface Props { | |||
line: T.SourceLine; | |||
onClick: (line: T.SourceLine) => void; | |||
} | |||
export default class LineDuplications extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
this.props.onClick(this.props.line); | |||
}; | |||
render() { | |||
const { line } = this.props; | |||
const className = classNames('source-meta', 'source-line-duplications', { | |||
'source-line-duplicated': line.duplicated | |||
}); | |||
const cell = ( | |||
<td | |||
className={className} | |||
onClick={line.duplicated ? this.handleClick : undefined} | |||
role={line.duplicated ? 'button' : undefined} | |||
tabIndex={line.duplicated ? 0 : undefined}> | |||
<div className="source-line-bar" /> | |||
</td> | |||
); | |||
return line.duplicated ? ( | |||
<Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right"> | |||
{cell} | |||
</Tooltip> | |||
) : ( | |||
cell | |||
); | |||
} | |||
} |
@@ -20,38 +20,44 @@ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import IssueIcon from 'sonar-ui-common/components/icons/IssueIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { sortByType } from '../../../helpers/issues'; | |||
interface Props { | |||
export interface LineIssuesIndicatorProps { | |||
issues: T.Issue[]; | |||
issuesOpen?: boolean; | |||
line: T.SourceLine; | |||
onClick: () => void; | |||
} | |||
export default class LineIssuesIndicator extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
this.props.onClick(); | |||
}; | |||
export function LineIssuesIndicator(props: LineIssuesIndicatorProps) { | |||
const { issues, issuesOpen, line } = props; | |||
const hasIssues = issues.length > 0; | |||
const className = classNames('source-meta', 'source-line-issues', { | |||
'source-line-with-issues': hasIssues | |||
}); | |||
const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null; | |||
render() { | |||
const { issues, line } = this.props; | |||
const hasIssues = issues.length > 0; | |||
const className = classNames('source-meta', 'source-line-issues', { | |||
'source-line-with-issues': hasIssues | |||
}); | |||
const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null; | |||
const handleClick = (e: React.MouseEvent<HTMLElement>) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
props.onClick(); | |||
}; | |||
return ( | |||
<td | |||
className={className} | |||
data-line-number={line.line} | |||
onClick={hasIssues ? this.handleClick : undefined} | |||
role={hasIssues ? 'button' : undefined} | |||
tabIndex={hasIssues ? 0 : undefined}> | |||
{mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />} | |||
{issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>} | |||
</td> | |||
); | |||
} | |||
return ( | |||
<td className={className} data-line-number={line.line}> | |||
{hasIssues && ( | |||
<span | |||
aria-label={translate('source_viewer.issues_on_line', issuesOpen ? 'hide' : 'show')} | |||
onClick={handleClick} | |||
role="button" | |||
tabIndex={0}> | |||
{mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />} | |||
{issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>} | |||
</span> | |||
)} | |||
</td> | |||
); | |||
} | |||
export default React.memo(LineIssuesIndicator); |
@@ -18,51 +18,33 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import Toggler from 'sonar-ui-common/components/controls/Toggler'; | |||
import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import LineOptionsPopup from './LineOptionsPopup'; | |||
interface Props { | |||
export interface LineNumberProps { | |||
line: T.SourceLine; | |||
onPopupToggle: (linePopup: T.LinePopup) => void; | |||
popupOpen: boolean; | |||
} | |||
export default class LineNumber extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.currentTarget.blur(); | |||
this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number' }); | |||
}; | |||
handleTogglePopup = (open: boolean) => { | |||
this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open }); | |||
}; | |||
closePopup = () => { | |||
this.handleTogglePopup(false); | |||
}; | |||
render() { | |||
const { line, popupOpen } = this.props; | |||
const { line: lineNumber } = line; | |||
const hasLineNumber = !!lineNumber; | |||
return hasLineNumber ? ( | |||
<td | |||
className="source-meta source-line-number" | |||
data-line-number={lineNumber} | |||
onClick={this.handleClick} | |||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role | |||
role="button" | |||
tabIndex={0}> | |||
<Toggler | |||
onRequestClose={this.closePopup} | |||
open={popupOpen} | |||
overlay={<LineOptionsPopup line={line} />} | |||
/> | |||
</td> | |||
) : ( | |||
<td className="source-meta source-line-number" /> | |||
); | |||
} | |||
export function LineNumber({ line }: LineNumberProps) { | |||
const { line: lineNumber } = line; | |||
const hasLineNumber = !!lineNumber; | |||
return hasLineNumber ? ( | |||
<td className="source-meta source-line-number" data-line-number={lineNumber}> | |||
<Dropdown | |||
overlay={<LineOptionsPopup line={line} />} | |||
overlayPlacement={PopupPlacement.RightTop}> | |||
<span | |||
aria-label={translateWithParameters('source_viewer.line_X', lineNumber)} | |||
role="button"> | |||
{lineNumber} | |||
</span> | |||
</Dropdown> | |||
</td> | |||
) : ( | |||
<td className="source-meta source-line-number" /> | |||
); | |||
} | |||
export default React.memo(LineNumber); |
@@ -19,35 +19,30 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getCodeUrl } from '../../../helpers/urls'; | |||
import { SourceViewerContext } from '../SourceViewerContext'; | |||
interface Props { | |||
interface LineOptionsPopupProps { | |||
line: T.SourceLine; | |||
} | |||
export default function LineOptionsPopup({ line }: Props) { | |||
export function LineOptionsPopup({ line }: LineOptionsPopupProps) { | |||
return ( | |||
<SourceViewerContext.Consumer> | |||
{({ branchLike, file }) => ( | |||
<DropdownOverlay placement={PopupPlacement.RightTop}> | |||
<div className="source-viewer-bubble-popup nowrap"> | |||
<Link | |||
className="js-get-permalink" | |||
onClick={event => { | |||
event.stopPropagation(); | |||
}} | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
to={getCodeUrl(file.project, branchLike, file.key, line.line)}> | |||
{translate('component_viewer.get_permalink')} | |||
</Link> | |||
</div> | |||
</DropdownOverlay> | |||
<div className="source-viewer-bubble-popup nowrap"> | |||
<Link | |||
className="js-get-permalink" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
to={getCodeUrl(file.project, branchLike, file.key, line.line)}> | |||
{translate('component_viewer.get_permalink')} | |||
</Link> | |||
</div> | |||
)} | |||
</SourceViewerContext.Consumer> | |||
); | |||
} | |||
export default React.memo(LineOptionsPopup); |
@@ -18,54 +18,44 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import Toggler from 'sonar-ui-common/components/controls/Toggler'; | |||
import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import SCMPopup from './SCMPopup'; | |||
interface Props { | |||
export interface LineSCMProps { | |||
line: T.SourceLine; | |||
onPopupToggle: (linePopup: T.LinePopup) => void; | |||
popupOpen: boolean; | |||
previousLine: T.SourceLine | undefined; | |||
} | |||
export default class LineSCM extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.currentTarget.blur(); | |||
this.props.onPopupToggle({ line: this.props.line.line, name: 'scm' }); | |||
}; | |||
export function LineSCM({ line, previousLine }: LineSCMProps) { | |||
const hasPopup = !!line.line; | |||
const cell = ( | |||
<div className="source-line-scm-inner"> | |||
{isSCMChanged(line, previousLine) ? line.scmAuthor || '…' : ' '} | |||
</div> | |||
); | |||
handleTogglePopup = (open: boolean) => { | |||
this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open }); | |||
}; | |||
if (hasPopup) { | |||
let ariaLabel = translate('source_viewer.click_for_scm_info'); | |||
if (line.scmAuthor) { | |||
ariaLabel = `${translateWithParameters( | |||
'source_viewer.author_X', | |||
line.scmAuthor | |||
)}, ${ariaLabel}`; | |||
} | |||
closePopup = () => { | |||
this.handleTogglePopup(false); | |||
}; | |||
render() { | |||
const { line, popupOpen, previousLine } = this.props; | |||
const hasPopup = !!line.line; | |||
const cell = isSCMChanged(line, previousLine) && ( | |||
<div className="source-line-scm-inner" data-author={line.scmAuthor || '…'} /> | |||
); | |||
return hasPopup ? ( | |||
<td | |||
className="source-meta source-line-scm" | |||
data-line-number={line.line} | |||
onClick={this.handleClick} | |||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role | |||
role="button" | |||
tabIndex={0}> | |||
<Toggler | |||
onRequestClose={this.closePopup} | |||
open={popupOpen} | |||
overlay={<SCMPopup line={line} />}> | |||
{cell} | |||
</Toggler> | |||
return ( | |||
<td className="source-meta source-line-scm" data-line-number={line.line}> | |||
<Dropdown overlay={<SCMPopup line={line} />} overlayPlacement={PopupPlacement.RightTop}> | |||
<div aria-label={ariaLabel} role="button"> | |||
{cell} | |||
</div> | |||
</Dropdown> | |||
</td> | |||
) : ( | |||
); | |||
} else { | |||
return ( | |||
<td className="source-meta source-line-scm" data-line-number={line.line}> | |||
{cell} | |||
</td> | |||
@@ -80,3 +70,5 @@ function isSCMChanged(s: T.SourceLine, p: T.SourceLine | undefined) { | |||
} | |||
return changed; | |||
} | |||
export default React.memo(LineSCM); |
@@ -19,27 +19,38 @@ | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; | |||
import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
interface Props { | |||
export interface SCMPopupProps { | |||
line: T.SourceLine; | |||
} | |||
export default function SCMPopup({ line }: Props) { | |||
const hasAuthor = line.scmAuthor !== ''; | |||
export function SCMPopup({ line }: SCMPopupProps) { | |||
const hasAuthor = line.scmAuthor !== undefined && line.scmAuthor !== ''; | |||
const hasDate = line.scmDate !== undefined; | |||
return ( | |||
<DropdownOverlay placement={PopupPlacement.RightTop}> | |||
<div className="source-viewer-bubble-popup abs-width-400"> | |||
{hasAuthor && <div>{line.scmAuthor}</div>} | |||
{line.scmDate && ( | |||
<div className={classNames({ 'spacer-top': hasAuthor })}> | |||
<DateFormatter date={line.scmDate} /> | |||
</div> | |||
)} | |||
{line.scmRevision && <div className="spacer-top">{line.scmRevision}</div>} | |||
</div> | |||
</DropdownOverlay> | |||
<div className="source-viewer-bubble-popup abs-width-400"> | |||
{hasAuthor && ( | |||
<div> | |||
<h4>{translate('author')}</h4> | |||
{line.scmAuthor} | |||
</div> | |||
)} | |||
{hasDate && ( | |||
<div className={classNames({ 'spacer-top': hasAuthor })}> | |||
<h4>{translate('source_viewer.tooltip.scm.commited_on')}</h4> | |||
<DateFormatter date={line.scmDate!} /> | |||
</div> | |||
)} | |||
{line.scmRevision && ( | |||
<div className={classNames({ 'spacer-top': hasAuthor || hasDate })}> | |||
<h4>{translate('source_viewer.tooltip.scm.revision')}</h4> | |||
{line.scmRevision} | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
export default React.memo(SCMPopup); |
@@ -106,9 +106,7 @@ function shallowRender(props: Partial<Line['props']> = {}) { | |||
issues={[mockIssue(), mockIssue(false, { type: 'VULNERABILITY' })]} | |||
last={false} | |||
line={mockSourceLine()} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
onLinePopupToggle={jest.fn()} | |||
onIssueChange={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onIssuesClose={jest.fn()} |
@@ -19,22 +19,24 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import LineCoverage from '../LineCoverage'; | |||
import { LineCoverage, LineCoverageProps } from '../LineCoverage'; | |||
it('render covered line', () => { | |||
const line: T.SourceLine = { line: 3, coverageStatus: 'covered' }; | |||
const wrapper = shallow(<LineCoverage line={line} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('covered'); | |||
expect(shallowRender({ line: { line: 3, coverageStatus: 'uncovered' } })).toMatchSnapshot( | |||
'uncovered' | |||
); | |||
expect(shallowRender({ line: { line: 3, coverageStatus: 'partially-covered' } })).toMatchSnapshot( | |||
'partially covered, 0 conditions' | |||
); | |||
expect( | |||
shallowRender({ line: { line: 3, coverageStatus: 'partially-covered', coveredConditions: 10 } }) | |||
).toMatchSnapshot('partially covered, 10 conditions'); | |||
expect(shallowRender({ line: { line: 3, coverageStatus: undefined } })).toMatchSnapshot( | |||
'no data' | |||
); | |||
}); | |||
it('render uncovered line', () => { | |||
const line: T.SourceLine = { line: 3, coverageStatus: 'uncovered' }; | |||
const wrapper = shallow(<LineCoverage line={line} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('render line with unknown coverage', () => { | |||
const line: T.SourceLine = { line: 3 }; | |||
const wrapper = shallow(<LineCoverage line={line} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<LineCoverageProps> = {}) { | |||
return shallow(<LineCoverage line={{ line: 3, coverageStatus: 'covered' }} {...props} />); | |||
} |
@@ -19,38 +19,50 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Toggler from 'sonar-ui-common/components/controls/Toggler'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import LineDuplicationBlock from '../LineDuplicationBlock'; | |||
import { LineDuplicationBlock, LineDuplicationBlockProps } from '../LineDuplicationBlock'; | |||
it('render duplicated line', () => { | |||
const line = { line: 3, duplicated: true }; | |||
const onPopupToggle = jest.fn(); | |||
const wrapper = shallow( | |||
<LineDuplicationBlock | |||
duplicated={true} | |||
index={1} | |||
line={line} | |||
onPopupToggle={onPopupToggle} | |||
popupOpen={false} | |||
renderDuplicationPopup={jest.fn()} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('[tabIndex]')); | |||
expect(onPopupToggle).toHaveBeenCalled(); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ line: { line: 3, duplicated: false }, duplicated: false }) | |||
).toMatchSnapshot('not duplicated'); | |||
}); | |||
it('should correctly open/close the dropdown', () => { | |||
const wrapper = shallowRender(); | |||
click(wrapper.find('div[role="button"]')); | |||
expect(wrapper.find(Toggler).prop('open')).toBe(true); | |||
wrapper.find(Toggler).prop('onRequestClose')(); | |||
expect(wrapper.find(Toggler).prop('open')).toBe(false); | |||
}); | |||
it('render not duplicated line', () => { | |||
const line = { line: 3, duplicated: false }; | |||
const wrapper = shallow( | |||
it('should correctly call the onCick prop', () => { | |||
const line = { line: 1, duplicated: true }; | |||
const onClick = jest.fn(); | |||
const wrapper = shallowRender({ line, onClick }); | |||
// Propagate if blocks aren't loaded. | |||
click(wrapper.find('div[role="button"]')); | |||
expect(onClick).toBeCalledWith(line); | |||
// Don't propagate if blocks were loaded. | |||
onClick.mockClear(); | |||
wrapper.setProps({ blocksLoaded: true }); | |||
click(wrapper.find('div[role="button"]')); | |||
expect(onClick).not.toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<LineDuplicationBlockProps> = {}) { | |||
return shallow<LineDuplicationBlockProps>( | |||
<LineDuplicationBlock | |||
duplicated={false} | |||
blocksLoaded={false} | |||
duplicated={true} | |||
index={1} | |||
line={line} | |||
onPopupToggle={jest.fn()} | |||
popupOpen={false} | |||
line={{ line: 3, duplicated: true }} | |||
renderDuplicationPopup={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
} |
@@ -1,39 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import LineDuplications from '../LineDuplications'; | |||
it('render duplicated line', () => { | |||
const line = { line: 3, duplicated: true }; | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('[tabIndex]')); | |||
expect(onClick).toHaveBeenCalled(); | |||
}); | |||
it('render not duplicated line', () => { | |||
const line = { line: 3, duplicated: false }; | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -21,29 +21,30 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockIssue } from '../../../../helpers/testMocks'; | |||
import LineIssuesIndicator from '../LineIssuesIndicator'; | |||
import { LineIssuesIndicator, LineIssuesIndicatorProps } from '../LineIssuesIndicator'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ | |||
issues: [ | |||
mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }), | |||
mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' }) | |||
] | |||
}) | |||
).toMatchSnapshot('diff issue types'); | |||
expect(shallowRender({ issues: [] })).toMatchSnapshot('no issues'); | |||
}); | |||
it('should correctly handle click', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = shallowRender({ onClick }); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
click(wrapper.find('span[role="button"]')); | |||
expect(onClick).toHaveBeenCalled(); | |||
const nextIssues = [ | |||
mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }), | |||
mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' }) | |||
]; | |||
wrapper.setProps({ issues: nextIssues }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render correctly for no issues', () => { | |||
expect(shallowRender({ issues: [] })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<LineIssuesIndicator['props']> = {}) { | |||
function shallowRender(props: Partial<LineIssuesIndicatorProps> = {}) { | |||
return shallow( | |||
<LineIssuesIndicator | |||
issues={[ |
@@ -19,18 +19,13 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import LineNumber from '../LineNumber'; | |||
import { LineNumber, LineNumberProps } from '../LineNumber'; | |||
it('render line 3', () => { | |||
const line = { line: 3 }; | |||
const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ line: { line: 0 } })).toMatchSnapshot('no line number'); | |||
}); | |||
it('render line 0', () => { | |||
const line = { line: 0 }; | |||
const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<LineNumberProps> = {}) { | |||
return shallow(<LineNumber line={{ line: 3 }} {...props} />); | |||
} |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import LineOptionsPopup from '../LineOptionsPopup'; | |||
import { LineOptionsPopup } from '../LineOptionsPopup'; | |||
jest.mock('../../SourceViewerContext', () => ({ | |||
SourceViewerContext: { | |||
@@ -32,7 +32,7 @@ jest.mock('../../SourceViewerContext', () => ({ | |||
} | |||
})); | |||
it('should render', () => { | |||
it('should render correctly', () => { | |||
const line = { line: 3 }; | |||
const wrapper = shallow(<LineOptionsPopup line={line} />).dive(); | |||
expect(wrapper).toMatchSnapshot(); |
@@ -17,52 +17,27 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
/* eslint-disable sonarjs/no-duplicate-string */ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import LineSCM from '../LineSCM'; | |||
import { LineSCM, LineSCMProps } from '../LineSCM'; | |||
it('render scm details', () => { | |||
const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }; | |||
const wrapper = shallow( | |||
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('render scm details for the first line', () => { | |||
const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
const wrapper = shallow( | |||
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render correctly', () => { | |||
const scmInfo = { scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
it('does not render scm details', () => { | |||
const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
const previousLine = { line: 2, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
const wrapper = shallow( | |||
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ line: { line: 3, ...scmInfo }, previousLine: { line: 2, ...scmInfo } }) | |||
).toMatchSnapshot('same commit'); | |||
expect(shallowRender({ line: { line: 3, scmDate: '2017-01-01' } })).toMatchSnapshot('no author'); | |||
}); | |||
it('renders ellipsis when no author info', () => { | |||
const line = { line: 3, scmRevision: 'foo', scmDate: '2017-01-01' }; | |||
const previousLine = { line: 2, scmRevision: 'bar', scmDate: '2017-01-01' }; | |||
const wrapper = shallow( | |||
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} /> | |||
function shallowRender(props: Partial<LineSCMProps> = {}) { | |||
return shallow( | |||
<LineSCM | |||
line={{ line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }} | |||
previousLine={{ line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }} | |||
{...props} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should open popup', () => { | |||
const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
const onPopupToggle = jest.fn(); | |||
const wrapper = shallow( | |||
<LineSCM line={line} onPopupToggle={onPopupToggle} popupOpen={false} previousLine={undefined} /> | |||
); | |||
click(wrapper.find('[role="button"]')); | |||
expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' }); | |||
}); | |||
} |
@@ -17,11 +17,29 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
/* eslint-disable sonarjs/no-duplicate-string */ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import SCMPopup from '../SCMPopup'; | |||
import { SCMPopup, SCMPopupProps } from '../SCMPopup'; | |||
it('should render', () => { | |||
const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; | |||
expect(shallow(<SCMPopup line={line} />)).toMatchSnapshot(); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ line: { line: 3, scmDate: '2017-01-01', scmRevision: 'bar' } }) | |||
).toMatchSnapshot('no author'); | |||
expect( | |||
shallowRender({ line: { line: 3, scmAuthor: 'foo', scmRevision: 'bar' } }) | |||
).toMatchSnapshot('no date'); | |||
expect( | |||
shallowRender({ line: { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' } }) | |||
).toMatchSnapshot('no revision'); | |||
}); | |||
function shallowRender(props: Partial<SCMPopupProps> = {}) { | |||
return shallow( | |||
<SCMPopup | |||
line={{ line: 3, scmAuthor: 'foo', scmDate: '2017-01-01', scmRevision: 'bar' }} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -5,7 +5,7 @@ exports[`should render correctly 1`] = ` | |||
className="source-line source-line-filtered" | |||
data-line-number={16} | |||
> | |||
<LineNumber | |||
<Memo(LineNumber) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -19,10 +19,8 @@ exports[`should render correctly 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<LineSCM | |||
<Memo(LineSCM) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -36,8 +34,6 @@ exports[`should render correctly 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<td | |||
className="source-meta source-line-issues" | |||
@@ -151,7 +147,7 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
className="source-line source-line-highlighted source-line-filtered source-line-last" | |||
data-line-number={16} | |||
> | |||
<LineNumber | |||
<Memo(LineNumber) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -165,10 +161,8 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<LineSCM | |||
<Memo(LineSCM) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -182,8 +176,6 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<td | |||
className="source-meta source-line-issues" | |||
@@ -297,7 +289,7 @@ exports[`should render correctly with coverage 1`] = ` | |||
className="source-line source-line-filtered" | |||
data-line-number={16} | |||
> | |||
<LineNumber | |||
<Memo(LineNumber) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -311,10 +303,8 @@ exports[`should render correctly with coverage 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<LineSCM | |||
<Memo(LineSCM) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -328,13 +318,11 @@ exports[`should render correctly with coverage 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<td | |||
className="source-meta source-line-issues" | |||
/> | |||
<LineCoverage | |||
<Memo(LineCoverage) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -458,7 +446,7 @@ exports[`should render correctly with duplication information 1`] = ` | |||
className="source-line source-line-filtered" | |||
data-line-number={16} | |||
> | |||
<LineNumber | |||
<Memo(LineNumber) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -472,10 +460,8 @@ exports[`should render correctly with duplication information 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<LineSCM | |||
<Memo(LineSCM) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -489,29 +475,12 @@ exports[`should render correctly with duplication information 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<td | |||
className="source-meta source-line-issues" | |||
/> | |||
<LineDuplications | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
"coverageStatus": "covered", | |||
"coveredConditions": 2, | |||
"duplicated": false, | |||
"isNew": true, | |||
"line": 16, | |||
"scmAuthor": "simon.brandhof@sonarsource.com", | |||
"scmDate": "2018-12-11T10:48:39+0100", | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onClick={[MockFunction]} | |||
/> | |||
<LineDuplicationBlock | |||
<Memo(LineDuplicationBlock) | |||
blocksLoaded={true} | |||
duplicated={false} | |||
index={0} | |||
key="0" | |||
@@ -528,11 +497,11 @@ exports[`should render correctly with duplication information 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
onClick={[MockFunction]} | |||
renderDuplicationPopup={[MockFunction]} | |||
/> | |||
<LineDuplicationBlock | |||
<Memo(LineDuplicationBlock) | |||
blocksLoaded={true} | |||
duplicated={false} | |||
index={1} | |||
key="1" | |||
@@ -549,11 +518,10 @@ exports[`should render correctly with duplication information 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
renderDuplicationPopup={[MockFunction]} | |||
/> | |||
<LineDuplicationBlock | |||
<Memo(LineDuplicationBlock) | |||
blocksLoaded={true} | |||
duplicated={false} | |||
index={2} | |||
key="2" | |||
@@ -570,8 +538,6 @@ exports[`should render correctly with duplication information 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
renderDuplicationPopup={[MockFunction]} | |||
/> | |||
<LineCode | |||
@@ -683,7 +649,7 @@ exports[`should render correctly with issues info 1`] = ` | |||
className="source-line source-line-filtered" | |||
data-line-number={16} | |||
> | |||
<LineNumber | |||
<Memo(LineNumber) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -697,10 +663,8 @@ exports[`should render correctly with issues info 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<LineSCM | |||
<Memo(LineSCM) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -714,10 +678,8 @@ exports[`should render correctly with issues info 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<LineIssuesIndicator | |||
<Memo(LineIssuesIndicator) | |||
issues={ | |||
Array [ | |||
Object { | |||
@@ -784,6 +746,7 @@ exports[`should render correctly with issues info 1`] = ` | |||
}, | |||
] | |||
} | |||
issuesOpen={false} | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -908,7 +871,7 @@ exports[`should render correctly: no SCM 1`] = ` | |||
className="source-line source-line-filtered" | |||
data-line-number={16} | |||
> | |||
<LineNumber | |||
<Memo(LineNumber) | |||
line={ | |||
Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
@@ -922,8 +885,6 @@ exports[`should render correctly: no SCM 1`] = ` | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onPopupToggle={[MockFunction]} | |||
popupOpen={false} | |||
/> | |||
<td | |||
className="source-meta source-line-issues" |
@@ -1,6 +1,6 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render covered line 1`] = ` | |||
exports[`should render correctly: covered 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage source-line-covered" | |||
data-line-number={3} | |||
@@ -10,13 +10,14 @@ exports[`render covered line 1`] = ` | |||
placement="right" | |||
> | |||
<div | |||
aria-label="source_viewer.tooltip.covered" | |||
className="source-line-bar" | |||
/> | |||
</Tooltip> | |||
</td> | |||
`; | |||
exports[`render line with unknown coverage 1`] = ` | |||
exports[`should render correctly: no data 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage" | |||
data-line-number={3} | |||
@@ -31,7 +32,41 @@ exports[`render line with unknown coverage 1`] = ` | |||
</td> | |||
`; | |||
exports[`render uncovered line 1`] = ` | |||
exports[`should render correctly: partially covered, 0 conditions 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage source-line-partially-covered" | |||
data-line-number={3} | |||
> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.partially-covered" | |||
placement="right" | |||
> | |||
<div | |||
aria-label="source_viewer.tooltip.partially-covered" | |||
className="source-line-bar" | |||
/> | |||
</Tooltip> | |||
</td> | |||
`; | |||
exports[`should render correctly: partially covered, 10 conditions 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage source-line-partially-covered" | |||
data-line-number={3} | |||
> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.partially-covered" | |||
placement="right" | |||
> | |||
<div | |||
aria-label="source_viewer.tooltip.partially-covered" | |||
className="source-line-bar" | |||
/> | |||
</Tooltip> | |||
</td> | |||
`; | |||
exports[`should render correctly: uncovered 1`] = ` | |||
<td | |||
className="source-meta source-line-coverage source-line-uncovered" | |||
data-line-number={3} | |||
@@ -41,6 +76,7 @@ exports[`render uncovered line 1`] = ` | |||
placement="right" | |||
> | |||
<div | |||
aria-label="source_viewer.tooltip.uncovered" | |||
className="source-line-bar" | |||
/> | |||
</Tooltip> |
@@ -1,33 +1,41 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render duplicated line 1`] = ` | |||
exports[`should render correctly: default 1`] = ` | |||
<td | |||
className="source-meta source-line-duplications-extra source-line-duplicated" | |||
className="source-meta source-line-duplications source-line-duplicated" | |||
data-index={1} | |||
data-line-number={3} | |||
> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
<Tooltip | |||
overlay="source_viewer.tooltip.duplicated_block" | |||
placement="right" | |||
> | |||
<Tooltip | |||
overlay="source_viewer.tooltip.duplicated_block" | |||
placement="right" | |||
> | |||
<div | |||
className="source-line-bar" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
/> | |||
</Tooltip> | |||
</Toggler> | |||
<div> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
overlay={ | |||
<DropdownOverlay | |||
placement="right-top" | |||
/> | |||
} | |||
> | |||
<div | |||
aria-label="source_viewer.tooltip.duplicated_block" | |||
className="source-line-bar" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
/> | |||
</Toggler> | |||
</div> | |||
</Tooltip> | |||
</td> | |||
`; | |||
exports[`render not duplicated line 1`] = ` | |||
exports[`should render correctly: not duplicated 1`] = ` | |||
<td | |||
className="source-meta source-line-duplications-extra" | |||
className="source-meta source-line-duplications" | |||
data-index={1} | |||
data-line-number={3} | |||
> |
@@ -1,29 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render duplicated line 1`] = ` | |||
<Tooltip | |||
overlay="source_viewer.tooltip.duplicated_line" | |||
placement="right" | |||
> | |||
<td | |||
className="source-meta source-line-duplications source-line-duplicated" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<div | |||
className="source-line-bar" | |||
/> | |||
</td> | |||
</Tooltip> | |||
`; | |||
exports[`render not duplicated line 1`] = ` | |||
<td | |||
className="source-meta source-line-duplications" | |||
> | |||
<div | |||
className="source-line-bar" | |||
/> | |||
</td> | |||
`; |
@@ -1,44 +1,52 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
exports[`should render correctly: default 1`] = ` | |||
<td | |||
className="source-meta source-line-issues source-line-with-issues" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<IssueIcon | |||
type="BUG" | |||
/> | |||
<span | |||
className="source-line-issues-counter" | |||
aria-label="source_viewer.issues_on_line.show" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
2 | |||
<IssueIcon | |||
type="BUG" | |||
/> | |||
<span | |||
className="source-line-issues-counter" | |||
> | |||
2 | |||
</span> | |||
</span> | |||
</td> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
exports[`should render correctly: diff issue types 1`] = ` | |||
<td | |||
className="source-meta source-line-issues source-line-with-issues" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<IssueIcon | |||
type="VULNERABILITY" | |||
/> | |||
<span | |||
className="source-line-issues-counter" | |||
aria-label="source_viewer.issues_on_line.show" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
2 | |||
<IssueIcon | |||
type="VULNERABILITY" | |||
/> | |||
<span | |||
className="source-line-issues-counter" | |||
> | |||
2 | |||
</span> | |||
</span> | |||
</td> | |||
`; | |||
exports[`should render correctly for no issues 1`] = ` | |||
exports[`should render correctly: no issues 1`] = ` | |||
<td | |||
className="source-meta source-line-issues" | |||
data-line-number={3} |
@@ -1,24 +1,13 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render line 0 1`] = ` | |||
<td | |||
className="source-meta source-line-number" | |||
/> | |||
`; | |||
exports[`render line 3 1`] = ` | |||
exports[`should render correctly: default 1`] = ` | |||
<td | |||
className="source-meta source-line-number" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
<Dropdown | |||
overlay={ | |||
<LineOptionsPopup | |||
<Memo(LineOptionsPopup) | |||
line={ | |||
Object { | |||
"line": 3, | |||
@@ -26,6 +15,20 @@ exports[`render line 3 1`] = ` | |||
} | |||
/> | |||
} | |||
/> | |||
overlayPlacement="right-top" | |||
> | |||
<span | |||
aria-label="source_viewer.line_X.3" | |||
role="button" | |||
> | |||
3 | |||
</span> | |||
</Dropdown> | |||
</td> | |||
`; | |||
exports[`should render correctly: no line number 1`] = ` | |||
<td | |||
className="source-meta source-line-number" | |||
/> | |||
`; |
@@ -1,33 +1,28 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<DropdownOverlay | |||
placement="right-top" | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="source-viewer-bubble-popup nowrap" | |||
> | |||
<div | |||
className="source-viewer-bubble-popup nowrap" | |||
> | |||
<Link | |||
className="js-get-permalink" | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
rel="noopener noreferrer" | |||
style={Object {}} | |||
target="_blank" | |||
to={ | |||
Object { | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "prj", | |||
"line": 3, | |||
"selected": "foo", | |||
}, | |||
} | |||
<Link | |||
className="js-get-permalink" | |||
onlyActiveOnIndex={false} | |||
rel="noopener noreferrer" | |||
style={Object {}} | |||
target="_blank" | |||
to={ | |||
Object { | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "prj", | |||
"line": 3, | |||
"selected": "foo", | |||
}, | |||
} | |||
> | |||
component_viewer.get_permalink | |||
</Link> | |||
</div> | |||
</DropdownOverlay> | |||
} | |||
> | |||
component_viewer.get_permalink | |||
</Link> | |||
</div> | |||
`; |
@@ -1,45 +1,13 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`does not render scm details 1`] = ` | |||
exports[`should render correctly: default 1`] = ` | |||
<td | |||
className="source-meta source-line-scm" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
<Dropdown | |||
overlay={ | |||
<SCMPopup | |||
line={ | |||
Object { | |||
"line": 3, | |||
"scmAuthor": "foo", | |||
"scmDate": "2017-01-01", | |||
"scmRevision": "foo", | |||
} | |||
} | |||
/> | |||
} | |||
/> | |||
</td> | |||
`; | |||
exports[`render scm details 1`] = ` | |||
<td | |||
className="source-meta source-line-scm" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
overlay={ | |||
<SCMPopup | |||
<Memo(SCMPopup) | |||
line={ | |||
Object { | |||
"line": 3, | |||
@@ -49,74 +17,84 @@ exports[`render scm details 1`] = ` | |||
} | |||
/> | |||
} | |||
overlayPlacement="right-top" | |||
> | |||
<div | |||
className="source-line-scm-inner" | |||
data-author="foo" | |||
/> | |||
</Toggler> | |||
aria-label="source_viewer.author_X.foo, source_viewer.click_for_scm_info" | |||
role="button" | |||
> | |||
<div | |||
className="source-line-scm-inner" | |||
> | |||
foo | |||
</div> | |||
</div> | |||
</Dropdown> | |||
</td> | |||
`; | |||
exports[`render scm details for the first line 1`] = ` | |||
exports[`should render correctly: no author 1`] = ` | |||
<td | |||
className="source-meta source-line-scm" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
<Dropdown | |||
overlay={ | |||
<SCMPopup | |||
<Memo(SCMPopup) | |||
line={ | |||
Object { | |||
"line": 3, | |||
"scmAuthor": "foo", | |||
"scmDate": "2017-01-01", | |||
"scmRevision": "foo", | |||
} | |||
} | |||
/> | |||
} | |||
overlayPlacement="right-top" | |||
> | |||
<div | |||
className="source-line-scm-inner" | |||
data-author="foo" | |||
/> | |||
</Toggler> | |||
aria-label="source_viewer.click_for_scm_info" | |||
role="button" | |||
> | |||
<div | |||
className="source-line-scm-inner" | |||
> | |||
… | |||
</div> | |||
</div> | |||
</Dropdown> | |||
</td> | |||
`; | |||
exports[`renders ellipsis when no author info 1`] = ` | |||
exports[`should render correctly: same commit 1`] = ` | |||
<td | |||
className="source-meta source-line-scm" | |||
data-line-number={3} | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<Toggler | |||
onRequestClose={[Function]} | |||
open={false} | |||
<Dropdown | |||
overlay={ | |||
<SCMPopup | |||
<Memo(SCMPopup) | |||
line={ | |||
Object { | |||
"line": 3, | |||
"scmAuthor": "foo", | |||
"scmDate": "2017-01-01", | |||
"scmRevision": "foo", | |||
} | |||
} | |||
/> | |||
} | |||
overlayPlacement="right-top" | |||
> | |||
<div | |||
className="source-line-scm-inner" | |||
data-author="…" | |||
/> | |||
</Toggler> | |||
aria-label="source_viewer.author_X.foo, source_viewer.click_for_scm_info" | |||
role="button" | |||
> | |||
<div | |||
className="source-line-scm-inner" | |||
> | |||
</div> | |||
</div> | |||
</Dropdown> | |||
</td> | |||
`; |
@@ -1,22 +1,101 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<DropdownOverlay | |||
placement="right-top" | |||
exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="source-viewer-bubble-popup abs-width-400" | |||
> | |||
<div> | |||
<h4> | |||
author | |||
</h4> | |||
foo | |||
</div> | |||
<div | |||
className="spacer-top" | |||
> | |||
<h4> | |||
source_viewer.tooltip.scm.commited_on | |||
</h4> | |||
<DateFormatter | |||
date="2017-01-01" | |||
/> | |||
</div> | |||
<div | |||
className="spacer-top" | |||
> | |||
<h4> | |||
source_viewer.tooltip.scm.revision | |||
</h4> | |||
bar | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: no author 1`] = ` | |||
<div | |||
className="source-viewer-bubble-popup abs-width-400" | |||
> | |||
<div | |||
className="" | |||
> | |||
<h4> | |||
source_viewer.tooltip.scm.commited_on | |||
</h4> | |||
<DateFormatter | |||
date="2017-01-01" | |||
/> | |||
</div> | |||
<div | |||
className="spacer-top" | |||
> | |||
<h4> | |||
source_viewer.tooltip.scm.revision | |||
</h4> | |||
bar | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: no date 1`] = ` | |||
<div | |||
className="source-viewer-bubble-popup abs-width-400" | |||
> | |||
<div> | |||
<h4> | |||
author | |||
</h4> | |||
foo | |||
</div> | |||
<div | |||
className="spacer-top" | |||
> | |||
<h4> | |||
source_viewer.tooltip.scm.revision | |||
</h4> | |||
bar | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: no revision 1`] = ` | |||
<div | |||
className="source-viewer-bubble-popup abs-width-400" | |||
> | |||
<div> | |||
<h4> | |||
author | |||
</h4> | |||
foo | |||
</div> | |||
<div | |||
className="source-viewer-bubble-popup abs-width-400" | |||
className="spacer-top" | |||
> | |||
<div> | |||
foo | |||
</div> | |||
<div | |||
className="spacer-top" | |||
> | |||
<DateFormatter | |||
date="2017-01-01" | |||
/> | |||
</div> | |||
</div> | |||
</DropdownOverlay> | |||
<h4> | |||
source_viewer.tooltip.scm.commited_on | |||
</h4> | |||
<DateFormatter | |||
date="2017-01-01" | |||
/> | |||
</div> | |||
</div> | |||
`; |
@@ -2488,6 +2488,9 @@ source_viewer.view_all_issues=See all issues in this file | |||
source_viewer.covered=Covered by the following tests | |||
source_viewer.not_covered=Not covered by tests | |||
source_viewer.conditions=conditions | |||
source_viewer.line_X=Line: {0} | |||
source_viewer.click_for_scm_info=Click to see SCM information | |||
source_viewer.author_X=Author: {0} | |||
source_viewer.tooltip.duplicated_line=This line is duplicated. Click to see duplicated blocks. | |||
source_viewer.tooltip.duplicated_block=Duplicated block. Click for details. | |||
@@ -2498,6 +2501,11 @@ source_viewer.tooltip.partially-covered.conditions=Partially covered by tests ({ | |||
source_viewer.tooltip.uncovered=Not covered by tests. | |||
source_viewer.tooltip.uncovered.conditions=Not covered by tests ({0} conditions). | |||
source_viewer.tooltip.no_information_about_tests=There is no extra information about test files. | |||
source_viewer.tooltip.scm.commited_on=Committed on | |||
source_viewer.tooltip.scm.revision=Revision | |||
source_viewer.issues_on_line.show=Click to show all issues on this line | |||
source_viewer.issues_on_line.hide=Click to hide all issues on this line | |||
source_viewer.load_more_code=Load More Code | |||
source_viewer.loading_more_code=Loading More Code... |