diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2022-08-09 15:04:16 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-08-10 10:04:06 +0000 |
commit | 1ef2bbc5841cd248c44058681d548c146ad7bffa (patch) | |
tree | ff8ccbb3c00746bf78bbeabd57027aa73d7a2f63 /server/sonar-web/src | |
parent | 51f2a5f82dab35a96ccdcee1d7e891af7f3c1394 (diff) | |
download | sonarqube-1ef2bbc5841cd248c44058681d548c146ad7bffa.tar.gz sonarqube-1ef2bbc5841cd248c44058681d548c146ad7bffa.zip |
SONAR-16538 On selecting issue, scrolling to the issue message box in code panel
Diffstat (limited to 'server/sonar-web/src')
10 files changed, 108 insertions, 100 deletions
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 d6038b1bea3..ee79ab27059 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 @@ -23,7 +23,7 @@ import { Issue } from '../../../types/types'; import CrossComponentSourceViewer from '../crossComponentSourceViewer/CrossComponentSourceViewer'; import { getLocations, getSelectedLocation } from '../utils'; -interface Props { +export interface IssuesSourceViewerProps { branchLike: BranchLike | undefined; issues: Issue[]; locationsNavigator: boolean; @@ -34,68 +34,39 @@ interface Props { selectedLocationIndex: number | undefined; } -export default class IssuesSourceViewer extends React.PureComponent<Props> { - node?: HTMLElement | null; +export default function IssuesSourceViewer(props: IssuesSourceViewerProps) { + const { + openIssue, + selectedFlowIndex, + selectedLocationIndex, + locationsNavigator, + branchLike, + issues + } = props; - componentDidUpdate(prevProps: Props) { - const { openIssue, selectedLocationIndex } = this.props; + const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => { + loc.index = index; + return loc; + }); + const selectedLocation = getSelectedLocation(openIssue, selectedFlowIndex, selectedLocationIndex); - // Scroll back to the issue when the selected location is set to -1 - const shouldScrollBackToIssue = - selectedLocationIndex === -1 && selectedLocationIndex !== prevProps.selectedLocationIndex; - if ( - prevProps.openIssue.component === openIssue.component && - (prevProps.openIssue !== openIssue || shouldScrollBackToIssue) - ) { - this.scrollToIssue(); - } - } - - scrollToIssue = () => { - if (this.node) { - const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`); - if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); - } - } - }; - - handleLoaded = () => { - this.scrollToIssue(); - }; - - render() { - const { openIssue, selectedFlowIndex, selectedLocationIndex } = this.props; - - const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => { - loc.index = index; - return loc; - }); - const selectedLocation = getSelectedLocation( - openIssue, - selectedFlowIndex, - selectedLocationIndex - ); - - const highlightedLocationMessage = - this.props.locationsNavigator && selectedLocationIndex !== undefined - ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg } - : undefined; - - return ( - <div ref={node => (this.node = node)}> - <CrossComponentSourceViewer - branchLike={this.props.branchLike} - highlightedLocationMessage={highlightedLocationMessage} - issue={openIssue} - issues={this.props.issues} - locations={locations} - onIssueSelect={this.props.onIssueSelect} - onLoaded={this.handleLoaded} - onLocationSelect={this.props.onLocationSelect} - selectedFlowIndex={selectedFlowIndex} - /> - </div> - ); - } + const highlightedLocationMessage = + locationsNavigator && selectedLocationIndex !== undefined + ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg } + : undefined; + return ( + <div> + <CrossComponentSourceViewer + branchLike={branchLike} + highlightedLocationMessage={highlightedLocationMessage} + issue={openIssue} + issues={issues} + locations={locations} + onIssueSelect={props.onIssueSelect} + onLocationSelect={props.onLocationSelect} + selectedFlowIndex={selectedFlowIndex} + selectedLocationIndex={selectedLocationIndex} + /> + </div> + ); } diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx index e43a9a96465..2a29aa7ea46 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; -import IssuesSourceViewer from '../IssuesSourceViewer'; +import IssuesSourceViewer, { IssuesSourceViewerProps } from '../IssuesSourceViewer'; it('should render SourceViewer correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); @@ -64,7 +64,7 @@ it('should render CrossComponentSourceViewer correctly', () => { ).toMatchSnapshot(); }); -function shallowRender(props: Partial<IssuesSourceViewer['props']> = {}) { +function shallowRender(props: Partial<IssuesSourceViewerProps> = {}) { return shallow( <IssuesSourceViewer branchLike={mockMainBranch()} 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 44f2305d623..17f3c6d7887 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 @@ -209,7 +209,6 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = ` ] } onIssueSelect={[MockFunction]} - onLoaded={[Function]} onLocationSelect={[MockFunction]} /> </div> @@ -444,7 +443,6 @@ exports[`should render SourceViewer correctly: all secondary locations on same l ] } onIssueSelect={[MockFunction]} - onLoaded={[Function]} onLocationSelect={[MockFunction]} /> </div> @@ -525,7 +523,6 @@ exports[`should render SourceViewer correctly: default 1`] = ` } locations={Array []} onIssueSelect={[MockFunction]} - onLoaded={[Function]} onLocationSelect={[MockFunction]} /> </div> @@ -720,7 +717,6 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = ` ] } onIssueSelect={[MockFunction]} - onLoaded={[Function]} onLocationSelect={[MockFunction]} /> </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 6f106817316..892849613c1 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 @@ -67,6 +67,7 @@ interface Props { line: number ) => React.ReactNode; snippetGroup: SnippetGroup; + selectedLocationIndex: number | undefined; } interface State { @@ -205,7 +206,13 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone }; renderIssuesList = (line: SourceLine) => { - const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props; + const { + isLastOccurenceOfPrimaryComponent, + issue, + issuesByLine, + snippetGroup, + selectedLocationIndex + } = this.props; const locations = issue.component === snippetGroup.component.key && issue.textRange !== undefined ? locationsByLine([issue]) @@ -225,6 +232,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone key={issueToDisplay.key} issue={issueToDisplay} onClick={this.props.onIssueSelect} + selectedLocationIndex={selectedLocationIndex} /> ))} </div> @@ -238,7 +246,8 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone isLastOccurenceOfPrimaryComponent, issue, lastSnippetGroup, - snippetGroup + snippetGroup, + selectedLocationIndex } = this.props; const { additionalLines, loading, snippets } = this.state; const locations = @@ -271,7 +280,12 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone /> {issue.component === snippetGroup.component.key && issue.textRange === undefined && ( - <IssueMessageBox selected={true} issue={issue} onClick={this.props.onIssueSelect} /> + <IssueMessageBox + selected={true} + issue={issue} + onClick={this.props.onIssueSelect} + selectedLocationIndex={selectedLocationIndex} + /> )} {snippetLines.map((snippet, index) => ( <SnippetViewer 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 b1ef51e80a0..77588f5fe94 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 @@ -60,9 +60,9 @@ interface Props { issues: Issue[]; locations: FlowLocation[]; onIssueSelect: (issueKey: string) => void; - onLoaded?: () => void; onLocationSelect: (index: number) => void; selectedFlowIndex: number | undefined; + selectedLocationIndex: number | undefined; } interface State { @@ -145,9 +145,6 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop components, loading: false }); - if (this.props.onLoaded) { - this.props.onLoaded(); - } } } catch (response) { const rsp = response as Response; @@ -187,6 +184,7 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop render() { const { loading, notAccessible } = this.state; + const { selectedLocationIndex } = this.props; if (loading) { return ( @@ -240,6 +238,7 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop onLocationSelect={this.props.onLocationSelect} renderDuplicationPopup={this.renderDuplicationPopup} snippetGroup={snippetGroup} + selectedLocationIndex={selectedLocationIndex} /> </SourceViewerContext.Provider> ); 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 30ca9f59af4..57e292a25d2 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 @@ -294,6 +294,7 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props'] <ComponentSourceSnippetGroupViewer branchLike={mockMainBranch()} highlightedLocationMessage={{ index: 0, text: '' }} + selectedLocationIndex={0} isLastOccurenceOfPrimaryComponent={true} issue={mockIssue()} issuesByLine={{}} 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 11d711d6422..22ba8be364b 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 @@ -116,6 +116,7 @@ function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {}) <CrossComponentSourceViewer branchLike={undefined} highlightedLocationMessage={undefined} + selectedLocationIndex={undefined} issue={mockIssue(true, { key: '1', component: 'project:main.js', @@ -123,7 +124,6 @@ function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {}) })} issues={[]} locations={[mockFlowLocation({ component: 'project:main.js' })]} - onLoaded={jest.fn()} onIssueSelect={jest.fn()} onLocationSelect={jest.fn()} selectedFlowIndex={0} diff --git a/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx b/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx index a24c0bfedac..cceb592fb35 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx +++ b/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx @@ -28,26 +28,56 @@ export interface IssueMessageBoxProps { selected: boolean; issue: Issue; onClick: (issueKey: string) => void; + selectedLocationIndex?: number; } -export default function IssueMessageBox(props: IssueMessageBoxProps) { - const { issue, selected } = props; - return ( - <div - className={classNames('issue-message-box display-flex-row display-flex-center padded-right', { - 'selected big-padded-top big-padded-bottom': selected, - 'secondary-issue padded-top padded-bottom': !selected - })} - key={issue.key} - onClick={() => props.onClick(issue.key)} - role="region" - aria-label={issue.message}> - <IssueTypeIcon - className="big-spacer-right spacer-left" - fill={colors.baseFontColor} - query={issue.type} - /> - {issue.message} - </div> - ); +export default class IssueMessageBox extends React.Component<IssueMessageBoxProps> { + messageBoxRef: React.RefObject<HTMLDivElement> = React.createRef(); + + componentDidMount() { + if (this.props.selected && this.messageBoxRef.current) { + this.messageBoxRef.current.scrollIntoView({ + block: 'center' + }); + } + } + + componentDidUpdate(prevProps: IssueMessageBoxProps) { + if ( + this.messageBoxRef.current && + ((prevProps.selected !== this.props.selected && this.props.selected) || + (prevProps.selectedLocationIndex !== this.props.selectedLocationIndex && + this.props.selectedLocationIndex === -1)) + ) { + this.messageBoxRef.current.scrollIntoView({ + block: 'center' + }); + } + } + + render() { + const { issue, selected } = this.props; + return ( + <div + className={classNames( + 'issue-message-box display-flex-row display-flex-center padded-right', + { + 'selected big-padded-top big-padded-bottom': selected, + 'secondary-issue padded-top padded-bottom': !selected + } + )} + key={issue.key} + onClick={() => this.props.onClick(issue.key)} + role="region" + ref={this.messageBoxRef} + aria-label={issue.message}> + <IssueTypeIcon + className="big-spacer-right spacer-left" + fill={colors.baseFontColor} + query={issue.type} + /> + {issue.message} + </div> + ); + } } diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/IssueView.tsx index 71224db95a3..c5b9aa303b3 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.tsx +++ b/server/sonar-web/src/main/js/components/issue/IssueView.tsx @@ -91,7 +91,6 @@ export default class IssueView extends React.PureComponent<Props> { return ( <div className={issueClass} - data-issue={issue.key} onClick={this.handleClick} role="region" aria-label={issue.message}> diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap index b6774a7e6d4..c0843b958a6 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap @@ -4,7 +4,6 @@ exports[`should render hotspots correctly 1`] = ` <div aria-label="Reduce the number of conditional operators (4) used in the expression" className="issue hotspot selected" - data-issue="AVsae-CQS-9G3txfbFN2" onClick={[Function]} role="region" > @@ -84,7 +83,6 @@ exports[`should render issues correctly 1`] = ` <div aria-label="Reduce the number of conditional operators (4) used in the expression" className="issue selected" - data-issue="AVsae-CQS-9G3txfbFN2" onClick={[Function]} role="region" > |