]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20500 Migrate custom rule form to the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 26 Sep 2023 12:22:59 +0000 (14:22 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 5 Oct 2023 20:02:47 +0000 (20:02 +0000)
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/SeveritySelect.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
server/sonar-web/src/main/js/components/shared/TypeHelper.tsx

index 60daacdc04b93e7d96f41f23f7102106b86b72cf..c81c1a1238cf4cea3428852d5376adf308a790c0 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { HttpStatusCode } from 'axios';
+import {
+  ButtonPrimary,
+  FlagMessage,
+  FormField,
+  InputField,
+  InputSelect,
+  InputTextArea,
+  LabelValueSelectOption,
+  LightLabel,
+  Modal,
+} from 'design-system';
 import * as React from 'react';
-import { components, OptionProps, SingleValueProps } from 'react-select';
+import { OptionProps, SingleValueProps, components } from 'react-select';
 import { createRule, updateRule } from '../../../api/rules';
 import FormattingTips from '../../../components/common/FormattingTips';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import Modal from '../../../components/controls/Modal';
-import Select, { LabelValueSelectOption } from '../../../components/controls/Select';
 import TypeHelper from '../../../components/shared/TypeHelper';
-import { Alert } from '../../../components/ui/Alert';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { RULE_STATUSES, RULE_TYPES } from '../../../helpers/constants';
 import { csvEscape } from '../../../helpers/csv';
 import { translate } from '../../../helpers/l10n';
 import { sanitizeString } from '../../../helpers/sanitize';
 import { latinize } from '../../../helpers/strings';
-import { Dict, RuleDetails, RuleParameter, RuleType } from '../../../types/types';
+import { Dict, RuleDetails, RuleParameter, RuleType, Status } from '../../../types/types';
 import { SeveritySelect } from './SeveritySelect';
 
 interface Props {
@@ -56,28 +63,30 @@ interface State {
   type: RuleType;
 }
 
+const FORM_ID = 'custom-rule-form';
+
 export default class CustomRuleFormModal extends React.PureComponent<Props, State> {
   mounted = false;
 
   constructor(props: Props) {
     super(props);
     const params: Dict<string> = {};
-    if (props.customRule && props.customRule.params) {
+    if (props.customRule?.params) {
       for (const param of props.customRule.params) {
-        params[param.key] = param.defaultValue || '';
+        params[param.key] = param.defaultValue ?? '';
       }
     }
     this.state = {
-      description: (props.customRule && props.customRule.mdDesc) || '',
+      description: props.customRule?.mdDesc ?? '',
       key: '',
       keyModifiedByUser: false,
-      name: (props.customRule && props.customRule.name) || '',
+      name: props.customRule?.name ?? '',
       params,
       reactivating: false,
-      severity: (props.customRule && props.customRule.severity) || props.templateRule.severity,
-      status: (props.customRule && props.customRule.status) || props.templateRule.status,
+      severity: props.customRule?.severity ?? props.templateRule.severity,
+      status: props.customRule?.status ?? props.templateRule.status,
       submitting: false,
-      type: (props.customRule && props.customRule.type) || props.templateRule.type,
+      type: props.customRule?.type ?? props.templateRule.type,
     };
   }
 
@@ -124,7 +133,10 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
       },
       (response: Response) => {
         if (this.mounted) {
-          this.setState({ reactivating: response.status === 409, submitting: false });
+          this.setState({
+            reactivating: response.status === HttpStatusCode.Conflict,
+            submitting: false,
+          });
         }
       },
     );
@@ -149,7 +161,8 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
 
   handleSeverityChange = ({ value }: { value: string }) => this.setState({ severity: value });
 
-  handleStatusChange = ({ value }: LabelValueSelectOption) => this.setState({ status: value });
+  handleStatusChange = ({ value }: LabelValueSelectOption<Status>) =>
+    this.setState({ status: value });
 
   handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
     const { name, value } = event.currentTarget;
@@ -157,61 +170,66 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
   };
 
   renderNameField = () => (
-    <div className="modal-field">
-      <label htmlFor="coding-rules-custom-rule-creation-name">
-        {translate('name')} <MandatoryFieldMarker />
-      </label>
-      <input
+    <FormField
+      ariaLabel={translate('name')}
+      label={translate('name')}
+      htmlFor="coding-rules-custom-rule-creation-name"
+      required
+    >
+      <InputField
         autoFocus
         disabled={this.state.submitting}
         id="coding-rules-custom-rule-creation-name"
         onChange={this.handleNameChange}
         required
+        size="full"
         type="text"
         value={this.state.name}
       />
-    </div>
+    </FormField>
   );
 
   renderKeyField = () => (
-    <div className="modal-field">
-      <label htmlFor="coding-rules-custom-rule-creation-key">
-        {translate('key')} {!this.props.customRule && <MandatoryFieldMarker />}
-      </label>
-
+    <FormField
+      ariaLabel={translate('key')}
+      label={translate('key')}
+      htmlFor="coding-rules-custom-rule-creation-key"
+      required
+    >
       {this.props.customRule ? (
-        <span className="coding-rules-detail-custom-rule-key" title={this.props.customRule.key}>
-          {this.props.customRule.key}
-        </span>
+        <span title={this.props.customRule.key}>{this.props.customRule.key}</span>
       ) : (
-        <input
+        <InputField
           disabled={this.state.submitting}
           id="coding-rules-custom-rule-creation-key"
           onChange={this.handleKeyChange}
           required
+          size="full"
           type="text"
           value={this.state.key}
         />
       )}
-    </div>
+    </FormField>
   );
 
   renderDescriptionField = () => (
-    <div className="modal-field">
-      <label htmlFor="coding-rules-custom-rule-creation-html-description">
-        {translate('description')}
-        <MandatoryFieldMarker />
-      </label>
-      <textarea
+    <FormField
+      ariaLabel={translate('description')}
+      label={translate('description')}
+      htmlFor="coding-rules-custom-rule-creation-html-description"
+      required
+    >
+      <InputTextArea
         disabled={this.state.submitting}
         id="coding-rules-custom-rule-creation-html-description"
         onChange={this.handleDescriptionChange}
         required
         rows={5}
+        size="full"
         value={this.state.description}
       />
-      <FormattingTips className="modal-field-descriptor text-right" />
-    </div>
+      <FormattingTips />
+    </FormField>
   );
 
   renderTypeOption = (props: OptionProps<LabelValueSelectOption<RuleType>, false>) => {
@@ -236,10 +254,13 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
       value: type,
     }));
     return (
-      <div className="modal-field flex-1 spacer-right">
-        <label id="coding-rules-custom-rule-type">{translate('type')}</label>
-        <Select
-          aria-labelledby="coding-rules-custom-rule-type"
+      <FormField
+        ariaLabel={translate('type')}
+        label={translate('type')}
+        htmlFor="coding-rules-custom-rule-type"
+      >
+        <InputSelect
+          inputId="coding-rules-custom-rule-type"
           isClearable={false}
           isDisabled={this.state.submitting}
           isSearchable={false}
@@ -251,20 +272,22 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
           options={ruleTypeOption}
           value={ruleTypeOption.find((t) => t.value === this.state.type)}
         />
-      </div>
+      </FormField>
     );
   };
 
   renderSeverityField = () => (
-    <div className="modal-field flex-1 spacer-right">
-      <label id="coding-rules-custom-rule-severity">{translate('severity')}</label>
+    <FormField
+      ariaLabel={translate('severity')}
+      label={translate('severity')}
+      htmlFor="coding-rules-severity-select"
+    >
       <SeveritySelect
-        ariaLabelledby="coding-rules-custom-rule-severity"
         isDisabled={this.state.submitting}
         onChange={this.handleSeverityChange}
         severity={this.state.severity}
       />
-    </div>
+    </FormField>
   );
 
   renderStatusField = () => {
@@ -273,11 +296,13 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
       value: status,
     }));
     return (
-      <div className="modal-field flex-1">
-        <label id="coding-rules-custom-rule-status">
-          {translate('coding_rules.filters.status')}
-        </label>
-        <Select
+      <FormField
+        ariaLabel={translate('coding_rules.filters.status')}
+        label={translate('coding_rules.filters.status')}
+        htmlFor="coding-rules-custom-rule-status"
+      >
+        <InputSelect
+          inputId="coding-rules-custom-rule-status"
           isClearable={false}
           isDisabled={this.state.submitting}
           aria-labelledby="coding-rules-custom-rule-status"
@@ -286,45 +311,48 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
           isSearchable={false}
           value={statusesOptions.find((s) => s.value === this.state.status)}
         />
-      </div>
+      </FormField>
     );
   };
 
   renderParameterField = (param: RuleParameter) => (
-    <div className="modal-field" key={param.key}>
-      <label className="capitalize" htmlFor={param.key}>
-        {param.key}
-      </label>
-
+    <FormField
+      ariaLabel={param.key}
+      className="sw-capitalize"
+      label={param.key}
+      htmlFor={`coding-rule-custom-rule-${param.key}`}
+      key={param.key}
+    >
       {param.type === 'TEXT' ? (
-        <textarea
+        <InputTextArea
           disabled={this.state.submitting}
-          id={param.key}
+          id={`coding-rule-custom-rule-${param.key}`}
           name={param.key}
           onChange={this.handleParameterChange}
           placeholder={param.defaultValue}
+          size="full"
           rows={3}
           value={this.state.params[param.key] || ''}
         />
       ) : (
-        <input
+        <InputField
           disabled={this.state.submitting}
-          id={param.key}
+          id={`coding-rule-custom-rule-${param.key}`}
           name={param.key}
           onChange={this.handleParameterChange}
           placeholder={param.defaultValue}
+          size="full"
           type="text"
-          value={this.state.params[param.key] || ''}
+          value={this.state.params[param.key] ?? ''}
         />
       )}
       {param.htmlDesc !== undefined && (
-        <div
-          className="modal-field-description"
+        <LightLabel
           // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
         />
       )}
-    </div>
+    </FormField>
   );
 
   render() {
@@ -339,44 +367,41 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
       submit = translate('coding_rules.reactivate');
     }
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-
-          <div className="modal-body modal-container">
+      <Modal
+        headerTitle={header}
+        onClose={this.props.onClose}
+        body={
+          <form
+            className="sw-flex sw-flex-col sw-justify-stretch sw-pb-4"
+            id={FORM_ID}
+            onSubmit={this.handleFormSubmit}
+          >
             {reactivating && (
-              <Alert variant="warning">{translate('coding_rules.reactivate.help')}</Alert>
+              <FlagMessage variant="warning" className="sw-mb-6">
+                {translate('coding_rules.reactivate.help')}
+              </FlagMessage>
             )}
 
-            <MandatoryFieldsExplanation className="spacer-bottom" />
+            <MandatoryFieldsExplanation className="sw-mb-4" />
 
             {this.renderNameField()}
             {this.renderKeyField()}
-            <div className="display-flex-space-between">
-              {/* do not allow to change the type of existing rule */}
-              {!customRule && this.renderTypeField()}
-              {this.renderSeverityField()}
-              {this.renderStatusField()}
-            </div>
+            {/* do not allow to change the type of existing rule */}
+            {!customRule && this.renderTypeField()}
+            {this.renderSeverityField()}
+            {this.renderStatusField()}
             {this.renderDescriptionField()}
             {params.map(this.renderParameterField)}
-          </div>
-
-          <div className="modal-foot">
-            {submitting && <i className="spinner spacer-right" />}
-            <SubmitButton disabled={this.state.submitting}>{submit}</SubmitButton>
-            <ResetButtonLink
-              disabled={submitting}
-              id="coding-rules-custom-rule-creation-cancel"
-              onClick={this.props.onClose}
-            >
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
+          </form>
+        }
+        primaryButton={
+          <ButtonPrimary disabled={submitting} type="submit" form={FORM_ID}>
+            {submit}
+          </ButtonPrimary>
+        }
+        loading={submitting}
+        secondaryButtonLabel={translate('cancel')}
+      />
     );
   }
 }
