aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2022-07-27 00:07:43 +0200
committersonartech <sonartech@sonarsource.com>2022-07-29 20:03:14 +0000
commitabafd63e37cf839bd31935f7ae360b0c1d835e5e (patch)
tree93faac57bbfad1bdb4c3e3137829c3dfd7158630
parent52b86027c2dcba6dcb773e24e12286177958b59a (diff)
downloadsonarqube-abafd63e37cf839bd31935f7ae360b0c1d835e5e.tar.gz
sonarqube-abafd63e37cf839bd31935f7ae360b0c1d835e5e.zip
SONAR-16537 Improve scrolling on issue page
-rw-r--r--server/sonar-web/src/main/js/app/styles/style.css2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx235
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx179
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap129
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css22
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx15
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/rules/TabViewer.tsx32
-rw-r--r--server/sonar-web/src/main/js/components/rules/style.css8
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
40 files changed, 192 insertions, 778 deletions
diff --git a/server/sonar-web/src/main/js/app/styles/style.css b/server/sonar-web/src/main/js/app/styles/style.css
index 227a519bc4c..cb1136bd639 100644
--- a/server/sonar-web/src/main/js/app/styles/style.css
+++ b/server/sonar-web/src/main/js/app/styles/style.css
@@ -122,7 +122,7 @@
margin-bottom: 16px;
}
-.rule-desc h2:first-child {
+.rule-desc *:first-child {
margin-top: 0;
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
index 37ab7387f28..744ac1dc509 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
@@ -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>
) : (
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx
index 861f7d075ac..de6447408c5 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx
@@ -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 },
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
index 9335ebc7657..e81c83983dc 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
@@ -102,7 +102,6 @@ exports[`should render correctly for a file 1`] = `
displayIssueLocationsLink={true}
displayLocationMarkers={true}
metricKey="bugs"
- scroll={[Function]}
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
index 8df2ddb0af6..858ff36453d 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
@@ -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 () => {
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
index 01112b42f09..95b4a3a3cca 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
@@ -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}
/>
</>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
index 79f773c9221..e2095c58daa 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
@@ -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')} />
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
index a91db3a73f0..9bbdaa767cf 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
index 395562973cf..f4a5d01a214 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
index 08a9d12d6aa..bbdff07c952 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
@@ -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>
+ </>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
index 0457ce290df..85ab58c3812 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
@@ -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>
+ </>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css
index 4f70cddf426..b06083adb3d 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
index 69b4465cbe1..7bbee7fcc1e 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
@@ -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">
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css
index b3b151ce897..69e8cc7756f 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css
@@ -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 {
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
index 9981db89700..1e21f308b1a 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
@@ -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">
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
index 12d34757886..0d92fa6b622 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
@@ -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 };
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
index 9f6472f08e3..1425e69291c 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
@@ -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}
/>
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
index 614c63fc735..1a3c685d6f8 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
@@ -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}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
index 9825cb22d5c..98d86611f24 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap
index f587717fbbc..489fd62f4f0 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap
index 0b6e42a0fb3..fb3dd212fc1 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap
index 085138cd9cf..0ee54422c94 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap
@@ -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}
/>
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
index dadf1c31958..bc9896d68a6 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
@@ -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', () => {
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
index d41b61cbe0d..7476c471fb1 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
@@ -181,6 +181,10 @@ export function groupLocationsByComponent(
currentGroup.locations.push(loc);
});
+ if (groups.length === 0) {
+ groups.push({ locations: [], ...components[issue.component] });
+ }
+
return groups;
}
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
index 44e4a054e81..f2bcbe98c86 100644
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
index e7c24596abc..3c8f12e0557 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
@@ -197,7 +197,6 @@ export default function HotspotSnippetContainerRenderer(
renderAdditionalChildInLine={renderHotspotBoxInLine}
renderDuplicationPopup={noop}
snippet={sourceLines}
- scroll={getScrollHandler(scrollableRef)}
/>
)}
</DeferredSpinner>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
index cb673a1e945..135d84e9c54 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
@@ -506,7 +506,6 @@ exports[`should render correctly: with sourcelines 1`] = `
onLocationSelect={[MockFunction]}
renderAdditionalChildInLine={[Function]}
renderDuplicationPopup={[Function]}
- scroll={[Function]}
snippet={
Array [
Object {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
index 7f24e85d95d..e4944df6251 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
index 2d681daf14f..0a42eadc3c2 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
@@ -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
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
index 0039a2b5d3d..df8ed428928 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
@@ -396,7 +396,6 @@ function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
onIssueSelect={jest.fn()}
onLoaded={jest.fn()}
onLocationSelect={jest.fn()}
- scroll={jest.fn()}
{...override}
/>
);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
index e8a060ba501..bafb2d54376 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
index f9216d65d18..de2f6ebc605 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
@@ -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'
+ });
}
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
index 6f9f74977b6..ee151802289 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
@@ -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' +
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
index 82add319eba..5d1ecb223e9 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
@@ -89,7 +89,6 @@ function shallowRender(props: Partial<Line['props']> = {}) {
openIssues={false}
previousLine={undefined}
renderDuplicationPopup={jest.fn()}
- scroll={jest.fn()}
secondaryIssueLocations={[]}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
index 68bb9399d3f..19e84a05c0a 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap
index d848cd62fe0..5f0d5e26add 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap
@@ -58,7 +58,6 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = `
}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
- scroll={[MockFunction]}
secondaryIssueLocations={Array []}
/>
</tr>
diff --git a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
index dc599849bb4..ff25f1a316f 100644
--- a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
@@ -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">
diff --git a/server/sonar-web/src/main/js/components/rules/TabViewer.tsx b/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
index 22651ca8b75..92c90d1e4e4 100644
--- a/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
+++ b/server/sonar-web/src/main/js/components/rules/TabViewer.tsx
@@ -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>
</>
);
}
diff --git a/server/sonar-web/src/main/js/components/rules/style.css b/server/sonar-web/src/main/js/components/rules/style.css
index 280b317fe65..dfe4762e8d0 100644
--- a/server/sonar-web/src/main/js/components/rules/style.css
+++ b/server/sonar-web/src/main/js/components/rules/style.css
@@ -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;
-}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 0b0167d6bd3..34094e1bfa8 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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