]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23582 Update custom rule dialog is MQR/Standard modes compatible
authorIsmail Cherri <ismail.cherri@sonarsource.com>
Wed, 13 Nov 2024 15:50:17 +0000 (16:50 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 15 Nov 2024 20:02:42 +0000 (20:02 +0000)
12 files changed:
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/api/rules.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormFieldsCCT.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/SeveritySelect.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6d14b4948c0a6bed1accdecf8a14034b870cf0e4..9627db0956d69aa8812332c6495670cccf739d62 100644 (file)
@@ -390,6 +390,12 @@ export default class CodingRulesServiceMock {
     rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote;
     rule.name = data.name !== undefined ? data.name : rule.name;
     rule.status = rule.status === RuleStatus.Removed ? RuleStatus.Ready : rule.status;
+    rule.cleanCodeAttribute =
+      data.cleanCodeAttribute !== undefined ? data.cleanCodeAttribute : rule.cleanCodeAttribute;
+    rule.impacts = data.impacts !== undefined ? data.impacts : rule.impacts;
+    rule.type = data.type !== undefined ? data.type : rule.type;
+    rule.severity = data.severity !== undefined ? data.severity : rule.severity;
+
     if (template && data.params) {
       rule.params = [];
       data.params.split(';').forEach((param) => {
index a1af5c270512fb61a777e8a703c5cc6525c0ac7e..3b75ebed8a9b5c766ea9ba6adceed0895b7e676e 100644 (file)
@@ -31,19 +31,22 @@ import {
   RuleActivation,
   RuleDetails,
   RulesUpdateRequest,
+  RuleType,
 } from '../types/types';
 
 const RULES_ENDPOINT = '/api/v2/clean-code-policy/rules';
 
 export interface CreateRuleData {
-  cleanCodeAttribute: CleanCodeAttribute;
+  cleanCodeAttribute?: CleanCodeAttribute;
   impacts: SoftwareImpact[];
   key: string;
   markdownDescription: string;
   name: string;
   parameters?: Partial<RestRuleParameter>[];
+  severity?: string;
   status?: string;
   templateKey: string;
+  type?: RuleType;
 }
 
 export function getRulesApp(): Promise<GetRulesAppResponse> {
@@ -94,5 +97,11 @@ export function deleteRule(parameters: { key: string }) {
 }
 
 export function updateRule(data: RulesUpdateRequest): Promise<RuleDetails> {
-  return postJSON('/api/rules/update', data).then((r) => r.rule, throwGlobalError);
+  const impacts =
+    data.impacts &&
+    Object.values(data.impacts)
+      .map((impact) => `${impact.softwareQuality}=${impact.severity}`)
+      .join(';');
+
+  return postJSON('/api/rules/update', { ...data, impacts }).then((r) => r.rule, throwGlobalError);
 }
index 73cbba1cfcc9e0a6037ab75d0876e169072d9427..68ed8ca3990d139510b667ec2699629c1dbaa520 100644 (file)
@@ -22,7 +22,9 @@ import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
 import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
-import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
+import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
+import { IssueSeverity, IssueType } from '../../../types/issues';
+import { SettingsKey } from '../../../types/settings';
 import { getPageObjects, renderCodingRulesApp } from '../utils-tests';
 
 const rulesHandler = new CodingRulesServiceMock();
@@ -42,7 +44,7 @@ afterEach(() => {
 });
 
 describe('custom rule', () => {
-  it('can create custom rule', async () => {
+  it('can create custom rule in MQR mode', async () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser());
@@ -99,7 +101,9 @@ describe('custom rule', () => {
       byRole('option', { name: 'severity_impact.MEDIUM severity_impact.MEDIUM' }).get(),
     );
 
-    expect(ui.createCustomRuleDialog.byText('severity_impact.MEDIUM').get()).toBeInTheDocument();
+    expect(
+      ui.createCustomRuleDialog.byRole('combobox', { name: 'severity' }).getAll()[1],
+    ).toHaveValue('severity_impact.MEDIUM');
 
     await user.click(ui.statusSelect.get());
     await user.click(byRole('option', { name: 'rules.status.BETA' }).get());
@@ -113,6 +117,97 @@ describe('custom rule', () => {
     expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
   });
 
+  it('hides severities if security hotspot is selected in MQR mode', async () => {
+    const { ui, user } = getPageObjects();
+    rulesHandler.setIsAdmin();
+    renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+    await ui.detailsloaded();
+
+    // Create custom rule
+    await user.click(ui.createCustomRuleButton.get());
+    // Switch type to Security hotspot
+    await user.click(ui.cctIssueTypeSelect.get());
+    await user.click(
+      byRole('option', { name: 'coding_rules.custom.type.option.SECURITY_HOTSPOT' }).get(),
+    );
+    expect(ui.cleanCodeCategorySelect.query()).not.toBeInTheDocument();
+
+    // Switch type back to Issue
+    await user.click(ui.cctIssueTypeSelect.get());
+    await user.click(byRole('option', { name: 'coding_rules.custom.type.option.ISSUE' }).get());
+    expect(ui.cleanCodeCategorySelect.get()).toBeInTheDocument();
+  });
+
+  it('can create custom rule in Standard mode', async () => {
+    settingsHandler.set(SettingsKey.MQRMode, 'false');
+    const { ui, user } = getPageObjects();
+    rulesHandler.setIsAdmin();
+    renderCodingRulesApp(mockLoggedInUser());
+    await ui.facetsLoaded();
+
+    await user.click(await ui.templateFacet.find());
+    await user.click(ui.facetItem('coding_rules.filters.template.is_template').get());
+
+    // Shows only one template rule
+    expect(ui.getAllRuleListItems()).toHaveLength(1);
+
+    // Show template rule details
+    await user.click(ui.ruleListItemLink('Template rule').get());
+    expect(ui.ruleTitle('Template rule').get()).toBeInTheDocument();
+    expect(ui.customRuleSectionTitle.get()).toBeInTheDocument();
+
+    // Create custom rule
+    await user.click(ui.createCustomRuleButton.get());
+    await user.type(ui.ruleNameTextbox.get(), 'New Custom Rule');
+    expect(ui.keyTextbox.get()).toHaveValue('New_Custom_Rule');
+    await user.clear(ui.keyTextbox.get());
+    await user.type(ui.keyTextbox.get(), 'new_custom_rule');
+
+    // Select type as bug
+    await user.click(ui.standardIssueTypeSelect.get());
+    await user.click(byRole('option', { name: 'issue.type.BUG' }).get());
+
+    // Select Severity as Major
+    await user.click(ui.standardSeveritySelect.get());
+    await user.click(byRole('option', { name: 'severity.MAJOR' }).get());
+
+    expect(ui.createCustomRuleDialog.byRole('combobox', { name: 'severity' }).get()).toHaveValue(
+      'severity.MAJOR',
+    );
+
+    await user.click(ui.statusSelect.get());
+    await user.click(byRole('option', { name: 'rules.status.BETA' }).get());
+
+    await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+    await user.type(ui.paramInput('1').get(), 'Default value');
+
+    await user.click(ui.createButton.get());
+
+    // Verify the rule is created
+    expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
+  });
+
+  it('hides severities if security hotspot is selected in Standard mode', async () => {
+    settingsHandler.set(SettingsKey.MQRMode, 'false');
+    const { ui, user } = getPageObjects();
+    rulesHandler.setIsAdmin();
+    renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+    await ui.detailsloaded();
+
+    // Create custom rule
+    await user.click(ui.createCustomRuleButton.get());
+    // Switch type to Security hotspot
+    await user.click(ui.standardIssueTypeSelect.get());
+    await user.click(byRole('option', { name: 'issue.type.SECURITY_HOTSPOT' }).get());
+
+    expect(ui.standardSeveritySelect.query()).not.toBeInTheDocument();
+
+    // Switch type back to Bug
+    await user.click(ui.standardIssueTypeSelect.get());
+    await user.click(byRole('option', { name: 'issue.type.BUG' }).get());
+    expect(ui.standardSeveritySelect.get()).toBeInTheDocument();
+  });
+
   it('can reactivate custom rule', async () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
@@ -136,7 +231,45 @@ describe('custom rule', () => {
     expect(ui.customRuleItemLink('Reactivate custom Rule').get()).toBeInTheDocument();
   });
 
-  it('can edit custom rule', async () => {
+  it('can edit custom rule in MQR mode', async () => {
+    const { ui, user } = getPageObjects();
+    rulesHandler.setIsAdmin();
+    renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
+    await ui.detailsloaded();
+
+    await user.click(ui.editCustomRuleButton.get());
+
+    // Change name and description of custom rule
+    await user.clear(ui.ruleNameTextbox.get());
+    await user.type(ui.ruleNameTextbox.get(), 'Updated custom rule name');
+    await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+
+    // Maintainability should not be checked and should be disabled
+    expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Maintainability).get()).not.toBeChecked();
+    expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Maintainability).get()).toHaveAttribute(
+      'aria-disabled',
+      'true',
+    );
+    expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Reliability).get()).toHaveAttribute(
+      'aria-disabled',
+      'true',
+    );
+    expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Reliability).get()).toBeChecked();
+
+    // Set severity
+    await user.click(ui.cleanCodeSeveritySelect(SoftwareQuality.Reliability).get());
+    await user.click(byRole('option', { name: 'severity_impact.HIGH severity_impact.HIGH' }).get());
+
+    await user.click(ui.saveButton.get(ui.updateCustomRuleDialog.get()));
+
+    expect(ui.ruleTitle('Updated custom rule name').get()).toBeInTheDocument();
+    expect(
+      ui.ruleSoftwareQualityPill(SoftwareQuality.Reliability, SoftwareImpactSeverity.High).get(),
+    ).toBeInTheDocument();
+  });
+
+  it('can edit custom rule in Standard Mode', async () => {
+    settingsHandler.set(SettingsKey.MQRMode, 'false');
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
@@ -149,9 +282,19 @@ describe('custom rule', () => {
     await user.type(ui.ruleNameTextbox.get(), 'Updated custom rule name');
     await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
 
+    // Type should be Bug and should be disabled
+    expect(ui.standardIssueTypeSelect.get()).toHaveValue('issue.type.BUG');
+    expect(ui.standardIssueTypeSelect.get()).toBeDisabled();
+
+    // Select Severity as INFO
+    await user.click(ui.standardSeveritySelect.get());
+    await user.click(byRole('option', { name: 'severity.INFO' }).get());
+
     await user.click(ui.saveButton.get(ui.updateCustomRuleDialog.get()));
 
     expect(ui.ruleTitle('Updated custom rule name').get()).toBeInTheDocument();
+    expect(ui.ruleIssueTypePill(IssueType.Bug).get()).toBeInTheDocument();
+    expect(ui.ruleIssueTypePillSeverity(IssueSeverity.Info).get()).toBeInTheDocument();
   });
 
   it('can delete custom rule', async () => {
index 2e6fe61a4e4751ec5acc8fedbbf789f496e747ee..237e753414ad8931e61c801f5b3b9b2ce58b18ec 100644 (file)
@@ -40,6 +40,7 @@ export default function CustomRuleButton(props: Props) {
           customRule={customRule}
           onClose={() => setModalOpen(false)}
           templateRule={templateRule}
+          isOpen={modalOpen}
         />
       )}
     </>
index e7e9ee0fc50025c654faf5e3e84df94e7aef19be..cb3040808df40c292ea4f925046e56d36490c80a 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import React from 'react';
+import { Checkbox, Select, Text } from '@sonarsource/echoes-react';
+import { useEffect, useMemo, useRef } from 'react';
 import { useIntl } from 'react-intl';
-import {
-  Checkbox,
-  FormField,
-  Highlight,
-  InputSelect,
-  LightPrimary,
-  RequiredIcon,
-  TextError,
-} from '~design-system';
+import { FormField, RequiredIcon } from '~design-system';
 import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
 import {
   CLEAN_CODE_ATTRIBUTES_BY_CATEGORY,
@@ -65,14 +58,16 @@ export function CleanCodeCategoryField(props: Readonly<Props<CleanCodeAttributeC
       label={intl.formatMessage({ id: 'category' })}
       htmlFor="coding-rules-custom-clean-code-category"
     >
-      <InputSelect
-        options={categories}
-        inputId="coding-rules-custom-clean-code-category"
-        onChange={(option) => props.onChange(option?.value as CleanCodeAttributeCategory)}
-        isClearable={false}
+      <Select
+        data={categories}
+        id="coding-rules-custom-clean-code-category"
+        onChange={(option) =>
+          option ? props.onChange(option as CleanCodeAttributeCategory) : undefined
+        }
         isDisabled={disabled}
         isSearchable={false}
-        value={categories.find((category) => category.value === value)}
+        isNotClearable
+        value={categories.find((category) => category.value === value)?.value}
       />
     </FormField>
   );
@@ -82,7 +77,7 @@ export function CleanCodeAttributeField(
   props: Readonly<Props<CleanCodeAttribute> & { category: CleanCodeAttributeCategory }>,
 ) {
   const { value, disabled, category, onChange } = props;
-  const initialAttribute = React.useRef(value);
+  const initialAttribute = useRef(value);
   const intl = useIntl();
 
   const attributes = CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].map((attribute) => ({
@@ -91,7 +86,7 @@ export function CleanCodeAttributeField(
   }));
 
   // Set default CC attribute when category changes
-  React.useEffect(() => {
+  useEffect(() => {
     if (CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].includes(value)) {
       return;
     }
@@ -111,37 +106,37 @@ export function CleanCodeAttributeField(
       label={intl.formatMessage({ id: 'attribute' })}
       htmlFor="coding-rules-custom-clean-code-attribute"
     >
-      <InputSelect
-        options={attributes}
-        inputId="coding-rules-custom-clean-code-attribute"
-        onChange={(option) => props.onChange(option?.value as CleanCodeAttribute)}
-        isClearable={false}
+      <Select
+        data={attributes}
+        id="coding-rules-custom-clean-code-attribute"
+        onChange={(option) => props.onChange(option as CleanCodeAttribute)}
         isDisabled={disabled}
         isSearchable={false}
-        value={attributes.find((attribute) => attribute.value === value)}
+        isNotClearable
+        value={attributes.find((attribute) => attribute.value === value)?.value}
       />
     </FormField>
   );
 }
 
 export function SoftwareQualitiesFields(
-  props: Readonly<Props<SoftwareImpact[]> & { error: boolean }>,
+  props: Readonly<Props<SoftwareImpact[]> & { error: boolean; qualityUpdateDisabled: boolean }>,
 ) {
-  const { value, disabled, error } = props;
+  const { value, disabled, error, qualityUpdateDisabled } = props;
   const intl = useIntl();
 
-  const severities = React.useMemo(
+  const severities = useMemo(
     () =>
       IMPACT_SEVERITIES.map((severity) => ({
         value: severity,
         label: intl.formatMessage({ id: `severity_impact.${severity}` }),
-        Icon: <SoftwareImpactSeverityIcon severity={severity} />,
+        prefix: <SoftwareImpactSeverityIcon severity={severity} />,
       })),
     [intl],
   );
 
-  const handleSoftwareQualityChange = (quality: SoftwareQuality, checked: boolean) => {
-    if (checked) {
+  const handleSoftwareQualityChange = (quality: SoftwareQuality, checked: boolean | string) => {
+    if (checked === true) {
       props.onChange([
         ...value,
         { softwareQuality: quality, severity: SoftwareImpactSeverity.Low },
@@ -162,19 +157,19 @@ export function SoftwareQualitiesFields(
   return (
     <fieldset className="sw-mt-2 sw-mb-4 sw-relative">
       <legend className="sw-w-full sw-flex sw-justify-between sw-gap-6 sw-mb-4">
-        <Highlight className="sw-w-full">
+        <Text isHighlighted className="sw-w-full">
           {intl.formatMessage({ id: 'software_quality' })}
           <RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
-        </Highlight>
-        <Highlight className="sw-w-full">
+        </Text>
+        <Text isHighlighted className="sw-w-full">
           {intl.formatMessage({ id: 'severity' })}
           <RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
-        </Highlight>
+        </Text>
       </legend>
       {SOFTWARE_QUALITIES.map((quality) => {
         const selectedQuality = value.find((impact) => impact.softwareQuality === quality);
         const selectedSeverity = selectedQuality
-          ? severities.find((severity) => severity.value === selectedQuality.severity)
+          ? severities.find((severity) => severity.value === selectedQuality.severity)?.value
           : null;
 
         return (
@@ -186,38 +181,42 @@ export function SoftwareQualitiesFields(
               )}
             </legend>
             <Checkbox
-              className="sw-w-full"
+              className="sw-w-full sw-items-center"
+              isDisabled={qualityUpdateDisabled}
               checked={Boolean(selectedQuality)}
               onCheck={(checked) => {
                 handleSoftwareQualityChange(quality, checked);
               }}
-              label={quality}
-            >
-              <LightPrimary className="sw-ml-3">
-                {intl.formatMessage({ id: `software_quality.${quality}` })}
-              </LightPrimary>
-            </Checkbox>
-            <InputSelect
+              label={
+                <Text className="sw-ml-3">
+                  {intl.formatMessage({ id: `software_quality.${quality}` })}
+                </Text>
+              }
+            />
+
+            <Select
+              id={`coding-rules-custom-software-impact-severity-${quality}`}
               aria-label={intl.formatMessage({ id: 'severity' })}
               className="sw-w-full"
-              options={severities}
+              data={severities}
               placeholder={intl.formatMessage({ id: 'none' })}
-              onChange={(option) =>
-                handleSeverityChange(quality, option?.value as SoftwareImpactSeverity)
-              }
-              isClearable={false}
+              onChange={(option) => handleSeverityChange(quality, option as SoftwareImpactSeverity)}
               isDisabled={disabled || !selectedQuality}
               isSearchable={false}
+              isNotClearable
               value={selectedSeverity}
+              valueIcon={<SoftwareImpactSeverityIcon severity={selectedSeverity} />}
             />
           </fieldset>
         );
       })}
       {error && (
-        <TextError
+        <Text
+          colorOverride="echoes-color-text-danger"
           className="sw-font-regular sw-absolute sw--bottom-3"
-          text={intl.formatMessage({ id: 'coding_rules.custom_rule.select_software_quality' })}
-        />
+        >
+          {intl.formatMessage({ id: 'coding_rules.custom_rule.select_software_quality' })}
+        </Text>
       )}
     </fieldset>
   );
index 033a51e30a9b906b3b99a0dd153cd5a561f14b12..12e43b222e87e957f33712cb9acda625aa3bf62c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { Button, ButtonVariety, Modal, ModalSize, Select, Text } from '@sonarsource/echoes-react';
 import { HttpStatusCode } from 'axios';
-import * as React from 'react';
+import { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import {
-  ButtonPrimary,
   FlagMessage,
   FormField,
   InputField,
-  InputSelect,
   InputTextArea,
   LabelValueSelectOption,
-  LightLabel,
-  Modal,
   SafeHTMLInjection,
   SanitizeLevel,
 } from '~design-system';
-import { Status } from '~sonar-aligned/types/common';
 import FormattingTips from '../../../components/common/FormattingTips';
+import IssueTypeIcon from '../../../components/icon-mappers/IssueTypeIcon';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { RULE_STATUSES } from '../../../helpers/constants';
+import { RULE_STATUSES, RULE_TYPES } from '../../../helpers/constants';
 import { csvEscape } from '../../../helpers/csv';
 import { translate } from '../../../helpers/l10n';
 import { latinize } from '../../../helpers/strings';
 import { useCreateRuleMutation, useUpdateRuleMutation } from '../../../queries/rules';
+import { useStandardExperienceMode } from '../../../queries/settings';
 import {
   CleanCodeAttribute,
   CleanCodeAttributeCategory,
   SoftwareImpact,
 } from '../../../types/clean-code-taxonomy';
-import { Dict, RuleDetails, RuleParameter } from '../../../types/types';
+import { CustomRuleType, Dict, RuleDetails, RuleParameter, RuleType } from '../../../types/types';
 import {
   CleanCodeAttributeField,
   CleanCodeCategoryField,
   SoftwareQualitiesFields,
 } from './CustomRuleFormFieldsCCT';
+import { SeveritySelect } from './SeveritySelect';
 
 interface Props {
   customRule?: RuleDetails;
+  isOpen: boolean;
   onClose: () => void;
   templateRule: RuleDetails;
 }
@@ -62,21 +62,29 @@ interface Props {
 const FORM_ID = 'custom-rule-form';
 
 export default function CustomRuleFormModal(props: Readonly<Props>) {
-  const { customRule, templateRule } = props;
-  const [description, setDescription] = React.useState(customRule?.mdDesc ?? '');
-  const [key, setKey] = React.useState(customRule?.key ?? '');
-  const [keyModifiedByUser, setKeyModifiedByUser] = React.useState(false);
-  const [name, setName] = React.useState(customRule?.name ?? '');
-  const [params, setParams] = React.useState(getParams(customRule));
-  const [reactivating, setReactivating] = React.useState(false);
-  const [status, setStatus] = React.useState(customRule?.status ?? templateRule.status);
-  const [ccCategory, setCCCategory] = React.useState<CleanCodeAttributeCategory>(
+  const { customRule, templateRule, isOpen } = props;
+  const { data: isStandardMode } = useStandardExperienceMode();
+  const [description, setDescription] = useState(customRule?.mdDesc ?? '');
+  const [key, setKey] = useState(customRule?.key ?? '');
+  const [keyModifiedByUser, setKeyModifiedByUser] = useState(false);
+  const [name, setName] = useState(customRule?.name ?? '');
+  const [params, setParams] = useState(getParams(customRule));
+  const [reactivating, setReactivating] = useState(false);
+  const [status, setStatus] = useState(customRule?.status ?? templateRule.status);
+  const [ccCategory, setCCCategory] = useState<CleanCodeAttributeCategory>(
     templateRule.cleanCodeAttributeCategory ?? CleanCodeAttributeCategory.Consistent,
   );
-  const [ccAttribute, setCCAtribute] = React.useState<CleanCodeAttribute>(
+  const [ccAttribute, setCCAttribute] = useState<CleanCodeAttribute>(
     templateRule.cleanCodeAttribute ?? CleanCodeAttribute.Conventional,
   );
-  const [impacts, setImpacts] = React.useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
+  const [impacts, setImpacts] = useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
+  const [standardSeverity, setStandardSeverity] = useState(
+    customRule?.severity ?? templateRule.severity,
+  );
+  const [standardType, setStandardType] = useState(customRule?.type ?? templateRule.type);
+  const [cctType, setCCTType] = useState<CustomRuleType>(
+    standardType === 'SECURITY_HOTSPOT' ? CustomRuleType.SECURITY_HOTSPOT : CustomRuleType.ISSUE,
+  );
   const customRulesSearchParams = {
     f: 'name,severity,params',
     template_key: templateRule.key,
@@ -92,58 +100,77 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
       setReactivating(response.status === HttpStatusCode.Conflict);
     },
   );
-  const warningRef = React.useRef<HTMLDivElement>(null);
+  const warningRef = useRef<HTMLDivElement>(null);
 
   const submitting = updatingRule || creatingRule;
-  const hasError = impacts.length === 0;
+  const hasError =
+    !isStandardMode && impacts.length === 0 && cctType !== CustomRuleType.SECURITY_HOTSPOT;
+  const isDisabledInUpdate = submitting || customRule !== undefined;
 
   const submit = () => {
+    const isSecurityHotspot =
+      standardType === 'SECURITY_HOTSPOT' || cctType === CustomRuleType.SECURITY_HOTSPOT;
     const stringifiedParams = Object.keys(params)
       .map((key) => `${key}=${csvEscape(params[key])}`)
       .join(';');
+
     const ruleData = {
       name,
       status,
       markdownDescription: description,
     };
 
+    const standardRule = {
+      type: standardType,
+      ...(isSecurityHotspot ? {} : { severity: standardSeverity }),
+    };
+
+    const cctRule = isSecurityHotspot
+      ? { type: cctType as RuleType }
+      : {
+          cleanCodeAttribute: ccAttribute,
+          impacts,
+        };
+
     if (customRule) {
       updateRule({
         ...ruleData,
+        ...(isStandardMode ? standardRule : cctRule),
         params: stringifiedParams,
         key: customRule.key,
       });
     } else if (reactivating) {
       updateRule({
         ...ruleData,
+        ...(isStandardMode ? standardRule : cctRule),
         params: stringifiedParams,
         key: `${templateRule.repo}:${key}`,
       });
     } else {
       createRule({
         ...ruleData,
+        impacts: [], // impacts are required in createRule
+        ...(isStandardMode ? standardRule : cctRule),
         key: `${templateRule.repo}:${key}`,
         templateKey: templateRule.key,
-        cleanCodeAttribute: ccAttribute,
-        impacts,
         parameters: Object.entries(params).map(([key, value]) => ({ key, defaultValue: value })),
       });
     }
   };
 
   // If key changes, then most likely user did it to create a new rule instead of reactivating one
-  React.useEffect(() => {
+  useEffect(() => {
     setReactivating(false);
   }, [key]);
 
   // scroll to warning when it appears
-  React.useEffect(() => {
+  useEffect(() => {
     if (reactivating) {
       warningRef.current?.scrollIntoView({ behavior: 'smooth' });
     }
   }, [reactivating]);
 
-  const NameField = React.useMemo(
+  const NameField = useMemo(
     () => (
       <FormField
         ariaLabel={translate('name')}
@@ -155,9 +182,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
           autoFocus
           disabled={submitting}
           id="coding-rules-custom-rule-creation-name"
-          onChange={({
-            currentTarget: { value: name },
-          }: React.SyntheticEvent<HTMLInputElement>) => {
+          onChange={({ currentTarget: { value: name } }: SyntheticEvent<HTMLInputElement>) => {
             setName(name);
             setKey(keyModifiedByUser ? key : latinize(name).replace(/[^A-Za-z0-9]/g, '_'));
           }}
@@ -171,7 +196,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
     [key, keyModifiedByUser, name, submitting],
   );
 
-  const KeyField = React.useMemo(
+  const KeyField = useMemo(
     () => (
       <FormField
         ariaLabel={translate('key')}
@@ -185,7 +210,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
           <InputField
             disabled={submitting}
             id="coding-rules-custom-rule-creation-key"
-            onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
+            onChange={(event: SyntheticEvent<HTMLInputElement>) => {
               setKey(event.currentTarget.value);
               setKeyModifiedByUser(true);
             }}
@@ -200,7 +225,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
     [customRule, key, submitting],
   );
 
-  const DescriptionField = React.useMemo(
+  const DescriptionField = useMemo(
     () => (
       <FormField
         ariaLabel={translate('description')}
@@ -211,7 +236,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
         <InputTextArea
           disabled={submitting}
           id="coding-rules-custom-rule-creation-html-description"
-          onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) =>
+          onChange={(event: SyntheticEvent<HTMLTextAreaElement>) =>
             setDescription(event.currentTarget.value)
           }
           required
@@ -225,8 +250,34 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
     [description, submitting],
   );
 
-  const StatusField = React.useMemo(() => {
-    const statusesOptions = RULE_STATUSES.map((status: Status) => ({
+  const CCTIssueTypeField = useMemo(() => {
+    const typeOptions = Object.values(CustomRuleType).map((value) => ({
+      label: translate(`coding_rules.custom.type.option.${value}`),
+      value,
+    }));
+
+    return (
+      <FormField
+        ariaLabel={translate('coding_rules.custom.type.label')}
+        label={translate('coding_rules.custom.type.label')}
+        htmlFor="coding-rules-custom-rule-type"
+      >
+        <Select
+          isRequired
+          id="coding-rules-custom-rule-type"
+          isDisabled={isDisabledInUpdate}
+          aria-labelledby="coding-rules-custom-rule-type"
+          onChange={(value) => (value ? setCCTType(value as CustomRuleType) : '')}
+          data={typeOptions}
+          isSearchable={false}
+          value={typeOptions.find((s) => s.value === cctType)?.value}
+        />
+      </FormField>
+    );
+  }, [cctType, isDisabledInUpdate]);
+
+  const StatusField = useMemo(() => {
+    const statusesOptions = RULE_STATUSES.map((status) => ({
       label: translate('rules.status', status),
       value: status,
     }));
@@ -237,32 +288,77 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
         label={translate('coding_rules.filters.status')}
         htmlFor="coding-rules-custom-rule-status"
       >
-        <InputSelect
-          inputId="coding-rules-custom-rule-status"
-          isClearable={false}
+        <Select
+          isRequired
+          id="coding-rules-custom-rule-status"
           isDisabled={submitting}
           aria-labelledby="coding-rules-custom-rule-status"
-          onChange={({ value }: LabelValueSelectOption<Status>) => setStatus(value)}
-          options={statusesOptions}
+          onChange={(value) => (value ? setStatus(value) : undefined)}
+          data={statusesOptions}
           isSearchable={false}
-          value={statusesOptions.find((s) => s.value === status)}
+          value={statusesOptions.find((s) => s.value === status)?.value}
         />
       </FormField>
     );
   }, [status, submitting]);
 
-  const handleParameterChange = React.useCallback(
-    (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+  const StandardTypeField = useMemo(() => {
+    const ruleTypeOption: LabelValueSelectOption<RuleType>[] = RULE_TYPES.map((type) => ({
+      label: translate('issue.type', type),
+      value: type,
+      prefix: <IssueTypeIcon type={type} />,
+    }));
+    return (
+      <FormField
+        ariaLabel={translate('type')}
+        label={translate('type')}
+        htmlFor="coding-rules-custom-rule-type"
+      >
+        <Select
+          id="coding-rules-custom-rule-type"
+          isNotClearable
+          isDisabled={isDisabledInUpdate}
+          isSearchable={false}
+          onChange={(value) => setStandardType(value as RuleType)}
+          data={ruleTypeOption}
+          value={ruleTypeOption.find((t) => t.value === standardType)?.value}
+          valueIcon={<IssueTypeIcon type={standardType} />}
+        />
+      </FormField>
+    );
+  }, [isDisabledInUpdate, standardType]);
+
+  const StandardSeverityField = useMemo(
+    () => (
+      <FormField
+        ariaLabel={translate('severity')}
+        label={translate('severity')}
+        htmlFor="coding-rules-severity-select"
+      >
+        <SeveritySelect
+          id="coding-rules-severity-select"
+          isDisabled={submitting}
+          onChange={(value) => setStandardSeverity(value)}
+          severity={standardSeverity}
+          recommendedSeverity={templateRule.severity ?? customRule?.severity}
+        />
+      </FormField>
+    ),
+    [customRule?.severity, standardSeverity, submitting, templateRule.severity],
+  );
+
+  const handleParameterChange = useCallback(
+    (event: SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
       const { name, value } = event.currentTarget;
       setParams({ ...params, [name]: value });
     },
     [params],
   );
 
-  const renderParameterField = React.useCallback(
+  const renderParameterField = useCallback(
     (param: RuleParameter) => {
       // Gets the actual value from params from the state.
-      // Without it, we have a issue with string 'constructor' as key
+      // Without it, we have an issue with string 'constructor' as key
       const actualValue = new Map(Object.entries(params)).get(param.key) ?? '';
 
       return (
@@ -302,7 +398,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
               htmlAsString={param.htmlDesc}
               sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
             >
-              <LightLabel />
+              <Text isSubdued />
             </SafeHTMLInjection>
           )}
         </FormField>
@@ -321,13 +417,15 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
   }
   return (
     <Modal
-      headerTitle={header}
-      onClose={props.onClose}
-      body={
+      size={ModalSize.Wide}
+      isOpen={isOpen}
+      title={header}
+      onOpenChange={props.onClose}
+      content={
         <form
           className="sw-flex sw-flex-col sw-justify-stretch sw-pb-4"
           id={FORM_ID}
-          onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
+          onSubmit={(event: SyntheticEvent<HTMLFormElement>) => {
             event.preventDefault();
             submit();
           }}
@@ -344,28 +442,39 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
 
           {NameField}
           {KeyField}
-          {/* do not allow to change CCT fields of existing rule */}
-          {!customRule && !reactivating && (
+          {isStandardMode && (
             <>
-              <div className="sw-flex sw-justify-between sw-gap-6">
-                <CleanCodeCategoryField
-                  value={ccCategory}
-                  disabled={submitting}
-                  onChange={setCCCategory}
-                />
-                <CleanCodeAttributeField
-                  value={ccAttribute}
-                  category={ccCategory}
-                  disabled={submitting}
-                  onChange={setCCAtribute}
-                />
-              </div>
-              <SoftwareQualitiesFields
-                error={hasError}
-                value={impacts}
-                onChange={setImpacts}
-                disabled={submitting}
-              />
+              {StandardTypeField}
+              {standardType !== 'SECURITY_HOTSPOT' && StandardSeverityField}
+            </>
+          )}
+          {!isStandardMode && (
+            <>
+              {CCTIssueTypeField}
+              {cctType !== 'SECURITY_HOTSPOT' && (
+                <>
+                  <div className="sw-flex sw-justify-between sw-gap-6">
+                    <CleanCodeCategoryField
+                      value={ccCategory}
+                      disabled={isDisabledInUpdate}
+                      onChange={setCCCategory}
+                    />
+                    <CleanCodeAttributeField
+                      value={ccAttribute}
+                      category={ccCategory}
+                      disabled={isDisabledInUpdate}
+                      onChange={setCCAttribute}
+                    />
+                  </div>
+                  <SoftwareQualitiesFields
+                    error={hasError}
+                    value={impacts}
+                    onChange={setImpacts}
+                    disabled={submitting}
+                    qualityUpdateDisabled={isDisabledInUpdate}
+                  />
+                </>
+              )}
             </>
           )}
           {StatusField}
@@ -374,12 +483,20 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
         </form>
       }
       primaryButton={
-        <ButtonPrimary disabled={submitting || hasError} type="submit" form={FORM_ID}>
+        <Button
+          variety={ButtonVariety.Primary}
+          isDisabled={submitting || hasError}
+          type="submit"
+          form={FORM_ID}
+        >
           {buttonText}
-        </ButtonPrimary>
+        </Button>
+      }
+      secondaryButton={
+        <Button onClick={props.onClose} variety={ButtonVariety.Default}>
+          {translate('cancel')}
+        </Button>
       }
-      loading={submitting}
-      secondaryButtonLabel={translate('cancel')}
     />
   );
 }
index e316ec4b16a2dcba0a7d508230533ccb6f391a4a..9fa7713847a597c359866b61f07415c3c3d8e0a1 100644 (file)
  */
 
 import styled from '@emotion/styled';
-import { Button, ButtonVariety } from '@sonarsource/echoes-react';
 import {
-  ButtonSecondary,
-  HelperHintIcon,
+  Button,
+  ButtonVariety,
+  Heading,
+  IconQuestionMark,
+  ModalAlert,
   Spinner,
-  SubHeadingHighlight,
-  themeBorder,
-  themeColor,
-} from '~design-system';
+} from '@sonarsource/echoes-react';
+import { useIntl } from 'react-intl';
+import { themeBorder, themeColor } from '~design-system';
 import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
 import { Profile } from '../../../api/quality-profiles';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
 import DateFormatter from '../../../components/intl/DateFormatter';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
 import {
   useDeleteRuleMutation,
   useRuleDetailsQuery,
@@ -70,6 +70,7 @@ export default function RuleDetails(props: Readonly<Props>) {
     selectedProfile,
     referencedRepositories,
   } = props;
+  const intl = useIntl();
   const { isLoading: loadingRule, data } = useRuleDetailsQuery({
     actives: true,
     key: ruleKey,
@@ -80,7 +81,7 @@ export default function RuleDetails(props: Readonly<Props>) {
   const { rule: ruleDetails, actives = [] } = data ?? {};
 
   const params = ruleDetails?.params ?? [];
-  const isCustom = !!ruleDetails?.templateKey;
+  const isCustom = ruleDetails?.templateKey !== undefined;
   const isEditable = canWrite && !!allowCustomRules && isCustom;
 
   const handleTagsChange = (tags: string[]) => {
@@ -104,7 +105,7 @@ export default function RuleDetails(props: Readonly<Props>) {
 
   return (
     <StyledRuleDetails className="it__coding-rule-details sw-p-6 sw-mt-6">
-      <Spinner loading={loadingRule}>
+      <Spinner isLoading={loadingRule}>
         {ruleDetails && (
           <>
             <RuleDetailsHeader
@@ -124,48 +125,54 @@ export default function RuleDetails(props: Readonly<Props>) {
                 {/* it's expected to pass the same rule to both parameters */}
                 <CustomRuleButton customRule={ruleDetails} templateRule={ruleDetails}>
                   {({ onClick }) => (
-                    <ButtonSecondary
+                    <Button
+                      variety={ButtonVariety.Default}
                       className="js-edit-custom"
                       id="coding-rules-detail-custom-rule-change"
                       onClick={onClick}
                     >
                       {translate('edit')}
-                    </ButtonSecondary>
+                    </Button>
                   )}
                 </CustomRuleButton>
-                <ConfirmButton
-                  confirmButtonText={translate('delete')}
-                  isDestructive
-                  modalBody={translateWithParameters(
-                    'coding_rules.delete.custom.confirm',
-                    ruleDetails.name,
+                <ModalAlert
+                  title={translate('coding_rules.delete_rule')}
+                  description={intl.formatMessage(
+                    {
+                      id: 'coding_rules.delete.custom.confirm',
+                    },
+                    {
+                      name: ruleDetails.name,
+                    },
                   )}
-                  modalHeader={translate('coding_rules.delete_rule')}
-                  onConfirm={() => deleteRule({ key: ruleKey })}
+                  primaryButton={
+                    <Button
+                      className="sw-ml-2 js-delete"
+                      id="coding-rules-detail-rule-delete"
+                      onClick={() => deleteRule({ key: ruleKey })}
+                      variety={ButtonVariety.DangerOutline}
+                    >
+                      {translate('delete')}
+                    </Button>
+                  }
+                  secondaryButtonLabel={translate('close')}
                 >
-                  {({ onClick }) => (
-                    <>
-                      <Button
-                        className="sw-ml-2 js-delete"
-                        id="coding-rules-detail-rule-delete"
-                        onClick={onClick}
-                        variety={ButtonVariety.DangerOutline}
-                      >
-                        {translate('delete')}
-                      </Button>
-                      <HelpTooltip
-                        className="sw-ml-2"
-                        overlay={
-                          <div className="sw-py-4">
-                            {translate('coding_rules.custom_rule.removal')}
-                          </div>
-                        }
-                      >
-                        <HelperHintIcon />
-                      </HelpTooltip>
-                    </>
-                  )}
-                </ConfirmButton>
+                  <Button
+                    className="sw-ml-2 js-delete"
+                    id="coding-rules-detail-rule-delete"
+                    variety={ButtonVariety.DangerOutline}
+                  >
+                    {translate('delete')}
+                  </Button>
+                </ModalAlert>
+                <HelpTooltip
+                  className="sw-ml-2"
+                  overlay={
+                    <div className="sw-py-4">{translate('coding_rules.custom_rule.removal')}</div>
+                  }
+                >
+                  <IconQuestionMark />
+                </HelpTooltip>
               </div>
             )}
 
@@ -192,9 +199,7 @@ export default function RuleDetails(props: Readonly<Props>) {
             )}
 
             <div className="sw-my-8" data-meta="available-since">
-              <SubHeadingHighlight as="h3">
-                {translate('coding_rules.available_since')}
-              </SubHeadingHighlight>
+              <Heading as="h3">{translate('coding_rules.available_since')}</Heading>
               <DateFormatter date={ruleDetails.createdAt} />
             </div>
           </>
index 8b6f4c6cd634ad86b946744e69817cab7578fb7a..a2d4e6fc2180989b266094b4222cbd5f5104f110 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { Button, ButtonVariety } from '@sonarsource/echoes-react';
-import { sortBy } from 'lodash';
-import * as React from 'react';
 import {
-  ButtonSecondary,
-  ContentCell,
-  HeadingDark,
+  Button,
+  ButtonVariety,
+  Heading,
   Link,
+  ModalAlert,
   Spinner,
-  Table,
-  TableRow,
-  UnorderedList,
-} from '~design-system';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+} from '@sonarsource/echoes-react';
+import { sortBy } from 'lodash';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { ContentCell, Table, TableRow, UnorderedList } from '~design-system';
+import { translate } from '../../../helpers/l10n';
 import { getRuleUrl } from '../../../helpers/urls';
 import { useDeleteRuleMutation, useSearchRulesQuery } from '../../../queries/rules';
 import { Rule, RuleDetails } from '../../../types/types';
@@ -68,14 +66,18 @@ export default function RuleDetailsCustomRules(props: Readonly<Props>) {
   return (
     <div className="js-rule-custom-rules">
       <div>
-        <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark>
+        <Heading as="h2">{translate('coding_rules.custom_rules')}</Heading>
 
         {props.canChange && (
           <CustomRuleButton templateRule={ruleDetails}>
             {({ onClick }) => (
-              <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}>
+              <Button
+                variety={ButtonVariety.Default}
+                className="js-create-custom-rule sw-mt-6"
+                onClick={onClick}
+              >
                 {translate('coding_rules.create')}
-              </ButtonSecondary>
+              </Button>
             )}
           </CustomRuleButton>
         )}
@@ -96,7 +98,7 @@ export default function RuleDetailsCustomRules(props: Readonly<Props>) {
             ))}
           </Table>
         )}
-        <Spinner className="sw-my-6" loading={loading} />
+        <Spinner className="sw-my-6" isLoading={loading} />
       </div>
     </div>
   );
@@ -110,6 +112,7 @@ function RuleListItem(
   }>,
 ) {
   const { rule, editable } = props;
+  const intl = useIntl();
   return (
     <TableRow data-rule={rule.key}>
       <ContentCell>
@@ -134,25 +137,39 @@ function RuleListItem(
 
       {editable && (
         <ContentCell>
-          <ConfirmButton
-            confirmButtonText={translate('delete')}
-            confirmData={rule.key}
-            isDestructive
-            modalBody={translateWithParameters('coding_rules.delete.custom.confirm', rule.name)}
-            modalHeader={translate('coding_rules.delete_rule')}
-            onConfirm={props.onDelete}
-          >
-            {({ onClick }) => (
+          <ModalAlert
+            title={translate('coding_rules.delete_rule')}
+            description={intl.formatMessage(
+              {
+                id: 'coding_rules.delete.custom.confirm',
+              },
+              {
+                name: rule.name,
+              },
+            )}
+            primaryButton={
               <Button
-                className="js-delete-custom-rule"
-                aria-label={translateWithParameters('coding_rules.delete_rule_x', rule.name)}
-                onClick={onClick}
+                className="sw-ml-2 js-delete"
+                id="coding-rules-detail-rule-delete"
+                onClick={() => props.onDelete(rule.key)}
                 variety={ButtonVariety.DangerOutline}
               >
                 {translate('delete')}
               </Button>
-            )}
-          </ConfirmButton>
+            }
+            secondaryButtonLabel={translate('close')}
+          >
+            <Button
+              className="js-delete-custom-rule"
+              aria-label={intl.formatMessage(
+                { id: 'coding_rules.delete_rule_x' },
+                { name: rule.name },
+              )}
+              variety={ButtonVariety.DangerOutline}
+            >
+              {translate('delete')}
+            </Button>
+          </ModalAlert>
         </ContentCell>
       )}
     </TableRow>
index dad2b8e38b806cc48a951ae6edc9372d386a9cf4..358c5e1ddbcd5274409dad56efc24f0e547a15e6 100644 (file)
@@ -21,7 +21,6 @@
 import { HelperText, Select } from '@sonarsource/echoes-react';
 import { isEmpty } from 'lodash';
 import { FormattedMessage, useIntl } from 'react-intl';
-import SeverityIcon from '../../../components/icon-mappers/SeverityIcon';
 import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
 import { SEVERITIES } from '../../../helpers/constants';
 import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy';
@@ -38,12 +37,11 @@ export interface SeveritySelectProps {
 export function SeveritySelect(props: SeveritySelectProps) {
   const { isDisabled, severity, recommendedSeverity, impactSeverity, id } = props;
   const intl = useIntl();
-  const Icon = impactSeverity ? SoftwareImpactSeverityIcon : SeverityIcon;
   const getSeverityTranslation = (severity: string) =>
     impactSeverity
       ? intl.formatMessage({ id: `severity_impact.${severity}` })
       : intl.formatMessage({ id: `severity.${severity}` });
-  const serverityOption = (impactSeverity ? Object.values(SoftwareImpactSeverity) : SEVERITIES).map(
+  const severityOption = (impactSeverity ? Object.values(SoftwareImpactSeverity) : SEVERITIES).map(
     (severity) => ({
       label:
         severity === recommendedSeverity
@@ -53,7 +51,7 @@ export function SeveritySelect(props: SeveritySelectProps) {
             )
           : getSeverityTranslation(severity),
       value: severity,
-      prefix: <Icon severity={severity} aria-hidden />,
+      prefix: <SoftwareImpactSeverityIcon severity={severity} aria-hidden />,
     }),
   );
 
@@ -63,14 +61,14 @@ export function SeveritySelect(props: SeveritySelectProps) {
         id={id}
         isDisabled={isDisabled}
         onChange={props.onChange}
-        data={serverityOption}
+        data={severityOption}
         isSearchable={false}
         isNotClearable
         placeholder={
           isDisabled && !isEmpty(severity) ? intl.formatMessage({ id: 'not_impacted' }) : undefined
         }
         value={severity}
-        valueIcon={<Icon severity={severity} aria-hidden />}
+        valueIcon={<SoftwareImpactSeverityIcon severity={severity} aria-hidden />}
       />
       {severity !== recommendedSeverity && (
         <HelperText className="sw-mt-2">
index 3398f943ff79b7cb9158de813c5afa2480084604..ba73dc80bde5c723be5d935a6709a3366a39fd12 100644 (file)
@@ -31,9 +31,11 @@ import { renderAppRoutes } from '../../helpers/testReactTestingUtils';
 import {
   CleanCodeAttribute,
   CleanCodeAttributeCategory,
+  SoftwareImpactSeverity,
   SoftwareQuality,
 } from '../../types/clean-code-taxonomy';
 import { Feature } from '../../types/features';
+import { IssueSeverity, IssueType } from '../../types/issues';
 import { CurrentUser } from '../../types/users';
 import routes from './routes';
 
@@ -132,6 +134,11 @@ const selectors = {
   ruleCleanCodeAttribute: (attribute: CleanCodeAttribute) =>
     byText(new RegExp(`rule\\.clean_code_attribute\\.${attribute}$`)),
   ruleSoftwareQuality: (quality: SoftwareQuality) => byText(`software_quality.${quality}`),
+  ruleSoftwareQualityPill: (quality: SoftwareQuality, severity: SoftwareImpactSeverity) =>
+    byRole('button', { name: `software_quality.${quality} severity_impact.${severity}` }),
+  ruleIssueTypePill: (issueType: IssueType) => byRole('banner').byText(`issue.type.${issueType}`),
+  ruleIssueTypePillSeverity: (severity: IssueSeverity) =>
+    byRole('banner').byLabelText(`severity.${severity}`),
 
   // Rule tags
   tagsDropdown: byLabelText(/tags_list_x/).byRole('button'),
@@ -195,6 +202,8 @@ const selectors = {
   deleteCustomRuleDialog: byRole('alertdialog', { name: 'coding_rules.delete_rule' }),
   ruleNameTextbox: byRole('textbox', { name: 'name' }),
   keyTextbox: byRole('textbox', { name: 'key' }),
+  cctIssueTypeSelect: byRole('combobox', { name: 'coding_rules.custom.type.label' }),
+  standardIssueTypeSelect: byRole('combobox', { name: 'type' }),
   cleanCodeCategorySelect: byRole('combobox', { name: 'category' }),
   cleanCodeAttributeSelect: byRole('combobox', { name: 'attribute' }),
   cleanCodeQualityCheckbox: (quality: SoftwareQuality) =>
@@ -206,6 +215,7 @@ const selectors = {
       'combobox',
       { name: 'severity' },
     ),
+  standardSeveritySelect: byRole('combobox', { name: 'severity' }),
   statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
   descriptionTextbox: byRole('textbox', { name: 'description' }),
   createButton: byRole('button', { name: 'create' }),
index 36cd407a64b601de8c07fa4de6633fdbc0a274d2..7192a7e83894c2d939f136c4497df2cd64ad6926 100644 (file)
@@ -532,6 +532,8 @@ export interface RuleActivation {
 }
 
 export interface RulesUpdateRequest {
+  cleanCodeAttribute?: CleanCodeAttribute;
+  impacts?: SoftwareImpact[];
   key: string;
   markdownDescription?: string;
   markdown_note?: string;
@@ -540,8 +542,10 @@ export interface RulesUpdateRequest {
   remediation_fn_base_effort?: string;
   remediation_fn_type?: string;
   remediation_fy_gap_multiplier?: string;
+  severity?: string;
   status?: string;
   tags?: string;
+  type?: RuleType;
 }
 
 export interface RuleDetails extends Rule {
@@ -612,6 +616,11 @@ export const RuleTypes = [
 ] as const;
 export type RuleType = (typeof RuleTypes)[number];
 
+export enum CustomRuleType {
+  ISSUE = 'ISSUE',
+  SECURITY_HOTSPOT = 'SECURITY_HOTSPOT',
+}
+
 export interface Snippet {
   end: number;
   index: number;
index c382b9ff75f035248d3ab3f478ce927facf1bda8..38b0f0c030d805b68ac77b93dbef105ea22d887b 100644 (file)
@@ -2648,8 +2648,8 @@ coding_rules.custom_rules=Custom Rules
 coding_rules.deactivate_in_quality_profile=Deactivate In Quality Profile
 coding_rules.deactivate_in_quality_profile_x=Deactivate In Quality Profile {0}
 coding_rules.delete_rule=Delete Rule
-coding_rules.delete_rule_x=Delete Rule {0}
-coding_rules.delete.custom.confirm=Are you sure you want to delete custom rule "{0}"?
+coding_rules.delete_rule_x=Delete Rule {name}
+coding_rules.delete.custom.confirm=Are you sure you want to delete custom rule "{name}"?
 coding_rules.extend_description=Extend Description
 coding_rules.deactivate_in=Deactivate In
 coding_rules.deactivate=Deactivate
@@ -2819,6 +2819,10 @@ coding_rules.create_tag=Create Tag
 coding_rules.select_profile=Select Profile
 coding_rules.selected_profiles=Selected Profiles
 
+coding_rules.custom.type.label=Type
+coding_rules.custom.type.option.ISSUE=Issue
+coding_rules.custom.type.option.SECURITY_HOTSPOT=Security Hotspot
+
 coding_rules.system_tags_tooltip=This tag can't be removed because it has been predefined by our system
 
 rule.impact.severity.tooltip=Issues found for this rule will have a {severity} impact on the {quality} of your software.