index feafa53b4daa13d552979911022de0782f6c9c4d..cdf4b644195b3b37365b526f401357e2d4bf79d4 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { SubHeadingHighlight } from 'design-system/lib';
+import {
+  ButtonSecondary,
+  DangerButtonSecondary,
+  HelperHintIcon,
+  SubHeadingHighlight,
+} from 'design-system';
 import * as React from 'react';
 import { Profile } from '../../../api/quality-profiles';
 import { deleteRule, getRuleDetails, updateRule } from '../../../api/rules';
 import ConfirmButton from '../../../components/controls/ConfirmButton';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { Button } from '../../../components/controls/buttons';
 import DateFormatter from '../../../components/intl/DateFormatter';
 import Spinner from '../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -191,13 +195,13 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
                 templateRule={ruleDetails}
               >
                 {({ onClick }) => (
-                  <Button
+                  <ButtonSecondary
                     className="js-edit-custom"
                     id="coding-rules-detail-custom-rule-change"
                     onClick={onClick}
                   >
                     {translate('edit')}
-                  </Button>
+                  </ButtonSecondary>
                 )}
               </CustomRuleButton>
               <ConfirmButton
@@ -212,21 +216,23 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
               >
                 {({ onClick }) => (
                   <>
-                    <Button
-                      className="button-red spacer-left js-delete"
+                    <DangerButtonSecondary
+                      className="sw-ml-2 js-delete"
                       id="coding-rules-detail-rule-delete"
                       onClick={onClick}
                     >
                       {translate('delete')}
-                    </Button>
+                    </DangerButtonSecondary>
                     <HelpTooltip
-                      className="spacer-left"
+                      className="sw-ml-2"
                       overlay={
-                        <div className="big-padded-top big-padded-bottom">
+                        <div className="sw-py-4">
                           {translate('coding_rules.custom_rule.removal')}
                         </div>
                       }
-                    />
+                    >
+                      <HelperHintIcon />
+                    </HelpTooltip>
                   </>
                 )}
               </ConfirmButton>
