]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18385 Improve focus state on issue box
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 3 Feb 2023 10:10:58 +0000 (11:10 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 7 Feb 2023 20:02:53 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx
server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/styles.css
server/sonar-web/src/main/js/components/issue/Issue.css
server/sonar-web/src/main/js/components/issue/IssueView.tsx
server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap
server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx

index 19862799f17bd9c10d44e743b3a86a29947b8498..a49bb1d9425064a62efae8784bf5e0009491db00 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { groupBy } from 'lodash';
 import * as React from 'react';
 import { BranchLike } from '../../../types/branch-like';
 import { Component, Issue } from '../../../types/types';
 import { Query } from '../utils';
+import ComponentBreadcrumbs from './ComponentBreadcrumbs';
 import ListItem from './ListItem';
 
 interface Props {
@@ -58,8 +60,38 @@ export default class IssuesList extends React.PureComponent<Props, State> {
     }
   }
 
+  renderIssueComponentList = (issues: Issue[], index: number) => {
+    const { branchLike, checked, component, openPopup, selectedIssue } = this.props;
+    return (
+      <React.Fragment key={index}>
+        <li>
+          <div className="issues-workspace-list-component note">
+            <ComponentBreadcrumbs component={component} issue={issues[0]} />
+          </div>
+        </li>
+        <ul>
+          {issues.map((issue) => (
+            <ListItem
+              branchLike={branchLike}
+              checked={checked.includes(issue.key)}
+              issue={issue}
+              key={issue.key}
+              onChange={this.props.onIssueChange}
+              onCheck={this.props.onIssueCheck}
+              onClick={this.props.onIssueClick}
+              onFilterChange={this.props.onFilterChange}
+              onPopupToggle={this.props.onPopupToggle}
+              openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : undefined}
+              selected={selectedIssue != null && selectedIssue.key === issue.key}
+            />
+          ))}
+        </ul>
+      </React.Fragment>
+    );
+  };
+
   render() {
-    const { branchLike, checked, component, issues, openPopup, selectedIssue } = this.props;
+    const { issues } = this.props;
     const { prerender } = this.state;
 
     if (prerender) {
@@ -70,26 +102,8 @@ export default class IssuesList extends React.PureComponent<Props, State> {
       );
     }
 
-    return (
-      <ul>
-        {issues.map((issue, index) => (
-          <ListItem
-            branchLike={branchLike}
-            checked={checked.includes(issue.key)}
-            component={component}
-            issue={issue}
-            key={issue.key}
-            onChange={this.props.onIssueChange}
-            onCheck={this.props.onIssueCheck}
-            onClick={this.props.onIssueClick}
-            onFilterChange={this.props.onFilterChange}
-            onPopupToggle={this.props.onPopupToggle}
-            openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : undefined}
-            previousIssue={index > 0 ? issues[index - 1] : undefined}
-            selected={selectedIssue != null && selectedIssue.key === issue.key}
-          />
-        ))}
-      </ul>
-    );
+    const issuesByComponent = groupBy(issues, (issue) => `(${issue.component} : ${issue.branch})`);
+
+    return <ul>{Object.values(issuesByComponent).map(this.renderIssueComponentList)}</ul>;
   }
 }
index 76c3601bab06b3e401c195fcc5b46660736bcaa2..78107922aacc17c61fd42cd38e8e1bb6fca39328 100644 (file)
 import * as React from 'react';
 import Issue from '../../../components/issue/Issue';
 import { BranchLike } from '../../../types/branch-like';
-import { Component, Issue as TypeIssue } from '../../../types/types';
+import { Issue as TypeIssue } from '../../../types/types';
 import { Query } from '../utils';
