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';
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
background-color: var(--badgeRedBackgroundOnIssue);
}
+.issue-category-fit {
+ font-weight: 400;
+}
+
.issue-message-box {
background-color: var(--issueBgColor);
border: 2px solid transparent;
*/
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';
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,
})}
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}
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';
});
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()] }) });
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}`),
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', {
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 () => {
--- /dev/null
+/*
+ * 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>
+ );
+}
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';
title={translate('issues.action_select')}
/>
)}
+ <IssueCharacteristicHeader characteristic={issue.characteristic} className="spacer-left" />
<IssueTitleBar
branchLike={branchLike}
onClick={this.handleDetailClick}
--- /dev/null
+/*
+ * 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} />
+ );
+}
issue.characteristic.description.PORTABLE=Code that supports the sustained evolution of the software environment and changing practices.
issue.characteristic.COMPLIANT=Compliance
issue.characteristic.description.COMPLIANT=Code that conforms to laws, regulations, and industry standards for its context.
+issue.characteristic.doc.link=Documentation
issue.characteristic.fit.DEVELOPMENT=Fit for Development
-issue.characteristic.fit.PRODUCTION=Fit for Operation
+issue.characteristic.fit.PRODUCTION=Fit for Production
issue.status.REOPENED=Reopened
issue.status.RESOLVED=Resolved