@@ -255,7 +261,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
             <RuleDetailsIssues ruleDetails={ruleDetails} />
           )}
 
-          <div className="sw-mb-8" data-meta="available-since">
+          <div className="sw-my-8" data-meta="available-since">
             <SubHeadingHighlight as="h3">
               {translate('coding_rules.available_since')}
             </SubHeadingHighlight>
index 69ce2fb8b60db5818d102e6bc6f685a6fd9e6906..e3b3a02f4404305c793a9b40df13a37abaa5c7d2 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InputSelect, LabelValueSelectOption } from 'design-system';
 import * as React from 'react';
-import { components, OptionProps, SingleValueProps } from 'react-select';
-import Select, { LabelValueSelectOption } from '../../../components/controls/Select';
+import { OptionProps, SingleValueProps, components } from 'react-select';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import { SEVERITIES } from '../../../helpers/constants';
 import { translate } from '../../../helpers/l10n';
+import { IssueSeverity } from '../../../types/issues';
 
 export interface SeveritySelectProps {
   isDisabled: boolean;
   severity: string;
-  ariaLabelledby: string;
-  onChange: (value: LabelValueSelectOption) => void;
+  onChange: (value: LabelValueSelectOption<IssueSeverity>) => void;
 }
 
-function Option(props: OptionProps<LabelValueSelectOption, false>) {
+function Option(props: Readonly<OptionProps<LabelValueSelectOption<IssueSeverity>, false>>) {
   return (
     <components.Option {...props}>
       <SeverityHelper className="display-flex-center" severity={props.data.value} />
@@ -39,7 +39,9 @@ function Option(props: OptionProps<LabelValueSelectOption, false>) {
   );
 }
 
-function SingleValue(props: SingleValueProps<LabelValueSelectOption, false>) {
+function SingleValue(
+  props: Readonly<SingleValueProps<LabelValueSelectOption<IssueSeverity>, false>>,
+) {
   return (
     <components.SingleValue {...props}>
       <SeverityHelper className="display-flex-center" severity={props.data.value} />
@@ -48,16 +50,16 @@ function SingleValue(props: SingleValueProps<LabelValueSelectOption, false>) {
 }
 
 export function SeveritySelect(props: SeveritySelectProps) {
-  const { isDisabled, severity, ariaLabelledby } = props;
+  const { isDisabled, severity } = props;
   const serverityOption = SEVERITIES.map((severity) => ({
     label: translate('severity', severity),
     value: severity,
   }));
 
   return (
-    <Select
-      id="coding-rules-severity-select"
-      aria-labelledby={ariaLabelledby}
+    <InputSelect
+      aria-label={translate('severity')}
+      inputId="coding-rules-severity-select"
       isDisabled={isDisabled}
       onChange={props.onChange}
       components={{ Option, SingleValue }}
index 2a7b9afd7e99813c03890bb70e3dd97cb4e8a203..89d62d04d69d158d9212a35ee9b9db8f0396c81b 100644 (file)
@@ -161,11 +161,11 @@ const selectors = {
   createCustomRuleDialog: byRole('dialog', { name: 'coding_rules.create_custom_rule' }),
   updateCustomRuleDialog: byRole('dialog', { name: 'coding_rules.update_custom_rule' }),
   deleteCustomRuleDialog: byRole('dialog', { name: 'coding_rules.delete_rule' }),
-  ruleNameTextbox: byRole('textbox', { name: 'name field_required' }),
-  keyTextbox: byRole('textbox', { name: 'key field_required' }),
+  ruleNameTextbox: byRole('textbox', { name: 'name' }),
+  keyTextbox: byRole('textbox', { name: 'key' }),
   typeSelect: byRole('combobox', { name: 'type' }),
   statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
-  descriptionTextbox: byRole('textbox', { name: 'description field_required' }),
+  descriptionTextbox: byRole('textbox', { name: 'description' }),
   createButton: byRole('button', { name: 'create' }),
   deleteButton: byRole('button', { name: 'delete' }),
 };
index 3eabdee5a18fcf820a1e3eabb9f1bc0f656cae55..78c9c821f9bccd11e0a9a716cea5f70ed5b0dac5 100644 (file)
@@ -18,9 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import IssueTypeIcon from '../../components/icons/IssueTypeIcon';
 import { translate } from '../../helpers/l10n';
 import { IssueType, RuleType } from '../../types/types';
+import IssueTypeIcon from '../icon-mappers/IssueTypeIcon';
 
 interface Props {
   className?: string;
@@ -30,7 +30,7 @@ interface Props {
 export default function TypeHelper(props: Props) {
   return (
     <span className={props.className}>
-      <IssueTypeIcon className="little-spacer-right" query={props.type} />
+      <IssueTypeIcon className="sw-mr-1" type={props.type} />
       {translate('issue.type', props.type)}
     </span>
   );