--- /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 styled from '@emotion/styled';
+import * as React from 'react';
+import { themeColor } from '../helpers';
+
+export interface MessageFormatting {
+ end: number;
+ start: number;
+ type: MessageFormattingType;
+}
+
+export enum MessageFormattingType {
+ CODE = 'CODE',
+}
+
+export interface IssueMessageHighlightingProps {
+ message?: string;
+ messageFormattings?: MessageFormatting[];
+}
+
+export function IssueMessageHighlighting(props: IssueMessageHighlightingProps) {
+ const { message, messageFormattings } = props;
+
+ if (!message) {
+ return null;
+ }
+
+ if (!(messageFormattings && messageFormattings.length > 0)) {
+ return <>{message}</>;
+ }
+
+ let previousEnd = 0;
+
+ const sanitizedFormattings = [...messageFormattings]
+ .sort((a, b) => a.start - b.start)
+ .reduce<typeof messageFormattings>((acc, messageFormatting) => {
+ const { type } = messageFormatting;
+
+ if (type !== MessageFormattingType.CODE) {
+ return acc;
+ }
+
+ const { start } = messageFormatting;
+ let { end } = messageFormatting;
+
+ end = Math.min(message.length, end);
+
+ if (start < 0 || end === start || end < start) {
+ return acc;
+ }
+
+ if (acc.length > 0) {
+ const { start: previousStart, end: previousEnd } = acc[acc.length - 1];
+
+ if (start <= previousEnd) {
+ acc[acc.length - 1] = {
+ start: previousStart,
+ end: Math.max(previousEnd, end),
+ type,
+ };
+
+ return acc;
+ }
+ }
+
+ acc.push({ start, end, type });
+
+ return acc;
+ }, []);
+
+ return (
+ <span>
+ {sanitizedFormattings.map(({ start, end, type }) => {
+ const beginning = previousEnd;
+ previousEnd = end;
+
+ return (
+ <React.Fragment key={`${message}-${start}-${end}`}>
+ {message.slice(beginning, start)}
+ {type === MessageFormattingType.CODE ? (
+ <SingleLineSnippet className="sw-code sw-rounded-1 sw-py-1/2 sw-px-1 sw-border sw-border-solid">
+ {message.slice(start, end)}
+ </SingleLineSnippet>
+ ) : (
+ <span>{message.slice(start, end)}</span>
+ )}
+ </React.Fragment>
+ );
+ })}
+
+ {message.slice(previousEnd)}
+ </span>
+ );
+}
+
+const SingleLineSnippet = styled.span`
+ background: ${themeColor('codeSnippetBackground')};
+ border-color: ${themeColor('codeSnippetBorder')};
+ color: ${themeColor('codeSnippetInline')};
+`;
--- /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 { render } from '@testing-library/react';
+import {
+ IssueMessageHighlighting,
+ IssueMessageHighlightingProps,
+ MessageFormattingType,
+} from '../IssueMessageHighlighting';
+
+it.each([
+ [undefined, undefined],
+ ['message', undefined],
+ ['message', []],
+ ['message', [{ start: 1, end: 4, type: 'something else' as MessageFormattingType }]],
+ [
+ 'message',
+ [
+ { start: 5, end: 6, type: MessageFormattingType.CODE },
+ { start: 1, end: 4, type: MessageFormattingType.CODE },
+ ],
+ ],
+ [
+ 'a somewhat longer message with overlapping ranges',
+ [{ start: -1, end: 1, type: MessageFormattingType.CODE }],
+ ],
+ [
+ 'a somewhat longer message with overlapping ranges',
+ [{ start: 48, end: 70, type: MessageFormattingType.CODE }],
+ ],
+ [
+ 'a somewhat longer message with overlapping ranges',
+ [{ start: 0, end: 0, type: MessageFormattingType.CODE }],
+ ],
+ [
+ 'a somewhat longer message with overlapping ranges',
+ [
+ { start: 11, end: 17, type: MessageFormattingType.CODE },
+ { start: 2, end: 25, type: MessageFormattingType.CODE },
+ { start: 25, end: 2, type: MessageFormattingType.CODE },
+ ],
+ ],
+ [
+ 'a somewhat longer message with overlapping ranges',
+ [
+ { start: 18, end: 30, type: MessageFormattingType.CODE },
+ { start: 2, end: 25, type: MessageFormattingType.CODE },
+ ],
+ ],
+])('should format the string with highlights', (message, messageFormattings) => {
+ const { asFragment } = renderIssueMessageHighlighting({ message, messageFormattings });
+ expect(asFragment()).toMatchSnapshot();
+});
+
+function renderIssueMessageHighlighting(props: Partial<IssueMessageHighlightingProps> = {}) {
+ return render(<IssueMessageHighlighting {...props} />);
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should format the string with highlights 1`] = `<DocumentFragment />`;
+
+exports[`should format the string with highlights 2`] = `
+<DocumentFragment>
+ message
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 3`] = `
+<DocumentFragment>
+ message
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 4`] = `
+<DocumentFragment>
+ <span>
+ message
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 5`] = `
+<DocumentFragment>
+ .emotion-0 {
+ background: rgb(252,252,253);
+ border-color: rgb(225,230,243);
+ color: rgb(62,67,87);
+}
+
+<span>
+ m
+ <span
+ class="sw-code sw-rounded-1 sw-py-1/2 sw-px-1 sw-border sw-border-solid emotion-0 emotion-1"
+ >
+ ess
+ </span>
+ a
+ <span
+ class="sw-code sw-rounded-1 sw-py-1/2 sw-px-1 sw-border sw-border-solid emotion-0 emotion-1"
+ >
+ g
+ </span>
+ e
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 6`] = `
+<DocumentFragment>
+ <span>
+ a somewhat longer message with overlapping ranges
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 7`] = `
+<DocumentFragment>
+ .emotion-0 {
+ background: rgb(252,252,253);
+ border-color: rgb(225,230,243);
+ color: rgb(62,67,87);
+}
+
+<span>
+ a somewhat longer message with overlapping range
+ <span
+ class="sw-code sw-rounded-1 sw-py-1/2 sw-px-1 sw-border sw-border-solid emotion-0 emotion-1"
+ >
+ s
+ </span>
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 8`] = `
+<DocumentFragment>
+ <span>
+ a somewhat longer message with overlapping ranges
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 9`] = `
+<DocumentFragment>
+ .emotion-0 {
+ background: rgb(252,252,253);
+ border-color: rgb(225,230,243);
+ color: rgb(62,67,87);
+}
+
+<span>
+ a
+ <span
+ class="sw-code sw-rounded-1 sw-py-1/2 sw-px-1 sw-border sw-border-solid emotion-0 emotion-1"
+ >
+ somewhat longer message
+ </span>
+ with overlapping ranges
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 10`] = `
+<DocumentFragment>
+ .emotion-0 {
+ background: rgb(252,252,253);
+ border-color: rgb(225,230,243);
+ color: rgb(62,67,87);
+}
+
+<span>
+ a
+ <span
+ class="sw-code sw-rounded-1 sw-py-1/2 sw-px-1 sw-border sw-border-solid emotion-0 emotion-1"
+ >
+ somewhat longer message with
+ </span>
+ overlapping ranges
+ </span>
+</DocumentFragment>
+`;
export { InputSearch } from './InputSearch';
export * from './InputSelect';
export * from './InteractiveIcon';
+export * from './IssueMessageHighlighting';
export * from './KeyboardHint';
export * from './Link';
export { StandoutLink as Link } from './Link';
import { TabKeys } from '../../../components/rules/RuleTabViewer';
import { renderOwaspTop102021Category } from '../../../helpers/security-standard';
import { mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks';
+import { findTooltipWithContent } from '../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../types/component';
import { IssueType } from '../../../types/issues';
import {
// open tags popup on key press 't'
- // needs to be fixed with the new header from ambroise!
- // await user.keyboard('t');
- // expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
- // expect(screen.getByText('android')).toBeInTheDocument();
- // expect(screen.getByText('accessibility')).toBeInTheDocument();
- // // closing tags popup
- // await user.click(screen.getByText('issue.no_tag'));
-
- // // open assign popup on key press 'a'
- // await user.keyboard('a');
- // expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+ await user.keyboard('t');
+ expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+ expect(screen.getByText('android')).toBeInTheDocument();
+ expect(screen.getByText('accessibility')).toBeInTheDocument();
+ // closing tags popup
+ await user.click(screen.getByText('issue.no_tag'));
+
+ // open assign popup on key press 'a'
+ await user.keyboard('a');
+ expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
});
it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => {
await user.click(await ui.issueItemAction7.find());
expect(
- screen.getByRole('heading', {
- name: 'Issue with tags issue.quick_fix_available_with_sonarlint_no_link issue.resolution.badge.DEPRECATED',
+ findTooltipWithContent('issue.quick_fix_available_with_sonarlint_no_link')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('status', {
+ name: 'issue.resolution.badge.DEPRECATED',
})
).toBeInTheDocument();
});
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ ClipboardIconButton,
+ IssueMessageHighlighting,
+ Link,
+ LinkIcon,
+ Note,
+ PageContentFontWrapper,
+} from 'design-system';
import * as React from 'react';
import { setIssueAssignee } from '../../../api/issues';
-import Link from '../../../components/common/Link';
-import LinkIcon from '../../../components/icons/LinkIcon';
-import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
import { updateIssue } from '../../../components/issue/actions';
import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
-import IssueChangelog from '../../../components/issue/components/IssueChangelog';
-import IssueMessageTags from '../../../components/issue/components/IssueMessageTags';
+import IssueTags from '../../../components/issue/components/IssueTags';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import { getKeyboardShortcutEnabled } from '../../../helpers/preferences';
-import { getComponentIssuesUrl, getRuleUrl } from '../../../helpers/urls';
+import { getComponentIssuesUrl, getPathUrlAsString, getRuleUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
-import { IssueType } from '../../../types/issues';
-import { RuleStatus } from '../../../types/rules';
+import { IssueActions, IssueType } from '../../../types/issues';
import { Issue, RuleDetails } from '../../../types/types';
interface Props {
open: issue.key,
types: issue.type === IssueType.SecurityHotspot ? issue.type : undefined,
});
- const ruleStatus = issue.ruleStatus as RuleStatus | undefined;
- const { quickFixAvailable } = issue;
+ const canSetTags = issue.actions.includes(IssueActions.SetTags);
return (
- <>
- <div className="display-flex-center display-flex-space-between big-padded-top">
- <h1 className="text-bold spacer-right">
- <span className="spacer-right issue-header" aria-label={issue.message}>
- <IssueMessageHighlighting
- message={issue.message}
- messageFormattings={issue.messageFormattings}
- />
- </span>
- <IssueMessageTags
- engine={issue.externalRuleEngine}
- quickFixAvailable={quickFixAvailable}
- ruleStatus={ruleStatus}
+ <header className="sw-flex sw-flex-col sw-gap-3 sw-my-6">
+ <div className="sw-flex sw-items-center">
+ <PageContentFontWrapper className="sw-body-md-highlight" as="h1">
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
/>
- </h1>
- <div className="issue-meta issue-get-perma-link">
- <Link
- className="js-issue-permalink link-no-underline"
- target="_blank"
- title={translate('permalink')}
- to={issueUrl}
- >
- {translate('issue.action.permalink')}
- <LinkIcon />
- </Link>
- </div>
+ </PageContentFontWrapper>
+ <ClipboardIconButton
+ Icon={LinkIcon}
+ aria-label={translate('permalink')}
+ className="sw-ml-1 sw-align-bottom"
+ copyValue={getPathUrlAsString(issueUrl, false)}
+ discreet={true}
+ />
</div>
- <div className="display-flex-center display-flex-space-between spacer-top big-spacer-bottom">
- <div>
- <span className="note padded-right">{name}</span>
+ <div className="sw-flex sw-items-center sw-justify-between">
+ <Note>
+ <span className="sw-pr-1">{name}</span>
{isExternal ? (
- <span className="note small">({key})</span>
+ <span>({key})</span>
) : (
- <Link className="small" to={getRuleUrl(key)} target="_blank">
+ <Link to={getRuleUrl(key)} target="_blank">
{key}
</Link>
)}
- </div>
- <div className="issue-meta-list">
- <div className="issue-meta">
- <IssueChangelog
- creationDate={issue.creationDate}
- isOpen={issuePopupName === 'changelog'}
- issue={issue}
- togglePopup={this.handleIssuePopupToggle}
- />
- </div>
- {issue.textRange != null && (
- <div className="issue-meta">
- <span className="issue-meta-label" title={translate('line_number')}>
- L{issue.textRange.endLine}
- </span>
- </div>
- )}
- </div>
+ </Note>
+ <IssueTags
+ canSetTags={canSetTags}
+ issue={issue}
+ onChange={this.props.onIssueChange}
+ open={issuePopupName === 'edit-tags' && canSetTags}
+ togglePopup={this.handleIssuePopupToggle}
+ />
</div>
<IssueActionsBar
className="issue-header-actions"
togglePopup={this.handleIssuePopupToggle}
showCommentsInPopup
/>
- </>
+ </header>
);
}
}
expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument();
});
- // Should be re-enabled when tags are re-enabled with ambroise code
// eslint-disable-next-line jest/no-commented-out-tests
- // it('should allow updating the tags', async () => {
- // const { ui } = getPageObject();
- // const issue = mockRawIssue(false, {
- // tags: [],
- // actions: [IssueActions.SetTags],
- // });
- // issuesHandler.setIssueList([{ issue, snippets: {} }]);
- // renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) });
-
- // await ui.addTag('accessibility');
- // await ui.addTag('android', ['accessibility']);
- // expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument();
- // });
+ it('should allow updating the tags', async () => {
+ const { ui } = getPageObject();
+ const issue = mockRawIssue(false, {
+ tags: [],
+ actions: [IssueActions.SetTags],
+ });
+ issuesHandler.setIssueList([{ issue, snippets: {} }]);
+ renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) });
+
+ await ui.addTag('accessibility');
+ await ui.addTag('android', ['accessibility']);
+ expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument();
+ });
});
it('should correctly handle keyboard shortcuts', async () => {
// Tags
tagsSearchInput: byRole('searchbox'),
updateTagsBtn: (currentTags?: string[]) =>
- byText(`tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`),
+ byRole('button', { name: `${currentTags ? currentTags.join(', ') : 'issue.no_tag'} +` }),
toggleTagCheckbox: (name: string) => byRole('checkbox', { name }),
};
import { Issue, RawQuery } from '../../../types/types';
import Tooltip from '../../controls/Tooltip';
import DateFromNow from '../../intl/DateFromNow';
+import { WorkspaceContext } from '../../workspace/context';
import { updateIssue } from '../actions';
import IssueAssign from './IssueAssign';
+import IssueBadges from './IssueBadges';
import IssueCommentAction from './IssueCommentAction';
-import IssueMessageTags from './IssueMessageTags';
import IssueSeverity from './IssueSeverity';
import IssueTransition from './IssueTransition';
import IssueType from './IssueType';
commentPlaceholder: string;
}
-export default class IssueActionsBar extends React.PureComponent<Props, State> {
- state: State = {
+export default function IssueActionsBar(props: Props) {
+ const {
+ issue,
+ currentPopup,
+ onAssign,
+ onChange,
+ togglePopup,
+ className,
+ showComments,
+ showCommentsInPopup,
+ showLine,
+ } = props;
+ const [commentState, setCommentState] = React.useState<State>({
commentAutoTriggered: false,
commentPlaceholder: '',
- };
+ });
- setIssueProperty = (
+ const setIssueProperty = (
property: keyof Issue,
popup: string,
apiCall: (query: RawQuery) => Promise<IssueResponse>,
value: string
) => {
- const { issue } = this.props;
if (issue[property] !== value) {
const newIssue = { ...issue, [property]: value };
- updateIssue(
- this.props.onChange,
- apiCall({ issue: issue.key, [property]: value }),
- issue,
- newIssue
- );
+ updateIssue(onChange, apiCall({ issue: issue.key, [property]: value }), issue, newIssue);
}
- this.props.togglePopup(popup, false);
+ togglePopup(popup, false);
};
- toggleComment = (open: boolean | undefined, placeholder = '', autoTriggered = false) => {
- this.setState({
+ const toggleComment = (open: boolean | undefined, placeholder = '', autoTriggered = false) => {
+ setCommentState({
commentPlaceholder: placeholder,
commentAutoTriggered: autoTriggered,
});
- this.props.togglePopup('comment', open);
+ togglePopup('comment', open);
};
- handleTransition = (issue: Issue) => {
- this.props.onChange(issue);
+ const handleTransition = (issue: Issue) => {
+ onChange(issue);
if (
issue.resolution === IssueResolution.FalsePositive ||
(issue.resolution === IssueResolution.WontFix && issue.type !== IssueTypeEnum.SecurityHotspot)
) {
- this.toggleComment(true, translate('issue.comment.explain_why'), true);
+ toggleComment(true, translate('issue.comment.explain_why'), true);
}
};
- render() {
- const { issue, className, showComments, showCommentsInPopup, showLine } = this.props;
- const canAssign = issue.actions.includes(IssueActions.Assign);
- const canComment = issue.actions.includes(IssueActions.Comment);
- const canSetSeverity = issue.actions.includes(IssueActions.SetSeverity);
- const canSetType = issue.actions.includes(IssueActions.SetType);
- const hasTransitions = issue.transitions.length > 0;
- const hasComments = issue.comments && issue.comments.length > 0;
- const IssueMetaLiClass = classNames(
- className,
- 'sw-body-sm sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150'
- );
+ const { externalRulesRepoNames } = React.useContext(WorkspaceContext);
+ const ruleEngine =
+ (issue.externalRuleEngine && externalRulesRepoNames[issue.externalRuleEngine]) ||
+ issue.externalRuleEngine;
+ const canAssign = issue.actions.includes(IssueActions.Assign);
+ const canComment = issue.actions.includes(IssueActions.Comment);
+ const canSetSeverity = issue.actions.includes(IssueActions.SetSeverity);
+ const canSetType = issue.actions.includes(IssueActions.SetType);
+ const hasTransitions = issue.transitions.length > 0;
+ const hasComments = !!issue.comments?.length;
+ const issueMetaListItemClassNames = classNames(
+ className,
+ 'sw-body-sm sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150'
+ );
- return (
- <div className="sw-flex sw-flex-wrap sw-items-center sw-justify-between">
- <ul className="sw-flex sw-items-center sw-gap-3 sw-body-sm">
- <li>
- <IssueType
- canSetType={canSetType}
- issue={issue}
- setIssueProperty={this.setIssueProperty}
- />
- </li>
- <li>
- <IssueSeverity
- isOpen={this.props.currentPopup === 'set-severity'}
- togglePopup={this.props.togglePopup}
- canSetSeverity={canSetSeverity}
- issue={issue}
- setIssueProperty={this.setIssueProperty}
- />
- </li>
- <li>
- <IssueTransition
- isOpen={this.props.currentPopup === 'transition'}
- togglePopup={this.props.togglePopup}
- hasTransitions={hasTransitions}
- issue={issue}
- onChange={this.handleTransition}
- />
- </li>
- <li>
- <IssueAssign
- isOpen={this.props.currentPopup === 'assign'}
- togglePopup={this.props.togglePopup}
- canAssign={canAssign}
- issue={issue}
- onAssign={this.props.onAssign}
- />
- </li>
- </ul>
- {(canComment || showCommentsInPopup) && (
- <IssueCommentAction
- commentAutoTriggered={this.state.commentAutoTriggered}
- commentPlaceholder={this.state.commentPlaceholder}
- currentPopup={this.props.currentPopup}
- issueKey={issue.key}
- onChange={this.props.onChange}
- toggleComment={this.toggleComment}
- comments={issue.comments}
- canComment={canComment}
- showCommentsInPopup={showCommentsInPopup}
+ return (
+ <div className="sw-flex sw-flex-wrap sw-items-center sw-justify-between">
+ <ul className="sw-flex sw-items-center sw-gap-3 sw-body-sm">
+ <li>
+ <IssueType canSetType={canSetType} issue={issue} setIssueProperty={setIssueProperty} />
+ </li>
+ <li>
+ <IssueSeverity
+ isOpen={currentPopup === 'set-severity'}
+ togglePopup={togglePopup}
+ canSetSeverity={canSetSeverity}
+ issue={issue}
+ setIssueProperty={setIssueProperty}
/>
- )}
+ </li>
+ <li>
+ <IssueTransition
+ isOpen={currentPopup === 'transition'}
+ togglePopup={togglePopup}
+ hasTransitions={hasTransitions}
+ issue={issue}
+ onChange={handleTransition}
+ />
+ </li>
+ <li>
+ <IssueAssign
+ isOpen={currentPopup === 'assign'}
+ togglePopup={togglePopup}
+ canAssign={canAssign}
+ issue={issue}
+ onAssign={onAssign}
+ />
+ </li>
+ </ul>
+ {(canComment || showCommentsInPopup) && (
+ <IssueCommentAction
+ commentAutoTriggered={commentState.commentAutoTriggered}
+ commentPlaceholder={commentState.commentPlaceholder}
+ currentPopup={currentPopup}
+ issueKey={issue.key}
+ onChange={onChange}
+ toggleComment={toggleComment}
+ comments={issue.comments}
+ canComment={canComment}
+ showCommentsInPopup={showCommentsInPopup}
+ />
+ )}
- <ul className="sw-flex sw-items-center sw-gap-2 sw-body-sm">
- <li className={IssueMetaLiClass}>
- <IssueMessageTags
- engine={issue.externalRuleEngine}
- quickFixAvailable={issue.quickFixAvailable}
- ruleStatus={issue.ruleStatus as RuleStatus | undefined}
- />
- </li>
+ <ul className="sw-flex sw-items-center sw-gap-2 sw-body-sm">
+ <li className={issueMetaListItemClassNames}>
+ <IssueBadges
+ quickFixAvailable={issue.quickFixAvailable}
+ ruleStatus={issue.ruleStatus as RuleStatus | undefined}
+ />
+ </li>
- {issue.externalRuleEngine && (
- <li className={IssueMetaLiClass}>
- <Tooltip
- overlay={translateWithParameters(
- 'issue.from_external_rule_engine',
- issue.externalRuleEngine
- )}
- >
- <Badge>{issue.externalRuleEngine}</Badge>
- </Tooltip>
- </li>
- )}
+ {ruleEngine && (
+ <li className={issueMetaListItemClassNames}>
+ <Tooltip
+ overlay={translateWithParameters('issue.from_external_rule_engine', ruleEngine)}
+ >
+ <Badge>{ruleEngine}</Badge>
+ </Tooltip>
+ </li>
+ )}
- {issue.codeVariants && issue.codeVariants.length > 0 && (
- <IssueMetaLi>
+ {!!issue.codeVariants?.length && (
+ <>
+ <IssueMetaListItem>
<Tooltip overlay={issue.codeVariants.join(', ')}>
<>
{issue.codeVariants.length > 1
: translate('issue.1_code_variant')}
</>
</Tooltip>
- <SeparatorCircleIcon aria-hidden={true} as="li" />
- </IssueMetaLi>
- )}
+ </IssueMetaListItem>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
- {showComments && hasComments && (
- <>
- <IssueMetaLi className={IssueMetaLiClass}>
- <CommentIcon aria-label={translate('issue.comment.formlink')} />
- {issue.comments?.length}
- </IssueMetaLi>
- <SeparatorCircleIcon aria-hidden={true} as="li" />
- </>
- )}
- {showLine && isDefined(issue.textRange) && (
- <>
- <Tooltip overlay={translate('line_number')}>
- <IssueMetaLi className={IssueMetaLiClass}>
- {translateWithParameters('issue.ncloc_x.short', issue.textRange.endLine)}
- </IssueMetaLi>
- </Tooltip>
- <SeparatorCircleIcon aria-hidden={true} as="li" />
- </>
- )}
- {issue.effort && (
- <>
- <IssueMetaLi className={IssueMetaLiClass}>
- {translateWithParameters('issue.x_effort', issue.effort)}
- </IssueMetaLi>
- <SeparatorCircleIcon aria-hidden={true} as="li" />
- </>
- )}
- <IssueMetaLi className={IssueMetaLiClass}>
- <DateFromNow date={issue.creationDate} />
- </IssueMetaLi>
- </ul>
- </div>
- );
- }
+ {showComments && hasComments && (
+ <>
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ <CommentIcon aria-label={translate('issue.comment.formlink')} />
+ {issue.comments?.length}
+ </IssueMetaListItem>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
+ {showLine && isDefined(issue.textRange) && (
+ <>
+ <Tooltip overlay={translate('line_number')}>
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ {translateWithParameters('issue.ncloc_x.short', issue.textRange.endLine)}
+ </IssueMetaListItem>
+ </Tooltip>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
+ {issue.effort && (
+ <>
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ {translateWithParameters('issue.x_effort', issue.effort)}
+ </IssueMetaListItem>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
+ <IssueMetaListItem className={issueMetaListItemClassNames}>
+ <DateFromNow date={issue.creationDate} />
+ </IssueMetaListItem>
+ </ul>
+ </div>
+ );
}
-const IssueMetaLi = styled.li`
+const IssueMetaListItem = styled.li`
color: ${themeColor('pageContentLight')};
`;
--- /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 { Badge } from 'design-system';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { RuleStatus } from '../../../types/rules';
+import DocumentationTooltip from '../../common/DocumentationTooltip';
+import Link from '../../common/Link';
+import Tooltip from '../../controls/Tooltip';
+import SonarLintIcon from '../../icons/SonarLintIcon';
+
+export interface IssueBadgesProps {
+ quickFixAvailable?: boolean;
+ ruleStatus?: RuleStatus;
+}
+
+export default function IssueBadges(props: IssueBadgesProps) {
+ const { quickFixAvailable, ruleStatus } = props;
+
+ return (
+ <div className="sw-flex">
+ {quickFixAvailable && (
+ <Tooltip
+ overlay={
+ <FormattedMessage
+ id="issue.quick_fix_available_with_sonarlint"
+ defaultMessage={translate('issue.quick_fix_available_with_sonarlint')}
+ values={{
+ link: (
+ <Link
+ to="https://www.sonarqube.org/sonarlint/?referrer=sonarqube-quick-fix"
+ target="_blank"
+ >
+ SonarLint
+ </Link>
+ ),
+ }}
+ />
+ }
+ mouseLeaveDelay={0.5}
+ >
+ <div className="sw-flex sw-items-center">
+ <SonarLintIcon
+ className="it__issues-sonarlint-quick-fix"
+ size={15}
+ description={translate('issue.quick_fix_available_with_sonarlint_no_link')}
+ />
+ </div>
+ </Tooltip>
+ )}
+ {ruleStatus &&
+ (ruleStatus === RuleStatus.Deprecated || ruleStatus === RuleStatus.Removed) && (
+ <DocumentationTooltip
+ className="sw-ml-2"
+ content={translate('rules.status', ruleStatus, 'help')}
+ links={[
+ {
+ href: '/user-guide/rules/overview/',
+ label: translateWithParameters('see_x', translate('rules')),
+ },
+ ]}
+ >
+ <Badge variant="deleted">{translate('issue.resolution.badge', ruleStatus)}</Badge>
+ </DocumentationTooltip>
+ )}
+ </div>
+ );
+}
+++ /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 * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import Tooltip from '../../../components/controls/Tooltip';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { RuleStatus } from '../../../types/rules';
-import DocumentationTooltip from '../../common/DocumentationTooltip';
-import Link from '../../common/Link';
-import SonarLintIcon from '../../icons/SonarLintIcon';
-import { WorkspaceContext } from '../../workspace/context';
-
-export interface IssueMessageTagsProps {
- engine?: string;
- quickFixAvailable?: boolean;
- ruleStatus?: RuleStatus;
-}
-
-export default function IssueMessageTags(props: IssueMessageTagsProps) {
- const { engine, quickFixAvailable, ruleStatus } = props;
-
- const { externalRulesRepoNames } = React.useContext(WorkspaceContext);
- const ruleEngine = (engine && externalRulesRepoNames[engine]) || engine;
-
- return (
- <>
- {quickFixAvailable && (
- <Tooltip
- overlay={
- <FormattedMessage
- id="issue.quick_fix_available_with_sonarlint"
- defaultMessage={translate('issue.quick_fix_available_with_sonarlint')}
- values={{
- link: (
- <Link
- to="https://www.sonarqube.org/sonarlint/?referrer=sonarqube-quick-fix"
- target="_blank"
- >
- SonarLint
- </Link>
- ),
- }}
- />
- }
- mouseLeaveDelay={0.5}
- >
- <SonarLintIcon
- className="it__issues-sonarlint-quick-fix spacer-right"
- size={15}
- description={translate('issue.quick_fix_available_with_sonarlint_no_link')}
- />
- </Tooltip>
- )}
- {ruleStatus &&
- (ruleStatus === RuleStatus.Deprecated || ruleStatus === RuleStatus.Removed) && (
- <DocumentationTooltip
- className="spacer-left"
- content={translate('rules.status', ruleStatus, 'help')}
- links={[
- {
- href: '/user-guide/rules/overview/',
- label: translateWithParameters('see_x', translate('rules')),
- },
- ]}
- >
- <span className="spacer-right badge badge-error">
- {translate('issue.resolution.badge', ruleStatus)}
- </span>
- </DocumentationTooltip>
- )}
- {ruleEngine && (
- <Tooltip overlay={translateWithParameters('issue.from_external_rule_engine', ruleEngine)}>
- <div className="badge spacer-right text-baseline">{ruleEngine}</div>
- </Tooltip>
- )}
- </>
- );
-}