@@ -122,7 +122,7 @@ | |||
margin-bottom: 16px; | |||
} | |||
.rule-desc h2:first-child { | |||
.rule-desc *:first-child { | |||
margin-top: 0; | |||
} | |||
@@ -29,7 +29,6 @@ import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { RequestData } from '../../../helpers/request'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isFile, isView } from '../../../types/component'; | |||
@@ -286,11 +285,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
return index !== -1 ? index : undefined; | |||
}; | |||
handleScroll = (element: Element) => { | |||
const offset = window.innerHeight / 2; | |||
scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth: true }); | |||
}; | |||
getDefaultShowBestMeasures() { | |||
const { asc, view } = this.props; | |||
if ((asc !== undefined && view === 'list') || view === 'tree') { | |||
@@ -420,7 +414,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
component={baseComponent.key} | |||
metricKey={this.state.metric?.key} | |||
onIssueChange={this.props.onIssueChange} | |||
scroll={this.handleScroll} | |||
/> | |||
</div> | |||
) : ( |
@@ -21,7 +21,6 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { getComponentTree } from '../../../../api/components'; | |||
import { mockComponentMeasure } from '../../../../helpers/mocks/component'; | |||
import { scrollToElement } from '../../../../helpers/scrolling'; | |||
import { mockRouter } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import MeasureContent from '../MeasureContent'; | |||
@@ -120,17 +119,6 @@ it('should render correctly for a file', async () => { | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should correctly handle scrolling', () => { | |||
const element = {} as Element; | |||
const wrapper = shallowRender(); | |||
wrapper.instance().handleScroll(element); | |||
expect(scrollToElement).toBeCalledWith(element, { | |||
topOffset: 300, | |||
bottomOffset: 400, | |||
smooth: true | |||
}); | |||
}); | |||
it('should test fetchMoreComponents to work correctly', async () => { | |||
(getComponentTree as jest.Mock).mockResolvedValueOnce({ | |||
paging: { pageIndex: 12, pageSize: 500, total: 0 }, |
@@ -102,7 +102,6 @@ exports[`should render correctly for a file 1`] = ` | |||
displayIssueLocationsLink={true} | |||
displayLocationMarkers={true} | |||
metricKey="bugs" | |||
scroll={[Function]} | |||
/> | |||
</div> | |||
</div> |
@@ -37,8 +37,9 @@ jest.mock('../../../api/users'); | |||
let handler: IssuesServiceMock; | |||
beforeEach(() => { | |||
window.scrollTo = jest.fn(); | |||
handler = new IssuesServiceMock(); | |||
window.scrollTo = jest.fn(); | |||
window.HTMLElement.prototype.scrollIntoView = jest.fn(); | |||
}); | |||
it('should show education principles', async () => { |
@@ -17,15 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router-dom'; | |||
import TabViewer from '../../../components/rules/TabViewer'; | |||
import { getRuleUrl } from '../../../helpers/urls'; | |||
import { Component, Issue, RuleDetails } from '../../../types/types'; | |||
import { Issue, RuleDetails } from '../../../types/types'; | |||
interface IssueViewerTabsProps { | |||
component?: Component; | |||
issue: Issue; | |||
codeTabContent: React.ReactNode; | |||
ruleDetails: RuleDetails; | |||
@@ -35,19 +33,12 @@ export default function IssueViewerTabs(props: IssueViewerTabsProps) { | |||
const { | |||
ruleDetails, | |||
codeTabContent, | |||
issue: { ruleDescriptionContextKey } | |||
} = props; | |||
const { | |||
component, | |||
ruleDetails: { name, key }, | |||
issue: { message } | |||
issue: { ruleDescriptionContextKey, message } | |||
} = props; | |||
return ( | |||
<> | |||
<div | |||
className={classNames('issue-header', { | |||
'issue-project-level': component !== undefined | |||
})}> | |||
<div className="big-padded-top"> | |||
<h1 className="text-bold">{message}</h1> | |||
<div className="spacer-top big-spacer-bottom"> | |||
<span className="note padded-right">{name}</span> | |||
@@ -61,7 +52,7 @@ export default function IssueViewerTabs(props: IssueViewerTabsProps) { | |||
extendedDescription={ruleDetails.htmlNote} | |||
ruleDescriptionContextKey={ruleDescriptionContextKey} | |||
codeTabContent={codeTabContent} | |||
pageType="issues" | |||
scrollInTab={true} | |||
/> | |||
</> | |||
); |
@@ -1089,7 +1089,6 @@ export class App extends React.PureComponent<Props, State> { | |||
paging, | |||
loadingRule | |||
} = this.state; | |||
const { component } = this.props; | |||
return ( | |||
<div className="layout-page-main-inner"> | |||
<DeferredSpinner loading={loadingRule}> | |||
@@ -1109,7 +1108,6 @@ export class App extends React.PureComponent<Props, State> { | |||
/> | |||
} | |||
issue={openIssue} | |||
component={component} | |||
ruleDetails={openRuleDetails} | |||
/> | |||
) : ( | |||
@@ -1140,10 +1138,13 @@ export class App extends React.PureComponent<Props, State> { | |||
} | |||
render() { | |||
const { component } = this.props; | |||
const { openIssue, paging } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
return ( | |||
<div className="layout-page issues" id="issues-page"> | |||
<div | |||
className={classNames('layout-page issues', { 'project-level': component !== undefined })} | |||
id="issues-page"> | |||
<Suggestions suggestions="issues" /> | |||
<Helmet defer={false} title={openIssue ? openIssue.message : translate('issues.page')} /> | |||
@@ -18,7 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Issue } from '../../../types/types'; | |||
import CrossComponentSourceViewer from '../crossComponentSourceViewer/CrossComponentSourceViewer'; | |||
@@ -53,21 +52,17 @@ export default class IssuesSourceViewer extends React.PureComponent<Props> { | |||
} | |||
} | |||
scrollToIssue = (smooth = true) => { | |||
scrollToIssue = () => { | |||
if (this.node) { | |||
const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`); | |||
if (element) { | |||
this.handleScroll(element, undefined, smooth); | |||
element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); | |||
} | |||
} | |||
}; | |||
handleScroll = (element: Element, offset = window.innerHeight / 2, smooth = true) => { | |||
scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth }); | |||
}; | |||
handleLoaded = () => { | |||
this.scrollToIssue(false); | |||
this.scrollToIssue(); | |||
}; | |||
render() { | |||
@@ -100,7 +95,6 @@ export default class IssuesSourceViewer extends React.PureComponent<Props> { | |||
onIssueSelect={this.props.onIssueSelect} | |||
onLoaded={this.handleLoaded} | |||
onLocationSelect={this.props.onLocationSelect} | |||
scroll={this.handleScroll} | |||
selectedFlowIndex={selectedFlowIndex} | |||
/> | |||
</div> |
@@ -214,7 +214,6 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` | |||
onIssueSelect={[MockFunction]} | |||
onLoaded={[Function]} | |||
onLocationSelect={[MockFunction]} | |||
scroll={[Function]} | |||
/> | |||
</div> | |||
`; | |||
@@ -453,7 +452,6 @@ exports[`should render SourceViewer correctly: all secondary locations on same l | |||
onIssueSelect={[MockFunction]} | |||
onLoaded={[Function]} | |||
onLocationSelect={[MockFunction]} | |||
scroll={[Function]} | |||
/> | |||
</div> | |||
`; | |||
@@ -538,7 +536,6 @@ exports[`should render SourceViewer correctly: default 1`] = ` | |||
onIssueSelect={[MockFunction]} | |||
onLoaded={[Function]} | |||
onLocationSelect={[MockFunction]} | |||
scroll={[Function]} | |||
/> | |||
</div> | |||
`; | |||
@@ -737,7 +734,6 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` | |||
onIssueSelect={[MockFunction]} | |||
onLoaded={[Function]} | |||
onLocationSelect={[MockFunction]} | |||
scroll={[Function]} | |||
/> | |||
</div> | |||
`; |
@@ -33,7 +33,6 @@ import { | |||
FlowLocation, | |||
Issue as TypeIssue, | |||
IssuesByLine, | |||
LinearIssueLocation, | |||
Snippet, | |||
SnippetGroup, | |||
SourceLine, | |||
@@ -45,6 +44,7 @@ import { | |||
createSnippets, | |||
expandSnippet, | |||
EXPAND_BY_LINES, | |||
getPrimaryLocation, | |||
linesForSnippets, | |||
MERGE_DISTANCE | |||
} from './utils'; | |||
@@ -70,7 +70,6 @@ interface Props { | |||
index: number, | |||
line: number | |||
) => React.ReactNode; | |||
scroll?: (element: HTMLElement, offset: number) => void; | |||
snippetGroup: SnippetGroup; | |||
} | |||
@@ -83,13 +82,16 @@ interface State { | |||
export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
rootNodeRef = React.createRef<HTMLDivElement>(); | |||
state: State = { | |||
additionalLines: {}, | |||
highlightedSymbols: [], | |||
loading: false, | |||
snippets: [] | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
additionalLines: {}, | |||
highlightedSymbols: [], | |||
loading: false, | |||
snippets: [] | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -106,76 +108,13 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
const snippets = createSnippets({ | |||
component: snippetGroup.component.key, | |||
issue, | |||
locations: snippetGroup.locations | |||
locations: | |||
snippetGroup.locations.length === 0 ? [getPrimaryLocation(issue)] : snippetGroup.locations | |||
}); | |||
this.setState({ snippets }); | |||
} | |||
getNodes(index: number): { wrapper: HTMLElement; table: HTMLElement } | undefined { | |||
const root = this.rootNodeRef.current; | |||
if (!root) { | |||
return undefined; | |||
} | |||
const element = root.querySelector(`#snippet-wrapper-${index}`); | |||
if (!element) { | |||
return undefined; | |||
} | |||
const wrapper = element.querySelector<HTMLElement>('.snippet'); | |||
if (!wrapper) { | |||
return undefined; | |||
} | |||
const table = wrapper.firstChild as HTMLElement; | |||
if (!table) { | |||
return undefined; | |||
} | |||
return { wrapper, table }; | |||
} | |||
/* | |||
* Clean after animation | |||
*/ | |||
cleanDom(index: number) { | |||
const nodes = this.getNodes(index); | |||
if (!nodes) { | |||
return; | |||
} | |||
const { wrapper, table } = nodes; | |||
table.style.marginTop = ''; | |||
wrapper.style.maxHeight = ''; | |||
} | |||
setMaxHeight(index: number, value?: number, up = false) { | |||
const nodes = this.getNodes(index); | |||
if (!nodes) { | |||
return; | |||
} | |||
const { wrapper, table } = nodes; | |||
const maxHeight = value !== undefined ? value : table.getBoundingClientRect().height; | |||
if (up) { | |||
const startHeight = wrapper.getBoundingClientRect().height; | |||
table.style.transition = 'none'; | |||
table.style.marginTop = `${startHeight - maxHeight}px`; | |||
// animate! | |||
setTimeout(() => { | |||
table.style.transition = ''; | |||
table.style.marginTop = '0px'; | |||
wrapper.style.maxHeight = `${maxHeight + 20}px`; | |||
}, 0); | |||
} else { | |||
wrapper.style.maxHeight = `${maxHeight + 20}px`; | |||
} | |||
} | |||
expandBlock = (snippetIndex: number, direction: ExpandDirection): Promise<void> => { | |||
const { branchLike, snippetGroup } = this.props; | |||
const { key } = snippetGroup.component; | |||
@@ -208,56 +147,22 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
return lineMap; | |||
}, {}) | |||
) | |||
.then(newLinesMapped => this.animateBlockExpansion(snippetIndex, direction, newLinesMapped)); | |||
}; | |||
animateBlockExpansion( | |||
snippetIndex: number, | |||
direction: ExpandDirection, | |||
newLinesMapped: Dict<SourceLine> | |||
): Promise<void> { | |||
if (this.mounted) { | |||
const { snippets } = this.state; | |||
const newSnippets = expandSnippet({ | |||
direction, | |||
snippetIndex, | |||
snippets | |||
}); | |||
const deletedSnippets = newSnippets.filter(s => s.toDelete); | |||
// set max-height to current height for CSS transitions | |||
deletedSnippets.forEach(s => this.setMaxHeight(s.index)); | |||
this.setMaxHeight(snippetIndex); | |||
return new Promise(resolve => { | |||
this.setState( | |||
({ additionalLines, snippets }) => { | |||
const combinedLines = { ...additionalLines, ...newLinesMapped }; | |||
return { | |||
additionalLines: combinedLines, | |||
snippets | |||
}; | |||
}, | |||
() => { | |||
// Set max-height 0 to trigger CSS transitions | |||
deletedSnippets.forEach(s => { | |||
this.setMaxHeight(s.index, 0); | |||
}); | |||
this.setMaxHeight(snippetIndex, undefined, direction === 'up'); | |||
// Wait for transition to finish before updating dom | |||
setTimeout(() => { | |||
this.setState({ snippets: newSnippets.filter(s => !s.toDelete) }, resolve); | |||
this.cleanDom(snippetIndex); | |||
}, 200); | |||
} | |||
); | |||
.then(newLinesMapped => { | |||
const newSnippets = expandSnippet({ | |||
direction, | |||
snippetIndex, | |||
snippets | |||
}); | |||
this.setState(({ additionalLines }) => { | |||
const combinedLines = { ...additionalLines, ...newLinesMapped }; | |||
return { | |||
additionalLines: combinedLines, | |||
snippets: newSnippets.filter(s => !s.toDelete) | |||
}; | |||
}); | |||
}); | |||
} | |||
return Promise.resolve(); | |||
} | |||
}; | |||
expandComponent = () => { | |||
const { branchLike, snippetGroup } = this.props; | |||
@@ -356,41 +261,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
); | |||
}; | |||
renderSnippet({ | |||
index, | |||
lastSnippetOfLastGroup, | |||
locationsByLine, | |||
snippet | |||
}: { | |||
index: number; | |||
lastSnippetOfLastGroup: boolean; | |||
locationsByLine: { [line: number]: LinearIssueLocation[] }; | |||
snippet: SourceLine[]; | |||
}) { | |||
return ( | |||
<SnippetViewer | |||
renderAdditionalChildInLine={this.renderIssuesList} | |||
component={this.props.snippetGroup.component} | |||
duplications={this.props.duplications} | |||
duplicationsByLine={this.props.duplicationsByLine} | |||
expandBlock={this.expandBlock} | |||
handleSymbolClick={this.handleSymbolClick} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
highlightedSymbols={this.state.highlightedSymbols} | |||
index={index} | |||
issue={this.props.issue} | |||
lastSnippetOfLastGroup={lastSnippetOfLastGroup} | |||
loadDuplications={this.loadDuplications} | |||
locations={this.props.locations} | |||
locationsByLine={locationsByLine} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
snippet={snippet} | |||
/> | |||
); | |||
} | |||
render() { | |||
const { | |||
branchLike, | |||
@@ -421,7 +291,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true; | |||
return ( | |||
<div className="component-source-container" ref={this.rootNodeRef}> | |||
<> | |||
<IssueSourceViewerHeader | |||
branchLike={branchLike} | |||
expandable={!fullyShown && isFile(snippetGroup.component.q)} | |||
@@ -429,28 +299,39 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone | |||
onExpand={this.expandComponent} | |||
sourceViewerFile={snippetGroup.component} | |||
/> | |||
{issue.component === snippetGroup.component.key && issue.textRange === undefined && ( | |||
<div className="padded-top padded-left padded-right"> | |||
<Issue | |||
issue={issue} | |||
onChange={this.props.onIssueChange} | |||
onPopupToggle={this.props.onIssuePopupToggle} | |||
openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined} | |||
selected={true} | |||
/> | |||
</div> | |||
<Issue | |||
issue={issue} | |||
onChange={this.props.onIssueChange} | |||
onPopupToggle={this.props.onIssuePopupToggle} | |||
openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined} | |||
selected={true} | |||
/> | |||
)} | |||
{snippetLines.map((snippet, index) => ( | |||
<div id={`snippet-wrapper-${snippets[index].index}`} key={snippets[index].index}> | |||
{this.renderSnippet({ | |||
snippet, | |||
index: snippets[index].index, | |||
locationsByLine: includeIssueLocation ? locations : {}, | |||
lastSnippetOfLastGroup: lastSnippetGroup && index === snippets.length - 1 | |||
})} | |||
</div> | |||
<SnippetViewer | |||
key={snippets[index].index} | |||
renderAdditionalChildInLine={this.renderIssuesList} | |||
component={this.props.snippetGroup.component} | |||
duplications={this.props.duplications} | |||
duplicationsByLine={this.props.duplicationsByLine} | |||
expandBlock={this.expandBlock} | |||
handleSymbolClick={this.handleSymbolClick} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
highlightedSymbols={this.state.highlightedSymbols} | |||
index={index} | |||
issue={this.props.issue} | |||
lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1} | |||
loadDuplications={this.loadDuplications} | |||
locations={this.props.locations} | |||
locationsByLine={includeIssueLocation ? locations : {}} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
snippet={snippet} | |||
/> | |||
))} | |||
</div> | |||
</> | |||
); | |||
} | |||
} |
@@ -51,7 +51,7 @@ import { | |||
SourceViewerFile | |||
} from '../../../types/types'; | |||
import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer'; | |||
import { getPrimaryLocation, groupLocationsByComponent } from './utils'; | |||
import { groupLocationsByComponent } from './utils'; | |||
interface Props { | |||
branchLike: BranchLike | undefined; | |||
@@ -63,7 +63,6 @@ interface Props { | |||
onIssueSelect: (issueKey: string) => void; | |||
onLoaded?: () => void; | |||
onLocationSelect: (index: number) => void; | |||
scroll?: (element: HTMLElement) => void; | |||
selectedFlowIndex: number | undefined; | |||
} | |||
@@ -226,9 +225,8 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop | |||
const issuesByComponent = issuesByComponentAndLine(this.props.issues); | |||
const locationsByComponent = groupLocationsByComponent(issue, locations, components); | |||
const lastOccurenceOfPrimaryComponent = findLastIndex( | |||
locationsByComponent, | |||
({ component }) => component.key === issue.component | |||
const lastOccurenceOfPrimaryComponent = findLastIndex(locationsByComponent, ({ component }) => | |||
component ? component.key === issue.component : true | |||
); | |||
if (components[issue.component] === undefined) { | |||
@@ -236,7 +234,7 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop | |||
} | |||
return ( | |||
<div> | |||
<> | |||
{locationsByComponent.map((snippetGroup, i) => { | |||
return ( | |||
<SourceViewerContext.Provider | |||
@@ -260,45 +258,12 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop | |||
onIssuePopupToggle={this.handleIssuePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
snippetGroup={snippetGroup} | |||
/> | |||
</SourceViewerContext.Provider> | |||
); | |||
})} | |||
{locationsByComponent.length === 0 && ( | |||
<SourceViewerContext.Provider | |||
value={{ | |||
branchLike: this.props.branchLike, | |||
file: components[issue.component].component | |||
}}> | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={this.props.branchLike} | |||
duplications={duplications} | |||
duplicationsByLine={duplicationsByLine} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
issue={issue} | |||
issuePopup={this.state.issuePopup} | |||
issuesByLine={issuesByComponent[issue.component] || {}} | |||
isLastOccurenceOfPrimaryComponent={true} | |||
lastSnippetGroup={true} | |||
loadDuplications={this.fetchDuplications} | |||
locations={[]} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssueSelect={this.props.onIssueSelect} | |||
onIssuePopupToggle={this.handleIssuePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
snippetGroup={{ | |||
locations: [getPrimaryLocation(issue)], | |||
...components[issue.component] | |||
}} | |||
/> | |||
</SourceViewerContext.Provider> | |||
)} | |||
</div> | |||
</> | |||
); | |||
} | |||
} |
@@ -17,10 +17,19 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.source-viewer-header-slim { | |||
.issue-source-viewer-header { | |||
padding: 4px 10px; | |||
border: 1px solid var(--gray80); | |||
background-color: var(--barBackgroundColor); | |||
align-items: center; | |||
min-height: 25px; | |||
position: sticky; | |||
z-index: 100; | |||
top: 0; | |||
margin-top: 8px; | |||
margin-bottom: -1px; | |||
} | |||
.issue-source-viewer-header:first-child { | |||
margin-top: 0; | |||
} |
@@ -65,7 +65,10 @@ export default function IssueSourceViewerHeader(props: Props) { | |||
const isProjectRoot = q === ComponentQualifier.Project; | |||
return ( | |||
<div className="source-viewer-header-slim display-flex-row display-flex-space-between"> | |||
<div | |||
className="issue-source-viewer-header display-flex-row display-flex-space-between" | |||
role="separator" | |||
aria-label={sourceViewerFile.path}> | |||
<div className="display-flex-center flex-1"> | |||
{displayProjectName && ( | |||
<div className="spacer-right"> |
@@ -18,11 +18,12 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.snippet { | |||
margin: var(--gridSize) 0; | |||
border: 1px solid var(--gray80); | |||
overflow-x: auto; | |||
overflow-y: hidden; | |||
transition: max-height 0.2s; | |||
} | |||
.snippet + .snippet { | |||
margin-top: 8px; | |||
} | |||
.snippet > div { |
@@ -28,7 +28,6 @@ import { | |||
optimizeLocationMessage | |||
} from '../../../components/SourceViewer/helpers/lines'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { scrollHorizontally } from '../../../helpers/scrolling'; | |||
import { | |||
Duplication, | |||
ExpandDirection, | |||
@@ -60,53 +59,12 @@ interface Props { | |||
onLocationSelect: (index: number) => void; | |||
renderAdditionalChildInLine?: (line: SourceLine) => React.ReactNode | undefined; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
scroll?: (element: HTMLElement, offset?: number) => void; | |||
snippet: SourceLine[]; | |||
} | |||
export default class SnippetViewer extends React.PureComponent<Props> { | |||
snippetNodeRef: React.RefObject<HTMLDivElement>; | |||
constructor(props: Props) { | |||
super(props); | |||
this.snippetNodeRef = React.createRef(); | |||
} | |||
doScroll = (element: HTMLElement) => { | |||
if (this.props.scroll) { | |||
this.props.scroll(element); | |||
} | |||
const parent = this.snippetNodeRef.current as Element; | |||
if (parent) { | |||
const offset = parent.getBoundingClientRect().width / 2; | |||
scrollHorizontally(element, { | |||
leftOffset: offset, | |||
rightOffset: offset, | |||
parent | |||
}); | |||
} | |||
}; | |||
scrollToLastExpandedRow = () => { | |||
if (this.props.scroll) { | |||
const snippetNode = this.snippetNodeRef.current as Element; | |||
if (!snippetNode) { | |||
return; | |||
} | |||
const rows = snippetNode.querySelectorAll('tr'); | |||
const lastRow = rows[rows.length - 1]; | |||
this.props.scroll(lastRow, 100); | |||
} | |||
}; | |||
expandBlock = (direction: ExpandDirection) => () => | |||
this.props.expandBlock(this.props.index, direction).then(() => { | |||
if (direction === 'down') { | |||
this.scrollToLastExpandedRow(); | |||
} | |||
}); | |||
this.props.expandBlock(this.props.index, direction); | |||
renderLine({ | |||
displayDuplications, | |||
@@ -169,7 +127,6 @@ export default class SnippetViewer extends React.PureComponent<Props> { | |||
openIssues={false} | |||
previousLine={index > 0 ? snippet[index - 1] : undefined} | |||
renderDuplicationPopup={this.props.renderDuplicationPopup} | |||
scroll={this.doScroll} | |||
secondaryIssueLocations={secondaryIssueLocations} | |||
verticalBuffer={verticalBuffer}> | |||
{this.props.renderAdditionalChildInLine && this.props.renderAdditionalChildInLine(line)} | |||
@@ -203,7 +160,7 @@ export default class SnippetViewer extends React.PureComponent<Props> { | |||
Boolean(this.props.loadDuplications) && snippet.some(s => !!s.duplicated); | |||
return ( | |||
<div className="source-viewer-code snippet" ref={this.snippetNodeRef}> | |||
<div className="source-viewer-code snippet"> | |||
<div> | |||
{snippet[0].line > 1 && ( | |||
<div className="expand-block expand-block-above"> |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { mount, ReactWrapper, shallow } from 'enzyme'; | |||
import { shallow } from 'enzyme'; | |||
import { range, times } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getSources } from '../../../../api/components'; | |||
@@ -38,23 +38,6 @@ jest.mock('../../../../api/components', () => ({ | |||
getSources: jest.fn().mockResolvedValue([]) | |||
})); | |||
/* | |||
* Quick & dirty fix to make the tests pass | |||
* this whole thing should be replaced by RTL tests! | |||
*/ | |||
jest.mock('react-router-dom', () => { | |||
const routerDom = jest.requireActual('react-router-dom'); | |||
function Link() { | |||
return <div>Link</div>; | |||
} | |||
return { | |||
...routerDom, | |||
Link | |||
}; | |||
}); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
@@ -301,121 +284,6 @@ it('should correctly handle lines actions', () => { | |||
); | |||
}); | |||
describe('getNodes', () => { | |||
const snippetGroup: SnippetGroup = { | |||
component: mockSourceViewerFile(), | |||
locations: [], | |||
sources: [] | |||
}; | |||
const wrapper = mount<ComponentSourceSnippetGroupViewer>( | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={mockMainBranch()} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
isLastOccurenceOfPrimaryComponent={true} | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetGroup={false} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
onIssueChange={jest.fn()} | |||
onIssueSelect={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
snippetGroup={snippetGroup} | |||
/> | |||
); | |||
it('should return undefined if any node is missing', async () => { | |||
await waitAndUpdate(wrapper); | |||
const rootNode = wrapper.instance().rootNodeRef; | |||
mockDom(rootNode.current!); | |||
expect(wrapper.instance().getNodes(0)).toBeUndefined(); | |||
expect(wrapper.instance().getNodes(1)).toBeUndefined(); | |||
expect(wrapper.instance().getNodes(2)).toBeUndefined(); | |||
}); | |||
it('should return elements if dom is correct', async () => { | |||
await waitAndUpdate(wrapper); | |||
const rootNode = wrapper.instance().rootNodeRef; | |||
mockDom(rootNode.current!); | |||
expect(wrapper.instance().getNodes(3)).not.toBeUndefined(); | |||
}); | |||
it('should enable cleaning the dom', async () => { | |||
await waitAndUpdate(wrapper); | |||
const rootNode = wrapper.instance().rootNodeRef; | |||
mockDom(rootNode.current!); | |||
wrapper.instance().cleanDom(3); | |||
const nodes = wrapper.instance().getNodes(3); | |||
expect(nodes!.wrapper.style.maxHeight).toBe(''); | |||
expect(nodes!.table.style.marginTop).toBe(''); | |||
}); | |||
}); | |||
describe('getHeight', () => { | |||
beforeAll(() => { | |||
jest.useFakeTimers(); | |||
}); | |||
afterAll(() => { | |||
jest.runOnlyPendingTimers(); | |||
jest.useRealTimers(); | |||
}); | |||
const snippetGroup: SnippetGroup = { | |||
component: mockSourceViewerFile(), | |||
locations: [], | |||
sources: [] | |||
}; | |||
const wrapper = mount<ComponentSourceSnippetGroupViewer>( | |||
<ComponentSourceSnippetGroupViewer | |||
branchLike={mockMainBranch()} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
isLastOccurenceOfPrimaryComponent={true} | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
lastSnippetGroup={false} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
onIssueChange={jest.fn()} | |||
onIssueSelect={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
snippetGroup={snippetGroup} | |||
/> | |||
); | |||
it('should set maxHeight to current height', async () => { | |||
await waitAndUpdate(wrapper); | |||
const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 }); | |||
wrapper.instance().setMaxHeight(0); | |||
expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;'); | |||
expect(nodes.table.getAttribute('style')).toBeNull(); | |||
}); | |||
it('should set margin and then maxHeight for a nice upwards animation', async () => { | |||
await waitAndUpdate(wrapper); | |||
const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 }); | |||
wrapper.instance().setMaxHeight(0, undefined, true); | |||
expect(nodes.wrapper.getAttribute('style')).toBeNull(); | |||
expect(nodes.table.getAttribute('style')).toBe('transition: none; margin-top: -26px;'); | |||
jest.runAllTimers(); | |||
expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;'); | |||
expect(nodes.table.getAttribute('style')).toBe('margin-top: 0px;'); | |||
}); | |||
}); | |||
function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']> = {}) { | |||
const snippetGroup: SnippetGroup = { | |||
component: mockSourceViewerFile(), | |||
@@ -437,53 +305,8 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props'] | |||
onIssuePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
snippetGroup={snippetGroup} | |||
{...props} | |||
/> | |||
); | |||
} | |||
function mockDom(refNode: HTMLDivElement) { | |||
refNode.querySelector = jest.fn(query => { | |||
const index = query.split('-').pop(); | |||
switch (index) { | |||
case '0': | |||
return null; | |||
case '1': | |||
return mount(<div />).getDOMNode(); | |||
case '2': | |||
return mount( | |||
<div> | |||
<div className="snippet" /> | |||
</div> | |||
).getDOMNode(); | |||
case '3': | |||
return mount( | |||
<div> | |||
<div className="snippet"> | |||
<div /> | |||
</div> | |||
</div> | |||
).getDOMNode(); | |||
default: | |||
return null; | |||
} | |||
}); | |||
} | |||
function mockDomForSizes( | |||
componentWrapper: ReactWrapper<{}, {}, ComponentSourceSnippetGroupViewer>, | |||
{ wrapperHeight = 0, tableHeight = 0 } | |||
) { | |||
const wrapper = mount(<div className="snippet" />).getDOMNode(); | |||
wrapper.getBoundingClientRect = jest.fn().mockReturnValue({ height: wrapperHeight }); | |||
const table = mount(<div />).getDOMNode(); | |||
table.getBoundingClientRect = jest.fn().mockReturnValue({ height: tableHeight }); | |||
componentWrapper.instance().getNodes = jest.fn().mockReturnValue({ | |||
wrapper, | |||
table | |||
}); | |||
return { wrapper, table }; | |||
} |
@@ -28,6 +28,7 @@ import { | |||
} from '../../../../helpers/mocks/sources'; | |||
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer'; | |||
import CrossComponentSourceViewer from '../CrossComponentSourceViewer'; | |||
jest.mock('../../../../api/issues', () => { | |||
@@ -102,10 +103,10 @@ it('should handle duplication popup', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('loadDuplications')( | |||
'foo', | |||
mockSourceLine() | |||
); | |||
wrapper | |||
.find(ComponentSourceSnippetGroupViewer) | |||
.props() | |||
.loadDuplications('foo', mockSourceLine()); | |||
await waitAndUpdate(wrapper); | |||
expect(getDuplications).toHaveBeenCalledWith({ key: 'foo' }); | |||
@@ -114,11 +115,10 @@ it('should handle duplication popup', async () => { | |||
expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] }); | |||
expect( | |||
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('renderDuplicationPopup')( | |||
mockSourceViewerFile(), | |||
0, | |||
16 | |||
) | |||
wrapper | |||
.find(ComponentSourceSnippetGroupViewer) | |||
.props() | |||
.renderDuplicationPopup(mockSourceViewerFile(), 0, 16) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -127,14 +127,17 @@ function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {}) | |||
<CrossComponentSourceViewer | |||
branchLike={undefined} | |||
highlightedLocationMessage={undefined} | |||
issue={mockIssue(true, { key: '1' })} | |||
issue={mockIssue(true, { | |||
key: '1', | |||
component: 'project:main.js', | |||
textRange: { startLine: 1, endLine: 2, startOffset: 0, endOffset: 15 } | |||
})} | |||
issues={[]} | |||
locations={[mockFlowLocation()]} | |||
locations={[mockFlowLocation({ component: 'project:main.js' })]} | |||
onIssueChange={jest.fn()} | |||
onLoaded={jest.fn()} | |||
onIssueSelect={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
scroll={jest.fn()} | |||
selectedFlowIndex={0} | |||
{...props} | |||
/> |
@@ -17,11 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { mount, shallow } from 'enzyme'; | |||
import { shallow } from 'enzyme'; | |||
import { range } from 'lodash'; | |||
import * as React from 'react'; | |||
import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/mocks/sources'; | |||
import { scrollHorizontally } from '../../../../helpers/scrolling'; | |||
import { mockIssue } from '../../../../helpers/testMocks'; | |||
import SnippetViewer from '../SnippetViewer'; | |||
@@ -115,29 +114,6 @@ it('should correctly handle expansion', () => { | |||
expect(expandBlock).toHaveBeenCalledWith(2, 'down'); | |||
}); | |||
it('should handle scrolling', () => { | |||
const scroll = jest.fn(); | |||
const wrapper = mountRender({ scroll }); | |||
const element = {} as HTMLElement; | |||
wrapper.instance().doScroll(element); | |||
expect(scroll).toHaveBeenCalledWith(element); | |||
expect(scrollHorizontally).toHaveBeenCalled(); | |||
expect((scrollHorizontally as jest.Mock).mock.calls[0][0]).toBe(element); | |||
}); | |||
it('should handle scrolling to expanded row', () => { | |||
const scroll = jest.fn(); | |||
const wrapper = mountRender({ scroll }); | |||
wrapper.instance().scrollToLastExpandedRow(); | |||
expect(scroll).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props: Partial<SnippetViewer['props']> = {}) { | |||
return shallow<SnippetViewer>( | |||
<SnippetViewer | |||
@@ -156,34 +132,8 @@ function shallowRender(props: Partial<SnippetViewer['props']> = {}) { | |||
locationsByLine={{}} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
snippet={[]} | |||
{...props} | |||
/> | |||
); | |||
} | |||
function mountRender(props: Partial<SnippetViewer['props']> = {}) { | |||
return mount<SnippetViewer>( | |||
<SnippetViewer | |||
component={mockSourceViewerFile()} | |||
duplications={undefined} | |||
duplicationsByLine={undefined} | |||
expandBlock={jest.fn()} | |||
handleSymbolClick={jest.fn()} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
highlightedSymbols={[]} | |||
index={0} | |||
issue={mockIssue()} | |||
lastSnippetOfLastGroup={false} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
locationsByLine={{}} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
snippet={[mockSourceLine()]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,9 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="component-source-container" | |||
> | |||
<Fragment> | |||
<IssueSourceViewerHeader | |||
branchLike={ | |||
Object { | |||
@@ -37,5 +35,5 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -22,30 +22,13 @@ exports[`should render correctly 1`] = ` | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<div> | |||
<Fragment> | |||
<ContextProvider | |||
key="1-0-0" | |||
value={ | |||
Object { | |||
"branchLike": undefined, | |||
"file": Object { | |||
"canMarkAsFavorite": true, | |||
"fav": false, | |||
"key": "project:main.js", | |||
"longName": "main.js", | |||
"measures": Object { | |||
"coverage": "85.2", | |||
"duplicationDensity": "1.0", | |||
"issues": "12", | |||
"lines": "56", | |||
}, | |||
"name": "main.js", | |||
"path": "main.js", | |||
"project": "project", | |||
"projectName": "MyProject", | |||
"q": "FIL", | |||
"uuid": "foo-bar", | |||
}, | |||
"file": Object {}, | |||
} | |||
} | |||
> | |||
@@ -55,7 +38,7 @@ exports[`should render correctly 2`] = ` | |||
issue={ | |||
Object { | |||
"actions": Array [], | |||
"component": "main.js", | |||
"component": "project:main.js", | |||
"componentLongName": "main.js", | |||
"componentQualifier": "FIL", | |||
"componentUuid": "foo1234", | |||
@@ -143,9 +126,9 @@ exports[`should render correctly 2`] = ` | |||
"severity": "MAJOR", | |||
"status": "OPEN", | |||
"textRange": Object { | |||
"endLine": 26, | |||
"endLine": 2, | |||
"endOffset": 15, | |||
"startLine": 25, | |||
"startLine": 1, | |||
"startOffset": 0, | |||
}, | |||
"transitions": Array [], | |||
@@ -158,7 +141,7 @@ exports[`should render correctly 2`] = ` | |||
locations={ | |||
Array [ | |||
Object { | |||
"component": "main.js", | |||
"component": "project:main.js", | |||
"index": 0, | |||
"textRange": Object { | |||
"endLine": 2, | |||
@@ -174,30 +157,12 @@ exports[`should render correctly 2`] = ` | |||
onIssueSelect={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
renderDuplicationPopup={[Function]} | |||
scroll={[MockFunction]} | |||
snippetGroup={ | |||
Object { | |||
"component": Object { | |||
"canMarkAsFavorite": true, | |||
"fav": false, | |||
"key": "project:main.js", | |||
"longName": "main.js", | |||
"measures": Object { | |||
"coverage": "85.2", | |||
"duplicationDensity": "1.0", | |||
"issues": "12", | |||
"lines": "56", | |||
}, | |||
"name": "main.js", | |||
"path": "main.js", | |||
"project": "project", | |||
"projectName": "MyProject", | |||
"q": "FIL", | |||
"uuid": "foo-bar", | |||
}, | |||
"component": Object {}, | |||
"locations": Array [ | |||
Object { | |||
"component": "main.js", | |||
"component": "project:main.js", | |||
"index": 0, | |||
"textRange": Object { | |||
"endLine": 2, | |||
@@ -207,28 +172,16 @@ exports[`should render correctly 2`] = ` | |||
}, | |||
}, | |||
], | |||
"sources": Object { | |||
"16": 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", | |||
}, | |||
}, | |||
"sources": Array [], | |||
} | |||
} | |||
/> | |||
</ContextProvider> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: no component found 1`] = ` | |||
<div> | |||
<Fragment> | |||
<ContextProvider | |||
key="unknown-0-0" | |||
value={ | |||
@@ -350,7 +303,6 @@ exports[`should render correctly: no component found 1`] = ` | |||
onIssueSelect={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
renderDuplicationPopup={[Function]} | |||
scroll={[MockFunction]} | |||
snippetGroup={ | |||
Object { | |||
"component": Object {}, | |||
@@ -365,24 +317,7 @@ exports[`should render correctly: no component found 1`] = ` | |||
value={ | |||
Object { | |||
"branchLike": undefined, | |||
"file": Object { | |||
"canMarkAsFavorite": true, | |||
"fav": false, | |||
"key": "project:main.js", | |||
"longName": "main.js", | |||
"measures": Object { | |||
"coverage": "85.2", | |||
"duplicationDensity": "1.0", | |||
"issues": "12", | |||
"lines": "56", | |||
}, | |||
"name": "main.js", | |||
"path": "main.js", | |||
"project": "project", | |||
"projectName": "MyProject", | |||
"q": "FIL", | |||
"uuid": "foo-bar", | |||
}, | |||
"file": Object {}, | |||
} | |||
} | |||
> | |||
@@ -495,7 +430,7 @@ exports[`should render correctly: no component found 1`] = ` | |||
locations={ | |||
Array [ | |||
Object { | |||
"component": "main.js", | |||
"component": "project:main.js", | |||
"index": 0, | |||
"textRange": Object { | |||
"endLine": 2, | |||
@@ -511,30 +446,12 @@ exports[`should render correctly: no component found 1`] = ` | |||
onIssueSelect={[MockFunction]} | |||
onLocationSelect={[MockFunction]} | |||
renderDuplicationPopup={[Function]} | |||
scroll={[MockFunction]} | |||
snippetGroup={ | |||
Object { | |||
"component": Object { | |||
"canMarkAsFavorite": true, | |||
"fav": false, | |||
"key": "project:main.js", | |||
"longName": "main.js", | |||
"measures": Object { | |||
"coverage": "85.2", | |||
"duplicationDensity": "1.0", | |||
"issues": "12", | |||
"lines": "56", | |||
}, | |||
"name": "main.js", | |||
"path": "main.js", | |||
"project": "project", | |||
"projectName": "MyProject", | |||
"q": "FIL", | |||
"uuid": "foo-bar", | |||
}, | |||
"component": Object {}, | |||
"locations": Array [ | |||
Object { | |||
"component": "main.js", | |||
"component": "project:main.js", | |||
"index": 0, | |||
"textRange": Object { | |||
"endLine": 2, | |||
@@ -544,22 +461,10 @@ exports[`should render correctly: no component found 1`] = ` | |||
}, | |||
}, | |||
], | |||
"sources": Object { | |||
"16": 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", | |||
}, | |||
}, | |||
"sources": Array [], | |||
} | |||
} | |||
/> | |||
</ContextProvider> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -2,7 +2,9 @@ | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="source-viewer-header-slim display-flex-row display-flex-space-between" | |||
aria-label="foo/bar.ts" | |||
className="issue-source-viewer-header display-flex-row display-flex-space-between" | |||
role="separator" | |||
> | |||
<div | |||
className="display-flex-center flex-1" | |||
@@ -82,7 +84,9 @@ exports[`should render correctly 1`] = ` | |||
exports[`should render correctly: no link to project 1`] = ` | |||
<div | |||
className="source-viewer-header-slim display-flex-row display-flex-space-between" | |||
aria-label="foo/bar.ts" | |||
className="issue-source-viewer-header display-flex-row display-flex-space-between" | |||
role="separator" | |||
> | |||
<div | |||
className="display-flex-center flex-1" | |||
@@ -157,7 +161,9 @@ exports[`should render correctly: no link to project 1`] = ` | |||
exports[`should render correctly: no project name 1`] = ` | |||
<div | |||
className="source-viewer-header-slim display-flex-row display-flex-space-between" | |||
aria-label="foo/bar.ts" | |||
className="issue-source-viewer-header display-flex-row display-flex-space-between" | |||
role="separator" | |||
> | |||
<div | |||
className="display-flex-center flex-1" | |||
@@ -221,7 +227,9 @@ exports[`should render correctly: no project name 1`] = ` | |||
exports[`should render correctly: project root 1`] = ` | |||
<div | |||
className="source-viewer-header-slim display-flex-row display-flex-space-between" | |||
aria-label="foo/bar.ts" | |||
className="issue-source-viewer-header display-flex-row display-flex-space-between" | |||
role="separator" | |||
> | |||
<div | |||
className="display-flex-center flex-1" |
@@ -55,7 +55,6 @@ exports[`should render correctly 1`] = ` | |||
onSymbolClick={[MockFunction]} | |||
openIssues={false} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -107,7 +106,6 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -159,7 +157,6 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -235,7 +232,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
onSymbolClick={[MockFunction]} | |||
openIssues={false} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -287,7 +283,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -339,7 +334,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -391,7 +385,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -456,7 +449,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
onSymbolClick={[MockFunction]} | |||
openIssues={false} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -508,7 +500,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -560,7 +551,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -612,7 +602,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -664,7 +653,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -716,7 +704,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -768,7 +755,6 @@ exports[`should render correctly when at the top of the file 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -845,7 +831,6 @@ exports[`should render correctly with no SCM 1`] = ` | |||
onSymbolClick={[MockFunction]} | |||
openIssues={false} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -898,7 +883,6 @@ exports[`should render correctly with no SCM 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> | |||
@@ -951,7 +935,6 @@ exports[`should render correctly with no SCM 1`] = ` | |||
} | |||
} | |||
renderDuplicationPopup={[MockFunction]} | |||
scroll={[Function]} | |||
secondaryIssueLocations={Array []} | |||
verticalBuffer={0} | |||
/> |
@@ -23,7 +23,7 @@ import { createSnippets, expandSnippet, groupLocationsByComponent } from '../uti | |||
describe('groupLocationsByComponent', () => { | |||
it('should handle empty args', () => { | |||
expect(groupLocationsByComponent(mockIssue(), [], {})).toEqual([]); | |||
expect(groupLocationsByComponent(mockIssue(), [], {})).toEqual([{ locations: [] }]); | |||
}); | |||
it('should group correctly', () => { |
@@ -181,6 +181,10 @@ export function groupLocationsByComponent( | |||
currentGroup.locations.push(loc); | |||
}); | |||
if (groups.length === 0) { | |||
groups.push({ locations: [], ...components[issue.component] }); | |||
} | |||
return groups; | |||
} | |||
@@ -137,15 +137,6 @@ | |||
color: white; | |||
} | |||
.component-source-container + .component-source-container { | |||
margin-top: var(--gridSize); | |||
} | |||
.component-source-container-header { | |||
background-color: var(--gray94); | |||
padding: var(--gridSize); | |||
} | |||
.issues-page-actions { | |||
display: inline-block; | |||
min-width: 80px; | |||
@@ -230,19 +221,6 @@ | |||
background-color: var(--barBackgroundColor); | |||
} | |||
.issue-header { | |||
z-index: 100; | |||
position: sticky; | |||
top: 48px; | |||
background-color: white; | |||
padding-top: 20px; | |||
height: 50px; | |||
} | |||
.issue-project-level.issue-header { | |||
top: 120px; | |||
} | |||
.layout-page-main.open-issue { | |||
padding-top: 0; | |||
} |
@@ -197,7 +197,6 @@ export default function HotspotSnippetContainerRenderer( | |||
renderAdditionalChildInLine={renderHotspotBoxInLine} | |||
renderDuplicationPopup={noop} | |||
snippet={sourceLines} | |||
scroll={getScrollHandler(scrollableRef)} | |||
/> | |||
)} | |||
</DeferredSpinner> |
@@ -506,7 +506,6 @@ exports[`should render correctly: with sourcelines 1`] = ` | |||
onLocationSelect={[MockFunction]} | |||
renderAdditionalChildInLine={[Function]} | |||
renderDuplicationPopup={[Function]} | |||
scroll={[Function]} | |||
snippet={ | |||
Array [ | |||
Object { |
@@ -87,7 +87,6 @@ export interface Props { | |||
onIssueChange?: (issue: Issue) => void; | |||
onIssueSelect?: (issueKey: string) => void; | |||
onIssueUnselect?: () => void; | |||
scroll?: (element: HTMLElement) => void; | |||
selectedIssue?: string; | |||
showMeasures?: boolean; | |||
metricKey?: string; | |||
@@ -594,7 +593,6 @@ export default class SourceViewer extends React.PureComponent<Props, State> { | |||
onSymbolClick={this.handleSymbolClick} | |||
openIssuesByLine={this.state.openIssuesByLine} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
metricKey={this.props.metricKey} | |||
selectedIssue={this.state.selectedIssue} | |||
sources={sources} |
@@ -78,7 +78,6 @@ interface Props { | |||
onSymbolClick: (symbols: string[]) => void; | |||
openIssuesByLine: { [line: number]: boolean }; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
scroll?: (element: HTMLElement) => void; | |||
metricKey?: string; | |||
selectedIssue: string | undefined; | |||
sources: SourceLine[]; | |||
@@ -185,7 +184,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
openIssues={this.props.openIssuesByLine[line.line] || false} | |||
previousLine={index > 0 ? sources[index - 1] : undefined} | |||
renderDuplicationPopup={this.props.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
scrollToUncoveredLine={scrollToUncoveredLine} | |||
secondaryIssueLocations={secondaryIssueLocations}> | |||
<LineIssuesList |
@@ -396,7 +396,6 @@ function getSourceViewerUi(override?: Partial<SourceViewer['props']>) { | |||
onIssueSelect={jest.fn()} | |||
onLoaded={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
scroll={jest.fn()} | |||
{...override} | |||
/> | |||
); |
@@ -58,7 +58,6 @@ interface Props { | |||
openIssues: boolean; | |||
previousLine: SourceLine | undefined; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
scroll?: (element: HTMLElement) => void; | |||
scrollToUncoveredLine?: boolean; | |||
secondaryIssueLocations: LinearIssueLocation[]; | |||
verticalBuffer?: number; | |||
@@ -165,11 +164,7 @@ export default class Line extends React.PureComponent<Props> { | |||
})} | |||
{displayCoverage && ( | |||
<LineCoverage | |||
line={line} | |||
scroll={this.props.scroll} | |||
scrollToUncoveredLine={scrollToUncoveredLine} | |||
/> | |||
<LineCoverage line={line} scrollToUncoveredLine={scrollToUncoveredLine} /> | |||
)} | |||
<LineCode | |||
@@ -181,7 +176,6 @@ export default class Line extends React.PureComponent<Props> { | |||
onLocationSelect={this.props.onLocationSelect} | |||
onSymbolClick={this.props.onSymbolClick} | |||
padding={bottomPadding} | |||
scroll={this.props.scroll} | |||
secondaryIssueLocations={secondaryIssueLocations}> | |||
{children} | |||
</LineCode> |
@@ -34,7 +34,6 @@ interface Props { | |||
onLocationSelect: ((index: number) => void) | undefined; | |||
onSymbolClick: (symbols: Array<string>) => void; | |||
padding?: number; | |||
scroll?: (element: HTMLElement) => void; | |||
secondaryIssueLocations: LinearIssueLocation[]; | |||
} | |||
@@ -42,22 +41,19 @@ export default class LineCode extends React.PureComponent<React.PropsWithChildre | |||
activeMarkerNode?: HTMLElement | null; | |||
symbols?: NodeListOf<HTMLElement>; | |||
componentDidMount() { | |||
if (this.props.highlightedLocationMessage && this.activeMarkerNode && this.props.scroll) { | |||
this.props.scroll(this.activeMarkerNode); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if ( | |||
this.props.highlightedLocationMessage && | |||
(!prevProps.highlightedLocationMessage || | |||
prevProps.highlightedLocationMessage.index !== | |||
this.props.highlightedLocationMessage.index) && | |||
this.activeMarkerNode && | |||
this.props.scroll | |||
this.activeMarkerNode | |||
) { | |||
this.props.scroll(this.activeMarkerNode); | |||
this.activeMarkerNode.scrollIntoView({ | |||
behavior: 'smooth', | |||
block: 'center', | |||
inline: 'center' | |||
}); | |||
} | |||
} | |||
@@ -24,17 +24,20 @@ import { SourceLine } from '../../../types/types'; | |||
export interface LineCoverageProps { | |||
line: SourceLine; | |||
scroll?: (element: HTMLElement) => void; | |||
scrollToUncoveredLine?: boolean; | |||
} | |||
export function LineCoverage({ line, scroll, scrollToUncoveredLine }: LineCoverageProps) { | |||
const coverageMarker = React.useRef<HTMLTableDataCellElement>(null); | |||
export function LineCoverage({ line, scrollToUncoveredLine }: LineCoverageProps) { | |||
const coverageMarker = React.useRef<HTMLTableCellElement>(null); | |||
React.useEffect(() => { | |||
if (scrollToUncoveredLine && scroll && coverageMarker.current) { | |||
scroll(coverageMarker.current); | |||
if (scrollToUncoveredLine && coverageMarker.current) { | |||
coverageMarker.current.scrollIntoView({ | |||
behavior: 'smooth', | |||
block: 'center', | |||
inline: 'center' | |||
}); | |||
} | |||
}, [scrollToUncoveredLine, scroll, coverageMarker]); | |||
}, [scrollToUncoveredLine, coverageMarker]); | |||
const className = | |||
'source-meta source-line-coverage' + |
@@ -89,7 +89,6 @@ function shallowRender(props: Partial<Line['props']> = {}) { | |||
openIssues={false} | |||
previousLine={undefined} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
secondaryIssueLocations={[]} | |||
{...props} | |||
/> |
@@ -30,16 +30,16 @@ jest.mock('react', () => { | |||
}); | |||
it('should correctly trigger a scroll', () => { | |||
const element = { current: {} }; | |||
const scroll = jest.fn(); | |||
const element = { current: { scrollIntoView: scroll } }; | |||
(React.useEffect as jest.Mock).mockImplementation(f => f()); | |||
(React.useRef as jest.Mock).mockImplementation(() => element); | |||
const scroll = jest.fn(); | |||
shallowRender({ scroll, scrollToUncoveredLine: true }); | |||
expect(scroll).toHaveBeenCalledWith(element.current); | |||
shallowRender({ scrollToUncoveredLine: true }); | |||
expect(scroll).toHaveBeenCalled(); | |||
scroll.mockReset(); | |||
shallowRender({ scroll, scrollToUncoveredLine: false }); | |||
shallowRender({ scrollToUncoveredLine: false }); | |||
expect(scroll).not.toHaveBeenCalled(); | |||
}); | |||
@@ -58,7 +58,6 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
} | |||
onLocationSelect={[MockFunction]} | |||
onSymbolClick={[MockFunction]} | |||
scroll={[MockFunction]} | |||
secondaryIssueLocations={Array []} | |||
/> | |||
</tr> |
@@ -55,7 +55,7 @@ export default class MoreInfoRuleDescription extends React.PureComponent<Props, | |||
educationPrinciplesRef | |||
} = this.props; | |||
return ( | |||
<div className="big-padded rule-desc"> | |||
<div className="padded rule-desc"> | |||
{displayEducationalPrinciplesNotification && ( | |||
<Alert variant="info"> | |||
<p className="little-spacer-bottom little-spacer-top"> |
@@ -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 classNames from 'classnames'; | |||
import { cloneDeep, debounce, groupBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { dismissNotice } from '../../api/users'; | |||
@@ -27,6 +26,7 @@ import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { RuleDetails } from '../../types/types'; | |||
import { NoticeType } from '../../types/users'; | |||
import ScreenPositionHelper from '../common/ScreenPositionHelper'; | |||
import BoxedTabs from '../controls/BoxedTabs'; | |||
import MoreInfoRuleDescription from './MoreInfoRuleDescription'; | |||
import RuleDescription from './RuleDescription'; | |||
@@ -37,7 +37,7 @@ interface TabViewerProps extends CurrentUserContextInterface { | |||
extendedDescription?: string; | |||
ruleDescriptionContextKey?: string; | |||
codeTabContent?: React.ReactNode; | |||
pageType?: string; | |||
scrollInTab?: boolean; | |||
} | |||
interface State { | |||
@@ -173,7 +173,7 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] || | |||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && ( | |||
<RuleDescription | |||
className="big-padded" | |||
className="padded" | |||
sections={ | |||
descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] || | |||
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE] | |||
@@ -188,7 +188,7 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue), | |||
content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && ( | |||
<RuleDescription | |||
className="big-padded" | |||
className="padded" | |||
sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]} | |||
/> | |||
) | |||
@@ -198,7 +198,7 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt), | |||
content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && ( | |||
<RuleDescription | |||
className="big-padded" | |||
className="padded" | |||
sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]} | |||
defaultContextKey={ruleDescriptionContextKey} | |||
/> | |||
@@ -228,7 +228,7 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
tabs.unshift({ | |||
key: TabKeys.Code, | |||
label: translate('issue.tabs', TabKeys.Code), | |||
content: <div className="padded">{codeTabContent}</div> | |||
content: codeTabContent | |||
}); | |||
} | |||
@@ -281,8 +281,8 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
}; | |||
render() { | |||
const { scrollInTab } = this.props; | |||
const { tabs, selectedTab } = this.state; | |||
const { pageType } = this.props; | |||
if (!tabs || tabs.length === 0 || !selectedTab) { | |||
return null; | |||
@@ -292,10 +292,7 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
return ( | |||
<> | |||
<div | |||
className={classNames({ | |||
'tab-view-header': pageType === 'issues' | |||
})}> | |||
<div> | |||
<BoxedTabs | |||
className="big-spacer-top" | |||
onSelect={this.handleSelectTabs} | |||
@@ -303,7 +300,18 @@ export class TabViewer extends React.PureComponent<TabViewerProps, State> { | |||
tabs={tabs} | |||
/> | |||
</div> | |||
<div className="bordered">{tabContent}</div> | |||
<ScreenPositionHelper> | |||
{({ top }) => ( | |||
<div | |||
style={{ | |||
// We substract the footer height with padding (80) and the main layout padding (20) | |||
maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial' | |||
}} | |||
className="bordered display-flex-column"> | |||
<div className="overflow-y-auto spacer">{tabContent}</div> | |||
</div> | |||
)} | |||
</ScreenPositionHelper> | |||
</> | |||
); | |||
} |
@@ -27,11 +27,3 @@ | |||
.education-principles h3:first-child { | |||
margin-top: 0px; | |||
} | |||
.tab-view-header { | |||
z-index: 100; | |||
position: sticky; | |||
top: 118px; | |||
background-color: white; | |||
padding-top: 20px; | |||
} |
@@ -826,7 +826,6 @@ issue.comment.posted_on=Comment posted on | |||
issue.comment.edit=Edit comment | |||
issue.comment.delete=Delete comment | |||
issue.comment.delete_confirm_message=Do you want to delete this comment? | |||
issue.get_permalink=Get Permalink | |||
issue.manual_vulnerability=Manual | |||
issue.manual_vulnerability.description=This Vulnerability was created from a Security Hotspot and has its own issue workflow. | |||
issue.rule_details=Rule Details |