-import ComponentBreadcrumbs from './ComponentBreadcrumbs';
 
 interface Props {
   branchLike: BranchLike | undefined;
   checked: boolean;
-  component: Component | undefined;
   issue: TypeIssue;
   onChange: (issue: TypeIssue) => void;
   onCheck: ((issueKey: string) => void) | undefined;
@@ -35,7 +33,6 @@ interface Props {
   onFilterChange: (changes: Partial<Query>) => void;
   onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
   openPopup: string | undefined;
-  previousIssue: TypeIssue | undefined;
   selected: boolean;
 }
 
@@ -102,20 +99,10 @@ export default class ListItem extends React.PureComponent<Props> {
   };
 
   render() {
-    const { branchLike, component, issue, previousIssue } = this.props;
-
-    const displayComponent =
-      !previousIssue ||
-      previousIssue.component !== issue.component ||
-      previousIssue.branch !== issue.branch;
+    const { branchLike, issue } = this.props;
 
     return (
       <li className="issues-workspace-list-item" ref={(node) => (this.nodeRef = node)}>
-        {displayComponent && (
-          <div className="issues-workspace-list-component note">
-            <ComponentBreadcrumbs component={component} issue={this.props.issue} />
-          </div>
-        )}
         <Issue
           branchLike={branchLike}
           checked={this.props.checked}
index 4355edb7269c5a2d0549d1cf9e795d81fdc37b9d..1be879f64fa7e63c61152575a155573614c7856d 100644 (file)
@@ -20,7 +20,6 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockBranch } from '../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockIssue } from '../../../../helpers/testMocks';
 import ListItem from '../ListItem';
 
@@ -34,7 +33,6 @@ function shallowRender(props: Partial<ListItem['props']> = {}) {
     <ListItem
       branchLike={mockBranch()}
       checked={false}
-      component={mockComponent()}
       issue={mockIssue()}
       onChange={jest.fn()}
       onCheck={jest.fn()}
@@ -42,7 +40,6 @@ function shallowRender(props: Partial<ListItem['props']> = {}) {
       onFilterChange={jest.fn()}
       onPopupToggle={jest.fn()}
       openPopup={undefined}
-      previousIssue={mockIssue(false, { branch: 'branch-8.7' })}
       selected={false}
       {...props}
     />
index d107ddc2693b6d3a1ab4044874d20b35019d0cda..896ba138392c4d48bc926cce4b544c4aa8a83d9a 100644 (file)
@@ -10,121 +10,131 @@ exports[`should render correctly 1`] = `
 
 exports[`should render correctly 2`] = `
 <ul>
-  <ListItem
-    checked={false}
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
+  <li>
+    <div
+      className="issues-workspace-list-component note"
+    >
+      <ComponentBreadcrumbs
+        issue={
+          {
+            "actions": [],
+            "component": "main.js",
+            "componentEnabled": true,
+            "componentLongName": "main.js",
+            "componentQualifier": "FIL",
+            "componentUuid": "foo1234",
+            "creationDate": "2017-03-01T09:36:01+0100",
+            "flows": [],
+            "flowsWithType": [],
+            "key": "AVsae-CQS-9G3txfbFN2",
+            "line": 25,
+            "message": "Reduce the number of conditional operators (4) used in the expression",
+            "project": "myproject",
+            "projectKey": "foo",
+            "projectName": "Foo",
+            "rule": "javascript:S1067",
+            "ruleName": "foo",
+            "secondaryLocations": [],
+            "severity": "MAJOR",
+            "status": "OPEN",
+            "textRange": {
+              "endLine": 26,
+              "endOffset": 15,
+              "startLine": 25,
+              "startOffset": 0,
+            },
+            "transitions": [],
+            "type": "BUG",
+          }
+        }
+      />
+    </div>
+  </li>
+  <ul>
+    <ListItem
+      checked={false}
+      issue={
+        {
+          "actions": [],
+          "component": "main.js",
+          "componentEnabled": true,
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": [],
+          "flowsWithType": [],
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": [],
+          "type": "BUG",
+        }
       }
-    }
-    key="AVsae-CQS-9G3txfbFN2"
-    onChange={[MockFunction]}
-    onCheck={[MockFunction]}
-    onClick={[MockFunction]}
-    onFilterChange={[MockFunction]}
-    onPopupToggle={[MockFunction]}
-    selected={false}
-  />
-  <ListItem
-    checked={false}
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN3",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
-      }
-    }
-    key="AVsae-CQS-9G3txfbFN3"
-    onChange={[MockFunction]}
-    onCheck={[MockFunction]}
-    onClick={[MockFunction]}
-    onFilterChange={[MockFunction]}
-    onPopupToggle={[MockFunction]}
-    previousIssue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
+      key="AVsae-CQS-9G3txfbFN2"
+      onChange={[MockFunction]}
+      onCheck={[MockFunction]}
+      onClick={[MockFunction]}
+      onFilterChange={[MockFunction]}
+      onPopupToggle={[MockFunction]}
+      selected={false}
+    />
+    <ListItem
+      checked={false}
+      issue={
+        {
+          "actions": [],
+          "component": "main.js",
+          "componentEnabled": true,
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": [],
+          "flowsWithType": [],
+          "key": "AVsae-CQS-9G3txfbFN3",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": [],
+          "type": "BUG",
+        }
       }
-    }
-    selected={false}
-  />
+      key="AVsae-CQS-9G3txfbFN3"
+      onChange={[MockFunction]}
+      onCheck={[MockFunction]}
+      onClick={[MockFunction]}
+      onFilterChange={[MockFunction]}
+      onPopupToggle={[MockFunction]}
+      selected={false}
+    />
+  </ul>
 </ul>
 `;
index 2924aed543c1388f0844f648ab7575f2bda8ddc8..d8cefe95f1bb3b7ee2a407b4cfbe63f21f639934 100644 (file)
@@ -4,66 +4,6 @@ exports[`should render correctly 1`] = `
 <li
   className="issues-workspace-list-item"
 >
