Ver código fonte

SONAR-16537 Improve scrolling on issue page

tags/9.6.0.59041
Mathieu Suen 1 ano atrás
pai
commit
abafd63e37
40 arquivos alterados com 192 adições e 778 exclusões
  1. 1
    1
      server/sonar-web/src/main/js/app/styles/style.css
  2. 0
    7
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
  3. 0
    12
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx
  4. 0
    1
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
  5. 2
    1
      server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
  6. 4
    13
      server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
  7. 4
    3
      server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
  8. 3
    9
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
  9. 0
    4
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
  10. 58
    177
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
  11. 5
    40
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
  12. 10
    1
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css
  13. 4
    1
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
  14. 4
    3
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css
  15. 2
    45
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
  16. 1
    178
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
  17. 15
    12
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
  18. 1
    51
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
  19. 2
    4
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
  20. 17
    112
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap
  21. 12
    4
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap
  22. 0
    17
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap
  23. 1
    1
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
  24. 4
    0
      server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
  25. 0
    22
      server/sonar-web/src/main/js/apps/issues/styles.css
  26. 0
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
  27. 0
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
  28. 0
    2
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
  29. 0
    2
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
  30. 0
    1
      server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
  31. 1
    7
      server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
  32. 6
    10
      server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
  33. 9
    6
      server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
  34. 0
    1
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
  35. 5
    5
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
  36. 0
    1
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap
  37. 1
    1
      server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
  38. 20
    12
      server/sonar-web/src/main/js/components/rules/TabViewer.tsx
  39. 0
    8
      server/sonar-web/src/main/js/components/rules/style.css
  40. 0
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/src/main/js/app/styles/style.css Ver arquivo

@@ -122,7 +122,7 @@
margin-bottom: 16px;
}

.rule-desc h2:first-child {
.rule-desc *:first-child {
margin-top: 0;
}


+ 0
- 7
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx Ver arquivo

@@ -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>
) : (

+ 0
- 12
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx Ver arquivo

@@ -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 },

+ 0
- 1
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap Ver arquivo

@@ -102,7 +102,6 @@ exports[`should render correctly for a file 1`] = `
displayIssueLocationsLink={true}
displayLocationMarkers={true}
metricKey="bugs"
scroll={[Function]}
/>
</div>
</div>

+ 2
- 1
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx Ver arquivo

@@ -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 () => {

+ 4
- 13
server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx Ver arquivo

@@ -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}
/>
</>
);

+ 4
- 3
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx Ver arquivo

@@ -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')} />


+ 3
- 9
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx Ver arquivo

@@ -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>

+ 0
- 4
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap Ver arquivo

@@ -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>
`;

+ 58
- 177
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx Ver arquivo

@@ -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>
</>
);
}
}

+ 5
- 40
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx Ver arquivo

@@ -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>
</>
);
}
}

+ 10
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css Ver arquivo

@@ -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;
}

+ 4
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx Ver arquivo

@@ -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">

+ 4
- 3
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css Ver arquivo

@@ -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 {

+ 2
- 45
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx Ver arquivo

@@ -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">

+ 1
- 178
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx Ver arquivo

@@ -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 };
}

+ 15
- 12
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx Ver arquivo

@@ -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}
/>

+ 1
- 51
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx Ver arquivo

@@ -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}
/>
);
}

+ 2
- 4
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap Ver arquivo

@@ -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>
`;

+ 17
- 112
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap Ver arquivo

@@ -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>
`;

+ 12
- 4
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap Ver arquivo

@@ -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"

+ 0
- 17
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap Ver arquivo

@@ -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}
/>

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts Ver arquivo

@@ -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', () => {

+ 4
- 0
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts Ver arquivo

@@ -181,6 +181,10 @@ export function groupLocationsByComponent(
currentGroup.locations.push(loc);
});

if (groups.length === 0) {
groups.push({ locations: [], ...components[issue.component] });
}

return groups;
}


+ 0
- 22
server/sonar-web/src/main/js/apps/issues/styles.css Ver arquivo

@@ -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;
}

+ 0
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx Ver arquivo

@@ -197,7 +197,6 @@ export default function HotspotSnippetContainerRenderer(
renderAdditionalChildInLine={renderHotspotBoxInLine}
renderDuplicationPopup={noop}
snippet={sourceLines}
scroll={getScrollHandler(scrollableRef)}
/>
)}
</DeferredSpinner>

+ 0
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap Ver arquivo

@@ -506,7 +506,6 @@ exports[`should render correctly: with sourcelines 1`] = `
onLocationSelect={[MockFunction]}
renderAdditionalChildInLine={[Function]}
renderDuplicationPopup={[Function]}
scroll={[Function]}
snippet={
Array [
Object {

+ 0
- 2
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx Ver arquivo

@@ -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}

+ 0
- 2
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx Ver arquivo

@@ -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

+ 0
- 1
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx Ver arquivo

@@ -396,7 +396,6 @@ function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
onIssueSelect={jest.fn()}
onLoaded={jest.fn()}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
{...override}
/>
);

+ 1
- 7
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx Ver arquivo

@@ -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>

+ 6
- 10
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx Ver arquivo

@@ -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'
});
}
}


+ 9
- 6
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx Ver arquivo

@@ -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' +

+ 0
- 1
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx Ver arquivo

@@ -89,7 +89,6 @@ function shallowRender(props: Partial<Line['props']> = {}) {
openIssues={false}
previousLine={undefined}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
secondaryIssueLocations={[]}
{...props}
/>

+ 5
- 5
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx Ver arquivo

@@ -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();
});


+ 0
- 1
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap Ver arquivo

@@ -58,7 +58,6 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = `
}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
scroll={[MockFunction]}
secondaryIssueLocations={Array []}
/>
</tr>

+ 1
- 1
server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx Ver arquivo

@@ -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">

+ 20
- 12
server/sonar-web/src/main/js/components/rules/TabViewer.tsx Ver arquivo

@@ -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>
</>
);
}

+ 0
- 8
server/sonar-web/src/main/js/components/rules/style.css Ver arquivo

@@ -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;
}

+ 0
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver arquivo

@@ -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

Carregando…
Cancelar
Salvar