]> source.dussan.org Git - sonarqube.git/commitdiff
Refactor Quality Profiles page
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 2 Mar 2021 08:41:21 +0000 (09:41 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 3 Mar 2021 20:12:51 +0000 (20:12 +0000)
- Move all API interactions to the parent component
- Merge copy, extend, and rename forms into a single component
- Simplify delete form

18 files changed:
server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx
deleted file mode 100644 (file)
index b375c9a..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import Modal from 'sonar-ui-common/components/controls/Modal';
-import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { copyProfile } from '../../../api/quality-profiles';
-import { Profile } from '../types';
-
-interface Props {
-  onClose: () => void;
-  onCopy: (name: string) => void;
-  profile: Profile;
-}
-
-interface State {
-  loading: boolean;
-  name: string | null;
-}
-
-export default class CopyProfileForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false, name: null };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
-  };
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLElement>) => {
-    event.preventDefault();
-
-    const { name } = this.state;
-
-    if (name != null) {
-      this.setState({ loading: true });
-      copyProfile(this.props.profile.key, name).then(
-        (profile: any) => this.props.onCopy(profile.name),
-        () => {
-          if (this.mounted) {
-            this.setState({ loading: false });
-          }
-        }
-      );
-    }
-  };
-
-  render() {
-    const { profile } = this.props;
-    const header = translateWithParameters(
-      'quality_profiles.copy_x_title',
-      profile.name,
-      profile.languageName
-    );
-    const submitDisabled =
-      this.state.loading || !this.state.name || this.state.name === profile.name;
-
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
-        <form id="copy-profile-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-          <div className="modal-body">
-            <MandatoryFieldsExplanation className="modal-field" />
-            <div className="modal-field">
-              <label htmlFor="copy-profile-name">
-                {translate('quality_profiles.copy_new_name')}
-                <MandatoryFieldMarker />
-              </label>
-              <input
-                autoFocus={true}
-                id="copy-profile-name"
-                maxLength={100}
-                name="name"
-                onChange={this.handleNameChange}
-                required={true}
-                size={50}
-                type="text"
-                value={this.state.name != null ? this.state.name : profile.name}
-              />
-            </div>
-          </div>
-          <div className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton disabled={submitDisabled} id="copy-profile-submit">
-              {translate('copy')}
-            </SubmitButton>
-            <ResetButtonLink id="copy-profile-cancel" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
-    );
-  }
-}
index 291d325733a7d1d9a3c91c9a8572930b743c2ef7..0e5b61310d9dd393672bc79c457a9c261a605a2d 100644 (file)
@@ -22,90 +22,61 @@ import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/contro
 import Modal from 'sonar-ui-common/components/controls/Modal';
 import { Alert } from 'sonar-ui-common/components/ui/Alert';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { deleteProfile } from '../../../api/quality-profiles';
 import { Profile } from '../types';
 
-interface Props {
+export interface DeleteProfileFormProps {
+  loading: boolean;
   onClose: () => void;
   onDelete: () => void;
   profile: Profile;
 }
 
-interface State {
-  loading: boolean;
-}
-
-export default class DeleteProfileForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    this.setState({ loading: true });
-    deleteProfile(this.props.profile).then(this.props.onDelete, () => {
-      if (this.mounted) {
-        this.setState({ loading: false });
-      }
-    });
-  };
-
-  render() {
-    const { profile } = this.props;
-    const header = translate('quality_profiles.delete_confirm_title');
+export default function DeleteProfileForm(props: DeleteProfileFormProps) {
+  const { loading, profile } = props;
+  const header = translate('quality_profiles.delete_confirm_title');
 
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form id="delete-profile-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-          <div className="modal-body">
-            <div className="js-modal-messages" />
-            {profile.childrenCount > 0 ? (
-              <div>
-                <Alert variant="warning">
-                  {translate('quality_profiles.this_profile_has_descendants')}
-                </Alert>
-                <p>
-                  {translateWithParameters(
-                    'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants',
-                    profile.name,
-                    profile.languageName
-                  )}
-                </p>
-              </div>
-            ) : (
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose}>
+      <form
+        onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
+          e.preventDefault();
+          props.onDelete();
+        }}>
+        <div className="modal-head">
+          <h2>{header}</h2>
+        </div>
+        <div className="modal-body">
+          {profile.childrenCount > 0 ? (
+            <div>
+              <Alert variant="warning">
+                {translate('quality_profiles.this_profile_has_descendants')}
+              </Alert>
               <p>
                 {translateWithParameters(
-                  'quality_profiles.are_you_sure_want_delete_profile_x',
+                  'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants',
                   profile.name,
                   profile.languageName
                 )}
               </p>
-            )}
-          </div>
-          <div className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton
-              className="button-red"
-              disabled={this.state.loading}
-              id="delete-profile-submit">
-              {translate('delete')}
-            </SubmitButton>
-            <ResetButtonLink id="delete-profile-cancel" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
-    );
-  }
+            </div>
+          ) : (
+            <p>
+              {translateWithParameters(
+                'quality_profiles.are_you_sure_want_delete_profile_x',
+                profile.name,
+                profile.languageName
+              )}
+            </p>
+          )}
+        </div>
+        <div className="modal-foot">
+          {loading && <i className="spinner spacer-right" />}
+          <SubmitButton className="button-red" disabled={loading}>
+            {translate('delete')}
+          </SubmitButton>
+          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+        </div>
+      </form>
+    </Modal>
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx
deleted file mode 100644 (file)
index 2317d7e..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import Modal from 'sonar-ui-common/components/controls/Modal';
-import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
-import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { changeProfileParent, createQualityProfile } from '../../../api/quality-profiles';
-import { Profile } from '../types';
-
-interface Props {
-  onClose: () => void;
-  onExtend: (name: string) => void;
-  profile: Profile;
-}
-
-interface State {
-  loading: boolean;
-  name?: string;
-}
-
-type ValidState = State & Required<Pick<State, 'name'>>;
-
-export default class ExtendProfileForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  canSubmit = (state: State): state is ValidState => {
-    return Boolean(state.name && state.name.length);
-  };
-
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
-  };
-
-  handleFormSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    if (this.canSubmit(this.state)) {
-      const { profile: parentProfile } = this.props;
-      const { name } = this.state;
-
-      const data = new FormData();
-
-      data.append('language', parentProfile.language);
-      data.append('name', name);
-
-      this.setState({ loading: true });
-
-      try {
-        const { profile: newProfile } = await createQualityProfile(data);
-        await changeProfileParent(newProfile, parentProfile);
-        this.props.onExtend(newProfile.name);
-      } finally {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    }
-  };
-
-  render() {
-    const { profile } = this.props;
-    const header = translateWithParameters(
-      'quality_profiles.extend_x_title',
-      profile.name,
-      profile.languageName
-    );
-
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
-        <form onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-          <div className="modal-body">
-            <MandatoryFieldsExplanation className="modal-field" />
-            <div className="modal-field">
-              <label htmlFor="extend-profile-name">
-                {translate('quality_profiles.copy_new_name')}
-                <MandatoryFieldMarker />
-              </label>
-              <input
-                autoFocus={true}
-                id="extend-profile-name"
-                maxLength={100}
-                name="name"
-                onChange={this.handleNameChange}
-                required={true}
-                size={50}
-                type="text"
-                value={this.state.name ? this.state.name : ''}
-              />
-            </div>
-          </div>
-          <div className="modal-foot">
-            <DeferredSpinner className="spacer-right" loading={this.state.loading} />
-            <SubmitButton
-              disabled={this.state.loading || !this.canSubmit(this.state)}
-              id="extend-profile-submit">
-              {translate('extend')}
-            </SubmitButton>
-            <ResetButtonLink id="extend-profile-cancel" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
-    );
-  }
-}
index 9ce4622be111c8e2b38df9d624ef8e46fe11b63f..c8ac23413f6e75c5ce89ad972416441626fb1caf 100644 (file)
@@ -23,16 +23,22 @@ import ActionsDropdown, {
   ActionsDropdownItem
 } from 'sonar-ui-common/components/controls/ActionsDropdown';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import { getQualityProfileBackupUrl, setDefaultProfile } from '../../../api/quality-profiles';
