]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20870 Update issue transitions to allow accepting issues
author7PH <benjamin.raymond@sonarsource.com>
Mon, 30 Oct 2023 13:39:03 +0000 (14:39 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 8 Nov 2023 20:02:52 +0000 (20:02 +0000)
28 files changed:
server/sonar-web/design-system/src/components/Dropdown.tsx
server/sonar-web/design-system/src/components/DropdownMenu.tsx
server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
server/sonar-web/design-system/src/components/input/index.ts
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/issues.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx
server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap
server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons/StatusIcon.tsx [deleted file]
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
server/sonar-web/src/main/js/components/issue/actions.ts
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
server/sonar-web/src/main/js/components/issue/components/IssueAssign.tsx
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx
server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx
server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/helpers.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx
server/sonar-web/src/main/js/components/shared/StatusHelper.tsx
server/sonar-web/src/main/js/helpers/issues.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/types/issues.ts
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6c03ec66f85bc9f0bb01fcfd7960d0d4a17aea30..3bd340bcdf9c4f05102668f9e167109f406d53d9 100644 (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 });
     }
   }
index e87d873e219e5183e207a22cf0ddefd1122f2148..bf983364dc3232e220f2f7d3a60c73c423a6d7ac 100644 (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`}
   }
index 71c64b2a831174c3f9019271a9f7a445170c4753..175a5b9c09fb18379c7bac10e6b73a48b0d1eb1a 100644 (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;
index ca90354aa44ce6a15fd70ee60193c5e14e34dfde..740a84caa85c9f7f679ad77d35aa72a4096ec808 100644 (file)
@@ -31,3 +31,4 @@ export * from './MultiSelectMenu';
 export * from './RadioButton';
 export * from './SearchSelect';
 export * from './SearchSelectDropdown';
+export * from './SearchSelectDropdownControl';
index 30c1a8dbf01a9c9cfb1bbd16cea317555a3a1d47..581b44578d21125e7c009ab27aedbd679ec01b0a 100644 (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,
     );
index 5a3085a5b805151515a2babae7d46e0b6b9c5576..f3d42d750bf2e6a1137a5d9af9fe7f94c3400d8b 100644 (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],
index 8ac1afbdaec2da456bb365bc723b2aa9f6c1d641..d333b922e690eeaf1ffd6d49f2accaec4ab15e62 100644 (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 () => {
index 03caf6a4ddb7227f9984c19b92de60489e05bf6d..70e4a21149a4a9ddd6c78be0280bcdc6c01d0281 100644 (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,
   });
 });
index 8b0bd1d189de299d58daf6b540be66a899d48300..8394bbce39af7125f76b8d2e74ef76cad36b644d 100644 (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
index 20f82563531fea9887f908f27f43505ed7e63a12..2aba8958d021a46d77a7036f53ccb726d28f477d 100644 (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",
diff --git a/server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx b/server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx
new file mode 100644 (file)
index 0000000..6e25c4f
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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;
+}
diff --git a/server/sonar-web/src/main/js/components/icons/StatusIcon.tsx b/server/sonar-web/src/main/js/components/icons/StatusIcon.tsx
deleted file mode 100644 (file)
index bbdca97..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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;
-}
index 4527ef3e43ba9c07766c05e58c2c71ad588bdf3b..eb9c9880faad4ead51f310879d49378d3085188f 100644 (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());
index 65952a7af1a568f916ffcf033624b16b48593bcf..299e7e24b2c89110153c1f41b9e8b50e781f5892 100644 (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);
     },
index 067fdc45339efb09285d4e115682b852d31e6996..6934289484de09224b80dfec083851ea597275fd 100644 (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}
index dc2dff2d815d2b2998421861fc79e472861900a3..28acf401b620693849f31979330816876a6ee1c3 100644 (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;
index 82ccade9cc646823f3d954356909811d394cc7f5..d2265635dc14ad23e1ebd4971d2cfde4084f2ae0 100644 (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}
index affd6b5d77473b5c96695910cb60488de54c58e8..5f0fab73147f2a3e632a60d30066072ca0a9c481 100644 (file)
  * 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} />;
 }
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx
new file mode 100644 (file)
index 0000000..c689f78
--- /dev/null
@@ -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>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx
new file mode 100644 (file)
index 0000000..f434902
--- /dev/null
@@ -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>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/helpers.ts b/server/sonar-web/src/main/js/components/issue/helpers.ts
new file mode 100644 (file)
index 0000000..6278999
--- /dev/null
@@ -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);
+}
index 0f6ae104b850fca17add0498a349b2964a4b75ca..d667c2a10329d3686bf30d578930a14fa946b5f0 100644 (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>
index 392a6c2e8c62e2c99490e1b8daa00dac6724a957..90ea9cf83b21660606b26476d2c89ece9626bd38 100644 (file)
  * 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>
   );
 }
index bd4ba2385dfa060d10f48aaa7cd959db31623d45..54db9489922e5a2142a57a310bc9813ad90450a6 100644 (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;
 }
 
index b0fece2070af0f209891e3bfd079aa10e58dfa9f..702d92e275962b6ccf7aae0de4b14e5d828dfb52 100644 (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',
index 4c14ed7c0d74f77a9bfac6c35de9413738362779..6dcba48dc6f6203381c2f738e37797a9dc1380a0 100644 (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;
index 570a3f239a4399018f415ed6d45d3c322c5b2924..c0d8973b32523389e0e4282f030b834f94704c4b 100644 (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;
 }
 
index f24fae9f333c14522cc6931b30e084570e85adc7..d0942e614b09b00110f01da67e2337314ad2711d 100644 (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