]> source.dussan.org Git - sonarqube.git/commitdiff
Revert "SONAR-19069 Disable the option to change an issue type in issues list and...
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 27 Apr 2023 09:53:27 +0000 (11:53 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 28 Apr 2023 20:02:58 +0000 (20:02 +0000)
This reverts commit 166f4620f857b53508fd737a28706b613848e48c.

server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
server/sonar-web/src/main/js/components/issue/Issue.css
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
server/sonar-web/src/main/js/components/issue/components/IssueType.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/types/issues.ts

index a60cbccd420105d2b3b6e058f7e55b15728c1fed..42eb29d92e23e4fd5b96bb311b494db55b287b0c 100644 (file)
@@ -62,8 +62,8 @@ import {
   editIssueComment,
   getIssueChangelog,
   getIssueFlowSnippets,
-  searchIssues,
   searchIssueTags,
+  searchIssues,
   setIssueAssignee,
   setIssueSeverity,
   setIssueTags,
@@ -529,11 +529,11 @@ export default class IssuesServiceMock {
   }
 
   handleBulkChangeIssues = (issueKeys: string[], query: RequestData) => {
-    //For now we only check for issue severity change.
+    //For now we only check for issue type change.
     this.list
       .filter((i) => issueKeys.includes(i.issue.key))
       .forEach((data) => {
-        data.issue.severity = query.set_severity;
+        data.issue.type = query.set_type;
       });
     return this.reply({});
   };
index ac00a46ee7e27e6e2cb8b1c2990cd9a6d1ca35e6..a7c11cbc3ca81e8326514e0f07d561573616c656 100644 (file)
@@ -246,6 +246,12 @@ describe('issues app', () => {
       // Check that we bulk change the selected issue
       const issueBoxFixThat = within(screen.getByRole('region', { name: 'Fix that' }));
 
+      expect(
+        issueBoxFixThat.getByRole('button', {
+          name: 'issue.type.type_x_click_to_change.issue.type.CODE_SMELL',
+        })
+      ).toBeInTheDocument();
+
       await user.click(
         screen.getByRole('checkbox', { name: 'issues.action_select.label.Fix that' })
       );
@@ -255,14 +261,14 @@ describe('issues app', () => {
       await user.keyboard('New Comment');
       expect(screen.getByRole('button', { name: 'apply' })).toBeDisabled();
 
-      await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_severity' }), [
-        'severity.BLOCKER',
+      await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_type' }), [
+        'issue.type.BUG',
       ]);
       await user.click(screen.getByRole('button', { name: 'apply' }));
 
       expect(
         issueBoxFixThat.getByRole('button', {
-          name: 'issue.severity.severity_x_click_to_change.severity.BLOCKER',
+          name: 'issue.type.type_x_click_to_change.issue.type.BUG',
         })
       ).toBeInTheDocument();
     });
@@ -617,6 +623,22 @@ describe('issues item', () => {
     // Get a specific issue list item
     const listItem = within(await screen.findByRole('region', { name: 'Fix that' }));
 
+    // Change issue type
+    await user.click(
+      listItem.getByRole('button', {
+        name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
+      })
+    );
+    expect(listItem.getByText('issue.type.BUG')).toBeInTheDocument();
+    expect(listItem.getByText('issue.type.VULNERABILITY')).toBeInTheDocument();
+
+    await user.click(listItem.getByText('issue.type.VULNERABILITY'));
+    expect(
+      listItem.getByRole('button', {
+        name: `issue.type.type_x_click_to_change.issue.type.VULNERABILITY`,
+      })
+    ).toBeInTheDocument();
+
     // Change issue severity
     expect(listItem.getByText('severity.MAJOR')).toBeInTheDocument();
 
index fb3e941c7bd51161de2be215c0c21376580a7b03..1a31ccfb7fffd2cb93f0e0112f042d45b2c87ec9 100644 (file)
@@ -33,13 +33,14 @@ import Select, {
   LabelValueSelectOption,
   SearchSelect,
 } from '../../../components/controls/Select';
+import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import { Alert } from '../../../components/ui/Alert';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { SEVERITIES } from '../../../helpers/constants';
 import { throwGlobalError } from '../../../helpers/error';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Component, Dict, Issue, Paging } from '../../../types/types';
+import { Component, Dict, Issue, IssueType, Paging } from '../../../types/types';
 import { CurrentUser } from '../../../types/users';
 import AssigneeSelect, { AssigneeOption } from './AssigneeSelect';
 
@@ -66,6 +67,7 @@ interface FormFields {
   removeTags?: Array<{ label: string; value: string }>;
   severity?: string;
   transition?: string;
+  type?: string;
 }
 
 interface State extends FormFields {
@@ -83,10 +85,30 @@ enum InputField {
   assignee = 'assignee',
   removeTags = 'removeTags',
   severity = 'severity',
+  type = 'type',
 }
 
 export const MAX_PAGE_SIZE = 500;
 
+function typeFieldTypeRenderer(option: LabelValueSelectOption) {
+  return (
+    <div className="display-flex-center">
+      <IssueTypeIcon query={option.value} />
+      <span className="little-spacer-left">{option.label}</span>
+    </div>
+  );
+}
+
+function TypeFieldOptionComponent(props: OptionProps<LabelValueSelectOption, false>) {
+  return <components.Option {...props}>{typeFieldTypeRenderer(props.data)}</components.Option>;
+}
+
+function TypeFieldSingleValueComponent(props: SingleValueProps<LabelValueSelectOption, false>) {
+  return (
+    <components.SingleValue {...props}>{typeFieldTypeRenderer(props.data)}</components.SingleValue>
+  );
+}
+
 function SeverityFieldOptionComponent(props: OptionProps<LabelValueSelectOption, false>) {
   return (
     <components.Option {...props}>
@@ -196,6 +218,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
         remove_tags: this.state.removeTags && this.state.removeTags.map((t) => t.value).join(),
         sendNotifications: this.state.notifications,
         set_severity: this.state.severity,
+        set_type: this.state.type,
       },
       (x) => x !== undefined
     );
@@ -235,14 +258,15 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
   }
 
   canSubmit = () => {
-    const { addTags, assignee, removeTags, severity, transition } = this.state;
+    const { addTags, assignee, removeTags, severity, transition, type } = this.state;
 
     return Boolean(
       (addTags && addTags.length > 0) ||
         (removeTags && removeTags.length > 0) ||
         assignee ||
         severity ||
-        transition
+        transition ||
+        type
     );
   };
 
@@ -307,6 +331,38 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     return this.renderField(field, 'issue.assign.formlink', affected, input);
   };
 
+  renderTypeField = () => {
+    const affected = this.state.issues.filter(hasAction('set_type')).length;
+    const field = InputField.type;
+
+    if (affected === 0) {
+      return null;
+    }
+
+    const types: IssueType[] = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+    const options: LabelValueSelectOption[] = types.map((type) => ({
+      label: translate('issue.type', type),
+      value: type,
+    }));
+
+    const input = (
+      <Select
+        className="input-super-large"
+        inputId={`issues-bulk-change-${field}`}
+        isClearable={true}
+        isSearchable={false}
+        components={{
+          Option: TypeFieldOptionComponent,
+          SingleValue: TypeFieldSingleValueComponent,
+        }}
+        onChange={this.handleSelectFieldChange('type')}
+        options={options}
+      />
+    );
+
+    return this.renderField(field, 'issue.set_type', affected, input);
+  };
+
   renderSeverityField = () => {
     const affected = this.state.issues.filter(hasAction('set_severity')).length;
     const field = InputField.severity;
@@ -463,6 +519,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
           )}
 
           {this.renderAssigneeField()}
+          {this.renderTypeField()}
           {this.renderSeverityField()}
           {this.renderTagsField(InputField.addTags, 'issue.add_tags', true)}
           {this.renderTagsField(InputField.removeTags, 'issue.remove_tags', false)}
index bfa2336519017777ffe277903f0df7dffc50bfc3..d629ed13c86ea3ed23ea12dc12c320cca4673b06 100644 (file)
@@ -25,6 +25,7 @@ import { bulkChangeIssues } from '../../../../api/issues';
 import { SEVERITIES } from '../../../../helpers/constants';
 import { mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { IssueType } from '../../../../types/issues';
 import { Issue } from '../../../../types/types';
 import BulkChangeModal, { MAX_PAGE_SIZE } from '../BulkChangeModal';
 
@@ -56,7 +57,10 @@ it('should display warning when too many issues are passed', async () => {
   expect(await screen.findByText('issue_bulk_change.max_issues_reached')).toBeInTheDocument();
 });
 
-it.each([['severity', 'set_severity']])('should render select for %s', async (_field, action) => {
+it.each([
+  ['type', 'set_type'],
+  ['severity', 'set_severity'],
+])('should render select for %s', async (_field, action) => {
   renderBulkChangeModal([mockIssue(false, { actions: [action] })]);
 
   expect(await screen.findByText('issue.' + action)).toBeInTheDocument();
@@ -139,6 +143,11 @@ it('should properly submit', async () => {
     'tag2',
   ]);
 
+  // Select a type
+  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_type' }), [
+    `issue.type.CODE_SMELL`,
+  ]);
+
   // Select a severity
   await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_severity' }), [
     `severity.${SEVERITIES[0]}`,
@@ -166,6 +175,7 @@ it('should properly submit', async () => {
     set_severity: 'BLOCKER',
     add_tags: 'tag1,tag2',
     do_transition: 'Transition2',
+    set_type: IssueType.CodeSmell,
     sendNotifications: true,
   });
 });
index 60f2ca41ac27da2f687586b7d53a9afa8a31c097..1439f083790ed4c96196c2b9d89d7cccc5440dbb 100644 (file)
@@ -145,6 +145,12 @@ it('should be able to interact with issue action', async () => {
   const user = userEvent.setup();
   renderSourceViewer();
 
+  //Open Issue type
+  await user.click(
+    await screen.findByRole('button', { name: 'issue.type.type_x_click_to_change.issue.type.BUG' })
+  );
+  expect(ui.codeSmellTypeButton.get()).toBeInTheDocument();
+
   // Open severity
   await user.click(
     await screen.findByRole('button', {
index a5c12dfd9de7bd282fa7c8318416d8dd0a614585..5be5920c71f4c237cc86295bc2e94e9a801447f5 100644 (file)
   display: flex;
 }
 
-.issue-meta.disabled {
-  color: var(--gray60);
-}
-
 .issue-meta + .issue-meta {
   margin-left: var(--gridSize);
 }
index 8d6b59b898184b96eedd1a85b01109c387fdc660..0f6d97998983f075f918cbdb61af3d46ef4f0bb3 100644 (file)
@@ -125,6 +125,19 @@ describe('rendering', () => {
 });
 
 describe('updating', () => {
+  it('should allow updating the type', async () => {
+    const { ui } = getPageObject();
+    const issue = mockRawIssue(false, {
+      type: IssueType.Bug,
+      actions: [IssueActions.SetType],
+    });
+    issuesHandler.setIssueList([{ issue, snippets: {} }]);
+    renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'type') }) });
+
+    await ui.updateType(IssueType.Bug, IssueType.CodeSmell);
+    expect(ui.updateTypeBtn(IssueType.CodeSmell).get()).toBeInTheDocument();
+  });
+
   it('should allow updating the severity', async () => {
     const { ui } = getPageObject();
     const issue = mockRawIssue(false, {
index fbf9d08f5c43ceb74149d7066a892e6e1e4caa97..1153e95b6c891b78e37769be4b5c8f0518fe6dda 100644 (file)
@@ -27,13 +27,13 @@ import {
   IssueType as IssueTypeEnum,
 } from '../../../types/issues';
 import { Issue, RawQuery } from '../../../types/types';
-import IssueTypeIcon from '../../icons/IssueTypeIcon';
 import { updateIssue } from '../actions';
 import IssueAssign from './IssueAssign';
 import IssueCommentAction from './IssueCommentAction';
 import IssueSeverity from './IssueSeverity';
 import IssueTags from './IssueTags';
 import IssueTransition from './IssueTransition';
+import IssueType from './IssueType';
 
 interface Props {
   issue: Issue;
@@ -98,15 +98,21 @@ export default class IssueActionsBar extends React.PureComponent<Props, State> {
     const canAssign = issue.actions.includes(IssueActions.Assign);
     const canComment = issue.actions.includes(IssueActions.Comment);
     const canSetSeverity = issue.actions.includes(IssueActions.SetSeverity);
+    const canSetType = issue.actions.includes(IssueActions.SetType);
     const canSetTags = issue.actions.includes(IssueActions.SetTags);
     const hasTransitions = issue.transitions.length > 0;
 
     return (
       <div className={classNames(className, 'issue-actions')}>
         <div className="issue-meta-list">
-          <div className="issue-meta display-flex-center disabled">
-            <IssueTypeIcon className="little-spacer-right" query={issue.type} />
-            {translate('issue.type', issue.type)}
+          <div className="issue-meta">
+            <IssueType
+              canSetType={canSetType}
+              isOpen={this.props.currentPopup === 'set-type' && canSetType}
+              issue={issue}
+              setIssueProperty={this.setIssueProperty}
+              togglePopup={this.props.togglePopup}
+            />
           </div>
           <div className="issue-meta">
             <IssueSeverity
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueType.tsx
new file mode 100644 (file)
index 0000000..048b1d3
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { setIssueType } from '../../../api/issues';
+import { colors } from '../../../app/theme';
+import { ButtonLink } from '../../../components/controls/buttons';
+import Toggler from '../../../components/controls/Toggler';
+import DropdownIcon from '../../../components/icons/DropdownIcon';
+import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { IssueResponse } from '../../../types/issues';
+import { Issue, RawQuery } from '../../../types/types';
+import SetTypePopup from '../popups/SetTypePopup';
+
+interface Props {
+  canSetType: boolean;
+  isOpen: boolean;
+  issue: Pick<Issue, 'type'>;
+  setIssueProperty: (
+    property: keyof Issue,
+    popup: string,
+    apiCall: (query: RawQuery) => Promise<IssueResponse>,
+    value: string
+  ) => void;
+  togglePopup: (popup: string, show?: boolean) => void;
+}
+
+export default class IssueType extends React.PureComponent<Props> {
+  toggleSetType = (open?: boolean) => {
+    this.props.togglePopup('set-type', open);
+  };
+
+  setType = (type: string) => {
+    this.props.setIssueProperty('type', 'set-type', setIssueType, type);
+  };
+
+  handleClose = () => {
+    this.toggleSetType(false);
+  };
+
+  render() {
+    const { issue } = this.props;
+    if (this.props.canSetType) {
+      return (
+        <div className="dropdown">
+          <Toggler
+            onRequestClose={this.handleClose}
+            open={this.props.isOpen && this.props.canSetType}
+            overlay={<SetTypePopup issue={issue} onSelect={this.setType} />}
+          >
+            <ButtonLink
+              aria-label={translateWithParameters(
+                'issue.type.type_x_click_to_change',
+                translate('issue.type', issue.type)
+              )}
+              aria-expanded={this.props.isOpen}
+              className="issue-action issue-action-with-options js-issue-set-type"
+              onClick={this.toggleSetType}
+            >
+              <IssueTypeIcon
+                className="little-spacer-right"
+                fill={colors.baseFontColor}
+                query={issue.type}
+              />
+              {translate('issue.type', issue.type)}
+              <DropdownIcon className="little-spacer-left" />
+            </ButtonLink>
+          </Toggler>
+        </div>
+      );
+    }
+
+    return (
+      <span>
+        <IssueTypeIcon className="little-spacer-right" query={issue.type} />
+        {translate('issue.type', issue.type)}
+      </span>
+    );
+  }
+}
index f812988ce803f8bec9099ea14c073d51b969e3c6..04ec365eaf133f88b0c8847a9c316b666296906c 100644 (file)
@@ -94,6 +94,7 @@ export enum IssueStatus {
 }
 
 export enum IssueActions {
+  SetType = 'set_type',
   SetTags = 'set_tags',
   SetSeverity = 'set_severity',
   Comment = 'comment',