]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16538 On selecting issue, scrolling to the issue message box in code panel
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 9 Aug 2022 13:04:16 +0000 (15:04 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 10 Aug 2022 10:04:06 +0000 (10:04 +0000)
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx
server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
server/sonar-web/src/main/js/components/issue/IssueView.tsx
server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap

index d6038b1bea34fb9ced40ff133c905e0273564e7b..ee79ab270591cceffea76750f54632f5d2568d67 100644 (file)
@@ -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>
+  );
 }
index e43a9a96465fec8bc0843b6f3f133f369fd2afb4..2a29aa7ea464a7463ec9712e817dde5e95301122 100644 (file)
@@ -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()}
index 44f2305d62393e55adb793f0a099dfcd4b82a8a9..17f3c6d788756b80600eb04780976b0637c5a1f7 100644 (file)
@@ -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>
index 6f1068173166bfa5225d341d1c84b062a3948686..892849613c1934cb66819aa96116c0b067df9d6d 100644 (file)
@@ -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
index b1ef51e80a0381b40f8f3ebaebbc6169374211c3..77588f5fe94196d4cebf94fd809cca23df018ca0 100644 (file)
@@ -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>
           );
index 30ca9f59af4151052917dbe7c4c847bbbf1de121..57e292a25d21599df0a6b813bff19b7c1aa8f2bd 100644 (file)
@@ -294,6 +294,7 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']
     <ComponentSourceSnippetGroupViewer
       branchLike={mockMainBranch()}
       highlightedLocationMessage={{ index: 0, text: '' }}
+      selectedLocationIndex={0}
       isLastOccurenceOfPrimaryComponent={true}
       issue={mockIssue()}
       issuesByLine={{}}
index 11d711d642240ee816614e5ba4a883cf511574d3..22ba8be364b8d049b3592122928aebf1667c9f36 100644 (file)
@@ -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}
index a24c0bfedac0ba9acadf918fd177e3ed17ba7299..cceb592fb354a2704114af12a13240264fbc7bfb 100644 (file)
@@ -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>
+    );
+  }
 }
index 71224db95a3d74aba2932ad77b66d77563b8ad71..c5b9aa303b3dc7dffe2bd3231364178c09e1f450 100644 (file)
@@ -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}>
index b6774a7e6d46563aca9d656ccac6d7d7bc6dc00c..c0843b958a6c5c902a1afc842000cd355b1cdd1c 100644 (file)
@@ -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"
 >