]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21131 Update rules app with react query
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 1 Dec 2023 13:02:46 +0000 (14:02 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 4 Dec 2023 20:03:21 +0000 (20:03 +0000)
16 files changed:
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.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/RuleDetailsDescription.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/FiltersHeader.tsx
server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx
server/sonar-web/src/main/js/queries/quality-profiles.ts
server/sonar-web/src/main/js/queries/rules.ts [new file with mode: 0644]

index d7f9b6020bd7219b32556fe736d0bc7db0a955f4..28bbeb45838e68997247cf8cbc68abd23ec94e7a 100644 (file)
@@ -359,7 +359,7 @@ export default class CodingRulesServiceMock {
       data.remediation_fn_type !== undefined ? data.remediation_fn_type : rule.remFnType;
     rule.severity = data.severity !== undefined ? data.severity : rule.severity;
     rule.status = data.status !== undefined ? data.status : rule.status;
-    rule.tags = data.tags !== undefined ? data.tags.split(';') : rule.tags;
+    rule.tags = data.tags !== undefined ? data.tags.split(',') : rule.tags;
 
     return this.reply(rule);
   };
index b58a77561fed5c06b2c5cca5f600d03476eab8d8..8f10efc1fccb769bbaca6f41832cd8e93c82a4ea 100644 (file)
@@ -323,18 +323,25 @@ export function bulkDeactivateRules(data: BulkActivateParameters) {
   return postJSON('/api/qualityprofiles/deactivate_rules', data);
 }
 
-export function activateRule(data: {
+export interface ActivateRuleParameters {
   key: string;
   params?: Dict<string>;
   reset?: boolean;
   rule: string;
   severity?: string;
-}) {
+}
+
+export function activateRule(data: ActivateRuleParameters) {
   const params =
     data.params && map(data.params, (value, key) => `${key}=${csvEscape(value)}`).join(';');
   return post('/api/qualityprofiles/activate_rule', { ...data, params }).catch(throwGlobalError);
 }
 
-export function deactivateRule(data: { key: string; rule: string }) {
+export interface DeactivateRuleParameters {
+  key: string;
+  rule: string;
+}
+
+export function deactivateRule(data: DeactivateRuleParameters) {
   return post('/api/qualityprofiles/deactivate_rule', data).catch(throwGlobalError);
 }
index b80658392a7302e16440417076568b5bbc0a4734..a6497ce29e70a733a6bc15a36897b211480ebad3 100644 (file)
@@ -380,7 +380,7 @@ describe('Rule app details', () => {
     it('shows rule with default description section and params', async () => {
       const { ui } = getPageObjects();
       renderCodingRulesApp(undefined, 'coding_rules?open=rule1');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       expect(ui.ruleTitle('Awsome java rule').get()).toBeInTheDocument();
       expect(
         ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Adaptable).get(),
@@ -400,7 +400,7 @@ describe('Rule app details', () => {
     it('shows external rule', async () => {
       const { ui } = getPageObjects();
       renderCodingRulesApp(undefined, 'coding_rules?open=rule6');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       expect(ui.ruleTitle('Bad Python rule').get()).toBeInTheDocument();
       expect(ui.externalDescription('Bad Python rule').get()).toBeInTheDocument();
     });
@@ -408,7 +408,7 @@ describe('Rule app details', () => {
     it('shows hotspot rule', async () => {
       const { ui, user } = getPageObjects();
       renderCodingRulesApp(undefined, 'coding_rules?open=rule2');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       expect(ui.ruleTitle('Hot hotspot').get()).toBeInTheDocument();
       expect(ui.introTitle.get()).toBeInTheDocument();
 
@@ -424,7 +424,7 @@ describe('Rule app details', () => {
     it('shows rule advanced section', async () => {
       const { ui } = getPageObjects();
       renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument();
       expect(ui.introTitle.get()).toBeInTheDocument();
       // Shows correct tabs
@@ -436,7 +436,7 @@ describe('Rule app details', () => {
     it('shows rule advanced section with context', async () => {
       const { ui, user } = getPageObjects();
       renderCodingRulesApp(undefined, 'coding_rules?open=rule7');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       expect(ui.ruleTitle('Python rule with context').get()).toBeInTheDocument();
 
       await user.click(ui.howToFixTab.get());
@@ -455,7 +455,7 @@ describe('Rule app details', () => {
     it('should show CYAC notification for rule advanced section and removes it after user`s visit', async () => {
       const { ui, user } = getPageObjects();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule10');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       await user.click(ui.moreInfoTab.get());
 
       expect(ui.caycNotificationButton.get()).toBeInTheDocument();
@@ -470,7 +470,7 @@ describe('Rule app details', () => {
     it('should show CAYC notification for rule advanced section and removes it when user scrolls to the principles', async () => {
       const { ui, user } = getPageObjects();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule10');
-      await ui.appLoaded();
+      await ui.detailsloaded();
       await user.click(ui.moreInfoTab.get());
       expect(ui.caycNotificationButton.get()).toBeInTheDocument();
 
@@ -487,7 +487,7 @@ describe('Rule app details', () => {
       const { ui, user } = getPageObjects();
       renderCodingRulesApp(mockCurrentUser(), 'coding_rules?open=rule10');
 
-      await ui.appLoaded();
+      await ui.detailsloaded();
       await user.click(ui.moreInfoTab.get());
 
       expect(ui.caycNotificationButton.query()).not.toBeInTheDocument();
@@ -498,7 +498,7 @@ describe('Rule app details', () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
-    await ui.appLoaded();
+    await ui.detailsloaded();
     expect(ui.qpLink('QP Foo').get()).toBeInTheDocument();
 
     // Activate rule in quality profile
@@ -542,7 +542,7 @@ describe('Rule app details', () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
-    await ui.appLoaded();
+    await ui.detailsloaded();
 
     // Should show 2 deactivate buttons: one for the parent, one for the child profile.
     expect(ui.deactivateInQPButton('QP FooBarBaz').get()).toBeInTheDocument();
@@ -559,7 +559,7 @@ describe('Rule app details', () => {
     rulesHandler.setIsAdmin();
     settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
     renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
-    await ui.appLoaded();
+    await ui.detailsloaded();
 
     // Should show 1 deactivate button: one for the parent, none for the child profile.
     expect(ui.deactivateInQPButton('QP FooBarBaz').get()).toBeInTheDocument();
@@ -570,7 +570,7 @@ describe('Rule app details', () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
-    await ui.appLoaded();
+    await ui.detailsloaded();
     expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument();
 
     // Add
@@ -603,7 +603,7 @@ describe('Rule app details', () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(undefined, 'coding_rules?open=rule10');
-    await ui.appLoaded();
+    await ui.detailsloaded();
 
     await user.click(ui.tagsDropdown.get());
 
@@ -670,7 +670,7 @@ describe('Rule app details', () => {
       const { ui, user } = getPageObjects();
       rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
-      await ui.appLoaded();
+      await ui.detailsloaded();
 
       await user.click(ui.editCustomRuleButton.get());
 
@@ -688,7 +688,7 @@ describe('Rule app details', () => {
       const { ui, user } = getPageObjects();
       rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
-      await ui.appLoaded();
+      await ui.detailsloaded();
 
       await user.click(ui.deleteButton.get());
       await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
@@ -701,7 +701,7 @@ describe('Rule app details', () => {
       const { ui, user } = getPageObjects();
       rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
-      await ui.appLoaded();
+      await ui.detailsloaded();
 
       await user.click(ui.deleteCustomRuleButton('Custom Rule based on rule8').get());
       await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
index 24aa404d8218a42d08eb690d97b4f2939b2cf368..b2610f5aa8c702db77ae4bd8bc115d1f35b99208 100644 (file)
@@ -28,7 +28,7 @@ interface Props {
   buttonText: string;
   className?: string;
   modalHeader: string;
-  onDone: (severity: string) => Promise<void>;
+  onDone?: (severity: string) => Promise<void> | void;
   profiles: BaseProfile[];
   rule: Rule | RuleDetails;
   ariaLabel?: string;
index f1edb4ec687a727ceb45723b7ca9daf344277a2c..01771fdf72ac28c06e9725738ba395ad8062c4fe 100644 (file)
@@ -29,10 +29,11 @@ import {
   Note,
 } from 'design-system';
 import * as React from 'react';
-import { Profile, activateRule } from '../../../api/quality-profiles';
+import { Profile } from '../../../api/quality-profiles';
 import DocLink from '../../../components/common/DocLink';
 import { translate } from '../../../helpers/l10n';
 import { sanitizeString } from '../../../helpers/sanitize';
+import { useActivateRuleMutation } from '../../../queries/quality-profiles';
 import { IssueSeverity } from '../../../types/issues';
 import { Dict, Rule, RuleActivation, RuleDetails } from '../../../types/types';
 import { sortProfiles } from '../../quality-profiles/utils';
@@ -42,8 +43,7 @@ interface Props {
   activation?: RuleActivation;
   modalHeader: string;
   onClose: () => void;
-  onDone: (severity: string) => Promise<void>;
-  // eslint-disable-next-line react/no-unused-prop-types
+  onDone?: (severity: string) => Promise<void> | void;
   profiles: Profile[];
   rule: Rule | RuleDetails;
 }
@@ -52,222 +52,190 @@ interface ProfileWithDepth extends Profile {
   depth: number;
 }
 
-interface State {
-  params: Dict<string>;
-  profile?: ProfileWithDepth;
-  submitting: boolean;
-  severity: IssueSeverity;
-}
-
 const MIN_PROFILES_TO_ENABLE_SELECT = 2;
 const FORM_ID = 'rule-activation-modal-form';
 
-export default class ActivationFormModal extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    const profilesWithDepth = this.getQualityProfilesWithDepth(props);
-    this.state = {
-      params: this.getParams(props),
-      profile: profilesWithDepth.length > 0 ? profilesWithDepth[0] : undefined,
-      submitting: false,
-      severity: (props.activation
-        ? props.activation.severity
-        : props.rule.severity) as IssueSeverity,
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  getParams = ({ activation, rule } = this.props) => {
-    const params: Dict<string> = {};
-    if (rule?.params) {
-      for (const param of rule.params) {
-        params[param.key] = param.defaultValue || '';
-      }
-      if (activation?.params) {
-        for (const param of activation.params) {
-          params[param.key] = param.value;
-        }
-      }
-    }
-    return params;
-  };
-
-  // Choose QP which a user can administrate, which are the same language and which are not built-in
-  getQualityProfilesWithDepth = ({ profiles } = this.props) => {
-    return sortProfiles(
-      profiles.filter(
-        (profile) =>
-          !profile.isBuiltIn &&
-          profile.actions &&
-          profile.actions.edit &&
-          profile.language === this.props.rule.lang,
-      ),
-    ).map((profile) => ({
-      ...profile,
-      // Decrease depth by 1, so the top level starts at 0
-      depth: profile.depth - 1,
-    }));
-  };
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+export default function ActivationFormModal(props: Readonly<Props>) {
+  const { activation, rule, profiles, modalHeader } = props;
+  const { mutate: activateRule, isLoading: submitting } = useActivateRuleMutation((data) => {
+    props.onDone?.(data.severity as string);
+    props.onClose();
+  });
+
+  const profilesWithDepth = getQualityProfilesWithDepth(profiles, rule.lang);
+  const [profile, setProfile] = React.useState(profilesWithDepth[0]);
+  const [params, setParams] = React.useState(getRuleParams({ activation, rule }));
+  const [severity, setSeverity] = React.useState(
+    (activation ? activation.severity : rule.severity) as IssueSeverity,
+  );
+
+  const profileOptions = profilesWithDepth.map((p) => ({ label: p.name, value: p }));
+  const isCustomRule = !!(rule as RuleDetails).templateKey;
+  const activeInAllProfiles = profilesWithDepth.length <= 0;
+  const isUpdateMode = !!activation;
+
+  const handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
-    this.setState({ submitting: true });
     const data = {
-      key: this.state.profile?.key ?? '',
-      params: this.state.params,
-      rule: this.props.rule.key,
-      severity: this.state.severity,
+      key: profile?.key ?? '',
+      params,
+      rule: rule.key,
+      severity,
     };
-    activateRule(data)
-      .then(() => this.props.onDone(data.severity))
-      .then(
-        () => {
-          if (this.mounted) {
-            this.setState({ submitting: false });
-            this.props.onClose();
-          }
-        },
-        () => {
-          if (this.mounted) {
-            this.setState({ submitting: false });
-          }
-        },
-      );
+    activateRule(data);
   };
 
-  handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+  const handleParameterChange = (
+    event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>,
+  ) => {
     const { name, value } = event.currentTarget;
-    this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
-  };
-
-  handleProfileChange = (value: LabelValueSelectOption<ProfileWithDepth>) => {
-    this.setState({ profile: value.value });
-  };
-
-  handleSeverityChange = ({ value }: LabelValueSelectOption<IssueSeverity>) => {
-    this.setState({ severity: value });
+    setParams({ ...params, [name]: value });
   };
 
-  render() {
-    const { activation, rule } = this.props;
-    const { profile, severity, submitting } = this.state;
-    const { params = [] } = rule;
-    const profilesWithDepth = this.getQualityProfilesWithDepth();
-    const profileOptions = profilesWithDepth.map((p) => ({ label: p.name, value: p }));
-    const isCustomRule = !!(rule as RuleDetails).templateKey;
-    const activeInAllProfiles = profilesWithDepth.length <= 0;
-    const isUpdateMode = !!activation;
-
-    return (
-      <Modal
-        headerTitle={this.props.modalHeader}
-        onClose={this.props.onClose}
-        loading={submitting}
-        isOverflowVisible
-        primaryButton={
-          <ButtonPrimary disabled={submitting || activeInAllProfiles} form={FORM_ID} type="submit">
-            {isUpdateMode ? translate('save') : translate('coding_rules.activate')}
-          </ButtonPrimary>
-        }
-        secondaryButtonLabel={translate('cancel')}
-        body={
-          <form id={FORM_ID} onSubmit={this.handleFormSubmit}>
-            {!isUpdateMode && activeInAllProfiles && (
-              <FlagMessage className="sw-mb-2" variant="info">
-                {translate('coding_rules.active_in_all_profiles')}
-              </FlagMessage>
-            )}
-
-            <FlagMessage className="sw-mb-4" variant="info">
-              {translate('coding_rules.severity_deprecated')}
-              <DocLink className="sw-ml-2 sw-whitespace-nowrap" to="/user-guide/clean-code/">
-                {translate('learn_more')}
-              </DocLink>
+  return (
+    <Modal
+      headerTitle={modalHeader}
+      onClose={props.onClose}
+      loading={submitting}
+      isOverflowVisible
+      primaryButton={
+        <ButtonPrimary disabled={submitting || activeInAllProfiles} form={FORM_ID} type="submit">
+          {isUpdateMode ? translate('save') : translate('coding_rules.activate')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+      body={
+        <form id={FORM_ID} onSubmit={handleFormSubmit}>
+          {!isUpdateMode && activeInAllProfiles && (
+            <FlagMessage className="sw-mb-2" variant="info">
+              {translate('coding_rules.active_in_all_profiles')}
             </FlagMessage>
+          )}
+
+          <FlagMessage className="sw-mb-4" variant="info">
+            {translate('coding_rules.severity_deprecated')}
+            <DocLink className="sw-ml-2 sw-whitespace-nowrap" to="/user-guide/clean-code/">
+              {translate('learn_more')}
+            </DocLink>
+          </FlagMessage>
+
+          <FormField
+            ariaLabel={translate('coding_rules.quality_profile')}
+            label={translate('coding_rules.quality_profile')}
+            htmlFor="coding-rules-quality-profile-select-input"
+          >
+            <InputSelect
+              id="coding-rules-quality-profile-select"
+              inputId="coding-rules-quality-profile-select-input"
+              isClearable={false}
+              isDisabled={submitting || profilesWithDepth.length < MIN_PROFILES_TO_ENABLE_SELECT}
+              onChange={({ value }: LabelValueSelectOption<ProfileWithDepth>) => {
+                setProfile(value);
+              }}
+              getOptionLabel={({ value }: LabelValueSelectOption<ProfileWithDepth>) =>
+                '   '.repeat(value.depth) + value.name
+              }
+              options={profileOptions}
+              value={profileOptions.find(({ value }) => value.key === profile?.key)}
+            />
+          </FormField>
+
+          <FormField
+            ariaLabel={translate('severity')}
+            label={translate('severity')}
+            htmlFor="coding-rules-severity-select"
+          >
+            <SeveritySelect
+              isDisabled={submitting}
+              onChange={({ value }: LabelValueSelectOption<IssueSeverity>) => {
+                setSeverity(value);
+              }}
+              severity={severity}
+            />
+          </FormField>
+
+          {isCustomRule ? (
+            <Note as="p" className="sw-my-4">
+              {translate('coding_rules.custom_rule.activation_notice')}
+            </Note>
+          ) : (
+            rule.params?.map((param) => (
+              <FormField label={param.key} key={param.key} htmlFor={param.key}>
+                {param.type === 'TEXT' ? (
+                  <InputTextArea
+                    id={param.key}
+                    disabled={submitting}
+                    name={param.key}
+                    onChange={handleParameterChange}
+                    placeholder={param.defaultValue}
+                    rows={3}
+                    size="full"
+                    value={params[param.key] ?? ''}
+                  />
+                ) : (
+                  <InputField
+                    id={param.key}
+                    disabled={submitting}
+                    name={param.key}
+                    onChange={handleParameterChange}
+                    placeholder={param.defaultValue}
+                    size="full"
+                    type="text"
+                    value={params[param.key] ?? ''}
+                  />
+                )}
+                {param.htmlDesc !== undefined && (
+                  <Note
+                    as="div"
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+                  />
+                )}
+              </FormField>
+            ))
+          )}
+        </form>
+      }
+    />
+  );
+}
 
-            <FormField
-              ariaLabel={translate('coding_rules.quality_profile')}
-              label={translate('coding_rules.quality_profile')}
-              htmlFor="coding-rules-quality-profile-select-input"
-            >
-              <InputSelect
-                id="coding-rules-quality-profile-select"
-                inputId="coding-rules-quality-profile-select-input"
-                isClearable={false}
-                isDisabled={submitting || profilesWithDepth.length < MIN_PROFILES_TO_ENABLE_SELECT}
-                onChange={this.handleProfileChange}
-                getOptionLabel={({ value }: LabelValueSelectOption<ProfileWithDepth>) =>
-                  '   '.repeat(value.depth) + value.name
-                }
-                options={profileOptions}
-                value={profileOptions.find(({ value }) => value.key === profile?.key)}
-              />
-            </FormField>
-
-            <FormField
-              ariaLabel={translate('severity')}
-              label={translate('severity')}
-              htmlFor="coding-rules-severity-select"
-            >
-              <SeveritySelect
-                isDisabled={submitting}
-                onChange={this.handleSeverityChange}
-                severity={severity}
-              />
-            </FormField>
+function getQualityProfilesWithDepth(
+  profiles: Profile[] = [],
+  ruleLang?: string,
+): ProfileWithDepth[] {
+  return sortProfiles(
+    profiles.filter(
+      (profile) =>
+        !profile.isBuiltIn &&
+        profile.actions &&
+        profile.actions.edit &&
+        profile.language === ruleLang,
+    ),
+  ).map((profile) => ({
+    ...profile,
+    // Decrease depth by 1, so the top level starts at 0
+    depth: profile.depth - 1,
+  }));
+}
 
-            {isCustomRule ? (
-              <Note as="p" className="sw-my-4">
-                {translate('coding_rules.custom_rule.activation_notice')}
-              </Note>
-            ) : (
-              params.map((param) => (
-                <FormField label={param.key} key={param.key} htmlFor={param.key}>
-                  {param.type === 'TEXT' ? (
-                    <InputTextArea
-                      id={param.key}
-                      disabled={submitting}
-                      name={param.key}
-                      onChange={this.handleParameterChange}
-                      placeholder={param.defaultValue}
-                      rows={3}
-                      size="full"
-                      value={this.state.params[param.key] ?? ''}
-                    />
-                  ) : (
-                    <InputField
-                      id={param.key}
-                      disabled={submitting}
-                      name={param.key}
-                      onChange={this.handleParameterChange}
-                      placeholder={param.defaultValue}
-                      size="full"
-                      type="text"
-                      value={this.state.params[param.key] ?? ''}
-                    />
-                  )}
-                  {param.htmlDesc !== undefined && (
-                    <Note
-                      as="div"
-                      // eslint-disable-next-line react/no-danger
-                      dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
-                    />
-                  )}
-                </FormField>
-              ))
-            )}
-          </form>
-        }
-      />
-    );
+function getRuleParams({
+  activation,
+  rule,
+}: {
+  activation?: RuleActivation;
+  rule: RuleDetails | Rule;
+}) {
+  const params: Dict<string> = {};
+  if (rule?.params) {
+    for (const param of rule.params) {
+      params[param.key] = param.defaultValue ?? '';
+    }
+    if (activation?.params) {
+      for (const param of activation.params) {
+        params[param.key] = param.value;
+      }
+    }
   }
+  return params;
 }
index 229d8dfb3be5665629189cc415824db464d44393..c11f794e06df11a32ae23ccc8e82bed268504e16 100644 (file)
@@ -24,7 +24,6 @@ import CustomRuleFormModal from './CustomRuleFormModal';
 interface Props {
   children: (props: { onClick: () => void }) => React.ReactNode;
   customRule?: RuleDetails;
-  onDone: (newRuleDetails: RuleDetails) => void;
   templateRule: RuleDetails;
 }
 
@@ -32,11 +31,6 @@ export default function CustomRuleButton(props: Props) {
   const { customRule, templateRule } = props;
   const [modalOpen, setModalOpen] = React.useState(false);
 
-  const handleDone = (newRuleDetails: RuleDetails) => {
-    setModalOpen(false);
-    props.onDone(newRuleDetails);
-  };
-
   return (
     <>
       {props.children({ onClick: () => setModalOpen(true) })}
@@ -44,7 +38,6 @@ export default function CustomRuleButton(props: Props) {
         <CustomRuleFormModal
           customRule={customRule}
           onClose={() => setModalOpen(false)}
-          onDone={handleDone}
           templateRule={templateRule}
         />
       )}
index a241a4b187e6625ad87636480e39a0631341bafb..a8c41fc8fd672f066c0ec1f1486634f2c159f8c6 100644 (file)
@@ -31,7 +31,6 @@ import {
 } from 'design-system';
 import * as React from 'react';
 import { OptionProps, SingleValueProps, components } from 'react-select';
-import { createRule, updateRule } from '../../../api/rules';
 import FormattingTips from '../../../components/common/FormattingTips';
 import TypeHelper from '../../../components/shared/TypeHelper';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
@@ -40,215 +39,148 @@ import { csvEscape } from '../../../helpers/csv';
 import { translate } from '../../../helpers/l10n';
 import { sanitizeString } from '../../../helpers/sanitize';
 import { latinize } from '../../../helpers/strings';
+import { useCreateRuleMutation, useUpdateRuleMutation } from '../../../queries/rules';
 import { Dict, RuleDetails, RuleParameter, RuleType, Status } from '../../../types/types';
 import { SeveritySelect } from './SeveritySelect';
 
 interface Props {
   customRule?: RuleDetails;
   onClose: () => void;
-  onDone: (newRuleDetails: RuleDetails) => void;
   templateRule: RuleDetails;
 }
 
-interface State {
-  description: string;
-  key: string;
-  keyModifiedByUser: boolean;
-  name: string;
-  params: Dict<string>;
-  reactivating: boolean;
-  severity: string;
-  status: string;
-  submitting: boolean;
-  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?.params) {
-      for (const param of props.customRule.params) {
-        params[param.key] = param.defaultValue ?? '';
-      }
-    }
-    this.state = {
-      description: props.customRule?.mdDesc ?? '',
-      key: '',
-      keyModifiedByUser: false,
-      name: props.customRule?.name ?? '',
-      params,
-      reactivating: false,
-      severity: props.customRule?.severity ?? props.templateRule.severity,
-      status: props.customRule?.status ?? props.templateRule.status,
-      submitting: false,
-      type: props.customRule?.type ?? props.templateRule.type,
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
+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 [severity, setSeverity] = React.useState(customRule?.severity ?? templateRule.severity);
+  const [status, setStatus] = React.useState(customRule?.status ?? templateRule.status);
+  const [type, setType] = React.useState(customRule?.type ?? templateRule.type);
+  const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(props.onClose);
+  const { mutate: createRule, isLoading: creatingRule } = useCreateRuleMutation(
+    {
+      f: 'name,severity,params',
+      template_key: templateRule.key,
+    },
+    props.onClose,
+    (response: Response) => {
+      setReactivating(response.status === HttpStatusCode.Conflict);
+    },
+  );
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const submitting = updatingRule || creatingRule;
 
-  prepareRequest = () => {
-    const { customRule, templateRule } = this.props;
-    const params = Object.keys(this.state.params)
-      .map((key) => `${key}=${csvEscape(this.state.params[key])}`)
+  const submit = () => {
+    const stringifiedParams = Object.keys(params)
+      .map((key) => `${key}=${csvEscape(params[key])}`)
       .join(';');
     const ruleData = {
-      markdownDescription: this.state.description,
-      name: this.state.name,
-      params,
-      severity: this.state.severity,
-      status: this.state.status,
+      markdownDescription: description,
+      name,
+      params: stringifiedParams,
+      severity,
+      status,
     };
     return customRule
       ? updateRule({ ...ruleData, key: customRule.key })
       : createRule({
           ...ruleData,
-          customKey: this.state.key,
-          preventReactivation: !this.state.reactivating,
+          customKey: key,
+          preventReactivation: !reactivating,
           templateKey: templateRule.key,
-          type: this.state.type,
+          type,
         });
   };
 
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    this.setState({ submitting: true });
-    this.prepareRequest().then(
-      (newRuleDetails) => {
-        if (this.mounted) {
-          this.setState({ submitting: false });
-          this.props.onDone(newRuleDetails);
-        }
-      },
-      (response: Response) => {
-        if (this.mounted) {
-          this.setState({
-            reactivating: response.status === HttpStatusCode.Conflict,
-            submitting: false,
-          });
-        }
-      },
-    );
-  };
-
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    const { value: name } = event.currentTarget;
-    this.setState((state) => ({
-      name,
-      key: state.keyModifiedByUser ? state.key : latinize(name).replace(/[^A-Za-z0-9]/g, '_'),
-    }));
-  };
-
-  handleKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
-    this.setState({ key: event.currentTarget.value, keyModifiedByUser: true });
-
-  handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
-    this.setState({ description: event.currentTarget.value });
-
-  handleTypeChange = ({ value }: LabelValueSelectOption<RuleType>) =>
-    this.setState({ type: value });
-
-  handleSeverityChange = ({ value }: { value: string }) => this.setState({ severity: value });
-
-  handleStatusChange = ({ value }: LabelValueSelectOption<Status>) =>
-    this.setState({ status: value });
-
-  handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
-    const { name, value } = event.currentTarget;
-    this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
-  };
-
-  renderNameField = () => (
-    <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}
+  const NameField = React.useMemo(
+    () => (
+      <FormField
+        ariaLabel={translate('name')}
+        label={translate('name')}
+        htmlFor="coding-rules-custom-rule-creation-name"
         required
-        size="full"
-        type="text"
-        value={this.state.name}
-      />
-    </FormField>
-  );
-
-  renderKeyField = () => (
-    <FormField
-      ariaLabel={translate('key')}
-      label={translate('key')}
-      htmlFor="coding-rules-custom-rule-creation-key"
-      required
-    >
-      {this.props.customRule ? (
-        <span title={this.props.customRule.key}>{this.props.customRule.key}</span>
-      ) : (
+      >
         <InputField
-          disabled={this.state.submitting}
-          id="coding-rules-custom-rule-creation-key"
-          onChange={this.handleKeyChange}
+          autoFocus
+          disabled={submitting}
+          id="coding-rules-custom-rule-creation-name"
+          onChange={({
+            currentTarget: { value: name },
+          }: React.SyntheticEvent<HTMLInputElement>) => {
+            setName(name);
+            setKey(keyModifiedByUser ? key : latinize(name).replace(/[^A-Za-z0-9]/g, '_'));
+          }}
           required
           size="full"
           type="text"
-          value={this.state.key}
+          value={name}
         />
-      )}
-    </FormField>
+      </FormField>
+    ),
+    [key, keyModifiedByUser, name, submitting],
   );
 
-  renderDescriptionField = () => (
-    <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}
+  const KeyField = React.useMemo(
+    () => (
+      <FormField
+        ariaLabel={translate('key')}
+        label={translate('key')}
+        htmlFor="coding-rules-custom-rule-creation-key"
         required
-        rows={5}
-        size="full"
-        value={this.state.description}
-      />
-      <FormattingTips />
-    </FormField>
+      >
+        {customRule ? (
+          <span title={customRule.key}>{customRule.key}</span>
+        ) : (
+          <InputField
+            disabled={submitting}
+            id="coding-rules-custom-rule-creation-key"
+            onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
+              setKey(event.currentTarget.value);
+              setKeyModifiedByUser(true);
+            }}
+            required
+            size="full"
+            type="text"
+            value={key}
+          />
+        )}
+      </FormField>
+    ),
+    [customRule, key, submitting],
   );
 
-  renderTypeOption = (props: OptionProps<LabelValueSelectOption<RuleType>, false>) => {
-    return (
-      <components.Option {...props}>
-        <TypeHelper type={props.data.value} />
-      </components.Option>
-    );
-  };
-
-  renderTypeSingleValue = (props: SingleValueProps<LabelValueSelectOption<RuleType>, false>) => {
-    return (
-      <components.SingleValue {...props}>
-        <TypeHelper className="display-flex-center" type={props.data.value} />
-      </components.SingleValue>
-    );
-  };
+  const DescriptionField = React.useMemo(
+    () => (
+      <FormField
+        ariaLabel={translate('description')}
+        label={translate('description')}
+        htmlFor="coding-rules-custom-rule-creation-html-description"
+        required
+      >
+        <InputTextArea
+          disabled={submitting}
+          id="coding-rules-custom-rule-creation-html-description"
+          onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) =>
+            setDescription(event.currentTarget.value)
+          }
+          required
+          rows={5}
+          size="full"
+          value={description}
+        />
+        <FormattingTips />
+      </FormField>
+    ),
+    [description, submitting],
+  );
 
-  renderTypeField = () => {
+  const TypeField = React.useMemo(() => {
     const ruleTypeOption: LabelValueSelectOption<RuleType>[] = RULE_TYPES.map((type) => ({
       label: translate('issue.type', type),
       value: type,
@@ -262,39 +194,43 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
         <InputSelect
           inputId="coding-rules-custom-rule-type"
           isClearable={false}
-          isDisabled={this.state.submitting}
+          isDisabled={submitting}
           isSearchable={false}
-          onChange={this.handleTypeChange}
+          onChange={({ value }: LabelValueSelectOption<RuleType>) => setType(value)}
           components={{
-            Option: this.renderTypeOption,
-            SingleValue: this.renderTypeSingleValue,
+            Option: TypeSelectOption,
+            SingleValue: TypeSelectValue,
           }}
           options={ruleTypeOption}
-          value={ruleTypeOption.find((t) => t.value === this.state.type)}
+          value={ruleTypeOption.find((t) => t.value === type)}
         />
       </FormField>
     );
-  };
+  }, [type, submitting]);
 
-  renderSeverityField = () => (
-    <FormField
-      ariaLabel={translate('severity')}
-      label={translate('severity')}
-      htmlFor="coding-rules-severity-select"
-    >
-      <SeveritySelect
-        isDisabled={this.state.submitting}
-        onChange={this.handleSeverityChange}
-        severity={this.state.severity}
-      />
-    </FormField>
+  const SeverityField = React.useMemo(
+    () => (
+      <FormField
+        ariaLabel={translate('severity')}
+        label={translate('severity')}
+        htmlFor="coding-rules-severity-select"
+      >
+        <SeveritySelect
+          isDisabled={submitting}
+          onChange={({ value }: { value: string }) => setSeverity(value)}
+          severity={severity}
+        />
+      </FormField>
+    ),
+    [severity, submitting],
   );
 
-  renderStatusField = () => {
+  const StatusField = React.useMemo(() => {
     const statusesOptions = RULE_STATUSES.map((status) => ({
       label: translate('rules.status', status),
       value: status,
     }));
+
     return (
       <FormField
         ariaLabel={translate('coding_rules.filters.status')}
@@ -304,110 +240,152 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat
         <InputSelect
           inputId="coding-rules-custom-rule-status"
           isClearable={false}
-          isDisabled={this.state.submitting}
+          isDisabled={submitting}
           aria-labelledby="coding-rules-custom-rule-status"
-          onChange={this.handleStatusChange}
+          onChange={({ value }: LabelValueSelectOption<Status>) => setStatus(value)}
           options={statusesOptions}
           isSearchable={false}
-          value={statusesOptions.find((s) => s.value === this.state.status)}
+          value={statusesOptions.find((s) => s.value === status)}
         />
       </FormField>
     );
-  };
+  }, [status, submitting]);
 
-  renderParameterField = (param: RuleParameter) => {
-    // Gets the actual value from params from the state.
-    // Without it, we have a issue with string 'constructor' as key
-    const actualValue = new Map(Object.entries(this.state.params)).get(param.key) ?? '';
+  const handleParameterChange = React.useCallback(
+    (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+      const { name, value } = event.currentTarget;
+      setParams({ ...params, [name]: value });
+    },
+    [params],
+  );
 
-    return (
-      <FormField
-        ariaLabel={param.key}
-        className="sw-capitalize"
-        label={param.key}
-        htmlFor={`coding-rule-custom-rule-${param.key}`}
-        key={param.key}
-      >
-        {param.type === 'TEXT' ? (
-          <InputTextArea
-            disabled={this.state.submitting}
-            id={`coding-rule-custom-rule-${param.key}`}
-            name={param.key}
-            onChange={this.handleParameterChange}
-            placeholder={param.defaultValue}
-            size="full"
-            rows={3}
-            value={actualValue}
-          />
-        ) : (
-          <InputField
-            disabled={this.state.submitting}
-            id={`coding-rule-custom-rule-${param.key}`}
-            name={param.key}
-            onChange={this.handleParameterChange}
-            placeholder={param.defaultValue}
-            size="full"
-            type="text"
-            value={actualValue}
-          />
-        )}
-        {param.htmlDesc !== undefined && (
-          <LightLabel
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
-          />
-        )}
-      </FormField>
-    );
-  };
+  const renderParameterField = React.useCallback(
+    (param: RuleParameter) => {
+      // Gets the actual value from params from the state.
+      // Without it, we have a issue with string 'constructor' as key
+      const actualValue = new Map(Object.entries(params)).get(param.key) ?? '';
 
-  render() {
-    const { customRule, templateRule } = this.props;
-    const { reactivating, submitting } = this.state;
-    const { params = [] } = templateRule;
-    const header = customRule
-      ? translate('coding_rules.update_custom_rule')
-      : translate('coding_rules.create_custom_rule');
-    let submit = translate(customRule ? 'save' : 'create');
-    if (this.state.reactivating) {
-      submit = translate('coding_rules.reactivate');
-    }
-    return (
-      <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 && (
-              <FlagMessage variant="warning" className="sw-mb-6">
-                {translate('coding_rules.reactivate.help')}
-              </FlagMessage>
-            )}
+      return (
+        <FormField
+          ariaLabel={param.key}
+          className="sw-capitalize"
+          label={param.key}
+          htmlFor={`coding-rule-custom-rule-${param.key}`}
+          key={param.key}
+        >
+          {param.type === 'TEXT' ? (
+            <InputTextArea
+              disabled={submitting}
+              id={`coding-rule-custom-rule-${param.key}`}
+              name={param.key}
+              onChange={handleParameterChange}
+              placeholder={param.defaultValue}
+              size="full"
+              rows={3}
+              value={actualValue}
+            />
+          ) : (
+            <InputField
+              disabled={submitting}
+              id={`coding-rule-custom-rule-${param.key}`}
+              name={param.key}
+              onChange={handleParameterChange}
+              placeholder={param.defaultValue}
+              size="full"
+              type="text"
+              value={actualValue}
+            />
+          )}
+          {param.htmlDesc !== undefined && (
+            <LightLabel
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+            />
+          )}
+        </FormField>
+      );
+    },
+    [params, submitting, handleParameterChange],
+  );
 
-            <MandatoryFieldsExplanation className="sw-mb-4" />
+  const { params: templateParams = [] } = templateRule;
+  const header = customRule
+    ? translate('coding_rules.update_custom_rule')
+    : translate('coding_rules.create_custom_rule');
+  let buttonText = translate(customRule ? 'save' : 'create');
+  if (reactivating) {
+    buttonText = translate('coding_rules.reactivate');
+  }
+  return (
+    <Modal
+      headerTitle={header}
+      onClose={props.onClose}
+      body={
+        <form
+          className="sw-flex sw-flex-col sw-justify-stretch sw-pb-4"
+          id={FORM_ID}
+          onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
+            event.preventDefault();
+            submit();
+          }}
+        >
+          {reactivating && (
+            <FlagMessage variant="warning" className="sw-mb-6">
+              {translate('coding_rules.reactivate.help')}
+            </FlagMessage>
+          )}
 
-            {this.renderNameField()}
-            {this.renderKeyField()}
-            {/* do not allow to change the type of existing rule */}
-            {!customRule && this.renderTypeField()}
-            {this.renderSeverityField()}
-            {this.renderStatusField()}
-            {this.renderDescriptionField()}
-            {params.map(this.renderParameterField)}
-          </form>
-        }
-        primaryButton={
-          <ButtonPrimary disabled={submitting} type="submit" form={FORM_ID}>
-            {submit}
-          </ButtonPrimary>
-        }
-        loading={submitting}
-        secondaryButtonLabel={translate('cancel')}
-      />
-    );
+          <MandatoryFieldsExplanation className="sw-mb-4" />
+
+          {NameField}
+          {KeyField}
+          {/* do not allow to change the type of existing rule */}
+          {!customRule && TypeField}
+          {SeverityField}
+          {StatusField}
+          {DescriptionField}
+          {templateParams.map(renderParameterField)}
+        </form>
+      }
+      primaryButton={
+        <ButtonPrimary disabled={submitting} type="submit" form={FORM_ID}>
+          {buttonText}
+        </ButtonPrimary>
+      }
+      loading={submitting}
+      secondaryButtonLabel={translate('cancel')}
+    />
+  );
+}
+
+function TypeSelectOption(
+  optionProps: Readonly<OptionProps<LabelValueSelectOption<RuleType>, false>>,
+) {
+  return (
+    <components.Option {...optionProps}>
+      <TypeHelper type={optionProps.data.value} />
+    </components.Option>
+  );
+}
+
+function TypeSelectValue(
+  valueProps: Readonly<SingleValueProps<LabelValueSelectOption<RuleType>, false>>,
+) {
+  return (
+    <components.SingleValue {...valueProps}>
+      <TypeHelper className="display-flex-center" type={valueProps.data.value} />
+    </components.SingleValue>
+  );
+}
+
+function getParams(customRule?: RuleDetails) {
+  const params: Dict<string> = {};
+
+  if (customRule?.params) {
+    for (const param of customRule.params) {
+      params[param.key] = param.defaultValue ?? '';
+    }
   }
+
+  return params;
 }
index ab5b963062429cfc831519baddcb6e2b11afe7db..0dc13d6d8f8e8029bb81c6f14a99fed6dec92f37 100644 (file)
@@ -29,12 +29,16 @@ import {
 } 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 DateFormatter from '../../../components/intl/DateFormatter';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Dict, RuleActivation, RuleDetails as TypeRuleDetails } from '../../../types/types';
+import {
+  useDeleteRuleMutation,
+  useRuleDetailsQuery,
+  useUpdateRuleMutation,
+} from '../../../queries/rules';
+import { Dict } from '../../../types/types';
 import { Activation } from '../query';
 import CustomRuleButton from './CustomRuleButton';
 import RuleDetailsCustomRules from './RuleDetailsCustomRules';
@@ -57,223 +61,147 @@ interface Props {
   selectedProfile?: Profile;
 }
 
-interface State {
-  actives?: RuleActivation[];
-  loading: boolean;
-  ruleDetails?: TypeRuleDetails;
-}
-
-export default class RuleDetails extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: true };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.setState({ loading: true });
-    this.fetchRuleDetails();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.ruleKey !== this.props.ruleKey) {
-      this.setState({ loading: true });
-      this.fetchRuleDetails();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchRuleDetails = () => {
-    return getRuleDetails({
-      actives: true,
-      key: this.props.ruleKey,
-    }).then(
-      ({ actives, rule }) => {
-        if (this.mounted) {
-          this.setState({ actives, loading: false, ruleDetails: rule });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      },
-    );
-  };
-
-  handleRuleChange = (ruleDetails: TypeRuleDetails) => {
-    if (this.mounted) {
-      this.setState({ ruleDetails });
-    }
-  };
-
-  handleTagsChange = (tags: string[]) => {
-    // optimistic update
-    const oldTags = this.state.ruleDetails && this.state.ruleDetails.tags;
-    this.setState((state) =>
-      state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags } } : null,
-    );
-    updateRule({
-      key: this.props.ruleKey,
-      tags: tags.join(),
-    }).catch(() => {
-      if (this.mounted) {
-        this.setState((state) =>
-          state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags: oldTags } } : null,
-        );
-      }
-    });
+export default function RuleDetails(props: Readonly<Props>) {
+  const {
+    ruleKey,
+    allowCustomRules,
+    canWrite,
+    referencedProfiles,
+    canDeactivateInherited,
+    selectedProfile,
+    referencedRepositories,
+  } = props;
+  const { isLoading: loadingRule, data } = useRuleDetailsQuery({
+    actives: true,
+    key: ruleKey,
+  });
+  const { mutate: updateRule } = useUpdateRuleMutation();
+  const { mutate: deleteRule } = useDeleteRuleMutation({}, props.onDelete);
+
+  const { rule: ruleDetails, actives = [] } = data ?? {};
+
+  const params = ruleDetails?.params ?? [];
+  const isCustom = !!ruleDetails?.templateKey;
+  const isEditable = canWrite && !!allowCustomRules && isCustom;
+
+  const handleTagsChange = (tags: string[]) => {
+    updateRule({ key: ruleKey, tags: tags.join() });
   };
 
-  handleActivate = () => {
-    return this.fetchRuleDetails().then(() => {
-      const { ruleKey, selectedProfile } = this.props;
-      if (selectedProfile && this.state.actives) {
-        const active = this.state.actives.find((active) => active.qProfile === selectedProfile.key);
-        if (active) {
-          this.props.onActivate(selectedProfile.key, ruleKey, active);
-        }
+  const handleActivate = () => {
+    if (selectedProfile) {
+      const active = actives.find((active) => active.qProfile === selectedProfile.key);
+      if (active) {
+        props.onActivate(selectedProfile.key, ruleKey, active);
       }
-    });
-  };
-
-  handleDeactivate = () => {
-    return this.fetchRuleDetails().then(() => {
-      const { ruleKey, selectedProfile } = this.props;
-      if (
-        selectedProfile &&
-        this.state.actives &&
-        !this.state.actives.find((active) => active.qProfile === selectedProfile.key)
-      ) {
-        this.props.onDeactivate(selectedProfile.key, ruleKey);
-      }
-    });
-  };
-
-  handleDelete = () => {
-    return deleteRule({ key: this.props.ruleKey }).then(() =>
-      this.props.onDelete(this.props.ruleKey),
-    );
+    }
   };
 
-  render() {
-    const { ruleDetails } = this.state;
-
-    if (!ruleDetails) {
-      return <div className="coding-rule-details" />;
+  const handleDeactivate = () => {
+    if (selectedProfile && actives.find((active) => active.qProfile === selectedProfile.key)) {
+      props.onDeactivate(selectedProfile.key, ruleKey);
     }
+  };
 
-    const { allowCustomRules, canWrite, referencedProfiles, canDeactivateInherited } = this.props;
-    const { params = [] } = ruleDetails;
-
-    const isCustom = !!ruleDetails.templateKey;
-    const isEditable = canWrite && !!this.props.allowCustomRules && isCustom;
-
-    return (
-      <StyledRuleDetails className="it__coding-rule-details sw-p-6 sw-mt-6">
-        <Spinner loading={this.state.loading}>
-          <RuleDetailsMeta
-            canWrite={canWrite}
-            onTagsChange={this.handleTagsChange}
-            referencedRepositories={this.props.referencedRepositories}
-            ruleDetails={ruleDetails}
-          />
+  return (
+    <StyledRuleDetails className="it__coding-rule-details sw-p-6 sw-mt-6">
+      <Spinner loading={loadingRule}>
+        {ruleDetails && (
+          <>
+            <RuleDetailsMeta
+              canWrite={canWrite}
+              onTagsChange={handleTagsChange}
+              referencedRepositories={referencedRepositories}
+              ruleDetails={ruleDetails}
+            />
 
-          <RuleDetailsDescription
-            canWrite={canWrite}
-            onChange={this.handleRuleChange}
-            ruleDetails={ruleDetails}
-          />
+            <RuleDetailsDescription canWrite={canWrite} ruleDetails={ruleDetails} />
 
-          {params.length > 0 && <RuleDetailsParameters params={params} />}
+            {params.length > 0 && <RuleDetailsParameters params={params} />}
 
-          {isEditable && (
-            <div className="coding-rules-detail-description display-flex-center">
-              {/* `templateRule` is used to get rule meta data, `customRule` is used to get parameter values */}
-              {/* it's expected to pass the same rule to both parameters */}
-              <CustomRuleButton
-                customRule={ruleDetails}
-                onDone={this.handleRuleChange}
-                templateRule={ruleDetails}
-              >
-                {({ onClick }) => (
-                  <ButtonSecondary
-                    className="js-edit-custom"
-                    id="coding-rules-detail-custom-rule-change"
-                    onClick={onClick}
-                  >
-                    {translate('edit')}
-                  </ButtonSecondary>
-                )}
-              </CustomRuleButton>
-              <ConfirmButton
-                confirmButtonText={translate('delete')}
-                isDestructive
-                modalBody={translateWithParameters(
-                  'coding_rules.delete.custom.confirm',
-                  ruleDetails.name,
-                )}
-                modalHeader={translate('coding_rules.delete_rule')}
-                onConfirm={this.handleDelete}
-              >
-                {({ onClick }) => (
-                  <>
-                    <DangerButtonSecondary
-                      className="sw-ml-2 js-delete"
-                      id="coding-rules-detail-rule-delete"
+            {isEditable && (
+              <div className="coding-rules-detail-description display-flex-center">
+                {/* `templateRule` is used to get rule meta data, `customRule` is used to get parameter values */}
+                {/* it's expected to pass the same rule to both parameters */}
+                <CustomRuleButton customRule={ruleDetails} templateRule={ruleDetails}>
+                  {({ onClick }) => (
+                    <ButtonSecondary
+                      className="js-edit-custom"
+                      id="coding-rules-detail-custom-rule-change"
                       onClick={onClick}
                     >
-                      {translate('delete')}
-                    </DangerButtonSecondary>
-                    <HelpTooltip
-                      className="sw-ml-2"
-                      overlay={
-                        <div className="sw-py-4">
-                          {translate('coding_rules.custom_rule.removal')}
-                        </div>
-                      }
-                    >
-                      <HelperHintIcon />
-                    </HelpTooltip>
-                  </>
-                )}
-              </ConfirmButton>
+                      {translate('edit')}
+                    </ButtonSecondary>
+                  )}
+                </CustomRuleButton>
+                <ConfirmButton
+                  confirmButtonText={translate('delete')}
+                  isDestructive
+                  modalBody={translateWithParameters(
+                    'coding_rules.delete.custom.confirm',
+                    ruleDetails.name,
+                  )}
+                  modalHeader={translate('coding_rules.delete_rule')}
+                  onConfirm={() => deleteRule({ key: ruleKey })}
+                >
+                  {({ onClick }) => (
+                    <>
+                      <DangerButtonSecondary
+                        className="sw-ml-2 js-delete"
+                        id="coding-rules-detail-rule-delete"
+                        onClick={onClick}
+                      >
+                        {translate('delete')}
+                      </DangerButtonSecondary>
+                      <HelpTooltip
+                        className="sw-ml-2"
+                        overlay={
+                          <div className="sw-py-4">
+                            {translate('coding_rules.custom_rule.removal')}
+                          </div>
+                        }
+                      >
+                        <HelperHintIcon />
+                      </HelpTooltip>
+                    </>
+                  )}
+                </ConfirmButton>
+              </div>
+            )}
+
+            {ruleDetails.isTemplate && (
+              <RuleDetailsCustomRules
+                canChange={allowCustomRules && canWrite}
+                ruleDetails={ruleDetails}
+              />
+            )}
+
+            {!ruleDetails.isTemplate && (
+              <RuleDetailsProfiles
+                activations={actives}
+                canDeactivateInherited={canDeactivateInherited}
+                onActivate={handleActivate}
+                onDeactivate={handleDeactivate}
+                referencedProfiles={referencedProfiles}
+                ruleDetails={ruleDetails}
+              />
+            )}
+
+            {!ruleDetails.isTemplate && ruleDetails.type !== 'SECURITY_HOTSPOT' && (
+              <RuleDetailsIssues ruleDetails={ruleDetails} />
+            )}
+
+            <div className="sw-my-8" data-meta="available-since">
+              <SubHeadingHighlight as="h3">
+                {translate('coding_rules.available_since')}
+              </SubHeadingHighlight>
+              <DateFormatter date={ruleDetails.createdAt} />
             </div>
-          )}
-
-          {ruleDetails.isTemplate && (
-            <RuleDetailsCustomRules
-              canChange={allowCustomRules && canWrite}
-              ruleDetails={ruleDetails}
-            />
-          )}
-
-          {!ruleDetails.isTemplate && (
-            <RuleDetailsProfiles
-              activations={this.state.actives}
-              canDeactivateInherited={canDeactivateInherited}
-              onActivate={this.handleActivate}
-              onDeactivate={this.handleDeactivate}
-              referencedProfiles={referencedProfiles}
-              ruleDetails={ruleDetails}
-            />
-          )}
-
-          {!ruleDetails.isTemplate && ruleDetails.type !== 'SECURITY_HOTSPOT' && (
-            <RuleDetailsIssues ruleDetails={ruleDetails} />
-          )}
-
-          <div className="sw-my-8" data-meta="available-since">
-            <SubHeadingHighlight as="h3">
-              {translate('coding_rules.available_since')}
-            </SubHeadingHighlight>
-            <DateFormatter date={ruleDetails.createdAt} />
-          </div>
-        </Spinner>
-      </StyledRuleDetails>
-    );
-  }
+          </>
+        )}
+      </Spinner>
+    </StyledRuleDetails>
+  );
 }
 
 const StyledRuleDetails = styled.div`
index 5a87734539c39853d78c01e114c0cdc4f2a069b3..074adafaf0f28a6970e62f4a0f2f2e188784666b 100644 (file)
@@ -30,11 +30,11 @@ import {
 } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import { deleteRule, searchRules } from '../../../api/rules';
 import ConfirmButton from '../../../components/controls/ConfirmButton';
 import IssueSeverityIcon from '../../../components/icon-mappers/IssueSeverityIcon';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getRuleUrl } from '../../../helpers/urls';
+import { useDeleteRuleMutation, useSearchRulesQuery } from '../../../queries/rules';
 import { IssueSeverity } from '../../../types/issues';
 import { Rule, RuleDetails } from '../../../types/types';
 import CustomRuleButton from './CustomRuleButton';
@@ -44,72 +44,74 @@ interface Props {
   ruleDetails: RuleDetails;
 }
 
-interface State {
-  loading: boolean;
-  rules?: Rule[];
-}
-
 const COLUMN_COUNT = 3;
 const COLUMN_COUNT_WITH_EDIT_PERMISSIONS = 4;
 
-export default class RuleDetailsCustomRules extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchRules();
-  }
+export default function RuleDetailsCustomRules(props: Readonly<Props>) {
+  const { ruleDetails } = props;
+  const rulesSearchParams = {
+    f: 'name,severity,params',
+    template_key: ruleDetails.key,
+  };
+  const { isLoading: loadingRules, data } = useSearchRulesQuery(rulesSearchParams);
+  const { mutate: deleteRules, isLoading: deletingRule } = useDeleteRuleMutation(rulesSearchParams);
 
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.ruleDetails.key !== this.props.ruleDetails.key) {
-      this.fetchRules();
-    }
-  }
+  const loading = loadingRules || deletingRule;
+  const rules = data?.rules ?? [];
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const handleRuleDelete = React.useCallback(
+    (ruleKey: string) => {
+      deleteRules({ key: ruleKey });
+    },
+    [deleteRules],
+  );
 
-  fetchRules = () => {
-    this.setState({ loading: true });
-    searchRules({
-      f: 'name,severity,params',
-      template_key: this.props.ruleDetails.key,
-    }).then(
-      ({ rules }) => {
-        if (this.mounted) {
-          this.setState({ rules, loading: false });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      },
-    );
-  };
+  return (
+    <div className="js-rule-custom-rules">
+      <div>
+        <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark>
 
-  handleRuleCreate = (newRuleDetails: RuleDetails) => {
-    if (this.mounted) {
-      this.setState(({ rules = [] }: State) => ({
-        rules: [...rules, newRuleDetails],
-      }));
-    }
-  };
-
-  handleRuleDelete = (ruleKey: string) => {
-    return deleteRule({ key: ruleKey }).then(() => {
-      if (this.mounted) {
-        this.setState(({ rules = [] }) => ({
-          rules: rules.filter((rule) => rule.key !== ruleKey),
-        }));
-      }
-    });
-  };
+        {props.canChange && (
+          <CustomRuleButton templateRule={ruleDetails}>
+            {({ onClick }) => (
+              <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}>
+                {translate('coding_rules.create')}
+              </ButtonSecondary>
+            )}
+          </CustomRuleButton>
+        )}
+        {rules.length > 0 && (
+          <Table
+            className="sw-my-6"
+            id="coding-rules-detail-custom-rules"
+            columnCount={props.canChange ? COLUMN_COUNT_WITH_EDIT_PERMISSIONS : COLUMN_COUNT}
+          >
+            {sortBy(rules, (rule) => rule.name).map((rule) => (
+              <RuleListItem
+                key={rule.key}
+                rule={rule}
+                editable={props.canChange}
+                onDelete={handleRuleDelete}
+              />
+            ))}
+          </Table>
+        )}
+        <Spinner className="sw-my-6" loading={loading} />
+      </div>
+    </div>
+  );
+}
 
-  renderRule = (rule: Rule) => (
-    <TableRow data-rule={rule.key} key={rule.key}>
+function RuleListItem(
+  props: Readonly<{
+    rule: Rule;
+    editable?: boolean;
+    onDelete: (ruleKey: string) => void;
+  }>,
+) {
+  const { rule, editable } = props;
+  return (
+    <TableRow data-rule={rule.key}>
       <ContentCell>
         <div>
           <Link to={getRuleUrl(rule.key)}>{rule.name}</Link>
@@ -139,7 +141,7 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S
         </UnorderedList>
       </ContentCell>
 
-      {this.props.canChange && (
+      {editable && (
         <ContentCell>
           <ConfirmButton
             confirmButtonText={translate('delete')}
@@ -147,7 +149,7 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S
             isDestructive
             modalBody={translateWithParameters('coding_rules.delete.custom.confirm', rule.name)}
             modalHeader={translate('coding_rules.delete_rule')}
-            onConfirm={this.handleRuleDelete}
+            onConfirm={props.onDelete}
           >
             {({ onClick }) => (
               <DangerButtonSecondary
@@ -163,40 +165,4 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S
       )}
     </TableRow>
   );
-
-  render() {
-    const { loading, rules = [] } = this.state;
-
-    return (
-      <div className="js-rule-custom-rules">
-        <div>
-          <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark>
-
-          {this.props.canChange && (
-            <CustomRuleButton onDone={this.handleRuleCreate} templateRule={this.props.ruleDetails}>
-              {({ onClick }) => (
-                <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}>
-                  {translate('coding_rules.create')}
-                </ButtonSecondary>
-              )}
-            </CustomRuleButton>
-          )}
-
-          <Spinner className="sw-my-6" loading={loading}>
-            {rules.length > 0 && (
-              <Table
-                className="sw-my-6"
-                id="coding-rules-detail-custom-rules"
-                columnCount={
-                  this.props.canChange ? COLUMN_COUNT_WITH_EDIT_PERMISSIONS : COLUMN_COUNT
-                }
-              >
-                {sortBy(rules, (rule) => rule.name).map(this.renderRule)}
-              </Table>
-            )}
-          </Spinner>
-        </div>
-      </div>
-    );
-  }
 }
index 832d94bfbcc7f7d8a01e7e0c9c59b246d62a9cd0..f3357a6a7939401674965fd997fe03946dbd14e8 100644 (file)
@@ -27,111 +27,55 @@ import {
   Spinner,
 } from 'design-system';
 import * as React from 'react';
-import { updateRule } from '../../../api/rules';
 import FormattingTips from '../../../components/common/FormattingTips';
 import RuleTabViewer from '../../../components/rules/RuleTabViewer';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { sanitizeString, sanitizeUserInput } from '../../../helpers/sanitize';
+import { useUpdateRuleMutation } from '../../../queries/rules';
 import { RuleDetails } from '../../../types/types';
 import { RuleDescriptionSections } from '../rule';
 import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
 
 interface Props {
   canWrite: boolean | undefined;
-  onChange: (newRuleDetails: RuleDetails) => void;
   ruleDetails: RuleDetails;
 }
 
-interface State {
-  description: string;
-  descriptionForm: boolean;
-  removeDescriptionModal: boolean;
-  submitting: boolean;
-}
-
-export default class RuleDetailsDescription extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    description: '',
-    descriptionForm: false,
-    removeDescriptionModal: false,
-    submitting: false,
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
-    this.setState({ description: event.currentTarget.value });
-
-  handleCancelClick = () => {
-    this.setState({ descriptionForm: false });
-  };
-
-  handleSaveClick = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    this.updateDescription(this.state.description);
-  };
-
-  handleRemoveDescriptionClick = () => {
-    this.setState({ removeDescriptionModal: true });
-  };
+export default function RuleDetailsDescription(props: Readonly<Props>) {
+  const { ruleDetails, canWrite } = props;
+  const [description, setDescription] = React.useState('');
+  const [descriptionForm, setDescriptionForm] = React.useState(false);
+  const [removeDescriptionModal, setDescriptionModal] = React.useState(false);
 
-  handleCancelRemoving = () => this.setState({ removeDescriptionModal: false });
-
-  handleConfirmRemoving = () => {
-    this.setState({ removeDescriptionModal: false });
-    this.updateDescription('');
-  };
-
-  updateDescription = (text: string) => {
-    this.setState({ submitting: true });
+  const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(() =>
+    setDescriptionForm(false),
+  );
 
+  const updateDescription = (text = '') => {
     updateRule({
-      key: this.props.ruleDetails.key,
+      key: ruleDetails.key,
       markdown_note: text,
-    }).then(
-      (ruleDetails) => {
-        this.props.onChange(ruleDetails);
-
-        if (this.mounted) {
-          this.setState({ submitting: false, descriptionForm: false });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ submitting: false });
-        }
-      },
-    );
-  };
-
-  handleExtendDescriptionClick = () => {
-    this.setState({
-      // set description` to the current `mdNote` each time the form is open
-      description: this.props.ruleDetails.mdNote ?? '',
-      descriptionForm: true,
     });
   };
 
-  renderExtendedDescription = () => (
+  const renderExtendedDescription = () => (
     <div id="coding-rules-detail-description-extra">
-      {this.props.ruleDetails.htmlNote !== undefined && (
+      {ruleDetails.htmlNote !== undefined && (
         <CodeSyntaxHighlighter
           className="markdown sw-my-6"
-          htmlAsString={sanitizeUserInput(this.props.ruleDetails.htmlNote)}
-          language={this.props.ruleDetails.lang}
+          htmlAsString={sanitizeUserInput(ruleDetails.htmlNote)}
+          language={ruleDetails.lang}
         />
       )}
 
       <div className="sw-my-6">
-        {this.props.canWrite && (
-          <ButtonSecondary onClick={this.handleExtendDescriptionClick}>
+        {canWrite && (
+          <ButtonSecondary
+            onClick={() => {
+              setDescription(ruleDetails.mdNote ?? '');
+              setDescriptionForm(true);
+            }}
+          >
             {translate('coding_rules.extend_description')}
           </ButtonSecondary>
         )}
@@ -139,46 +83,54 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
     </div>
   );
 
-  renderForm = () => (
+  const renderForm = () => (
     <form
       aria-label={translate('coding_rules.detail.extend_description.form')}
       className="sw-my-6"
-      onSubmit={this.handleSaveClick}
+      onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
+        event.preventDefault();
+        updateDescription(description);
+      }}
     >
       <InputTextArea
         aria-label={translate('coding_rules.extend_description')}
         className="sw-mb-2 sw-resize-y"
         id="coding-rules-detail-extend-description-text"
         size="full"
-        onChange={this.handleDescriptionChange}
+        onChange={({ currentTarget: { value } }: React.SyntheticEvent<HTMLTextAreaElement>) =>
+          setDescription(value)
+        }
         rows={4}
-        value={this.state.description}
+        value={description}
       />
 
       <div className="sw-flex sw-items-center sw-justify-between">
         <div className="sw-flex sw-items-center">
           <ButtonPrimary
             id="coding-rules-detail-extend-description-submit"
-            disabled={this.state.submitting}
+            disabled={updatingRule}
             type="submit"
           >
             {translate('save')}
           </ButtonPrimary>
 
-          {this.props.ruleDetails.mdNote !== undefined && (
+          {ruleDetails.mdNote !== undefined && (
             <>
               <DangerButtonSecondary
                 className="sw-ml-2"
-                disabled={this.state.submitting}
+                disabled={updatingRule}
                 id="coding-rules-detail-extend-description-remove"
-                onClick={this.handleRemoveDescriptionClick}
+                onClick={() => setDescriptionModal(true)}
               >
                 {translate('remove')}
               </DangerButtonSecondary>
-              {this.state.removeDescriptionModal && (
+              {removeDescriptionModal && (
                 <RemoveExtendedDescriptionModal
-                  onCancel={this.handleCancelRemoving}
-                  onSubmit={this.handleConfirmRemoving}
+                  onCancel={() => setDescriptionModal(false)}
+                  onSubmit={() => {
+                    setDescriptionModal(false);
+                    updateDescription();
+                  }}
                 />
               )}
             </>
@@ -186,14 +138,14 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
 
           <ButtonSecondary
             className="sw-ml-2"
-            disabled={this.state.submitting}
+            disabled={updatingRule}
             id="coding-rules-detail-extend-description-cancel"
-            onClick={this.handleCancelClick}
+            onClick={() => setDescriptionForm(false)}
           >
             {translate('cancel')}
           </ButtonSecondary>
 
-          <Spinner className="sw-ml-2" loading={this.state.submitting} />
+          <Spinner className="sw-ml-2" loading={updatingRule} />
         </div>
 
         <FormattingTips />
@@ -201,55 +153,50 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S
     </form>
   );
 
-  render() {
-    const { ruleDetails } = this.props;
-    const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
-
-    const hasDescriptionSection =
-      hasDescription &&
-      ruleDetails.descriptionSections &&
-      ruleDetails.descriptionSections.length > 0;
-
-    const defaultSection =
-      hasDescriptionSection &&
-      ruleDetails.descriptionSections?.length === 1 &&
-      ruleDetails.descriptionSections[0].key === RuleDescriptionSections.DEFAULT
-        ? ruleDetails.descriptionSections[0]
-        : undefined;
-
-    const introductionSection = ruleDetails.descriptionSections?.find(
-      (section) => section.key === RuleDescriptionSections.INTRODUCTION,
-    )?.content;
-
-    return (
-      <div className="js-rule-description">
-        {hasDescriptionSection && !defaultSection && (
-          <>
-            {introductionSection && (
-              <CodeSyntaxHighlighter
-                className="rule-desc"
-                htmlAsString={sanitizeString(introductionSection)}
-                language={ruleDetails.lang}
-              />
-            )}
-          </>
-        )}
+  const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
+
+  const hasDescriptionSection =
+    hasDescription && ruleDetails.descriptionSections && ruleDetails.descriptionSections.length > 0;
+
+  const defaultSection =
+    hasDescriptionSection &&
+    ruleDetails.descriptionSections?.length === 1 &&
+    ruleDetails.descriptionSections[0].key === RuleDescriptionSections.DEFAULT
+      ? ruleDetails.descriptionSections[0]
+      : undefined;
+
+  const introductionSection = ruleDetails.descriptionSections?.find(
+    (section) => section.key === RuleDescriptionSections.INTRODUCTION,
+  )?.content;
+
+  return (
+    <div className="js-rule-description">
+      {hasDescriptionSection && !defaultSection && (
+        <>
+          {introductionSection && (
+            <CodeSyntaxHighlighter
+              className="rule-desc"
+              htmlAsString={sanitizeString(introductionSection)}
+              language={ruleDetails.lang}
+            />
+          )}
+        </>
+      )}
 
-        <RuleTabViewer ruleDetails={ruleDetails} />
+      <RuleTabViewer ruleDetails={ruleDetails} />
 
-        {ruleDetails.isExternal && (
-          <div className="coding-rules-detail-description rule-desc markdown">
-            {translateWithParameters('issue.external_issue_description', ruleDetails.name)}
-          </div>
-        )}
+      {ruleDetails.isExternal && (
+        <div className="coding-rules-detail-description rule-desc markdown">
+          {translateWithParameters('issue.external_issue_description', ruleDetails.name)}
+        </div>
+      )}
 
-        {!ruleDetails.templateKey && (
-          <div className="sw-mt-6">
-            {!this.state.descriptionForm && this.renderExtendedDescription()}
-            {this.state.descriptionForm && this.props.canWrite && this.renderForm()}
-          </div>
-        )}
-      </div>
-    );
-  }
+      {!ruleDetails.templateKey && (
+        <div className="sw-mt-6">
+          {!descriptionForm && renderExtendedDescription()}
+          {descriptionForm && canWrite && renderForm()}
+        </div>
+      )}
+    </div>
+  );
 }
index f1776c6052f091a0b98efe2be00db4e29fb9078d..8bb943bd7369445380727af73c98b529e9ee1928 100644 (file)
@@ -34,10 +34,14 @@ import {
 import { filter } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { Profile, activateRule, deactivateRule } from '../../../api/quality-profiles';
+import { Profile } from '../../../api/quality-profiles';
 import ConfirmButton from '../../../components/controls/ConfirmButton';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getQualityProfileUrl } from '../../../helpers/urls';
+import {
+  useActivateRuleMutation,
+  useDeactivateRuleMutation,
+} from '../../../queries/quality-profiles';
 import { Dict, RuleActivation, RuleDetails } from '../../../types/types';
 import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge';
 import ActivationButton from './ActivationButton';
@@ -45,8 +49,8 @@ import ActivationButton from './ActivationButton';
 interface Props {
   activations: RuleActivation[] | undefined;
   canDeactivateInherited?: boolean;
-  onActivate: () => Promise<void>;
-  onDeactivate: () => Promise<void>;
+  onActivate: () => void;
+  onDeactivate: () => void;
   referencedProfiles: Dict<Profile>;
   ruleDetails: RuleDetails;
 }
@@ -56,84 +60,38 @@ const COLUMN_COUNT_WITHOUT_PARAMS = 2;
 
 const PROFILES_HEADING_ID = 'rule-details-profiles-heading';
 
-export default class RuleDetailsProfiles extends React.PureComponent<Props> {
-  handleActivate = () => this.props.onActivate();
+export default function RuleDetailsProfiles(props: Readonly<Props>) {
+  const { activations = [], referencedProfiles, ruleDetails, canDeactivateInherited } = props;
+  const { mutate: activateRule } = useActivateRuleMutation(props.onActivate);
+  const { mutate: deactivateRule } = useDeactivateRuleMutation(props.onDeactivate);
+
+  const canActivate = Object.values(referencedProfiles).some((profile) =>
+    Boolean(profile.actions?.edit && profile.language === ruleDetails.lang),
+  );
 
-  handleDeactivate = (key?: string) => {
+  const handleDeactivate = (key?: string) => {
     if (key !== undefined) {
       deactivateRule({
         key,
-        rule: this.props.ruleDetails.key,
-      }).then(this.props.onDeactivate, () => {});
+        rule: ruleDetails.key,
+      });
     }
   };
 
-  handleRevert = (key?: string) => {
+  const handleRevert = (key?: string) => {
     if (key !== undefined) {
       activateRule({
         key,
-        rule: this.props.ruleDetails.key,
+        rule: ruleDetails.key,
         reset: true,
-      }).then(this.props.onActivate, () => {});
+      });
     }
   };
 
-  renderInheritedProfile = (activation: RuleActivation, profile: Profile) => {
-    if (!profile.parentName) {
-      return null;
-    }
-    const profilePath = getQualityProfileUrl(profile.parentName, profile.language);
-    return (
-      (activation.inherit === 'OVERRIDES' || activation.inherit === 'INHERITED') && (
-        <Note as="div" className="sw-flex sw-items-center sw-mt-2">
-          <InheritanceIcon
-            fill={activation.inherit === 'OVERRIDES' ? 'destructiveIconFocus' : 'currentColor'}
-          />
-          <DiscreetLink
-            className="sw-ml-1"
-            aria-label={`${translate('quality_profiles.parent')} ${profile.parentName}`}
-            to={profilePath}
-          >
-            {profile.parentName}
-          </DiscreetLink>
-        </Note>
-      )
-    );
-  };
-
-  renderParameter = (param: { key: string; value: string }, parentActivation?: RuleActivation) => {
-    const originalParam = parentActivation?.params.find((p) => p.key === param.key);
-    const originalValue = originalParam?.value;
-
-    return (
-      <StyledParameter className="sw-my-4" key={param.key}>
-        <span className="key">{param.key}</span>
-        <span className="sep sw-mr-1">: </span>
-        <span className="value" title={param.value}>
-          {param.value}
-        </span>
-        {parentActivation && param.value !== originalValue && (
-          <div className="sw-flex sw-ml-4">
-            {translate('coding_rules.original')}
-            <span className="value sw-ml-1" title={originalValue}>
-              {originalValue}
-            </span>
-          </div>
-        )}
-      </StyledParameter>
-    );
-  };
-
-  renderParameters = (activation: RuleActivation, parentActivation?: RuleActivation) => (
-    <CellComponent>
-      {activation.params.map((param) => this.renderParameter(param, parentActivation))}
-    </CellComponent>
-  );
-
-  renderActions = (activation: RuleActivation, profile: Profile) => {
+  const renderRowActions = (activation: RuleActivation, profile: Profile) => {
     const canEdit = profile.actions?.edit && !profile.isBuiltIn;
-    const { ruleDetails } = this.props;
     const hasParent = activation.inherit !== 'NONE' && profile.parentKey;
+
     return (
       <ActionCell>
         {canEdit && (
@@ -144,7 +102,7 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> {
                 ariaLabel={translateWithParameters('coding_rules.change_details_x', profile.name)}
                 buttonText={translate('change_verb')}
                 modalHeader={translate('coding_rules.change_details')}
-                onDone={this.handleActivate}
+                onDone={props.onActivate}
                 profiles={[profile]}
                 rule={ruleDetails}
               />
@@ -160,7 +118,7 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> {
                 )}
                 isDestructive
                 modalHeader={translate('coding_rules.revert_to_parent_definition')}
-                onConfirm={this.handleRevert}
+                onConfirm={handleRevert}
               >
                 {({ onClick }) => (
                   <DangerButtonSecondary className="sw-ml-2" onClick={onClick}>
@@ -170,13 +128,13 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> {
               </ConfirmButton>
             )}
 
-            {(!hasParent || this.props.canDeactivateInherited) && (
+            {(!hasParent || canDeactivateInherited) && (
               <ConfirmButton
                 confirmButtonText={translate('yes')}
                 confirmData={profile.key}
                 modalBody={translate('coding_rules.deactivate.confirm')}
                 modalHeader={translate('coding_rules.deactivate')}
-                onConfirm={this.handleDeactivate}
+                onConfirm={handleDeactivate}
               >
                 {({ onClick }) => (
                   <DangerButtonSecondary
@@ -198,15 +156,32 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> {
     );
   };
 
-  renderActivation = (activation: RuleActivation) => {
-    const { activations = [], ruleDetails } = this.props;
-    const profile = this.props.referencedProfiles[activation.qProfile];
+  const renderActivationRow = (activation: RuleActivation) => {
+    const profile = referencedProfiles[activation.qProfile];
+
     if (!profile) {
       return null;
     }
 
     const parentActivation = activations.find((x) => x.qProfile === profile.parentKey);
 
+    const inheritedProfileSection = profile.parentName
+      ? (activation.inherit === 'OVERRIDES' || activation.inherit === 'INHERITED') && (
+          <Note as="div" className="sw-flex sw-items-center sw-mt-2">
+            <InheritanceIcon
+              fill={activation.inherit === 'OVERRIDES' ? 'destructiveIconFocus' : 'currentColor'}
+            />
+            <DiscreetLink
+              className="sw-ml-1"
+              aria-label={`${translate('quality_profiles.parent')} ${profile.parentName}`}
+              to={getQualityProfileUrl(profile.parentName, profile.language)}
+            >
+              {profile.parentName}
+            </DiscreetLink>
+          </Note>
+        )
+      : null;
+
     return (
       <TableRowInteractive key={profile.key}>
         <ContentCell className="coding-rules-detail-quality-profile-name">
@@ -220,57 +195,74 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props> {
               </Link>
               {profile.isBuiltIn && <BuiltInQualityProfileBadge className="sw-ml-2" />}
             </div>
-            {this.renderInheritedProfile(activation, profile)}
+            {inheritedProfileSection}
           </div>
         </ContentCell>
 
-        {!ruleDetails.templateKey && this.renderParameters(activation, parentActivation)}
-        {this.renderActions(activation, profile)}
+        {!ruleDetails.templateKey && (
+          <CellComponent>
+            {activation.params.map((param: { key: string; value: string }) => {
+              const originalParam = parentActivation?.params.find((p) => p.key === param.key);
+              const originalValue = originalParam?.value;
+
+              return (
+                <StyledParameter className="sw-my-4" key={param.key}>
+                  <span className="key">{param.key}</span>
+                  <span className="sep sw-mr-1">: </span>
+                  <span className="value" title={param.value}>
+                    {param.value}
+                  </span>
+                  {parentActivation && param.value !== originalValue && (
+                    <div className="sw-flex sw-ml-4">
+                      {translate('coding_rules.original')}
+                      <span className="value sw-ml-1" title={originalValue}>
+                        {originalValue}
+                      </span>
+                    </div>
+                  )}
+                </StyledParameter>
+              );
+            })}
+          </CellComponent>
+        )}
+        {renderRowActions(activation, profile)}
       </TableRowInteractive>
     );
   };
+  return (
+    <div className="js-rule-profiles sw-mb-8">
+      <SubHeadingHighlight as="h2" id={PROFILES_HEADING_ID}>
+        <FormattedMessage id="coding_rules.quality_profiles" />
+      </SubHeadingHighlight>
 
-  render() {
-    const { activations = [], referencedProfiles, ruleDetails } = this.props;
-    const canActivate = Object.values(referencedProfiles).some((profile) =>
-      Boolean(profile.actions?.edit && profile.language === ruleDetails.lang),
-    );
-
-    return (
-      <div className="js-rule-profiles sw-mb-8">
-        <SubHeadingHighlight as="h2" id={PROFILES_HEADING_ID}>
-          <FormattedMessage id="coding_rules.quality_profiles" />
-        </SubHeadingHighlight>
-
-        {canActivate && (
-          <ActivationButton
-            buttonText={translate('coding_rules.activate')}
-            className="sw-mt-6"
-            modalHeader={translate('coding_rules.activate_in_quality_profile')}
-            onDone={this.handleActivate}
-            profiles={filter(
-              this.props.referencedProfiles,
-              (profile) => !activations.find((activation) => activation.qProfile === profile.key),
-            )}
-            rule={ruleDetails}
-          />
-        )}
+      {canActivate && (
+        <ActivationButton
+          buttonText={translate('coding_rules.activate')}
+          className="sw-mt-6"
+          modalHeader={translate('coding_rules.activate_in_quality_profile')}
+          onDone={props.onActivate}
+          profiles={filter(
+            referencedProfiles,
+            (profile) => !activations.find((activation) => activation.qProfile === profile.key),
+          )}
+          rule={ruleDetails}
+        />
+      )}
 
-        {activations.length > 0 && (
-          <Table
-            aria-labelledby={PROFILES_HEADING_ID}
-            className="sw-my-6"
-            columnCount={
-              ruleDetails.templateKey ? COLUMN_COUNT_WITHOUT_PARAMS : COLUMN_COUNT_WITH_PARAMS
-            }
-            id="coding-rules-detail-quality-profiles"
-          >
-            {activations.map(this.renderActivation)}
-          </Table>
-        )}
-      </div>
-    );
-  }
+      {activations.length > 0 && (
+        <Table
+          aria-labelledby={PROFILES_HEADING_ID}
+          className="sw-my-6"
+          columnCount={
+            ruleDetails.templateKey ? COLUMN_COUNT_WITHOUT_PARAMS : COLUMN_COUNT_WITH_PARAMS
+          }
+          id="coding-rules-detail-quality-profiles"
+        >
+          {activations.map(renderActivationRow)}
+        </Table>
+      )}
+    </div>
+  );
 }
 
 const StyledParameter = styled.div`
index b668e0fab23ecf481d0ddf202019dab9666a812f..734c2155efc859462e73b2d13c9f95f09885a326 100644 (file)
@@ -192,6 +192,11 @@ export function getPageObjects() {
       });
     },
 
+    async detailsloaded() {
+      expect(await byRole('heading', { level: 1 }).find()).toBeInTheDocument();
+      await ui.appLoaded();
+    },
+
     async bulkActivate(rulesCount: number, profile: Profile) {
       await user.click(ui.bulkChangeButton.get());
       await user.click(ui.activateIn.get());
index ed8fb4e07a35fabaec775f0d52de10139d4f8417..969fa123cbac91a38dd2df82d69a42f3fdc285c2 100644 (file)
@@ -31,7 +31,7 @@ export function FiltersHeader({ displayReset, onReset }: Props) {
   return (
     <div className="sw-mb-5">
       <div className="sw-flex sw-h-9 sw-items-center sw-justify-between">
-        <PageTitle className="sw-body-md-highlight" text={translate('filters')} />
+        <PageTitle className="sw-body-md-highlight" as="h2" text={translate('filters')} />
 
         {displayReset && (
           <DangerButtonSecondary onClick={onReset}>
index abf0cfba14937536c4a0ada17ac15a4d7a388ff6..57ac78f9c1899773470e5bcc3932aca43436208c 100644 (file)
@@ -19,7 +19,8 @@
  */
 
 import classNames from 'classnames';
-import { cloneDeep, debounce, groupBy } from 'lodash';
+import { ToggleButton } from 'design-system';
+import { cloneDeep, debounce, groupBy, isEqual } from 'lodash';
 import * as React from 'react';
 import { Location } from 'react-router-dom';
 import { dismissNotice } from '../../api/users';
@@ -27,25 +28,17 @@ import { CurrentUserContextInterface } from '../../app/components/current-user/C
 import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
 import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
 import { translate } from '../../helpers/l10n';
-import { Issue, RuleDetails } from '../../types/types';
+import { RuleDetails } from '../../types/types';
 import { NoticeType } from '../../types/users';
 import { getTabId, getTabPanelId } from '../controls/BoxedTabs';
 import withLocation from '../hoc/withLocation';
 import MoreInfoRuleDescription from './MoreInfoRuleDescription';
 import RuleDescription from './RuleDescription';
-
-import { ToggleButton } from 'design-system/lib';
 import './style.css';
 
 interface RuleTabViewerProps extends CurrentUserContextInterface {
   ruleDetails: RuleDetails;
-  extendedDescription?: string;
-  ruleDescriptionContextKey?: string;
-  activityTabContent?: React.ReactNode;
   location: Location;
-  selectedFlowIndex?: number;
-  selectedLocationIndex?: number;
-  issue?: Issue;
 }
 
 interface State {
@@ -107,34 +100,15 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
   }
 
   componentDidUpdate(prevProps: RuleTabViewerProps, prevState: State) {
-    const {
-      ruleDetails,
-      ruleDescriptionContextKey,
-      currentUser,
-      issue,
-      selectedFlowIndex,
-      selectedLocationIndex,
-    } = this.props;
+    const { ruleDetails, currentUser } = this.props;
 
     const { selectedTab } = this.state;
 
     if (
-      prevProps.ruleDetails.key !== ruleDetails.key ||
-      prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
-      prevProps.issue !== issue ||
-      prevProps.selectedFlowIndex !== selectedFlowIndex ||
-      prevProps.selectedLocationIndex !== selectedLocationIndex ||
-      prevProps.currentUser !== currentUser
+      !isEqual(prevProps.ruleDetails, ruleDetails) ||
+      !isEqual(prevProps.currentUser, currentUser)
     ) {
-      this.setState((pState) =>
-        this.computeState(
-          pState,
-          prevProps.ruleDetails !== ruleDetails ||
-            (prevProps.issue && issue && prevProps.issue.key !== issue.key) ||
-            prevProps.selectedFlowIndex !== selectedFlowIndex ||
-            prevProps.selectedLocationIndex !== selectedLocationIndex,
-        ),
-      );
+      this.setState((pState) => this.computeState(pState, prevProps.ruleDetails !== ruleDetails));
     }
 
     if (selectedTab?.value === TabKeys.MoreInfo) {
@@ -178,8 +152,6 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
   computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
     const {
       ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType },
-      extendedDescription,
-      activityTabContent,
     } = this.props;
 
     // As we might tamper with the description later on, we clone to avoid any side effect
@@ -187,22 +159,6 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
       groupBy(descriptionSections, (section) => section.key),
     );
 
-    if (extendedDescription) {
-      if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
-        // We add the extended description (htmlNote) in the first context, in case there are contexts
-        // Extended description will get reworked in future
-        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
-          '<br/>' + extendedDescription;
-      } else {
-        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
-          {
-            content: extendedDescription,
-            key: RuleDescriptionSections.RESOURCES,
-          },
-        ];
-      }
-    }
-
     const tabs: Tab[] = [
       {
         content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
@@ -241,11 +197,6 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State
         value: TabKeys.HowToFixIt,
         label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
       },
-      {
-        content: activityTabContent,
-        value: TabKeys.Activity,
-        label: translate('coding_rules.description_section.title', TabKeys.Activity),
-      },
       {
         content: ((educationPrinciples && educationPrinciples.length > 0) ||
           descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
index cc5367e3838b0e968397e17a7a86b113f68f0537..716ac775cbbe476dea2aba50b537f164a076f70c 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 { UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
+import { UseQueryResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import {
+  ActivateRuleParameters,
   AddRemoveGroupParameters,
   AddRemoveUserParameters,
+  DeactivateRuleParameters,
   Profile,
+  activateRule,
   addGroup,
   addUser,
   compareProfiles,
+  deactivateRule,
   getProfileInheritance,
 } from '../api/quality-profiles';
 import { ProfileInheritanceDetails } from '../types/types';
@@ -63,6 +67,30 @@ export function useProfilesCompareQuery(leftKey: string, rightKey: string) {
   });
 }
 
+export function useActivateRuleMutation(onSuccess: (data: ActivateRuleParameters) => unknown) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: activateRule,
+    onSuccess: (_, data) => {
+      queryClient.invalidateQueries({ queryKey: ['rules', 'details'] });
+      onSuccess(data);
+    },
+  });
+}
+
+export function useDeactivateRuleMutation(onSuccess: (data: DeactivateRuleParameters) => unknown) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: deactivateRule,
+    onSuccess: (_, data) => {
+      queryClient.invalidateQueries({ queryKey: ['rules', 'details'] });
+      onSuccess(data);
+    },
+  });
+}
+
 export function useAddUserMutation(onSuccess: () => unknown) {
   return useMutation({
     mutationFn: (data: AddRemoveUserParameters) => addUser(data),
diff --git a/server/sonar-web/src/main/js/queries/rules.ts b/server/sonar-web/src/main/js/queries/rules.ts
new file mode 100644 (file)
index 0000000..b7c303b
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { createRule, deleteRule, getRuleDetails, searchRules, updateRule } from '../api/rules';
+import { SearchRulesResponse } from '../types/coding-rules';
+import { SearchRulesQuery } from '../types/rules';
+import { RuleActivation, RuleDetails } from '../types/types';
+
+function getRulesQueryKey(type: 'search' | 'details', data?: SearchRulesQuery | string) {
+  const key = ['rules', type] as (string | SearchRulesQuery)[];
+  if (data) {
+    key.push(data);
+  }
+  return key;
+}
+
+export function useSearchRulesQuery(data: SearchRulesQuery) {
+  return useQuery({
+    queryKey: getRulesQueryKey('search', data),
+    queryFn: ({ queryKey: [, , query] }) => {
+      if (!query) {
+        return null;
+      }
+
+      return searchRules(data);
+    },
+  });
+}
+
+export function useRuleDetailsQuery(data: { key: string; actives?: boolean }) {
+  return useQuery({
+    queryKey: getRulesQueryKey('details', data.key),
+    queryFn: () => getRuleDetails(data),
+  });
+}
+
+export function useCreateRuleMutation(
+  searchQuery?: SearchRulesQuery,
+  onSuccess?: (rule: RuleDetails) => unknown,
+  onError?: (error: Response) => unknown,
+) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: createRule,
+    onError,
+    onSuccess: (rule) => {
+      onSuccess?.(rule);
+      queryClient.setQueryData<SearchRulesResponse>(
+        getRulesQueryKey('search', searchQuery),
+        (oldData) => {
+          return oldData ? { ...oldData, rules: [rule, ...oldData.rules] } : undefined;
+        },
+      );
+    },
+  });
+}
+
+export function useUpdateRuleMutation(onSuccess?: (rule: RuleDetails) => unknown) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: updateRule,
+    onSuccess: (rule) => {
+      onSuccess?.(rule);
+      queryClient.setQueryData<{ actives?: RuleActivation[]; rule: RuleDetails }>(
+        getRulesQueryKey('details', rule.key),
+        (oldData) => {
+          return {
+            ...oldData,
+            rule,
+          };
+        },
+      );
+    },
+  });
+}
+
+export function useDeleteRuleMutation(
+  searchQuery?: SearchRulesQuery,
+  onSuccess?: (key: string) => unknown,
+) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (params: { key: string }) => deleteRule(params),
+    onSuccess: (_, data) => {
+      onSuccess?.(data.key);
+      queryClient.setQueryData<SearchRulesResponse>(
+        getRulesQueryKey('search', searchQuery),
+        (oldData) => {
+          return oldData
+            ? { ...oldData, rules: oldData.rules.filter((rule) => rule.key !== data.key) }
+            : undefined;
+        },
+      );
+    },
+  });
+}