Browse Source

SONAR-20870 Update issue transitions to allow accepting issues

tags/10.4.0.87286
7PH 7 months ago
parent
commit
a2d67088ee
27 changed files with 529 additions and 217 deletions
  1. 4
    1
      server/sonar-web/design-system/src/components/Dropdown.tsx
  2. 28
    7
      server/sonar-web/design-system/src/components/DropdownMenu.tsx
  3. 2
    2
      server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
  4. 1
    0
      server/sonar-web/design-system/src/components/input/index.ts
  5. 18
    24
      server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
  6. 16
    2
      server/sonar-web/src/main/js/api/mocks/data/issues.ts
  7. 23
    16
      server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
  8. 7
    6
      server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
  9. 2
    1
      server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx
  10. 2
    0
      server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap
  11. 9
    3
      server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx
  12. 7
    7
      server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
  13. 4
    4
      server/sonar-web/src/main/js/components/issue/actions.ts
  14. 7
    33
      server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
  15. 1
    1
      server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx
  16. 0
    2
      server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx
  17. 65
    80
      server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx
  18. 86
    0
      server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx
  19. 136
    0
      server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx
  20. 37
    0
      server/sonar-web/src/main/js/components/issue/helpers.ts
  21. 1
    3
      server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx
  22. 5
    7
      server/sonar-web/src/main/js/components/shared/StatusHelper.tsx
  23. 11
    1
      server/sonar-web/src/main/js/helpers/issues.ts
  24. 10
    1
      server/sonar-web/src/main/js/helpers/testMocks.ts
  25. 11
    1
      server/sonar-web/src/main/js/types/issues.ts
  26. 3
    2
      server/sonar-web/src/main/js/types/types.ts
  27. 33
    13
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 4
- 1
server/sonar-web/design-system/src/components/Dropdown.tsx View File

@@ -67,7 +67,10 @@ export class Dropdown extends React.PureComponent<Readonly<Props>, State> {
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 });
}
}

+ 28
- 7
server/sonar-web/design-system/src/components/DropdownMenu.tsx View File

@@ -68,6 +68,7 @@ interface ListItemProps {
onFocus?: VoidFunction;
onPointerEnter?: VoidFunction;
onPointerLeave?: VoidFunction;
selected?: boolean;
}

type ItemLinkProps = Omit<ListItemProps, 'innerRef'> &
@@ -76,12 +77,22 @@ 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}
@@ -102,11 +113,12 @@ interface ItemNavLinkProps extends ItemLinkProps {
}

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}
@@ -128,10 +140,15 @@ interface ItemButtonProps extends ListItemProps {
}

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>
@@ -336,6 +353,10 @@ const itemStyle = (props: ThemedProps) => css`
${tw`sw-cursor-not-allowed`};
}

&.selected {
background-color: ${themeColor('selectOptionSelected')(props)};
}

& > svg {
${tw`sw-mr-2`}
}

+ 2
- 2
server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx View File

@@ -104,7 +104,7 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr
);
}

const StyledControl = styled.div`
export const StyledControl = styled.div`
color: ${themeContrast('inputBackground')};
background: ${themeColor('inputBackground')};
border: ${themeBorder('default', 'inputBorder')};
@@ -121,7 +121,7 @@ const StyledControl = styled.div`