-  <div
-    className="issues-workspace-list-component note"
-  >
-    <ComponentBreadcrumbs
-      component={
-        {
-          "breadcrumbs": [],
-          "key": "my-project",
-          "name": "MyProject",
-          "qualifier": "TRK",
-          "qualityGate": {
-            "isDefault": true,
-            "key": "30",
-            "name": "Sonar way",
-          },
-          "qualityProfiles": [
-            {
-              "deleted": false,
-              "key": "my-qp",
-              "language": "ts",
-              "name": "Sonar way",
-            },
-          ],
-          "tags": [],
-        }
-      }
-      issue={
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN2",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        }
-      }
-    />
-  </div>
   <Issue
     branchLike={
       {
index bc12ffb35a638505a305ba716a311029751581dd..822e84ba537d97cd15f68b6de71b60a1c0ca821b 100644 (file)
 }
 
 .issues-workspace-list-component {
-  padding: 10px 0 6px;
+  padding: 15px 0 6px;
 }
 
 .issues-workspace-list-item + .issues-workspace-list-item {
   margin-top: 5px;
 }
 
-.issues-workspace-list-component + .issues-workspace-list-item {
-  margin-top: 10px;
-}
-
-.issues-workspace-list-item:first-child .issues-workspace-list-component {
+li:first-child .issues-workspace-list-component {
   padding-top: 0;
 }
 
-.issues-workspace-list-component + .issues-workspace-list-item {
-  margin-top: 0;
-}
-
 .issues-predefined-periods {
   display: flex;
 }
index 6a6731bb901b0cace7ebf4b0055db8947d4a4fc3..b9fd5495faafe88a48533913666cf7237db2616a 100644 (file)
   white-space: nowrap;
 }
 
+.issue-message .button-plain {
+  line-height: 18px;
+  font-size: var(--baseFontSize);
+  font-weight: 600;
+  text-align: left;
+}
+
 .issue-message {
   flex: 1;
   padding-left: var(--gridSize);
 }
 
 .issue-message-box.secondary-issue:hover,
-.issue:focus-within,
 .issue:hover {
   border: 2px dashed var(--blue);
   outline: 0;
index 9de1be2a607c23970ea08ab4f56e8e75c0537997..09a2ae2f4bba3b3088216b81b278a0f9f2982020 100644 (file)
@@ -53,9 +53,15 @@ export default class IssueView extends React.PureComponent<Props> {
     }
   };
 
-  handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
+  handleBoxClick = (event: React.MouseEvent<HTMLDivElement>) => {
     if (!isClickable(event.target as HTMLElement) && this.props.onClick) {
       event.preventDefault();
+      this.handleDetailClick();
+    }
+  };
+
+  handleDetailClick = () => {
+    if (this.props.onClick) {
       this.props.onClick(this.props.issue.key);
     }
   };
@@ -91,7 +97,7 @@ export default class IssueView extends React.PureComponent<Props> {
     return (
       <div
         className={issueClass}
-        onClick={this.handleClick}
+        onClick={this.handleBoxClick}
         role="region"
         aria-label={issue.message}
       >
@@ -106,6 +112,7 @@ export default class IssueView extends React.PureComponent<Props> {
         )}
         <IssueTitleBar
           branchLike={branchLike}
+          onClick={this.handleDetailClick}
           currentPopup={currentPopup}
           displayLocationsCount={displayLocationsCount}
           displayLocationsLink={displayLocationsLink}
index ffeac450c75d13d642dc8680d9798c505e18ac32..849ce18835131f512fa668cec87276aae9b2678f 100644 (file)
@@ -40,6 +40,7 @@ exports[`should render hotspots correctly 1`] = `
         "type": "SECURITY_HOTSPOT",
       }
     }
+    onClick={[Function]}
     togglePopup={[MockFunction]}
   />
   <IssueActionsBar
@@ -137,6 +138,7 @@ exports[`should render issues correctly 1`] = `
         "type": "BUG",
       }
     }
