]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19069 Add common IssueCharacteristicHeader
authorstanislavh <stanislav.honcharov@sonarsource.com>
Thu, 20 Apr 2023 14:52:36 +0000 (16:52 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 25 Apr 2023 20:03:00 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
server/sonar-web/src/main/js/components/issue/Issue.css
server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
server/sonar-web/src/main/js/components/issue/components/IssueCharacteristicHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/IssueView.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCharacteristicHeader-test.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2f6efa9eb55a6394c1cf885b76dc5d52b652201b..ed06fda8e96fba8d9fa062a5e6616f21ea590b31 100644 (file)
@@ -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
index a5c12dfd9de7bd282fa7c8318416d8dd0a614585..e67a3edc49f3636668cc468325ecb3f1a24d9359 100644 (file)
   background-color: var(--badgeRedBackgroundOnIssue);
 }
 
+.issue-category-fit {
+  font-weight: 400;
+}
+
 .issue-message-box {
   background-color: var(--issueBgColor);
   border: 2px solid transparent;
index b9c4319120c21af6cb75c5ab71c2e6da1c48cc20..d4abf2f045845bba9ac17dd6f1a1224fafb3f222 100644 (file)
@@ -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}
index 8d6b59b898184b96eedd1a85b01109c387fdc660..c1acbe6dceefdc7399a1895abcb107e714b121ff 100644 (file)
@@ -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 (file)
index 0000000..3a46d8a
--- /dev/null
@@ -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>
+  );
+}
index ccc675fae262dfb160565d49121e197ee17b065d..f6bfccbfd53c53edcec72d8ac3b74e1104d342eb 100644 (file)
@@ -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 (file)
index 0000000..2ff8c2b
--- /dev/null
@@ -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} />
+  );
+}
index ca3f7c541e920a14570c24b0a54ec34be86abf38..60d6e5daaa12b6e3bac9ed287e132fdcf2d141ef 100644 (file)
@@ -932,9 +932,10 @@ issue.characteristic.PORTABLE=Portability
 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