From 42cb161d1bfde44f942a0347057d916ea2113713 Mon Sep 17 00:00:00 2001 From: stanislavh Date: Thu, 20 Apr 2023 16:52:36 +0200 Subject: [PATCH] SONAR-19069 Add common IssueCharacteristicHeader --- .../js/apps/issues/components/IssueHeader.tsx | 7 +- .../src/main/js/components/issue/Issue.css | 4 + .../js/components/issue/IssueMessageBox.tsx | 11 +- .../components/issue/__tests__/Issue-it.tsx | 24 ++-- .../components/IssueCharacteristicHeader.tsx | 103 ++++++++++++++++++ .../components/issue/components/IssueView.tsx | 2 + .../IssueCharacteristicHeader-test.tsx | 69 ++++++++++++ .../resources/org/sonar/l10n/core.properties | 3 +- 8 files changed, 201 insertions(+), 22 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/issue/components/IssueCharacteristicHeader.tsx create mode 100644 server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCharacteristicHeader-test.tsx 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 { return ( <> -
+ +

- + { }); 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(); + const badgeRef = React.useRef(null); + const linkRef = React.useRef(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 ( +
+ + {translate('issue.characteristic.description', characteristic)} +
+
+ {translate('learn_more')}: + + {translate('issue.characteristic.doc.link')} + +
+
+ } + > + + {translate('issue.characteristic', characteristic)} + + + + {translate('issue.characteristic.fit', ISSUE_CHARACTERISTIC_TO_FIT_FOR[characteristic])} + +

+ ); +} 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 { title={translate('issues.action_select')} /> )} + + 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 = {}) { + return renderComponent( + + ); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ca3f7c541e9..60d6e5daaa1 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 -- 2.39.5