aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2023-04-20 16:52:36 +0200
committersonartech <sonartech@sonarsource.com>2023-04-25 20:03:00 +0000
commit42cb161d1bfde44f942a0347057d916ea2113713 (patch)
tree23d366741bb1c1c1484609c81174108bf071aa07 /server/sonar-web
parent9aeb44c7c2343e8c58aad7f985d12a10969860bf (diff)
downloadsonarqube-42cb161d1bfde44f942a0347057d916ea2113713.tar.gz
sonarqube-42cb161d1bfde44f942a0347057d916ea2113713.zip
SONAR-19069 Add common IssueCharacteristicHeader
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.css4
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx24
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCharacteristicHeader.tsx103
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueView.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCharacteristicHeader-test.tsx69
7 files changed, 199 insertions, 21 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
index 2f6efa9eb55..ed06fda8e96 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
@@ -24,6 +24,7 @@ import LinkIcon from '../../../components/icons/LinkIcon';
import { updateIssue } from '../../../components/issue/actions';
import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
import IssueChangelog from '../../../components/issue/components/IssueChangelog';
+import IssueCharacteristicHeader from '../../../components/issue/components/IssueCharacteristicHeader';
import IssueMessageTags from '../../../components/issue/components/IssueMessageTags';
import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
@@ -135,7 +136,11 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
return (
<>
- <div className="display-flex-center display-flex-space-between big-padded-top">
+ <IssueCharacteristicHeader
+ characteristic={issue.characteristic}
+ className="big-padded-top"
+ />
+ <div className="display-flex-center display-flex-space-between">
<h1 className="text-bold spacer-right">
<span className="spacer-right issue-header" aria-label={issue.message}>
<IssueMessageHighlighting
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 a5c12dfd9de..e67a3edc49f 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.css
+++ b/server/sonar-web/src/main/js/components/issue/Issue.css
@@ -253,6 +253,10 @@
background-color: var(--badgeRedBackgroundOnIssue);
}
+.issue-category-fit {
+ font-weight: 400;
+}
+
.issue-message-box {
background-color: var(--issueBgColor);
border: 2px solid transparent;
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 b9c4319120c..d4abf2f0458 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
+++ b/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
@@ -19,9 +19,8 @@
*/
import classNames from 'classnames';
import * as React from 'react';
-import { colors } from '../../app/theme';
import { Issue } from '../../types/types';
-import IssueTypeIcon from '../icons/IssueTypeIcon';
+import IssueCharacteristicHeader from './components/IssueCharacteristicHeader';
import './Issue.css';
import { IssueMessageHighlighting } from './IssueMessageHighlighting';
@@ -36,7 +35,7 @@ export function IssueMessageBox(props: IssueMessageBoxProps, ref: React.Forwarde
return (
<div
- className={classNames('issue-message-box display-flex-row display-flex-center padded-right', {
+ className={classNames('issue-message-box padded-left padded-right', {
'selected big-padded-top big-padded-bottom text-bold': selected,
'secondary-issue padded-top padded-bottom': !selected,
})}
@@ -46,11 +45,7 @@ export function IssueMessageBox(props: IssueMessageBoxProps, ref: React.Forwarde
ref={ref}
aria-label={issue.message}
>
- <IssueTypeIcon
- className="big-spacer-right spacer-left"
- fill={colors.baseFontColor}
- query={issue.type}
- />
+ <IssueCharacteristicHeader characteristic={issue.characteristic} />
<IssueMessageHighlighting
message={issue.message}
messageFormattings={issue.messageFormattings}
diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
index 8d6b59b8981..c1acbe6dcee 100644
--- a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
+++ b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
@@ -30,10 +30,10 @@ import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/test
import { findTooltipWithContent, renderApp } from '../../../helpers/testReactTestingUtils';
import {
IssueActions,
+ IssueCharacteristic,
IssueSeverity,
IssueStatus,
IssueTransition,
- IssueType,
} from '../../../types/issues';
import { RuleStatus } from '../../../types/rules';
import { IssueComment } from '../../../types/types';
@@ -54,6 +54,14 @@ beforeEach(() => {
});
describe('rendering', () => {
+ it('should render correctly with Clean Code characteristic label', () => {
+ const { ui } = getPageObject();
+ renderIssue({ issue: mockIssue(false) });
+
+ expect(ui.cleanCodeCharacteristic(IssueCharacteristic.Robust).get()).toBeInTheDocument();
+ expect(ui.fitForProduction.get()).toBeInTheDocument();
+ });
+
it('should render correctly with comments', () => {
const { ui } = getPageObject();
renderIssue({ issue: mockIssue(false, { comments: [mockIssueCommentPosted4YearsAgo()] }) });
@@ -316,6 +324,9 @@ function getPageObject() {
const selectors = {
// Issue
+ cleanCodeCharacteristic: (characteristic: IssueCharacteristic) =>
+ byText(`issue.characteristic.${characteristic}`),
+ fitForProduction: byText('issue.characteristic.fit.PRODUCTION'),
ruleStatusBadge: (status: RuleStatus) => byText(`issue.resolution.badge.${status}`),
locationsBadge: (count: number) => byText(count),
lineInfo: (line: number) => byText(`L${line}`),
@@ -366,11 +377,6 @@ function getPageObject() {
commentDeleteBtn: byRole('button', { name: 'issue.comment.delete' }),
commentConfirmDeleteBtn: byRole('button', { name: 'delete' }),
- // Type
- updateTypeBtn: (currentType: IssueType) =>
- byRole('button', { name: `issue.type.type_x_click_to_change.issue.type.${currentType}` }),
- setTypeBtn: (type: IssueType) => byRole('button', { name: `issue.type.${type}` }),
-
// Severity
updateSeverityBtn: (currentSeverity: IssueSeverity) =>
byRole('button', {
@@ -425,12 +431,6 @@ function getPageObject() {
await user.click(selectors.commentConfirmDeleteBtn.get());
});
},
- async updateType(currentType: IssueType, newType: IssueType) {
- await user.click(selectors.updateTypeBtn(currentType).get());
- await act(async () => {
- await user.click(selectors.setTypeBtn(newType).get());
- });
- },
async updateSeverity(currentSeverity: IssueSeverity, newSeverity: IssueSeverity) {
await user.click(selectors.updateSeverityBtn(currentSeverity).get());
await act(async () => {
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCharacteristicHeader.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueCharacteristicHeader.tsx
new file mode 100644
index 00000000000..3a46d8a3449
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCharacteristicHeader.tsx
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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 { KeyboardKeys } from '../../../helpers/keycodes';
+import { translate } from '../../../helpers/l10n';
+import { IssueCharacteristic, ISSUE_CHARACTERISTIC_TO_FIT_FOR } from '../../../types/issues';
+import DocLink from '../../common/DocLink';
+import Tooltip from '../../controls/Tooltip';
+
+export interface IssueCharacteristicHeaderProps {
+ characteristic: IssueCharacteristic;
+ className?: string;
+}
+
+export default function IssueCharacteristicHeader({
+ characteristic,
+ className,
+}: IssueCharacteristicHeaderProps) {
+ const nextSelectableNode = React.useRef<HTMLElement | undefined | null>();
+ const badgeRef = React.useRef<HTMLElement>(null);
+ const linkRef = React.useRef<HTMLAnchorElement | null>(null);
+
+ function handleShowTooltip() {
+ document.addEventListener('keydown', handleTabPress);
+ }
+
+ function handleHideTooltip() {
+ document.removeEventListener('keydown', handleTabPress);
+ nextSelectableNode.current = undefined;
+ }
+
+ function handleTabPress(event: KeyboardEvent) {
+ if (event.code !== KeyboardKeys.Tab) {
+ return;
+ }
+
+ if (event.shiftKey) {
+ if (event.target === linkRef.current) {
+ (badgeRef.current as HTMLElement).focus();
+ }
+ return;
+ }
+
+ if (nextSelectableNode.current) {
+ event.preventDefault();
+ nextSelectableNode.current.focus();
+ }
+
+ if (event.target === badgeRef.current) {
+ event.preventDefault();
+ nextSelectableNode.current = badgeRef.current;
+ (linkRef.current as HTMLAnchorElement).focus();
+ }
+ }
+
+ return (
+ <div className={classNames('spacer-bottom', className)}>
+ <Tooltip
+ mouseLeaveDelay={0.25}
+ onShow={handleShowTooltip}
+ onHide={handleHideTooltip}
+ isInteractive={true}
+ overlay={
+ <div className="padded-bottom">
+ {translate('issue.characteristic.description', characteristic)}
+ <hr className="big-spacer-top big-spacer-bottom" />
+ <div className="display-flex-center">
+ <span className="spacer-right">{translate('learn_more')}:</span>
+ <DocLink to="/user-guide/issues/" innerRef={linkRef}>
+ {translate('issue.characteristic.doc.link')}
+ </DocLink>
+ </div>
+ </div>
+ }
+ >
+ <span className="badge" ref={badgeRef}>
+ {translate('issue.characteristic', characteristic)}
+ </span>
+ </Tooltip>
+ <span className="muted spacer-left issue-category-fit">
+ {translate('issue.characteristic.fit', ISSUE_CHARACTERISTIC_TO_FIT_FOR[characteristic])}
+ </span>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
index ccc675fae26..f6bfccbfd53 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
@@ -26,6 +26,7 @@ import { Issue } from '../../../types/types';
import Checkbox from '../../controls/Checkbox';
import { updateIssue } from '../actions';
import IssueActionsBar from './IssueActionsBar';
+import IssueCharacteristicHeader from './IssueCharacteristicHeader';
import IssueCommentLine from './IssueCommentLine';
import IssueTitleBar from './IssueTitleBar';
@@ -109,6 +110,7 @@ export default class IssueView extends React.PureComponent<Props> {
title={translate('issues.action_select')}
/>
)}
+ <IssueCharacteristicHeader characteristic={issue.characteristic} className="spacer-left" />
<IssueTitleBar
branchLike={branchLike}
onClick={this.handleDetailClick}
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCharacteristicHeader-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCharacteristicHeader-test.tsx
new file mode 100644
index 00000000000..2ff8c2b8626
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCharacteristicHeader-test.tsx
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { byRole, byText } from 'testing-library-selector';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { IssueCharacteristic } from '../../../../types/issues';
+import IssueCharacteristicHeader, {
+ IssueCharacteristicHeaderProps,
+} from '../IssueCharacteristicHeader';
+
+const ui = {
+ characteristicLabel: (characteristic: IssueCharacteristic) =>
+ byText(`issue.characteristic.${characteristic}`),
+ docLink: byRole('link', { name: /issue.characteristic.doc.link/ }),
+};
+
+it('should render correctly', async () => {
+ renderIssueCharacteristicHeader();
+
+ expect(await ui.characteristicLabel(IssueCharacteristic.Clear).find()).toBeInTheDocument();
+});
+
+it('can select a link in tooltip using tab', async () => {
+ renderIssueCharacteristicHeader();
+
+ await userEvent.tab();
+ expect(ui.characteristicLabel(IssueCharacteristic.Clear).get()).toHaveFocus();
+
+ // Tooltip ignores any keyboard event if it is not Tab
+ await userEvent.keyboard('A');
+
+ await userEvent.tab();
+ expect(ui.docLink.get()).toHaveFocus();
+
+ await userEvent.tab();
+ expect(ui.characteristicLabel(IssueCharacteristic.Clear).get()).toHaveFocus();
+ expect(ui.docLink.query()).not.toBeInTheDocument();
+
+ await userEvent.tab({ shift: true });
+ await userEvent.tab();
+
+ expect(ui.docLink.get()).toBeInTheDocument();
+ await userEvent.tab({ shift: true });
+ expect(ui.characteristicLabel(IssueCharacteristic.Clear).get()).not.toHaveFocus();
+});
+
+function renderIssueCharacteristicHeader(props: Partial<IssueCharacteristicHeaderProps> = {}) {
+ return renderComponent(
+ <IssueCharacteristicHeader characteristic={IssueCharacteristic.Clear} {...props} />
+ );
+}