&.is-discreet {
${tw`sw-border-none`};
${tw`sw-p-0`};
${tw`sw-px-1`};
${tw`sw-w-auto sw-h-auto`};

background: inherit;

+ 1
- 0
server/sonar-web/design-system/src/components/input/index.ts View File

@@ -31,3 +31,4 @@ export * from './MultiSelectMenu';
export * from './RadioButton';
export * from './SearchSelect';
export * from './SearchSelectDropdown';
export * from './SearchSelectDropdownControl';

+ 18
- 24
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts View File

@@ -39,7 +39,7 @@ import {
import { SearchRulesResponse } from '../../types/coding-rules';
import {
ASSIGNEE_ME,
IssueResolution,
IssueSimpleStatus,
IssueStatus,
IssueTransition,
IssueType,
@@ -525,45 +525,39 @@ export default class IssuesServiceMock {
};

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,
);

+ 16
- 2
server/sonar-web/src/main/js/api/mocks/data/issues.ts View File

@@ -31,7 +31,9 @@ import {
IssueResolution,
IssueScope,
IssueSeverity,
IssueSimpleStatus,
IssueStatus,
IssueTransition,
IssueType,
RawIssue,
} from '../../../types/issues';
@@ -296,7 +298,13 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
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],
@@ -312,6 +320,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
ruleDescriptionContextKey: 'spring',
resolution: IssueResolution.Unresolved,
status: IssueStatus.Open,
simpleStatus: IssueSimpleStatus.Open,
}),
snippets: keyBy(
[
@@ -354,7 +363,12 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
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],

+ 23
- 16
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx View File

@@ -154,36 +154,43 @@ describe('issue app', () => {
// 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 () => {

+ 7
- 6
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx View File

@@ -26,6 +26,7 @@ import CurrentUserContextProvider from '../../../../app/components/current-user/
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';
@@ -70,11 +71,11 @@ it('should render tags correctly', async () => {

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 () => {
@@ -108,12 +109,12 @@ it('should properly submit', 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],
}),
],
{
@@ -136,7 +137,7 @@ it('should properly submit', async () => {
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 () => {
@@ -161,7 +162,7 @@ it('should properly submit', async () => {
add_tags: 'tag1,tag2',
assign: 'toto',
comment: 'some comment',
do_transition: 'Transition2',
do_transition: 'accept',
sendNotifications: true,
});
});

+ 2
- 1
server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx View File

@@ -68,7 +68,8 @@ export default function Assignee(props: Props) {

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

+ 2
- 0
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap View File

@@ -33,6 +33,7 @@ exports[`loadIssues should load issues with listIssues if re-indexing 1`] = `
"projectQualifier": "TRK",
"rule": "squid:S4797",
"secondaryLocations": [],
"simpleStatus": "OPEN",
"status": "OPEN",
"tags": [
"cert",
@@ -95,6 +96,7 @@ exports[`loadIssues should load issues with searchIssues if not re-indexing 1`]
"ruleName": "Handling files is security-sensitive",
"ruleStatus": "READY",
"secondaryLocations": [],
"simpleStatus": "OPEN",
"status": "OPEN",
"tags": [
"cert",

server/sonar-web/src/main/js/components/icons/StatusIcon.tsx → server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx View File

@@ -25,14 +25,20 @@ import {
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 {
status: string;
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,
@@ -49,8 +55,8 @@ const statusIcons: Dict<(props: IconProps) => React.ReactElement> = {
wontfix: StatusResolvedIcon,
};

export default function StatusIcon({ status, ...iconProps }: Props) {
const DesiredStatusIcon = statusIcons[status.toLowerCase()];
export default function SimpleStatusIcon({ simpleStatus, ...iconProps }: Props) {
const DesiredStatusIcon = statusIcons[simpleStatus.toLowerCase()];

return DesiredStatusIcon ? <DesiredStatusIcon {...iconProps} /> : null;
}

+ 7
- 7
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx View File

@@ -32,7 +32,7 @@ import { ComponentPropsType } from '../../../helpers/testUtils';
import {
IssueActions,
IssueSeverity,
IssueStatus,
IssueSimpleStatus,
IssueTransition,
IssueType,
} from '../../../types/issues';
@@ -97,7 +97,7 @@ describe('updating', () => {
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: {} }]);
@@ -105,8 +105,8 @@ describe('updating', () => {
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 () => {
@@ -244,8 +244,8 @@ function getPageObject() {
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
@@ -297,7 +297,7 @@ function getPageObject() {
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());

+ 4
- 4
server/sonar-web/src/main/js/components/issue/actions.ts View File

@@ -27,13 +27,13 @@ export const updateIssue = (
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(
@@ -47,7 +47,7 @@ export const updateIssue = (
},
(param) => {
if (optimisticUpdate) {
onChange(oldIssue!);
onChange(oldIssue);
}
throwGlobalError(param);
},

+ 7
- 33
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx View File

@@ -19,8 +19,7 @@
*/

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';
@@ -40,11 +39,6 @@ interface Props {
showSonarLintBadge?: boolean;
}

interface State {
commentAutoTriggered: boolean;
commentPlaceholder: string;
}

export default function IssueActionsBar(props: Props) {
const {
issue,
@@ -56,45 +50,26 @@ export default function IssueActionsBar(props: Props) {
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>

@@ -132,8 +107,7 @@ export default function IssueActionsBar(props: Props) {

{canComment && (
<IssueCommentAction
commentAutoTriggered={commentState.commentAutoTriggered}
commentPlaceholder={commentState.commentPlaceholder}
commentPlaceholder={commentPlaceholder}
currentPopup={currentPopup === 'comment'}
issueKey={issue.key}
onChange={onChange}

+ 1
- 1
server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx View File

@@ -49,7 +49,7 @@ export default function IssueAssignee(props: Props) {
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;

+ 0
- 2
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx View File

@@ -25,7 +25,6 @@ import { updateIssue } from '../actions';
import CommentPopup from '../popups/CommentPopup';

interface Props {
commentAutoTriggered?: boolean;
commentPlaceholder: string;
currentPopup?: boolean;
issueKey: string;
@@ -60,7 +59,6 @@ export default class IssueCommentAction extends React.PureComponent<Props> {
open={!!this.props.currentPopup}
overlay={
<CommentPopup
autoTriggered={this.props.commentAutoTriggered}
onComment={this.addComment}
placeholder={this.props.commentPlaceholder}
toggleComment={this.props.toggleComment}

+ 65
- 80
server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx View File

@@ -18,105 +18,90 @@
* 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} />;
}

+ 86
- 0
server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx View File

@@ -0,0 +1,86 @@
/*
* 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>
);
}

+ 136
- 0
server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx View File

@@ -0,0 +1,136 @@
/*
* 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>
);
}

+ 37
- 0
server/sonar-web/src/main/js/components/issue/helpers.ts View File

@@ -0,0 +1,37 @@
/*
* 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);
}

+ 1
- 3
server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx View File

@@ -29,7 +29,6 @@ export interface CommentPopupProps {
toggleComment: (visible: boolean) => void;
placeholder: string;
placement?: PopupPlacement;
autoTriggered?: boolean;
}

export default class CommentPopup extends React.PureComponent<CommentPopupProps> {
@@ -38,7 +37,7 @@ export default class CommentPopup extends React.PureComponent<CommentPopupProps>
};

render() {
const { comment, autoTriggered } = this.props;
const { comment } = this.props;

return (
<DropdownOverlay placement={this.props.placement}>
@@ -49,7 +48,6 @@ export default class CommentPopup extends React.PureComponent<CommentPopupProps>
onSaveComment={this.props.onComment}
showFormatHelp
comment={comment?.markdown}
autoTriggered={autoTriggered}
/>
</div>
</DropdownOverlay>

+ 5
- 7
server/sonar-web/src/main/js/components/shared/StatusHelper.tsx View File

@@ -18,22 +18,20 @@
* 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>
);
}

+ 11
- 1
server/sonar-web/src/main/js/helpers/issues.ts View File

@@ -19,7 +19,7 @@
*/
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';
@@ -160,6 +160,16 @@ export function parseIssueFromResponse(
...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;
}


+ 10
- 1
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -32,7 +32,14 @@ import {
} 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';
@@ -304,6 +311,7 @@ export function mockRawIssue(withLocations = false, overrides: Partial<RawIssue>
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: [],
@@ -358,6 +366,7 @@ export function mockIssue(withLocations = false, overrides: Partial<Issue> = {})
secondaryLocations: [],
severity: IssueSeverity.Major,
status: IssueStatus.Open,
simpleStatus: IssueSimpleStatus.Open,
textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 },
transitions: [],
type: 'BUG',

+ 11
- 1
server/sonar-web/src/main/js/types/issues.ts View File

@@ -66,6 +66,14 @@ export enum IssueStatus {
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',
@@ -75,6 +83,7 @@ export enum IssueActions {
}

export enum IssueTransition {
Accept = 'accept',
Confirm = 'confirm',
UnConfirm = 'unconfirm',
Resolve = 'resolve',
@@ -112,7 +121,7 @@ export interface RawFlowLocation {

export interface RawIssue {
actions: string[];
transitions: string[];
transitions: IssueTransition[];
tags?: string[];
assignee?: string;
author?: string;
@@ -140,6 +149,7 @@ export interface RawIssue {
message?: string;
severity: string;
status: string;
simpleStatus: IssueSimpleStatus;
textRange?: TextRange;
type: IssueType;
scope: string;

+ 3
- 2
server/sonar-web/src/main/js/types/types.ts View File

@@ -25,7 +25,7 @@ import {
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';

@@ -291,9 +291,10 @@ export interface Issue {
secondaryLocations: FlowLocation[];
severity: string;
status: string;
simpleStatus: IssueSimpleStatus;
tags?: string[];
textRange?: TextRange;
transitions: string[];
transitions: IssueTransition[];
type: IssueType;
}


+ 33
- 13
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -192,6 +192,7 @@ reset_verb=Reset
reset_to_default=Reset To Default
reset_date=Reset dates
resolution=Resolution
resolve=Resolve
restart=Restart
restore=Restore
result=Result
@@ -923,22 +924,34 @@ issue.severity.severity_x_click_to_change=Severity: {0}, click to change
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
@@ -1040,6 +1053,13 @@ issue.clean_code_attribute.TRUSTWORTHY=Not trustworthy
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

Loading…
Cancel
Save