diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2023-02-03 11:10:58 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-02-07 20:02:53 +0000 |
commit | b51a3bd354c6739b3a82e50b163992384e7b640d (patch) | |
tree | 67b9ec116de2c61a5cf8312dcff8caf7058c2b07 /server | |
parent | 204e3d4e66c441b2fd0fa1efec72f5001d9e3f23 (diff) | |
download | sonarqube-b51a3bd354c6739b3a82e50b163992384e7b640d.tar.gz sonarqube-b51a3bd354c6739b3a82e50b163992384e7b640d.zip |
SONAR-18385 Improve focus state on issue box
Diffstat (limited to 'server')
11 files changed, 195 insertions, 230 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx index 19862799f17..a49bb1d9425 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx @@ -17,10 +17,12 @@ * 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>; } } diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx index 76c3601bab0..78107922aac 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx @@ -20,14 +20,12 @@ 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} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx index 4355edb7269..1be879f64fa 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap index d107ddc2693..896ba138392 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap @@ -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> `; diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap index 2924aed543c..d8cefe95f1b 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap @@ -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={ { 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 bc12ffb35a6..822e84ba537 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -196,25 +196,17 @@ } .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; } diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css index 6a6731bb901..b9fd5495faa 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.css +++ b/server/sonar-web/src/main/js/components/issue/Issue.css @@ -58,6 +58,13 @@ 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); @@ -266,7 +273,6 @@ } .issue-message-box.secondary-issue:hover, -.issue:focus-within, .issue:hover { border: 2px dashed var(--blue); outline: 0; 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 9de1be2a607..09a2ae2f4bb 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.tsx +++ b/server/sonar-web/src/main/js/components/issue/IssueView.tsx @@ -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} 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 ffeac450c75..849ce188351 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 @@ -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 diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx index 9e7f9049ddb..09c9f294967 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx index 8e63e16a494..881e8b34924 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx @@ -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"> |