diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-04-20 16:52:36 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-04-25 20:03:00 +0000 |
commit | 42cb161d1bfde44f942a0347057d916ea2113713 (patch) | |
tree | 23d366741bb1c1c1484609c81174108bf071aa07 /server/sonar-web | |
parent | 9aeb44c7c2343e8c58aad7f985d12a10969860bf (diff) | |
download | sonarqube-42cb161d1bfde44f942a0347057d916ea2113713.tar.gz sonarqube-42cb161d1bfde44f942a0347057d916ea2113713.zip |
SONAR-19069 Add common IssueCharacteristicHeader
Diffstat (limited to 'server/sonar-web')
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} /> + ); +} |