if (!prevState.open && this.state.open && this.props.onOpen) {
this.props.onOpen();
}
- if (props.openDropdown !== this.props.openDropdown && this.props.openDropdown) {
+ if (
+ props.openDropdown !== this.props.openDropdown &&
+ typeof this.props.openDropdown === 'boolean'
+ ) {
this.setState({ open: this.props.openDropdown });
}
}
onFocus?: VoidFunction;
onPointerEnter?: VoidFunction;
onPointerLeave?: VoidFunction;
+ selected?: boolean;
}
type ItemLinkProps = Omit<ListItemProps, 'innerRef'> &
};
export function ItemLink(props: ItemLinkProps) {
- const { children, className, disabled, icon, isExternal, onClick, innerRef, to, ...liProps } =
- props;
+ const {
+ children,
+ className,
+ disabled,
+ icon,
+ isExternal,
+ onClick,
+ selected,
+ innerRef,
+ to,
+ ...liProps
+ } = props;
return (
<li {...liProps}>
<ItemLinkStyled
- className={classNames(className, { disabled })}
+ className={classNames(className, { disabled, selected })}
disabled={disabled}
icon={icon}
isExternal={isExternal}
}
export function ItemNavLink(props: ItemNavLinkProps) {
- const { children, className, disabled, end, icon, onClick, innerRef, to, ...liProps } = props;
+ const { children, className, disabled, end, icon, onClick, selected, innerRef, to, ...liProps } =
+ props;
return (
<li {...liProps}>
<ItemNavLinkStyled
- className={classNames(className, { disabled })}
+ className={classNames(className, { disabled, selected })}
disabled={disabled}
end={end}
onClick={onClick}
}
export function ItemButton(props: ItemButtonProps) {
- const { children, className, disabled, icon, innerRef, onClick, ...liProps } = props;
+ const { children, className, disabled, icon, innerRef, onClick, selected, ...liProps } = props;
return (
<li ref={innerRef} role="none" {...liProps}>
- <ItemButtonStyled className={className} disabled={disabled} onClick={onClick} role="menuitem">
+ <ItemButtonStyled
+ className={classNames(className, { disabled, selected })}
+ disabled={disabled}
+ onClick={onClick}
+ role="menuitem"
+ >
{icon}
{children}
</ItemButtonStyled>
${tw`sw-cursor-not-allowed`};
}
+ &.selected {
+ background-color: ${themeColor('selectOptionSelected')(props)};
+ }
+
& > svg {
${tw`sw-mr-2`}
}
);
}
-const StyledControl = styled.div`
+export const StyledControl = styled.div`
color: ${themeContrast('inputBackground')};
background: ${themeColor('inputBackground')};
border: ${themeBorder('default', 'inputBorder')};
&.is-discreet {
${tw`sw-border-none`};
- ${tw`sw-p-0`};
+ ${tw`sw-px-1`};
${tw`sw-w-auto sw-h-auto`};
background: inherit;
export * from './RadioButton';
export * from './SearchSelect';
export * from './SearchSelectDropdown';
+export * from './SearchSelectDropdownControl';
import { SearchRulesResponse } from '../../types/coding-rules';
import {
ASSIGNEE_ME,
- IssueResolution,
+ IssueSimpleStatus,
IssueStatus,
IssueTransition,
IssueType,
};
handleSetIssueTransition = (data: { issue: string; transition: string }) => {
- const statusMap: { [key: string]: IssueStatus } = {
- [IssueTransition.Confirm]: IssueStatus.Confirmed,
- [IssueTransition.UnConfirm]: IssueStatus.Reopened,
- [IssueTransition.Resolve]: IssueStatus.Resolved,
- [IssueTransition.WontFix]: IssueStatus.Resolved,
- [IssueTransition.FalsePositive]: IssueStatus.Resolved,
+ const simpleStatusMap: { [key: string]: IssueSimpleStatus } = {
+ [IssueTransition.Accept]: IssueSimpleStatus.Accepted,
+ [IssueTransition.Confirm]: IssueSimpleStatus.Confirmed,
+ [IssueTransition.UnConfirm]: IssueSimpleStatus.Open,
+ [IssueTransition.Resolve]: IssueSimpleStatus.Fixed,
+ [IssueTransition.WontFix]: IssueSimpleStatus.Accepted,
+ [IssueTransition.FalsePositive]: IssueSimpleStatus.FalsePositive,
};
+
const transitionMap: Dict<IssueTransition[]> = {
- [IssueStatus.Reopened]: [
- IssueTransition.Confirm,
- IssueTransition.Resolve,
- IssueTransition.FalsePositive,
- IssueTransition.WontFix,
- ],
- [IssueStatus.Open]: [
+ [IssueSimpleStatus.Open]: [
+ IssueTransition.Accept,
IssueTransition.Confirm,
IssueTransition.Resolve,
IssueTransition.FalsePositive,
IssueTransition.WontFix,
],
- [IssueStatus.Confirmed]: [
+ [IssueSimpleStatus.Confirmed]: [
+ IssueTransition.Accept,
IssueTransition.Resolve,
IssueTransition.UnConfirm,
IssueTransition.FalsePositive,
IssueTransition.WontFix,
],
- [IssueStatus.Resolved]: [IssueTransition.Reopen],
- };
-
- const resolutionMap: Dict<string> = {
- [IssueTransition.WontFix]: IssueResolution.WontFix,
- [IssueTransition.FalsePositive]: IssueResolution.FalsePositive,
+ [IssueSimpleStatus.FalsePositive]: [IssueTransition.Reopen],
+ [IssueSimpleStatus.Accepted]: [IssueTransition.Reopen],
+ [IssueSimpleStatus.Fixed]: [IssueTransition.Reopen],
};
return this.getActionsResponse(
{
- status: statusMap[data.transition],
- transitions: transitionMap[statusMap[data.transition]],
- resolution: resolutionMap[data.transition],
+ simpleStatus: simpleStatusMap[data.transition],
+ transitions: transitionMap[simpleStatusMap[data.transition]],
},
data.issue,
);
IssueResolution,
IssueScope,
IssueSeverity,
+ IssueSimpleStatus,
IssueStatus,
+ IssueTransition,
IssueType,
RawIssue,
} from '../../../types/issues';
issue: mockRawIssue(false, {
key: ISSUE_2,
actions: Object.values(IssueActions),
- transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'],
+ transitions: [
+ IssueTransition.Accept,
+ IssueTransition.Confirm,
+ IssueTransition.Resolve,
+ IssueTransition.FalsePositive,
+ IssueTransition.WontFix,
+ ],
component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_2][0]}`,
message: 'Fix that',
rule: ISSUE_TO_RULE[ISSUE_2],
ruleDescriptionContextKey: 'spring',
resolution: IssueResolution.Unresolved,
status: IssueStatus.Open,
+ simpleStatus: IssueSimpleStatus.Open,
}),
snippets: keyBy(
[
issue: mockRawIssue(false, {
key: ISSUE_4,
actions: Object.values(IssueActions),
- transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'],
+ transitions: [
+ IssueTransition.Confirm,
+ IssueTransition.Resolve,
+ IssueTransition.FalsePositive,
+ IssueTransition.WontFix,
+ ],
component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_4][0]}`,
message: 'Issue with tags',
rule: ISSUE_TO_RULE[ISSUE_4],
// Get a specific issue list item
const listItem = within(await screen.findByRole('region', { name: 'Fix that' }));
- // Change issue status
- expect(listItem.getByText('issue.status.OPEN')).toBeInTheDocument();
+ expect(listItem.getByText('issue.simple_status.OPEN')).toBeInTheDocument();
await act(async () => {
- await user.click(listItem.getByText('issue.status.OPEN'));
+ await user.click(listItem.getByText('issue.simple_status.OPEN'));
});
+ expect(listItem.getByText('issue.transition.accept')).toBeInTheDocument();
expect(listItem.getByText('issue.transition.confirm')).toBeInTheDocument();
- expect(listItem.getByText('issue.transition.resolve')).toBeInTheDocument();
await act(async () => {
await user.click(listItem.getByText('issue.transition.confirm'));
});
- expect(
- listItem.getByLabelText('issue.transition.status_x_click_to_change.issue.status.CONFIRMED'),
- ).toBeInTheDocument();
- // As won't fix
+ expect(listItem.getByRole('textbox')).toBeInTheDocument();
+
await act(async () => {
- await user.click(listItem.getByText('issue.status.CONFIRMED'));
- await user.click(listItem.getByText('issue.transition.wontfix'));
+ await user.type(listItem.getByRole('textbox'), 'test');
+ await user.click(listItem.getByText('resolve'));
});
- // Comment should open and close
- expect(listItem.getByRole('button', { name: 'issue.comment.formlink' })).toBeInTheDocument();
+
+ expect(
+ listItem.getByLabelText(
+ 'issue.transition.status_x_click_to_change.issue.simple_status.CONFIRMED',
+ ),
+ ).toBeInTheDocument();
+
+ // Change status again
await act(async () => {
- await user.keyboard('test');
- await user.click(listItem.getByRole('button', { name: 'issue.comment.formlink' }));
+ await user.click(listItem.getByText('issue.simple_status.CONFIRMED'));
+ await user.click(listItem.getByText('issue.transition.accept'));
+ await user.click(listItem.getByText('resolve'));
});
+
expect(
- listItem.queryByRole('button', { name: 'issue.comment.submit' }),
- ).not.toBeInTheDocument();
+ listItem.getByLabelText(
+ 'issue.transition.status_x_click_to_change.issue.simple_status.ACCEPTED',
+ ),
+ ).toBeInTheDocument();
// Assign issue to a different user
await act(async () => {
import { mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { ComponentPropsType } from '../../../../helpers/testUtils';
+import { IssueTransition } from '../../../../types/issues';
import { Issue } from '../../../../types/types';
import { CurrentUser } from '../../../../types/users';
import BulkChangeModal, { MAX_PAGE_SIZE } from '../BulkChangeModal';
it('should render transitions correctly', async () => {
renderBulkChangeModal([
- mockIssue(false, { actions: ['set_transition'], transitions: ['Transition1'] }),
+ mockIssue(false, { actions: ['set_transition'], transitions: [IssueTransition.FalsePositive] }),
]);
expect(await screen.findByText('issue.transition')).toBeInTheDocument();
- expect(await screen.findByText('issue.transition.Transition1')).toBeInTheDocument();
+ expect(await screen.findByText('issue.transition.falsepositive')).toBeInTheDocument();
});
it('should disable the submit button unless some change is configured', async () => {
mockIssue(false, {
actions: ['assign', 'set_transition', 'set_tags', 'set_type', 'set_severity', 'comment'],
key: 'issue1',
- transitions: ['Transition1', 'Transition2'],
+ transitions: [IssueTransition.Accept, IssueTransition.FalsePositive],
}),
mockIssue(false, {
actions: ['assign', 'set_transition', 'set_tags', 'set_type', 'set_severity', 'comment'],
key: 'issue2',
- transitions: ['Transition1', 'Transition2'],
+ transitions: [IssueTransition.Accept, IssueTransition.FalsePositive],
}),
],
{
await user.click(await screen.findByText('Toto'));
// Transition
- await user.click(await screen.findByText('issue.transition.Transition2'));
+ await user.click(await screen.findByText('issue.transition.accept'));
// Add a tag
await act(async () => {
add_tags: 'tag1,tag2',
assign: 'toto',
comment: 'some comment',
- do_transition: 'Transition2',
+ do_transition: 'accept',
sendNotifications: true,
});
});
const controlLabel = assigneeUser ? (
<>
- {renderAvatar(assigneeUser?.name, assigneeUser.avatar)} {assigneeUser.name}
+ <Avatar hash={assigneeUser.avatar} name={assigneeUser?.name} size="xs" className="sw-mt-1" />{' '}
+ {assigneeUser.name}
</>
) : (
UNASSIGNED.label
"projectQualifier": "TRK",
"rule": "squid:S4797",
"secondaryLocations": [],
+ "simpleStatus": "OPEN",
"status": "OPEN",
"tags": [
"cert",
"ruleName": "Handling files is security-sensitive",
"ruleStatus": "READY",
"secondaryLocations": [],
+ "simpleStatus": "OPEN",
"status": "OPEN",
"tags": [
"cert",
--- /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 {
+ StatusConfirmedIcon,
+ StatusOpenIcon,
+ StatusReopenedIcon,
+ StatusResolvedIcon,
+} from 'design-system';
+import * as React from 'react';
+import { IssueSimpleStatus } from '../../types/issues';
+import { Dict } from '../../types/types';
+import { IconProps } from './Icon';
+
+interface Props extends IconProps {
+ simpleStatus: IssueSimpleStatus;
+}
+
+const statusIcons: Dict<(props: IconProps) => React.ReactElement> = {
+ [IssueSimpleStatus.Accepted]: StatusConfirmedIcon,
+ [IssueSimpleStatus.Confirmed]: StatusConfirmedIcon,
+ [IssueSimpleStatus.FalsePositive]: StatusResolvedIcon,
+ [IssueSimpleStatus.Fixed]: StatusResolvedIcon,
+ [IssueSimpleStatus.Open]: StatusOpenIcon,
+ closed: StatusResolvedIcon,
+ confirm: StatusConfirmedIcon,
+ confirmed: StatusConfirmedIcon,
+ falsepositive: StatusResolvedIcon,
+ in_review: StatusConfirmedIcon,
+ open: StatusOpenIcon,
+ reopened: StatusReopenedIcon,
+ reopen: StatusReopenedIcon,
+ unconfirm: StatusReopenedIcon,
+ resolve: StatusResolvedIcon,
+ resolved: StatusResolvedIcon,
+ reviewed: StatusResolvedIcon,
+ to_review: StatusOpenIcon,
+ wontfix: StatusResolvedIcon,
+};
+
+export default function SimpleStatusIcon({ simpleStatus, ...iconProps }: Props) {
+ const DesiredStatusIcon = statusIcons[simpleStatus.toLowerCase()];
+
+ return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
+}
+++ /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 {
- StatusConfirmedIcon,
- StatusOpenIcon,
- StatusReopenedIcon,
- StatusResolvedIcon,
-} from 'design-system';
-import * as React from 'react';
-import { Dict } from '../../types/types';
-import { IconProps } from './Icon';
-
-interface Props extends IconProps {
- status: string;
-}
-
-const statusIcons: Dict<(props: IconProps) => React.ReactElement> = {
- closed: StatusResolvedIcon,
- confirm: StatusConfirmedIcon,
- confirmed: StatusConfirmedIcon,
- falsepositive: StatusResolvedIcon,
- in_review: StatusConfirmedIcon,
- open: StatusOpenIcon,
- reopened: StatusReopenedIcon,
- reopen: StatusReopenedIcon,
- unconfirm: StatusReopenedIcon,
- resolve: StatusResolvedIcon,
- resolved: StatusResolvedIcon,
- reviewed: StatusResolvedIcon,
- to_review: StatusOpenIcon,
- wontfix: StatusResolvedIcon,
-};
-
-export default function StatusIcon({ status, ...iconProps }: Props) {
- const DesiredStatusIcon = statusIcons[status.toLowerCase()];
-
- return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
-}
import {
IssueActions,
IssueSeverity,
- IssueStatus,
+ IssueSimpleStatus,
IssueTransition,
IssueType,
} from '../../../types/issues';
it('should allow updating the status', async () => {
const { ui } = getPageObject();
const issue = mockRawIssue(false, {
- status: IssueStatus.Open,
+ simpleStatus: IssueSimpleStatus.Open,
transitions: [IssueTransition.Confirm, IssueTransition.UnConfirm],
});
issuesHandler.setIssueList([{ issue, snippets: {} }]);
issue: mockIssue(false, { ...pick(issue, 'key', 'status', 'transitions') }),
});
- await ui.updateStatus(IssueStatus.Open, IssueTransition.Confirm);
- expect(ui.updateStatusBtn(IssueStatus.Confirmed).get()).toBeInTheDocument();
+ await ui.updateStatus(IssueSimpleStatus.Open, IssueTransition.Confirm);
+ expect(ui.updateStatusBtn(IssueSimpleStatus.Confirmed).get()).toBeInTheDocument();
});
it('should allow assigning', async () => {
setSeverityBtn: (severity: IssueSeverity) => byText(`severity.${severity}`),
// Status
- updateStatusBtn: (currentStatus: IssueStatus) =>
- byLabelText(`issue.transition.status_x_click_to_change.issue.status.${currentStatus}`),
+ updateStatusBtn: (currentStatus: IssueSimpleStatus) =>
+ byLabelText(`issue.transition.status_x_click_to_change.issue.simple_status.${currentStatus}`),
setStatusBtn: (transition: IssueTransition) => byText(`issue.transition.${transition}`),
// Assignee
await user.click(selectors.setSeverityBtn(newSeverity).get());
});
},
- async updateStatus(currentStatus: IssueStatus, transition: IssueTransition) {
+ async updateStatus(currentStatus: IssueSimpleStatus, transition: IssueTransition) {
await user.click(selectors.updateStatusBtn(currentStatus).get());
await act(async () => {
await user.click(selectors.setStatusBtn(transition).get());
resultPromise: Promise<IssueResponse>,
oldIssue?: Issue,
newIssue?: Issue,
-) => {
+): Promise<void> => {
const optimisticUpdate = oldIssue !== undefined && newIssue !== undefined;
if (optimisticUpdate) {
- onChange(newIssue!);
+ onChange(newIssue);
}
- resultPromise.then(
+ return resultPromise.then(
(response) => {
if (!optimisticUpdate) {
const issue = parseIssueFromResponse(
},
(param) => {
if (optimisticUpdate) {
- onChange(oldIssue!);
+ onChange(oldIssue);
}
throwGlobalError(param);
},
*/
import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
-import { IssueActions, IssueResolution, IssueType as IssueTypeEnum } from '../../../types/issues';
+import { IssueActions } from '../../../types/issues';
import { Issue } from '../../../types/types';
import SoftwareImpactPillList from '../../shared/SoftwareImpactPillList';
import IssueAssign from './IssueAssign';
showSonarLintBadge?: boolean;
}
-interface State {
- commentAutoTriggered: boolean;
- commentPlaceholder: string;
-}
-
export default function IssueActionsBar(props: Props) {
const {
issue,
showSonarLintBadge,
} = props;
- const [commentState, setCommentState] = React.useState<State>({
- commentAutoTriggered: false,
- commentPlaceholder: '',
- });
+ const [commentPlaceholder, setCommentPlaceholder] = React.useState('');
- const toggleComment = (open: boolean, placeholder = '', autoTriggered = false) => {
- setCommentState({
- commentPlaceholder: placeholder,
- commentAutoTriggered: autoTriggered,
- });
+ const toggleComment = (open: boolean, placeholder = '') => {
+ setCommentPlaceholder(placeholder);
togglePopup('comment', open);
};
- const handleTransition = (issue: Issue) => {
- onChange(issue);
-
- if (
- issue.resolution === IssueResolution.FalsePositive ||
- (issue.resolution === IssueResolution.WontFix && issue.type !== IssueTypeEnum.SecurityHotspot)
- ) {
- toggleComment(true, translate('issue.comment.explain_why'), true);
- }
- };
-
const canAssign = issue.actions.includes(IssueActions.Assign);
const canComment = issue.actions.includes(IssueActions.Comment);
- const hasTransitions = issue.transitions.length > 0;
return (
<div className="sw-flex sw-gap-3">
<ul className="it__issue-header-actions sw-flex sw-items-center sw-gap-3 sw-body-sm">
- <li>
+ <li className="sw-relative">
<IssueTransition
isOpen={currentPopup === 'transition'}
togglePopup={togglePopup}
- hasTransitions={hasTransitions}
issue={issue}
- onChange={handleTransition}
+ onChange={onChange}
/>
</li>
{canComment && (
<IssueCommentAction
- commentAutoTriggered={commentState.commentAutoTriggered}
- commentPlaceholder={commentState.commentPlaceholder}
+ commentPlaceholder={commentPlaceholder}
currentPopup={currentPopup === 'comment'}
issueKey={issue.key}
onChange={onChange}
issue: { assignee, assigneeName, assigneeLogin, assigneeAvatar },
} = props;
- const assinedUser = assigneeName || assignee;
+ const assinedUser = assigneeName ?? assignee;
const { currentUser } = React.useContext(CurrentUserContext);
const allowCurrentUserSelection = isLoggedIn(currentUser) && currentUser?.login !== assigneeLogin;
import CommentPopup from '../popups/CommentPopup';
interface Props {
- commentAutoTriggered?: boolean;
commentPlaceholder: string;
currentPopup?: boolean;
issueKey: string;
open={!!this.props.currentPopup}
overlay={
<CommentPopup
- autoTriggered={this.props.commentAutoTriggered}
onComment={this.addComment}
placeholder={this.props.commentPlaceholder}
toggleComment={this.props.toggleComment}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { DiscreetSelect } from 'design-system';
+import { Dropdown, PopupPlacement, PopupZLevel, SearchSelectDropdownControl } from 'design-system';
import * as React from 'react';
-import { GroupBase, OptionProps, components } from 'react-select';
-import { setIssueTransition } from '../../../api/issues';
+import { addIssueComment, setIssueTransition } from '../../../api/issues';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Issue } from '../../../types/types';
-import { LabelValueSelectOption } from '../../controls/Select';
-import StatusIcon from '../../icons/StatusIcon';
import StatusHelper from '../../shared/StatusHelper';
import { updateIssue } from '../actions';
+import { IssueTransitionOverlay } from './IssueTransitionOverlay';
interface Props {
- hasTransitions: boolean;
isOpen: boolean;
- issue: Pick<Issue, 'key' | 'resolution' | 'status' | 'transitions' | 'type'>;
+ issue: Pick<Issue, 'key' | 'resolution' | 'simpleStatus' | 'transitions' | 'type' | 'actions'>;
onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
}
-function SingleValueFactory(issue: Props['issue']) {
- return function SingleValue<
- V,
- Option extends LabelValueSelectOption<V>,
- IsMulti extends boolean = false,
- Group extends GroupBase<Option> = GroupBase<Option>,
- >(props: OptionProps<Option, IsMulti, Group>) {
- return (
- <components.SingleValue {...props}>
- <StatusHelper
- className="sw-flex sw-items-center"
- resolution={issue.resolution}
- status={issue.status}
- />
- </components.SingleValue>
- );
- };
-}
-
-export default class IssueTransition extends React.PureComponent<Props> {
- setTransition = ({ value }: { value: string }) => {
- updateIssue(
- this.props.onChange,
- // eslint-disable-next-line local-rules/no-api-imports
- setIssueTransition({ issue: this.props.issue.key, transition: value }),
- );
-
- this.toggleSetTransition(false);
- };
-
- toggleSetTransition = (open: boolean) => {
- this.props.togglePopup('transition', open);
- };
-
- handleClose = () => {
- this.toggleSetTransition(false);
- };
+export default function IssueTransition(props: Readonly<Props>) {
+ const { isOpen, issue, onChange, togglePopup } = props;
- render() {
- const { issue } = this.props;
+ const [transitioning, setTransitioning] = React.useState(false);
- const transitions = issue.transitions.map((transition) => ({
- label: translate('issue.transition', transition),
- value: transition,
- Icon: <StatusIcon status={transition} />,
- }));
+ async function handleSetTransition(transition: string, comment?: string) {
+ setTransitioning(true);
- if (this.props.hasTransitions) {
- return (
- <DiscreetSelect
- aria-label={translateWithParameters(
- 'issue.transition.status_x_click_to_change',
- translate('issue.status', issue.status),
- )}
- size="medium"
- className="it__issue-transition"
- components={{ SingleValue: SingleValueFactory(issue) }}
- menuIsOpen={this.props.isOpen && this.props.hasTransitions}
- options={transitions}
- setValue={this.setTransition}
- onMenuClose={this.handleClose}
- onMenuOpen={() => this.toggleSetTransition(true)}
- value={issue.resolution ?? 'OPEN'}
- customValue={
- <StatusHelper className="sw-flex" resolution={issue.resolution} status={issue.status} />
- }
- />
- );
+ try {
+ if (typeof comment === 'string' && comment.length > 0) {
+ await setIssueTransition({ issue: issue.key, transition });
+ await updateIssue(onChange, addIssueComment({ issue: issue.key, text: comment }));
+ } else {
+ await updateIssue(onChange, setIssueTransition({ issue: issue.key, transition }));
+ }
+ togglePopup('transition', false);
+ } finally {
+ setTransitioning(false);
}
+ }
- const resolution = issue.resolution && ` (${translate('issue.resolution', issue.resolution)})`;
-
- return (
- <span className="sw-flex sw-items-center sw-gap-1">
- <StatusIcon status={issue.status} />
+ function handleClose() {
+ togglePopup('transition', false);
+ }
- {translate('issue.status', issue.status)}
+ function onToggleClick() {
+ togglePopup('transition', !isOpen);
+ }
- {resolution}
- </span>
+ if (issue.transitions?.length) {
+ return (
+ <Dropdown
+ allowResizing
+ closeOnClick={false}
+ id="issue-transition"
+ onClose={handleClose}
+ openDropdown={isOpen}
+ overlay={
+ <IssueTransitionOverlay
+ issue={issue}
+ onClose={handleClose}
+ onSetTransition={handleSetTransition}
+ loading={transitioning}
+ />
+ }
+ placement={PopupPlacement.Bottom}
+ zLevel={PopupZLevel.Absolute}
+ size="medium"
+ >
+ {({ a11yAttrs }) => (
+ <SearchSelectDropdownControl
+ {...a11yAttrs}
+ onClick={onToggleClick}
+ onClear={handleClose}
+ isDiscreet
+ className="it__issue-transition sw-px-1"
+ label={
+ <StatusHelper className="sw-flex sw-items-center" simpleStatus={issue.simpleStatus} />
+ }
+ ariaLabel={translateWithParameters(
+ 'issue.transition.status_x_click_to_change',
+ translate('issue.simple_status', issue.simpleStatus),
+ )}
+ />
+ )}
+ </Dropdown>
);
}
+
+ return <StatusHelper simpleStatus={issue.simpleStatus} />;
}
--- /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 {
+ HelperHintIcon,
+ ItemButton,
+ PageContentFontWrapper,
+ PopupPlacement,
+ TextBold,
+ TextMuted,
+ Tooltip,
+} from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { translate } from '../../../helpers/l10n';
+import { IssueTransition } from '../../../types/issues';
+
+type Props = {
+ transition: IssueTransition;
+ selectedTransition?: IssueTransition;
+ onSelectTransition: (transition: IssueTransition) => void;
+};
+
+export function IssueTransitionItem({
+ transition,
+ selectedTransition,
+ onSelectTransition,
+}: Readonly<Props>) {
+ const intl = useIntl();
+
+ const tooltips: Record<string, React.ReactFragment> = {
+ [IssueTransition.Confirm]: (
+ <div className="sw-flex sw-flex-col sw-gap-2">
+ <span>{translate('issue.transition.confirm.deprecated_tooltip.1')}</span>
+ <span>{translate('issue.transition.confirm.deprecated_tooltip.2')}</span>
+ <span>{translate('issue.transition.confirm.deprecated_tooltip.3')}</span>
+ <span>{translate('issue.transition.confirm.deprecated_tooltip.4')}</span>
+ </div>
+ ),
+ [IssueTransition.Resolve]: (
+ <div className="sw-flex sw-flex-col sw-gap-2">
+ <span>{translate('issue.transition.resolve.deprecated_tooltip.1')}</span>
+ <span>{translate('issue.transition.resolve.deprecated_tooltip.2')}</span>
+ <span>{translate('issue.transition.resolve.deprecated_tooltip.3')}</span>
+ </div>
+ ),
+ };
+
+ return (
+ <ItemButton
+ key={transition}
+ onClick={() => onSelectTransition(transition)}
+ selected={selectedTransition === transition}
+ className="sw-px-4"
+ >
+ <div className="it__issue-transition-option sw-flex sw-flex-col">
+ <PageContentFontWrapper className="sw-font-semibold sw-flex sw-gap-1 sw-items-center">
+ <TextBold name={intl.formatMessage({ id: `issue.transition.${transition}` })} />
+ {tooltips[transition] && (
+ <Tooltip overlay={<div>{tooltips[transition]}</div>} placement={PopupPlacement.Right}>
+ <HelperHintIcon />
+ </Tooltip>
+ )}
+ </PageContentFontWrapper>
+ <TextMuted text={translate('issue.transition', transition, 'description')} />
+ </div>
+ </ItemButton>
+ );
+}
--- /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 {
+ ButtonPrimary,
+ ButtonSecondary,
+ InputTextArea,
+ ItemDivider,
+ PageContentFontWrapper,
+ Spinner,
+} from 'design-system';
+import * as React from 'react';
+import { useState } from 'react';
+import { useIntl } from 'react-intl';
+import { translate } from '../../../helpers/l10n';
+import { IssueActions, IssueTransition } from '../../../types/issues';
+import { Issue } from '../../../types/types';
+import { isTransitionDeprecated, isTransitionHidden, transitionRequiresComment } from '../helpers';
+import { IssueTransitionItem } from './IssueTransitionItem';
+
+export type Props = {
+ issue: Pick<Issue, 'transitions' | 'actions'>;
+ onClose: () => void;
+ onSetTransition: (transition: IssueTransition, comment?: string) => void;
+ loading?: boolean;
+};
+
+export function IssueTransitionOverlay(props: Readonly<Props>) {
+ const { issue, onClose, onSetTransition, loading } = props;
+
+ const intl = useIntl();
+
+ const [comment, setComment] = useState('');
+ const [selectedTransition, setSelectedTransition] = useState<IssueTransition>();
+
+ const hasCommentAction = issue.actions.includes(IssueActions.Comment);
+
+ function selectTransition(transition: IssueTransition) {
+ if (!transitionRequiresComment(transition) || !hasCommentAction) {
+ onSetTransition(transition);
+ } else {
+ setSelectedTransition(transition);
+ }
+ }
+
+ function handleResolve() {
+ if (selectedTransition) {
+ onSetTransition(selectedTransition, comment);
+ }
+ }
+
+ // Filter out hidden transitions and separate deprecated transitions in a different list
+ const filteredTransitions = issue.transitions.filter(
+ (transition) => !isTransitionHidden(transition),
+ );
+ const filteredTransitionsRecommended = filteredTransitions.filter(
+ (t) => !isTransitionDeprecated(t),
+ );
+ const filteredTransitionsDeprecated = filteredTransitions.filter(isTransitionDeprecated);
+
+ return (
+ <ul className="sw-flex sw-flex-col">
+ {filteredTransitionsRecommended.map((transition) => (
+ <IssueTransitionItem
+ key={transition}
+ transition={transition}
+ selectedTransition={selectedTransition}
+ onSelectTransition={selectTransition}
+ />
+ ))}
+ {filteredTransitionsRecommended.length > 0 && filteredTransitionsDeprecated.length > 0 && (
+ <ItemDivider />
+ )}
+ {filteredTransitionsDeprecated.map((transition) => (
+ <IssueTransitionItem
+ key={transition}
+ transition={transition}
+ selectedTransition={selectedTransition}
+ onSelectTransition={selectTransition}
+ />
+ ))}
+
+ {selectedTransition && (
+ <>
+ <ItemDivider />
+ <div className="sw-mx-4 sw-mt-2">
+ <PageContentFontWrapper className="sw-font-semibold">
+ {intl.formatMessage({ id: 'issue.transition.comment' })}
+ </PageContentFontWrapper>
+ <InputTextArea
+ autoFocus
+ onChange={(event) => setComment(event.currentTarget.value)}
+ placeholder={translate(
+ 'issue.transition.comment.placeholder',
+ selectedTransition ?? '',
+ )}
+ rows={5}
+ value={comment}
+ size="auto"
+ className="sw-mt-2 sw-resize-y sw-w-full"
+ />
+ <Spinner loading={loading} className="sw-float-right sw-m-2">
+ <div className="sw-mt-2 sw-flex sw-gap-3 sw-justify-end">
+ <ButtonPrimary onClick={handleResolve}>{translate('resolve')}</ButtonPrimary>
+ <ButtonSecondary onClick={onClose}>{translate('cancel')}</ButtonSecondary>
+ </div>
+ </Spinner>
+ </div>
+ </>
+ )}
+
+ {!selectedTransition && loading && (
+ <div className="sw-flex sw-justify-center sw-m-2">
+ <Spinner loading className="sw-float-right sw-2" />
+ </div>
+ )}
+ </ul>
+ );
+}
--- /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 { IssueTransition } from '../../types/issues';
+
+export function isTransitionDeprecated(transition: IssueTransition) {
+ return transition === IssueTransition.Confirm || transition === IssueTransition.Resolve;
+}
+
+export function isTransitionHidden(transition: IssueTransition) {
+ return transition === IssueTransition.WontFix;
+}
+
+export function transitionRequiresComment(transition: IssueTransition) {
+ return [
+ IssueTransition.Accept,
+ IssueTransition.Confirm,
+ IssueTransition.FalsePositive,
+ IssueTransition.Resolve,
+ ].includes(transition);
+}
toggleComment: (visible: boolean) => void;
placeholder: string;
placement?: PopupPlacement;
- autoTriggered?: boolean;
}
export default class CommentPopup extends React.PureComponent<CommentPopupProps> {
};
render() {
- const { comment, autoTriggered } = this.props;
+ const { comment } = this.props;
return (
<DropdownOverlay placement={this.props.placement}>
onSaveComment={this.props.onComment}
showFormatHelp
comment={comment?.markdown}
- autoTriggered={autoTriggered}
/>
</div>
</DropdownOverlay>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import StatusIcon from '../../components/icons/StatusIcon';
import { translate } from '../../helpers/l10n';
+import { IssueSimpleStatus } from '../../types/issues';
+import SimpleStatusIcon from '../icons/SimpleStatusIcon';
interface Props {
className?: string;
- resolution: string | undefined;
- status: string;
+ simpleStatus: IssueSimpleStatus;
}
export default function StatusHelper(props: Props) {
- const resolution = props.resolution && ` (${translate('issue.resolution', props.resolution)})`;
return (
<span className={props.className}>
- <StatusIcon className="little-spacer-right" status={props.status} />
- {translate('issue.status', props.status)}
- {resolution}
+ <SimpleStatusIcon className="sw-mr-1" simpleStatus={props.simpleStatus} />
+ {translate('issue.simple_status', props.simpleStatus)}
</span>
);
}
*/
import { BugIcon, CodeSmellIcon, SecurityHotspotIcon, VulnerabilityIcon } from 'design-system';
import { flatten, sortBy } from 'lodash';
-import { IssueType, RawIssue } from '../types/issues';
+import { IssueSimpleStatus, IssueStatus, IssueType, RawIssue } from '../types/issues';
import { MetricKey } from '../types/metrics';
import { Dict, Flow, FlowLocation, FlowType, Issue, TextRange } from '../types/types';
import { UserBase } from '../types/users';
...splitFlows(issue, components),
...prepareClosed(issue),
...ensureTextRange(issue),
+ simpleStatus:
+ issue.simpleStatus ??
+ {
+ [IssueStatus.Open]: IssueSimpleStatus.Open,
+ [IssueStatus.Reopened]: IssueSimpleStatus.Open,
+ [IssueStatus.Closed]: IssueSimpleStatus.Fixed,
+ [IssueStatus.Resolved]: IssueSimpleStatus.Fixed,
+ [IssueStatus.Confirmed]: IssueSimpleStatus.Confirmed,
+ }[issue.status] ??
+ IssueSimpleStatus.Open,
} as Issue;
}
} from '../types/clean-code-taxonomy';
import { RuleRepository } from '../types/coding-rules';
import { EditionKey } from '../types/editions';
-import { IssueScope, IssueSeverity, IssueStatus, IssueType, RawIssue } from '../types/issues';
+import {
+ IssueScope,
+ IssueSeverity,
+ IssueSimpleStatus,
+ IssueStatus,
+ IssueType,
+ RawIssue,
+} from '../types/issues';
import { Language } from '../types/languages';
import { MetricKey, MetricType } from '../types/metrics';
import { Notification } from '../types/notifications';
rule: 'javascript:S1067',
severity: IssueSeverity.Major,
status: IssueStatus.Open,
+ simpleStatus: IssueSimpleStatus.Open,
textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 },
type: IssueType.CodeSmell,
transitions: [],
secondaryLocations: [],
severity: IssueSeverity.Major,
status: IssueStatus.Open,
+ simpleStatus: IssueSimpleStatus.Open,
textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 },
transitions: [],
type: 'BUG',
Closed = 'CLOSED',
}
+export enum IssueSimpleStatus {
+ Open = 'OPEN',
+ Fixed = 'FIXED',
+ Confirmed = 'CONFIRMED',
+ Accepted = 'ACCEPTED',
+ FalsePositive = 'FALSE_POSITIVE',
+}
+
export enum IssueActions {
SetType = 'set_type',
SetTags = 'set_tags',
}
export enum IssueTransition {
+ Accept = 'accept',
Confirm = 'confirm',
UnConfirm = 'unconfirm',
Resolve = 'resolve',
export interface RawIssue {
actions: string[];
- transitions: string[];
+ transitions: IssueTransition[];
tags?: string[];
assignee?: string;
author?: string;
message?: string;
severity: string;
status: string;
+ simpleStatus: IssueSimpleStatus;
textRange?: TextRange;
type: IssueType;
scope: string;
SoftwareQuality,
} from './clean-code-taxonomy';
import { ComponentQualifier, Visibility } from './component';
-import { MessageFormatting } from './issues';
+import { IssueSimpleStatus, IssueTransition, MessageFormatting } from './issues';
import { NewCodeDefinitionType } from './new-code-definition';
import { UserActive, UserBase } from './users';
secondaryLocations: FlowLocation[];
severity: string;
status: string;
+ simpleStatus: IssueSimpleStatus;
tags?: string[];
textRange?: TextRange;
- transitions: string[];
+ transitions: IssueTransition[];
type: IssueType;
}
reset_to_default=Reset To Default
reset_date=Reset dates
resolution=Resolution
+resolve=Resolve
restart=Restart
restore=Restore
result=Result
issue.transition.community_plug_link=SonarSource Community
issue.transition.status_x_click_to_change=Issue status: {0}, click to change
issue.transition=Transition
+issue.transition.accept=Accept
+issue.transition.accept.description="Won't fix now"
issue.transition.confirm=Confirm
-issue.transition.confirm.description=This issue has been reviewed and something should be done eventually to handle it.
-issue.transition.unconfirm=Unconfirm
-issue.transition.unconfirm.description=This issue should be reviewed again to decide what to do with it.
-issue.transition.resolve=Resolve as fixed
-issue.transition.resolve.description=This issue has been fixed in the code and is waiting for the next analysis to close it - or reopen it if it was not actually fixed.
-issue.transition.falsepositive=Resolve as false positive
-issue.transition.falsepositive.description=This issue can be suppressed as it was not raised accurately. Please report false-positives to the {community_plug_link}!
-issue.transition.reopen=Reopen
-issue.transition.reopen.description=This issue is not resolved, and should be reviewed again.
+issue.transition.confirm.description=Deprecated
+issue.transition.confirm.deprecated_tooltip.1=The Confirm action is deprecated.
+issue.transition.confirm.deprecated_tooltip.2=The next analysis result will show if the issue has been fixed, otherwise it will reopen it automatically.
+issue.transition.confirm.deprecated_tooltip.3=If you were using Confirm to communicate with team members, consider assigning the issue or using comments and tags instead.
+issue.transition.confirm.deprecated_tooltip.4=If you have reviewed this issue but cannot fix it now, consider marking it as Accepted.
+issue.transition.unconfirm=Open
+issue.transition.unconfirm.description=Reopen issue
+issue.transition.resolve=Fixed
+issue.transition.resolve.description=Deprecated
+issue.transition.resolve.deprecated_tooltip.1=The Resolve as Fixed action is deprecated.
+issue.transition.resolve.deprecated_tooltip.2=The next analysis result will show if the issue has been fixed, otherwise it will reopen the issue automatically.
+issue.transition.resolve.deprecated_tooltip.3=If you were using Resolve as Fixed to communicate with team members that an issue is being fixed, consider assigning it or using comments and tags instead.
+issue.transition.falsepositive=False Positive
+issue.transition.falsepositive.description=Analysis is mistaken
+issue.transition.reopen=Open
+issue.transition.reopen.description=Reopen issue
+issue.transition.comment=Status change comment
+issue.transition.comment.placeholder.accept=Share why (optional)
+issue.transition.comment.placeholder.confirm=Share why this is confirmed (optional)
+issue.transition.comment.placeholder.resolve=Share why this is fixed (optional)
+issue.transition.comment.placeholder.falsepositive=Share why this is a false positive (optional)
issue.transition.close=Close
issue.transition.close.description=
-issue.transition.wontfix=Resolve as won't fix
-issue.transition.wontfix.description=This issue can be suppressed because the rule is irrelevant in this context.
-issue.transition.setinreview=Set as In Review
-issue.transition.setinreview.description=A review is in progress to check for a vulnerability
+issue.transition.wontfix=Won't Fix
+issue.transition.wontfix.description=Deprecated
issue.transition.openasvulnerability=Open as Vulnerability
issue.transition.openasvulnerability.description=There's a Vulnerability in the code that must be fixed
issue.transition.resolveasreviewed=Resolve as Reviewed
issue.clean_code_attribute.TRUSTWORTHY.title=This is a responsibility issue, the code is not trustworthy enough.
issue.clean_code_attribute.TRUSTWORTHY.advice=To be trustworthy, the code needs to abstain from revealing or hard-coding private information.
+issue.simple_status.OPEN=Open
+issue.simple_status.ACCEPTED=Accepted
+issue.simple_status.CONFIRMED=Confirmed
+issue.simple_status.FIXED=Fixed
+issue.simple_status.FALSE_POSITIVE=False Positive
+
+issue.status.ACCEPTED=Accepted
issue.status.REOPENED=Reopened
issue.status.RESOLVED=Resolved
issue.status.OPEN=Open