+    onClick={[Function]}
     togglePopup={[MockFunction]}
   />
   <IssueActionsBar
index 9e7f9049ddb897c87cee3cd4689d56ba3cba256b..09c9f294967681a13dbd98372f1d9db1ef58d06b 100644 (file)
@@ -25,10 +25,12 @@ import { BranchLike } from '../../../types/branch-like';
 import { RuleStatus } from '../../../types/rules';
 import { Issue } from '../../../types/types';
 import Link from '../../common/Link';
+import { ButtonPlain } from '../../controls/buttons';
 import { IssueMessageHighlighting } from '../IssueMessageHighlighting';
 import IssueMessageTags from './IssueMessageTags';
 
 export interface IssueMessageProps {
+  onClick?: () => void;
   issue: Issue;
   branchLike?: BranchLike;
   displayWhyIsThisAnIssue?: boolean;
@@ -50,9 +52,15 @@ export default function IssueMessage(props: IssueMessageProps) {
   return (
     <>
       <div className="display-inline-flex-center issue-message break-word">
-        <span className="spacer-right">
-          <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
-        </span>
+        {props.onClick ? (
+          <ButtonPlain preventDefault={true} className="spacer-right" onClick={props.onClick}>
+            <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
+          </ButtonPlain>
+        ) : (
+          <span className="spacer-right">
+            <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
+          </span>
+        )}
         <IssueMessageTags
           engine={externalRuleEngine}
           quickFixAvailable={quickFixAvailable}
index 8e63e16a49446331fd287541f0e97a7096f853f3..881e8b349244e9c2f14b233d8f01865c7915d23a 100644 (file)
@@ -34,6 +34,7 @@ import SimilarIssuesFilter from './SimilarIssuesFilter';
 
 export interface IssueTitleBarProps {
   branchLike?: BranchLike;
+  onClick?: () => void;
   currentPopup?: string;
   displayWhyIsThisAnIssue?: boolean;
   displayLocationsCount?: boolean;
@@ -78,6 +79,7 @@ export default function IssueTitleBar(props: IssueTitleBarProps) {
         issue={issue}
         branchLike={props.branchLike}
         displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
+        onClick={props.onClick}
       />
       <div className="issue-row-meta">
         <div className="issue-meta-list">