+import {
+  changeProfileParent,
+  copyProfile,
+  createQualityProfile,
+  deleteProfile,
+  getQualityProfileBackupUrl,
+  renameProfile,
+  setDefaultProfile
+} from '../../../api/quality-profiles';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { getBaseUrl } from '../../../helpers/system';
 import { getRulesUrl } from '../../../helpers/urls';
-import { Profile } from '../types';
+import { Profile, ProfileActionModals } from '../types';
 import { getProfileComparePath, getProfilePath, PROFILE_PATH } from '../utils';
-import CopyProfileForm from './CopyProfileForm';
 import DeleteProfileForm from './DeleteProfileForm';
-import ExtendProfileForm from './ExtendProfileForm';
-import RenameProfileForm from './RenameProfileForm';
+import ProfileModalForm from './ProfileModalForm';
 
 interface Props {
   className?: string;
@@ -43,94 +49,129 @@ interface Props {
 }
 
 interface State {
-  copyFormOpen: boolean;
-  extendFormOpen: boolean;
-  deleteFormOpen: boolean;
-  renameFormOpen: boolean;
+  loading: boolean;
+  openModal?: ProfileActionModals;
 }
 
 export class ProfileActions extends React.PureComponent<Props, State> {
+  mounted = false;
   state: State = {
-    copyFormOpen: false,
-    extendFormOpen: false,
-    deleteFormOpen: false,
-    renameFormOpen: false
+    loading: false
   };
 
-  closeCopyForm = () => {
-    this.setState({ copyFormOpen: false });
-  };
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
 
-  closeDeleteForm = () => {
-    this.setState({ deleteFormOpen: false });
+  handleCloseModal = () => {
+    this.setState({ openModal: undefined });
   };
 
-  closeExtendForm = () => {
-    this.setState({ extendFormOpen: false });
+  handleCopyClick = () => {
+    this.setState({ openModal: ProfileActionModals.Copy });
   };
 
-  closeRenameForm = () => {
-    this.setState({ renameFormOpen: false });
+  handleExtendClick = () => {
+    this.setState({ openModal: ProfileActionModals.Extend });
   };
 
-  handleCopyClick = () => {
-    this.setState({ copyFormOpen: true });
+  handleRenameClick = () => {
+    this.setState({ openModal: ProfileActionModals.Rename });
   };
 
   handleDeleteClick = () => {
-    this.setState({ deleteFormOpen: true });
+    this.setState({ openModal: ProfileActionModals.Delete });
   };
 
-  handleExtendClick = () => {
-    this.setState({ extendFormOpen: true });
-  };
+  handleProfileCopy = async (name: string) => {
+    this.setState({ loading: true });
 
-  handleRenameClick = () => {
-    this.setState({ renameFormOpen: true });
+    try {
+      await copyProfile(this.props.profile.key, name);
+      this.profileActionPerformed(name);
+    } catch {
+      this.profileActionError();
+    }
   };
 
-  handleProfileCopy = (name: string) => {
-    this.closeCopyForm();
-    this.navigateToNewProfile(name);
-  };
+  handleProfileExtend = async (name: string) => {
+    const { profile: parentProfile } = this.props;
+
+    const data = {
+      language: parentProfile.language,
+      name
+    };
+
+    this.setState({ loading: true });
 
-  handleProfileDelete = () => {
-    this.props.router.replace(PROFILE_PATH);
-    this.props.updateProfiles();
+    try {
+      const { profile: newProfile } = await createQualityProfile(data);
+      await changeProfileParent(newProfile, parentProfile);
+      this.profileActionPerformed(name);
+    } catch {
+      this.profileActionError();
+    }
   };
 
-  handleProfileExtend = (name: string) => {
-    this.closeExtendForm();
-    this.navigateToNewProfile(name);
+  handleProfileRename = async (name: string) => {
+    this.setState({ loading: true });
+
+    try {
+      await renameProfile(this.props.profile.key, name);
+      this.profileActionPerformed(name);
+    } catch {
+      this.profileActionError();
+    }
   };
 
-  handleProfileRename = (name: string) => {
-    this.closeRenameForm();
-    this.props.updateProfiles().then(
-      () => {
-        if (!this.props.fromList) {
-          this.props.router.replace(getProfilePath(name, this.props.profile.language));
-        }
-      },
-      () => {}
-    );
+  handleProfileDelete = async () => {
+    this.setState({ loading: true });
+
+    try {
+      await deleteProfile(this.props.profile);
+
+      if (this.mounted) {
+        this.setState({ loading: false, openModal: undefined });
+        this.props.router.replace(PROFILE_PATH);
+        this.props.updateProfiles();
+      }
+    } catch {
+      this.profileActionError();
+    }
   };
 
   handleSetDefaultClick = () => {
     setDefaultProfile(this.props.profile).then(this.props.updateProfiles, () => {});
   };
 
-  navigateToNewProfile = (name: string) => {
-    this.props.updateProfiles().then(
-      () => {
-        this.props.router.push(getProfilePath(name, this.props.profile.language));
-      },
-      () => {}
-    );
+  profileActionPerformed = (name: string) => {
+    const { profile, router } = this.props;
+    if (this.mounted) {
+      this.setState({ loading: false, openModal: undefined });
+      this.props.updateProfiles().then(
+        () => {
+          router.push(getProfilePath(name, profile.language));
+        },
+        () => {
+          /* noop */
+        }
+      );
+    }
+  };
+
+  profileActionError = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
   };
 
   render() {
     const { profile } = this.props;
+    const { loading, openModal } = this.state;
     const { actions = {} } = profile;
 
     const backupUrl = `${getBaseUrl()}${getQualityProfileBackupUrl(profile)}`;
@@ -144,86 +185,110 @@ export class ProfileActions extends React.PureComponent<Props, State> {
       <>
         <ActionsDropdown className={this.props.className}>
           {actions.edit && (
-            <ActionsDropdownItem to={activateMoreUrl}>
-              <span data-test="quality-profiles__activate-more-rules">
-                {translate('quality_profiles.activate_more_rules')}
-              </span>
+            <ActionsDropdownItem
+              className="it__quality-profiles__activate-more-rules"
+              to={activateMoreUrl}>
+              {translate('quality_profiles.activate_more_rules')}
             </ActionsDropdownItem>
           )}
 
           {!profile.isBuiltIn && (
-            <ActionsDropdownItem download={`${profile.key}.xml`} to={backupUrl}>
-              <span data-test="quality-profiles__backup">{translate('backup_verb')}</span>
+            <ActionsDropdownItem
+              className="it__quality-profiles__backup"
+              download={`${profile.key}.xml`}
+              to={backupUrl}>
+              {translate('backup_verb')}
             </ActionsDropdownItem>
           )}
 
-          <ActionsDropdownItem to={getProfileComparePath(profile.name, profile.language)}>
-            <span data-test="quality-profiles__compare">{translate('compare')}</span>
+          <ActionsDropdownItem
+            className="it__quality-profiles__compare"
+            to={getProfileComparePath(profile.name, profile.language)}>
+            {translate('compare')}
           </ActionsDropdownItem>
 
           {actions.copy && (
             <>
-              <ActionsDropdownItem onClick={this.handleCopyClick}>
-                <span data-test="quality-profiles__copy">{translate('copy')}</span>
+              <ActionsDropdownItem
+                className="it__quality-profiles__copy"
+                onClick={this.handleCopyClick}>
+                {translate('copy')}
               </ActionsDropdownItem>
 
-              <ActionsDropdownItem onClick={this.handleExtendClick}>
-                <span data-test="quality-profiles__extend">{translate('extend')}</span>
+              <ActionsDropdownItem
+                className="it__quality-profiles__extend"
+                onClick={this.handleExtendClick}>
+                {translate('extend')}
               </ActionsDropdownItem>
             </>
           )}
 
           {actions.edit && (
-            <ActionsDropdownItem onClick={this.handleRenameClick}>
-              <span data-test="quality-profiles__rename">{translate('rename')}</span>
+            <ActionsDropdownItem
+              className="it__quality-profiles__rename"
+              onClick={this.handleRenameClick}>
+              {translate('rename')}
             </ActionsDropdownItem>
           )}
 
           {actions.setAsDefault && (
-            <ActionsDropdownItem onClick={this.handleSetDefaultClick}>
-              <span data-test="quality-profiles__set-as-default">
-                {translate('set_as_default')}
-              </span>
+            <ActionsDropdownItem
+              className="it__quality-profiles__set-as-default"
+              onClick={this.handleSetDefaultClick}>
+              {translate('set_as_default')}
             </ActionsDropdownItem>
           )}
 
           {actions.delete && <ActionsDropdownDivider />}
 
           {actions.delete && (
-            <ActionsDropdownItem destructive={true} onClick={this.handleDeleteClick}>
-              <span data-test="quality-profiles__delete">{translate('delete')}</span>
+            <ActionsDropdownItem
+              className="it__quality-profiles__delete"
+              destructive={true}
+              onClick={this.handleDeleteClick}>
+              {translate('delete')}
             </ActionsDropdownItem>
           )}
         </ActionsDropdown>
 
-        {this.state.copyFormOpen && (
-          <CopyProfileForm
-            onClose={this.closeCopyForm}
-            onCopy={this.handleProfileCopy}
+        {openModal === ProfileActionModals.Copy && (
+          <ProfileModalForm
+            btnLabelKey="copy"
+            headerKey="quality_profiles.copy_x_title"
+            loading={loading}
+            onClose={this.handleCloseModal}
+            onSubmit={this.handleProfileCopy}
             profile={profile}
           />
         )}
 
-        {this.state.extendFormOpen && (
-          <ExtendProfileForm
-            onClose={this.closeExtendForm}
-            onExtend={this.handleProfileExtend}
+        {openModal === ProfileActionModals.Extend && (
+          <ProfileModalForm
+            btnLabelKey="extend"
+            headerKey="quality_profiles.extend_x_title"
+            loading={loading}
+            onClose={this.handleCloseModal}
+            onSubmit={this.handleProfileExtend}
             profile={profile}
           />
         )}
 
-        {this.state.deleteFormOpen && (
-          <DeleteProfileForm
-            onClose={this.closeDeleteForm}
-            onDelete={this.handleProfileDelete}
+        {openModal === ProfileActionModals.Rename && (
+          <ProfileModalForm
+            btnLabelKey="rename"
+            headerKey="quality_profiles.rename_x_title"
+            loading={loading}
+            onClose={this.handleCloseModal}
+            onSubmit={this.handleProfileRename}
             profile={profile}
           />
         )}
 
-        {this.state.renameFormOpen && (
-          <RenameProfileForm
-            onClose={this.closeRenameForm}
-            onRename={this.handleProfileRename}
+        {openModal === ProfileActionModals.Delete && (
+          <DeleteProfileForm
+            loading={loading}
+            onClose={this.handleCloseModal}
+            onDelete={this.handleProfileDelete}
             profile={profile}
           />
         )}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx
new file mode 100644 (file)
index 0000000..d532c30
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Modal from 'sonar-ui-common/components/controls/Modal';
+import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
+import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { Profile } from '../types';
+
+export interface ProfileModalFormProps {
+  btnLabelKey: string;
+  headerKey: string;
+  loading: boolean;
+  onClose: () => void;
+  onSubmit: (name: string) => void;
+  profile: Profile;
+}
+
+export default function ProfileModalForm(props: ProfileModalFormProps) {
+  const { btnLabelKey, headerKey, loading, profile } = props;
+  const [name, setName] = React.useState<string | undefined>(undefined);
+
+  const submitDisabled = loading || !name || name === profile.name;
+  const header = translateWithParameters(headerKey, profile.name, profile.languageName);
+
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose} size="small">
+      <form
+        onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
+          e.preventDefault();
+          if (name) {
+            props.onSubmit(name);
+          }
+        }}>
+        <div className="modal-head">
+          <h2>{header}</h2>
+        </div>
+        <div className="modal-body">
+          <MandatoryFieldsExplanation className="modal-field" />
+          <div className="modal-field">
+            <label htmlFor="profile-name">
+              {translate('quality_profiles.new_name')}
+              <MandatoryFieldMarker />
+            </label>
+            <input
+              autoFocus={true}
+              id="profile-name"
+              maxLength={100}
+              name="name"
+              onChange={(e: React.SyntheticEvent<HTMLInputElement>) => {
+                setName(e.currentTarget.value);
+              }}
+              required={true}
+              size={50}
+              type="text"
+              value={name ?? profile.name}
+            />
+          </div>
+        </div>
+        <div className="modal-foot">
+          {loading && <i className="spinner spacer-right" />}
+          <SubmitButton disabled={submitDisabled}>{translate(btnLabelKey)}</SubmitButton>
+          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+        </div>
+      </form>
+    </Modal>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx
deleted file mode 100644 (file)
index 43ef7a0..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import Modal from 'sonar-ui-common/components/controls/Modal';
-import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { renameProfile } from '../../../api/quality-profiles';
-import { Profile } from '../types';
-
-interface Props {
-  onClose: () => void;
-  onRename: (name: string) => void;
-  profile: Profile;
-}
-
-interface State {
-  loading: boolean;
-  name: string | null;
-}
-
-export default class RenameProfileForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false, name: null };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
-  };
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    const { name } = this.state;
-
-    if (name != null) {
-      this.setState({ loading: true });
-      renameProfile(this.props.profile.key, name).then(
-        () => this.props.onRename(name),
-        () => {
-          if (this.mounted) {
-            this.setState({ loading: false });
-          }
-        }
-      );
-    }
-  };
-
-  render() {
-    const { profile } = this.props;
-    const header = translateWithParameters(
-      'quality_profiles.rename_x_title',
-      profile.name,
-      profile.languageName
-    );
-    const submitDisabled =
-      this.state.loading || !this.state.name || this.state.name === profile.name;
-
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
-        <form id="rename-profile-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-          <div className="modal-body">
-            <MandatoryFieldsExplanation className="modal-field" />
-            <div className="modal-field">
-              <label htmlFor="rename-profile-name">
-                {translate('quality_profiles.new_name')}
-                <MandatoryFieldMarker />
-              </label>
-              <input
-                autoFocus={true}
-                id="rename-profile-name"
-                maxLength={100}
-                name="name"
-                onChange={this.handleNameChange}
-                required={true}
-                size={50}
-                type="text"
-                value={this.state.name != null ? this.state.name : profile.name}
-              />
-            </div>
-          </div>
-          <div className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton disabled={submitDisabled} id="rename-profile-submit">
-              {translate('rename')}
-            </SubmitButton>
-            <ResetButtonLink id="rename-profile-cancel" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
-    );
-  }
-}
index 801c736874ef5a99e4994b00c5fced50d40f9711..58cbfe7eb0dce9380336697b4730b0afc9d362e1 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 { shallow } from 'enzyme';
 import * as React from 'react';
-import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { deleteProfile } from '../../../../api/quality-profiles';
 import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks';
-import DeleteProfileForm from '../DeleteProfileForm';
-
-beforeEach(() => jest.clearAllMocks());
-
-jest.mock('../../../../api/quality-profiles', () => ({
-  deleteProfile: jest.fn().mockResolvedValue({})
-}));
+import DeleteProfileForm, { DeleteProfileFormProps } from '../DeleteProfileForm';
 
 it('should render correctly', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+  expect(shallowRender({ profile: mockQualityProfile({ childrenCount: 2 }) })).toMatchSnapshot(
+    'profile has children'
+  );
 });
 
-it('should handle form submit correctly', async () => {
-  const wrapper = shallowRender();
-  wrapper.instance().handleFormSubmit(mockEvent());
-  await waitAndUpdate(wrapper);
+it('should correctly submit the form', () => {
+  const onDelete = jest.fn();
+  const wrapper = shallowRender({ onDelete });
 
-  expect(deleteProfile).toHaveBeenCalled();
+  const formOnSubmit = wrapper.find('form').props().onSubmit;
+  if (formOnSubmit) {
+    formOnSubmit(mockEvent());
+  }
+  expect(onDelete).toBeCalled();
 });
 
-function shallowRender(props: Partial<DeleteProfileForm['props']> = {}) {
-  return shallow<DeleteProfileForm>(
+function shallowRender(props: Partial<DeleteProfileFormProps> = {}) {
+  return shallow<DeleteProfileFormProps>(
     <DeleteProfileForm
+      loading={false}
       onClose={jest.fn()}
       onDelete={jest.fn()}
       profile={mockQualityProfile()}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx
deleted file mode 100644 (file)
index 47e61da..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { changeProfileParent, createQualityProfile } from '../../../../api/quality-profiles';
-import { mockQualityProfile } from '../../../../helpers/testMocks';
-import ExtendProfileForm from '../ExtendProfileForm';
-
-jest.mock('../../../../api/quality-profiles', () => ({
-  createQualityProfile: jest.fn().mockResolvedValue({ profile: { key: 'new-profile' } }),
-  changeProfileParent: jest.fn().mockResolvedValue(true)
-}));
-
-it('should render correctly', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should correctly create a new profile and extend the existing one', async () => {
-  const profile = mockQualityProfile();
-  const name = 'New name';
-  const wrapper = shallowRender({ profile });
-
-  expect(wrapper.find('SubmitButton').props().disabled).toBe(true);
-
-  wrapper.setState({ name }).update();
-  wrapper.instance().handleFormSubmit(mockEvent());
-  await waitAndUpdate(wrapper);
-
-  const data = new FormData();
-  data.append('language', profile.language);
-  data.append('name', name);
-  expect(createQualityProfile).toHaveBeenCalledWith(data);
-  expect(changeProfileParent).toHaveBeenCalledWith({ key: 'new-profile' }, profile);
-});
-
-function shallowRender(props: Partial<ExtendProfileForm['props']> = {}) {
-  return shallow<ExtendProfileForm>(
-    <ExtendProfileForm
-      onClose={jest.fn()}
-      onExtend={jest.fn()}
-      profile={mockQualityProfile()}
-      {...props}
-    />
-  );
-}
index 4c14e43e20883ca7ba5ce6cebfaf100160f2f786..a6ad0fffb57c9b7fc9e665aed5807563fc5e9a31 100644 (file)
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { setDefaultProfile } from '../../../../api/quality-profiles';
+import {
+  changeProfileParent,
+  copyProfile,
+  createQualityProfile,
+  deleteProfile,
+  renameProfile,
+  setDefaultProfile
+} from '../../../../api/quality-profiles';
 import { mockQualityProfile, mockRouter } from '../../../../helpers/testMocks';
+import { ProfileActionModals } from '../../types';
+import { PROFILE_PATH } from '../../utils';
+import DeleteProfileForm from '../DeleteProfileForm';
 import { ProfileActions } from '../ProfileActions';
+import ProfileModalForm from '../ProfileModalForm';
 
-beforeEach(() => jest.clearAllMocks());
+jest.mock('../../../../api/quality-profiles', () => {
+  const { mockQualityProfile } = jest.requireActual('../../../../helpers/testMocks');
 
-jest.mock('../../../../api/quality-profiles', () => ({
-  ...jest.requireActual('../../../../api/quality-profiles'),
-  setDefaultProfile: jest.fn().mockResolvedValue({})
-}));
+  return {
+    ...jest.requireActual('../../../../api/quality-profiles'),
+    copyProfile: jest.fn().mockResolvedValue(null),
+    changeProfileParent: jest.fn().mockResolvedValue(null),
+    createQualityProfile: jest
+      .fn()
+      .mockResolvedValue({ profile: mockQualityProfile({ key: 'newProfile' }) }),
+    deleteProfile: jest.fn().mockResolvedValue(null),
+    setDefaultProfile: jest.fn().mockResolvedValue(null),
+    renameProfile: jest.fn().mockResolvedValue(null)
+  };
+});
 
 const PROFILE = mockQualityProfile({
   activeRuleCount: 68,
@@ -39,15 +59,13 @@ const PROFILE = mockQualityProfile({
   rulesUpdatedAt: '2017-06-28T12:58:44+0000'
 });
 
-it('renders with no permissions', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-it('renders with permission to edit only', () => {
-  expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot();
-});
+beforeEach(() => jest.clearAllMocks());
 
-it('renders with all permissions', () => {
+it('renders correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('no permissions');
+  expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot(
+    'edit only'
+  );
   expect(
     shallowRender({
       profile: {
@@ -61,58 +79,240 @@ it('renders with all permissions', () => {
         }
       }
     })
-  ).toMatchSnapshot();
+  ).toMatchSnapshot('all permissions');
+
+  expect(shallowRender().setState({ openModal: ProfileActionModals.Copy })).toMatchSnapshot(
+    'copy modal'
+  );
+  expect(shallowRender().setState({ openModal: ProfileActionModals.Extend })).toMatchSnapshot(
+    'extend modal'
+  );
+  expect(shallowRender().setState({ openModal: ProfileActionModals.Rename })).toMatchSnapshot(
+    'rename modal'
+  );
+  expect(shallowRender().setState({ openModal: ProfileActionModals.Delete })).toMatchSnapshot(
+    'delete modal'
+  );
 });
 
-it('should copy profile', async () => {
-  const name = 'new-name';
-  const updateProfiles = jest.fn(() => Promise.resolve());
-  const push = jest.fn();
-  const wrapper = shallowRender({
-    profile: { ...PROFILE, actions: { copy: true } },
-    router: { push, replace: jest.fn() },
-    updateProfiles
+describe('copy a profile', () => {
+  it('should correctly copy a profile', async () => {
+    const name = 'new-name';
+    const updateProfiles = jest.fn().mockResolvedValue(null);
+    const push = jest.fn();
+    const wrapper = shallowRender({
+      profile: { ...PROFILE, actions: { copy: true } },
+      router: mockRouter({ push }),
+      updateProfiles
+    });
+
+    click(wrapper.find('.it__quality-profiles__copy'));
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(true);
+
+    wrapper
+      .find(ProfileModalForm)
+      .props()
+      .onSubmit(name);
+    expect(copyProfile).toBeCalledWith(PROFILE.key, name);
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).toBeCalled();
+    expect(push).toBeCalledWith({
+      pathname: '/profiles/show',
+      query: { language: 'js', name }
+    });
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
   });
 
-  click(wrapper.find('[data-test="quality-profiles__copy"]').parent());
-  expect(wrapper.find('CopyProfileForm').exists()).toBe(true);
+  it('should correctly keep the modal open in case of an error', async () => {
+    (copyProfile as jest.Mock).mockRejectedValueOnce(null);
 
-  wrapper.find('CopyProfileForm').prop<Function>('onCopy')(name);
-  expect(updateProfiles).toBeCalled();
-  await waitAndUpdate(wrapper);
+    const name = 'new-name';
+    const updateProfiles = jest.fn();
+    const push = jest.fn();
+    const wrapper = shallowRender({
+      profile: { ...PROFILE, actions: { copy: true } },
+      router: mockRouter({ push }),
+      updateProfiles
+    });
+    wrapper.setState({ openModal: ProfileActionModals.Copy });
+
+    wrapper.instance().handleProfileCopy(name);
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).not.toBeCalled();
+    await waitAndUpdate(wrapper);
 
-  expect(push).toBeCalledWith({
-    pathname: '/profiles/show',
-    query: { language: 'js', name }
+    expect(push).not.toBeCalled();
+    expect(wrapper.state().openModal).toBe(ProfileActionModals.Copy);
   });
-  expect(wrapper.find('CopyProfileForm').exists()).toBe(false);
 });
 
-it('should extend profile', async () => {
-  const name = 'new-name';
-  const updateProfiles = jest.fn(() => Promise.resolve());
-  const push = jest.fn();
-  const wrapper = shallowRender({
-    profile: { ...PROFILE, actions: { copy: true } },
-    router: { push, replace: jest.fn() },
-    updateProfiles
+describe('extend a profile', () => {
+  it('should correctly extend a profile', async () => {
+    const name = 'new-name';
+    const profile = { ...PROFILE, actions: { copy: true } };
+    const updateProfiles = jest.fn().mockResolvedValue(null);
+    const push = jest.fn();
+    const wrapper = shallowRender({
+      profile,
+      router: mockRouter({ push }),
+      updateProfiles
+    });
+
+    click(wrapper.find('.it__quality-profiles__extend'));
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(true);
+
+    wrapper
+      .find(ProfileModalForm)
+      .props()
+      .onSubmit(name);
+    expect(createQualityProfile).toBeCalledWith({ language: profile.language, name });
+    await waitAndUpdate(wrapper);
+    expect(changeProfileParent).toBeCalledWith(
+      expect.objectContaining({
+        key: 'newProfile'
+      }),
+      profile
+    );
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).toBeCalled();
+    await waitAndUpdate(wrapper);
+
+    expect(push).toBeCalledWith({
+      pathname: '/profiles/show',
+      query: { language: 'js', name }
+    });
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
   });
 
-  click(wrapper.find('[data-test="quality-profiles__extend"]').parent());
-  expect(wrapper.find('ExtendProfileForm').exists()).toBe(true);
+  it('should correctly keep the modal open in case of an error', async () => {
+    (createQualityProfile as jest.Mock).mockRejectedValueOnce(null);
 
-  wrapper.find('ExtendProfileForm').prop<Function>('onExtend')(name);
-  expect(updateProfiles).toBeCalled();
-  await waitAndUpdate(wrapper);
+    const name = 'new-name';
+    const updateProfiles = jest.fn();
+    const push = jest.fn();
+    const wrapper = shallowRender({
+      profile: { ...PROFILE, actions: { copy: true } },
+      router: mockRouter({ push }),
+      updateProfiles
+    });
+    wrapper.setState({ openModal: ProfileActionModals.Extend });
+
+    wrapper.instance().handleProfileExtend(name);
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).not.toBeCalled();
+    expect(changeProfileParent).not.toBeCalled();
+    expect(push).not.toBeCalled();
+    expect(wrapper.state().openModal).toBe(ProfileActionModals.Extend);
+  });
+});
+
+describe('rename a profile', () => {
+  it('should correctly rename a profile', async () => {
+    const name = 'new-name';
+    const updateProfiles = jest.fn().mockResolvedValue(null);
+    const push = jest.fn();
+    const wrapper = shallowRender({
+      profile: { ...PROFILE, actions: { edit: true } },
+      router: mockRouter({ push }),
+      updateProfiles
+    });
+
+    click(wrapper.find('.it__quality-profiles__rename'));
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(true);
+
+    wrapper
+      .find(ProfileModalForm)
+      .props()
+      .onSubmit(name);
+    expect(renameProfile).toBeCalledWith(PROFILE.key, name);
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).toBeCalled();
+    expect(push).toBeCalledWith({
+      pathname: '/profiles/show',
+      query: { language: 'js', name }
+    });
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
+  });
+
+  it('should correctly keep the modal open in case of an error', async () => {
+    (renameProfile as jest.Mock).mockRejectedValueOnce(null);
+
+    const name = 'new-name';
+    const updateProfiles = jest.fn();
+    const push = jest.fn();
+    const wrapper = shallowRender({
+      profile: { ...PROFILE, actions: { copy: true } },
+      router: mockRouter({ push }),
+      updateProfiles
+    });
+    wrapper.setState({ openModal: ProfileActionModals.Rename });
+
+    wrapper.instance().handleProfileRename(name);
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).not.toBeCalled();
+    await waitAndUpdate(wrapper);
+
+    expect(push).not.toBeCalled();
+    expect(wrapper.state().openModal).toBe(ProfileActionModals.Rename);
+  });
+});
+
+describe('delete a profile', () => {
+  it('should correctly delete a profile', async () => {
+    const updateProfiles = jest.fn().mockResolvedValue(null);
+    const replace = jest.fn();
+    const profile = { ...PROFILE, actions: { delete: true } };
+    const wrapper = shallowRender({
+      profile,
+      router: mockRouter({ replace }),
+      updateProfiles
+    });
+
+    click(wrapper.find('.it__quality-profiles__delete'));
+    expect(wrapper.find(DeleteProfileForm).exists()).toBe(true);
+
+    wrapper
+      .find(DeleteProfileForm)
+      .props()
+      .onDelete();
+    expect(deleteProfile).toBeCalledWith(profile);
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).toBeCalled();
+    expect(replace).toBeCalledWith(PROFILE_PATH);
+    expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
+  });
+
+  it('should correctly keep the modal open in case of an error', async () => {
+    (deleteProfile as jest.Mock).mockRejectedValueOnce(null);
+
+    const updateProfiles = jest.fn();
+    const replace = jest.fn();
+    const wrapper = shallowRender({
+      profile: { ...PROFILE, actions: { copy: true } },
+      router: mockRouter({ replace }),
+      updateProfiles
+    });
+    wrapper.setState({ openModal: ProfileActionModals.Delete });
+
+    wrapper.instance().handleProfileDelete();
+    await waitAndUpdate(wrapper);
+
+    expect(updateProfiles).not.toBeCalled();
+    await waitAndUpdate(wrapper);
 
-  expect(push).toBeCalledWith({
-    pathname: '/profiles/show',
-    query: { language: 'js', name }
+    expect(replace).not.toBeCalled();
+    expect(wrapper.state().openModal).toBe(ProfileActionModals.Delete);
   });
-  expect(wrapper.find('ExtendProfileForm').exists()).toBe(false);
 });
 
-it('should delete profile properly', async () => {
+it('should correctly set a profile as the default', async () => {
   const updateProfiles = jest.fn();
 
   const wrapper = shallowRender({ updateProfiles });
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx
new file mode 100644 (file)
index 0000000..6029ef6
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { change } from 'sonar-ui-common/helpers/testUtils';
+import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks';
+import ProfileModalForm, { ProfileModalFormProps } from '../ProfileModalForm';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+
+  const wrapper = shallowRender();
+  change(wrapper.find('#profile-name'), 'new name');
+  expect(wrapper).toMatchSnapshot('can submit');
+});
+
+it('should correctly submit the form', () => {
+  const onSubmit = jest.fn();
+  const wrapper = shallowRender({ onSubmit });
+
+  // Won't submit unless a new name was given.
+  let formOnSubmit = wrapper.find('form').props().onSubmit;
+  if (formOnSubmit) {
+    formOnSubmit(mockEvent());
+  }
+  expect(onSubmit).not.toBeCalled();
+
+  // Input a new name.
+  change(wrapper.find('#profile-name'), 'new name');
+
+  // Now will submit the form.
+  formOnSubmit = wrapper.find('form').props().onSubmit;
+  if (formOnSubmit) {
+    formOnSubmit(mockEvent());
+  }
+  expect(onSubmit).toBeCalledWith('new name');
+});
+
+function shallowRender(props: Partial<ProfileModalFormProps> = {}) {
+  return shallow<ProfileModalFormProps>(
+    <ProfileModalForm
+      btnLabelKey="btn-label"
+      headerKey="header-label"
+      loading={false}
+      onClose={jest.fn()}
+      onSubmit={jest.fn()}
+      profile={mockQualityProfile()}
+      {...props}
+    />
+  );
+}
index 41855ab772cb1cdc5a527411c6da8fc4c2ca050e..d24c01159c2a5182119100dded48672108ebb192 100644 (file)
@@ -1,12 +1,52 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
+<Modal
+  contentLabel="quality_profiles.delete_confirm_title"
+  onRequestClose={[MockFunction]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        quality_profiles.delete_confirm_title
+      </h2>
+    </div>
+    <div
+      className="modal-body"
+    >
+      <p>
+        quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript
+      </p>
+    </div>
+    <div
+      className="modal-foot"
+    >
+      <SubmitButton
+        className="button-red"
+        disabled={false}
+      >
+        delete
+      </SubmitButton>
+      <ResetButtonLink
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </div>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: loading 1`] = `
 <Modal
   contentLabel="quality_profiles.delete_confirm_title"
   onRequestClose={[MockFunction]}
 >
   <form
-    id="delete-profile-form"
     onSubmit={[Function]}
   >
     <div
@@ -19,25 +59,71 @@ exports[`should render correctly 1`] = `
     <div
       className="modal-body"
     >
-      <div
-        className="js-modal-messages"
-      />
       <p>
         quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript
       </p>
     </div>
+    <div
+      className="modal-foot"
+    >
+      <i
+        className="spinner spacer-right"
+      />
+      <SubmitButton
+        className="button-red"
+        disabled={true}
+      >
+        delete
+      </SubmitButton>
+      <ResetButtonLink
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </div>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: profile has children 1`] = `
+<Modal
+  contentLabel="quality_profiles.delete_confirm_title"
+  onRequestClose={[MockFunction]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        quality_profiles.delete_confirm_title
+      </h2>
+    </div>
+    <div
+      className="modal-body"
+    >
+      <div>
+        <Alert
+          variant="warning"
+        >
+          quality_profiles.this_profile_has_descendants
+        </Alert>
+        <p>
+          quality_profiles.are_you_sure_want_delete_profile_x_and_descendants.name.JavaScript
+        </p>
+      </div>
+    </div>
     <div
       className="modal-foot"
     >
       <SubmitButton
         className="button-red"
         disabled={false}
-        id="delete-profile-submit"
       >
         delete
       </SubmitButton>
       <ResetButtonLink
-        id="delete-profile-cancel"
         onClick={[MockFunction]}
       >
         cancel
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap
deleted file mode 100644 (file)
index 227de45..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Modal
-  contentLabel="quality_profiles.extend_x_title.name.JavaScript"
-  onRequestClose={[MockFunction]}
-  size="small"
->
-  <form
-    onSubmit={[Function]}
-  >
-    <div
-      className="modal-head"
-    >
-      <h2>
-        quality_profiles.extend_x_title.name.JavaScript
-      </h2>
-    </div>
-    <div
-      className="modal-body"
-    >
-      <MandatoryFieldsExplanation
-        className="modal-field"
-      />
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="extend-profile-name"
-        >
-          quality_profiles.copy_new_name
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          autoFocus={true}
-          id="extend-profile-name"
-          maxLength={100}
-          name="name"
-          onChange={[Function]}
-          required={true}
-          size={50}
-          type="text"
-          value=""
-        />
-      </div>
-    </div>
-    <div
-      className="modal-foot"
-    >
-      <DeferredSpinner
-        className="spacer-right"
-        loading={false}
-      />
-      <SubmitButton
-        disabled={true}
-        id="extend-profile-submit"
-      >
-        extend
-      </SubmitButton>
-      <ResetButtonLink
-        id="extend-profile-cancel"
-        onClick={[MockFunction]}
-      >
-        cancel
-      </ResetButtonLink>
-    </div>
-  </form>
-</Modal>
-`;
index f59b868a8710d86893b4d3b6bd8dc9f629bd290c..23a829c4d9acd6f4bcd0f14323bb3ebee1eebb02 100644 (file)
@@ -1,9 +1,10 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`renders with all permissions 1`] = `
+exports[`renders correctly: all permissions 1`] = `
 <Fragment>
   <ActionsDropdown>
     <ActionsDropdownItem
+      className="it__quality-profiles__activate-more-rules"
       to={
         Object {
           "pathname": "/coding_rules",
@@ -14,23 +15,17 @@ exports[`renders with all permissions 1`] = `
         }
       }
     >
-      <span
-        data-test="quality-profiles__activate-more-rules"
-      >
-        quality_profiles.activate_more_rules
-      </span>
+      quality_profiles.activate_more_rules
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__backup"
       download="key.xml"
       to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
     >
-      <span
-        data-test="quality-profiles__backup"
-      >
-        backup_verb
-      </span>
+      backup_verb
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__compare"
       to={
         Object {
           "pathname": "/profiles/compare",
@@ -41,77 +36,56 @@ exports[`renders with all permissions 1`] = `
         }
       }
     >
-      <span
-        data-test="quality-profiles__compare"
-      >
-        compare
-      </span>
+      compare
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__copy"
       onClick={[Function]}
     >
-      <span
-        data-test="quality-profiles__copy"
-      >
-        copy
-      </span>
+      copy
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__extend"
       onClick={[Function]}
     >
-      <span
-        data-test="quality-profiles__extend"
-      >
-        extend
-      </span>
+      extend
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__rename"
       onClick={[Function]}
     >
-      <span
-        data-test="quality-profiles__rename"
-      >
-        rename
-      </span>
+      rename
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__set-as-default"
       onClick={[Function]}
     >
-      <span
-        data-test="quality-profiles__set-as-default"
-      >
-        set_as_default
-      </span>
+      set_as_default
     </ActionsDropdownItem>
     <ActionsDropdownDivider />
     <ActionsDropdownItem
+      className="it__quality-profiles__delete"
       destructive={true}
       onClick={[Function]}
     >
-      <span
-        data-test="quality-profiles__delete"
-      >
-        delete
-      </span>
+      delete
     </ActionsDropdownItem>
   </ActionsDropdown>
 </Fragment>
 `;
 
-exports[`renders with no permissions 1`] = `
+exports[`renders correctly: copy modal 1`] = `
 <Fragment>
   <ActionsDropdown>
     <ActionsDropdownItem
+      className="it__quality-profiles__backup"
       download="key.xml"
       to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
     >
-      <span
-        data-test="quality-profiles__backup"
-      >
-        backup_verb
-      </span>
+      backup_verb
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__compare"
       to={
         Object {
           "pathname": "/profiles/compare",
@@ -122,20 +96,91 @@ exports[`renders with no permissions 1`] = `
         }
       }
     >
-      <span
-        data-test="quality-profiles__compare"
-      >
-        compare
-      </span>
+      compare
     </ActionsDropdownItem>
   </ActionsDropdown>
+  <ProfileModalForm
+    btnLabelKey="copy"
+    headerKey="quality_profiles.copy_x_title"
+    loading={false}
+    onClose={[Function]}
+    onSubmit={[Function]}
+    profile={
+      Object {
+        "activeDeprecatedRuleCount": 0,
+        "activeRuleCount": 68,
+        "childrenCount": 0,
+        "depth": 0,
+        "isBuiltIn": false,
+        "isDefault": false,
+        "isInherited": false,
+        "key": "key",
+        "language": "js",
+        "languageName": "JavaScript",
+        "name": "name",
+        "projectCount": 3,
+        "rulesUpdatedAt": "2017-06-28T12:58:44+0000",
+      }
+    }
+  />
+</Fragment>
+`;
+
+exports[`renders correctly: delete modal 1`] = `
+<Fragment>
+  <ActionsDropdown>
+    <ActionsDropdownItem
+      className="it__quality-profiles__backup"
+      download="key.xml"
+      to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
+    >
+      backup_verb
+    </ActionsDropdownItem>
+    <ActionsDropdownItem
+      className="it__quality-profiles__compare"
+      to={
+        Object {
+          "pathname": "/profiles/compare",
+          "query": Object {
+            "language": "js",
+            "name": "name",
+          },
+        }
+      }
+    >
+      compare
+    </ActionsDropdownItem>
+  </ActionsDropdown>
+  <DeleteProfileForm
+    loading={false}
+    onClose={[Function]}
+    onDelete={[Function]}
+    profile={
+      Object {
+        "activeDeprecatedRuleCount": 0,
+        "activeRuleCount": 68,
+        "childrenCount": 0,
+        "depth": 0,
+        "isBuiltIn": false,
+        "isDefault": false,
+        "isInherited": false,
+        "key": "key",
+        "language": "js",
+        "languageName": "JavaScript",
+        "name": "name",
+        "projectCount": 3,
+        "rulesUpdatedAt": "2017-06-28T12:58:44+0000",
+      }
+    }
+  />
 </Fragment>
 `;
 
-exports[`renders with permission to edit only 1`] = `
+exports[`renders correctly: edit only 1`] = `
 <Fragment>
   <ActionsDropdown>
     <ActionsDropdownItem
+      className="it__quality-profiles__activate-more-rules"
       to={
         Object {
           "pathname": "/coding_rules",
@@ -146,23 +191,17 @@ exports[`renders with permission to edit only 1`] = `
         }
       }
     >
-      <span
-        data-test="quality-profiles__activate-more-rules"
-      >
-        quality_profiles.activate_more_rules
-      </span>
+      quality_profiles.activate_more_rules
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__backup"
       download="key.xml"
       to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
     >
-      <span
-        data-test="quality-profiles__backup"
-      >
-        backup_verb
-      </span>
+      backup_verb
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__compare"
       to={
         Object {
           "pathname": "/profiles/compare",
@@ -173,21 +212,146 @@ exports[`renders with permission to edit only 1`] = `
         }
       }
     >
-      <span
-        data-test="quality-profiles__compare"
-      >
-        compare
-      </span>
+      compare
     </ActionsDropdownItem>
     <ActionsDropdownItem
+      className="it__quality-profiles__rename"
       onClick={[Function]}
     >
-      <span
-        data-test="quality-profiles__rename"
-      >
-        rename
-      </span>
+      rename
+    </ActionsDropdownItem>
+  </ActionsDropdown>
+</Fragment>
+`;
+
+exports[`renders correctly: extend modal 1`] = `
+<Fragment>
+  <ActionsDropdown>
+    <ActionsDropdownItem
+      className="it__quality-profiles__backup"
+      download="key.xml"
+      to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
+    >
+      backup_verb
+    </ActionsDropdownItem>
+    <ActionsDropdownItem
+      className="it__quality-profiles__compare"
+      to={
+        Object {
+          "pathname": "/profiles/compare",
+          "query": Object {
+            "language": "js",
+            "name": "name",
+          },
+        }
+      }
+    >
+      compare
     </ActionsDropdownItem>
   </ActionsDropdown>
+  <ProfileModalForm
+    btnLabelKey="extend"
+    headerKey="quality_profiles.extend_x_title"
+    loading={false}
+    onClose={[Function]}
+    onSubmit={[Function]}
+    profile={
+      Object {
+        "activeDeprecatedRuleCount": 0,
+        "activeRuleCount": 68,
+        "childrenCount": 0,
+        "depth": 0,
+        "isBuiltIn": false,
+        "isDefault": false,
+        "isInherited": false,
+        "key": "key",
+        "language": "js",
+        "languageName": "JavaScript",
+        "name": "name",
+        "projectCount": 3,
+        "rulesUpdatedAt": "2017-06-28T12:58:44+0000",
+      }
+    }
+  />
+</Fragment>
+`;
+
+exports[`renders correctly: no permissions 1`] = `
+<Fragment>
+  <ActionsDropdown>
+    <ActionsDropdownItem
+      className="it__quality-profiles__backup"
+      download="key.xml"
+      to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
+    >
+      backup_verb
+    </ActionsDropdownItem>
+    <ActionsDropdownItem
+      className="it__quality-profiles__compare"
+      to={
+        Object {
+          "pathname": "/profiles/compare",
+          "query": Object {
+            "language": "js",
+            "name": "name",
+          },
+        }
+      }
+    >
+      compare
+    </ActionsDropdownItem>
+  </ActionsDropdown>
+</Fragment>
+`;
+
+exports[`renders correctly: rename modal 1`] = `
+<Fragment>
+  <ActionsDropdown>
+    <ActionsDropdownItem
+      className="it__quality-profiles__backup"
+      download="key.xml"
+      to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
+    >
+      backup_verb
+    </ActionsDropdownItem>
+    <ActionsDropdownItem
+      className="it__quality-profiles__compare"
+      to={
+        Object {
+          "pathname": "/profiles/compare",
+          "query": Object {
+            "language": "js",
+            "name": "name",
+          },
+        }
+      }
+    >
+      compare
+    </ActionsDropdownItem>
+  </ActionsDropdown>
+  <ProfileModalForm
+    btnLabelKey="rename"
+    headerKey="quality_profiles.rename_x_title"
+    loading={false}
+    onClose={[Function]}
+    onSubmit={[Function]}
+    profile={
+      Object {
+        "activeDeprecatedRuleCount": 0,
+        "activeRuleCount": 68,
+        "childrenCount": 0,
+        "depth": 0,
+        "isBuiltIn": false,
+        "isDefault": false,
+        "isInherited": false,
+        "key": "key",
+        "language": "js",
+        "languageName": "JavaScript",
+        "name": "name",
+        "projectCount": 3,
+        "rulesUpdatedAt": "2017-06-28T12:58:44+0000",
+      }
+    }
+  />
 </Fragment>
 `;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..96d5721
--- /dev/null
@@ -0,0 +1,190 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: can submit 1`] = `
+<Modal
+  contentLabel="header-label.name.JavaScript"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        header-label.name.JavaScript
+      </h2>
+    </div>
+    <div
+      className="modal-body"
+    >
+      <MandatoryFieldsExplanation
+        className="modal-field"
+      />
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="profile-name"
+        >
+          quality_profiles.new_name
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          autoFocus={true}
+          id="profile-name"
+          maxLength={100}
+          name="name"
+          onChange={[Function]}
+          required={true}
+          size={50}
+          type="text"
+          value="new name"
+        />
+      </div>
+    </div>
+    <div
+      className="modal-foot"
+    >
+      <SubmitButton
+        disabled={false}
+      >
+        btn-label
+      </SubmitButton>
+      <ResetButtonLink
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </div>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: default 1`] = `
+<Modal
+  contentLabel="header-label.name.JavaScript"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        header-label.name.JavaScript
+      </h2>
+    </div>
+    <div
+      className="modal-body"
+    >
+      <MandatoryFieldsExplanation
+        className="modal-field"
+      />
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="profile-name"
+        >
+          quality_profiles.new_name
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          autoFocus={true}
+          id="profile-name"
+          maxLength={100}
+          name="name"
+          onChange={[Function]}
+          required={true}
+          size={50}
+          type="text"
+          value="name"
+        />
+      </div>
+    </div>
+    <div
+      className="modal-foot"
+    >
+      <SubmitButton
+        disabled={true}
+      >
+        btn-label
+      </SubmitButton>
+      <ResetButtonLink
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </div>
+  </form>
+</Modal>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<Modal
+  contentLabel="header-label.name.JavaScript"
+  onRequestClose={[MockFunction]}
+  size="small"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="modal-head"
+    >
+      <h2>
+        header-label.name.JavaScript
+      </h2>
+    </div>
+    <div
+      className="modal-body"
+    >
+      <MandatoryFieldsExplanation
+        className="modal-field"
+      />
+      <div
+        className="modal-field"
+      >
+        <label
+          htmlFor="profile-name"
+        >
+          quality_profiles.new_name
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          autoFocus={true}
+          id="profile-name"
+          maxLength={100}
+          name="name"
+          onChange={[Function]}
+          required={true}
+          size={50}
+          type="text"
+          value="name"
+        />
+      </div>
+    </div>
+    <div
+      className="modal-foot"
+    >
+      <i
+        className="spinner spacer-right"
+      />
+      <SubmitButton
+        disabled={true}
+      >
+        btn-label
+      </SubmitButton>
+      <ResetButtonLink
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </div>
+  </form>
+</Modal>
+`;
index 834d936cd607929965735a9192c482184bfb9cfe..096152f33725e0a72cda33d2de11cee7773bbf39 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import * as classNames from 'classnames';
 import * as React from 'react';
 import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
@@ -46,7 +47,7 @@ export default function ProfileInheritanceBox(props: Props) {
   const offset = 25 * depth;
 
   return (
-    <tr className={className} data-test={`quality-profiles__inheritance-${type}`}>
+    <tr className={classNames(`it__quality-profiles__inheritance-${type}`, className)}>
       <td>
         <div style={{ paddingLeft: offset }}>
           {displayLink ? (
index c42184e6a857e28968da0573670e1025592b73ba..18416a4f9e34528c2c3e39c205c0c776e38330e9 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`should render correctly 1`] = `
 <tr
-  data-test="quality-profiles__inheritance-current"
+  className="it__quality-profiles__inheritance-current"
 >
   <td>
     <div
@@ -34,7 +34,7 @@ exports[`should render correctly 1`] = `
 
 exports[`should render correctly 2`] = `
 <tr
-  data-test="quality-profiles__inheritance-current"
+  className="it__quality-profiles__inheritance-current"
 >
   <td>
     <div
@@ -69,7 +69,7 @@ exports[`should render correctly 2`] = `
 
 exports[`should render correctly 3`] = `
 <tr
-  data-test="quality-profiles__inheritance-current"
+  className="it__quality-profiles__inheritance-current"
 >
   <td>
     <div
@@ -105,7 +105,7 @@ exports[`should render correctly 3`] = `
 
 exports[`should render correctly 4`] = `
 <tr
-  data-test="quality-profiles__inheritance-current"
+  className="it__quality-profiles__inheritance-current"
 >
   <td>
     <div
index 61e9e4ae71ce4b600e9e844d9846fc2d108520aa..03c64fcac44b6750baaa8c64ae998dac45f620ee 100644 (file)
@@ -38,3 +38,10 @@ export interface ProfileChangelogEvent {
   ruleKey: string;
   ruleName: string;
 }
+
+export enum ProfileActionModals {
+  Copy,
+  Extend,
+  Rename,
+  Delete
+}
index b97f6a7e60dc66f31c73e41ce61e71ba9d883d34..04d2de164c0834c732e6ac8dbf89732ad54172d5 100644 (file)
@@ -1506,7 +1506,6 @@ quality_profiles.x_rules_only_in={0} rules only in
 quality_profiles.x_rules_have_different_configuration={0} rules have a different configuration
 quality_profiles.copy_x_title=Copy Profile "{0}" - {1}
 quality_profiles.extend_x_title=Extend Profile "{0}" - {1}
-quality_profiles.copy_new_name=New name
 quality_profiles.rename_x_title=Rename Profile {0} - {1}
 quality_profiles.deprecated=deprecated
 quality_profiles.severity_set_to=Severity set to