diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-07-27 00:07:43 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-29 20:03:14 +0000 |
commit | abafd63e37cf839bd31935f7ae360b0c1d835e5e (patch) | |
tree | 93faac57bbfad1bdb4c3e3137829c3dfd7158630 | |
parent | 52b86027c2dcba6dcb773e24e12286177958b59a (diff) | |
download | sonarqube-abafd63e37cf839bd31935f7ae360b0c1d835e5e.tar.gz sonarqube-abafd63e37cf839bd31935f7ae360b0c1d835e5e.zip |
SONAR-16537 Improve scrolling on issue page
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 |