]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20366 Migrate quality profile create modal to MIUI
author7PH <benjamin.raymond@sonarsource.com>
Tue, 3 Oct 2023 11:23:28 +0000 (13:23 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 6 Oct 2023 20:02:52 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/input/FileInput.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/input/__tests__/FileInput-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/input/index.ts
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/src/components/input/FileInput.tsx b/server/sonar-web/design-system/src/components/input/FileInput.tsx
new file mode 100644 (file)
index 0000000..533d10e
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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 classNames from 'classnames';
+import { useCallback, useRef, useState } from 'react';
+import { Note } from '../Text';
+import { ButtonSecondary } from '../buttons/ButtonSecondary';
+
+interface Props {
+  chooseLabel: string;
+  className?: string;
+  clearLabel: string;
+  id?: string;
+  name?: string;
+  noFileLabel: string;
+  onFileSelected?: (file?: File) => void;
+  required?: boolean;
+}
+
+export function FileInput(props: Readonly<Props>) {
+  const { className, id, name, onFileSelected, required } = props;
+  const { chooseLabel, clearLabel, noFileLabel } = props;
+
+  const [selectedFileName, setSelectedFileName] = useState<string | undefined>(undefined);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const handleFileInputChange = useCallback(
+    (event: React.ChangeEvent<HTMLInputElement>) => {
+      const file = event.target.files?.[0];
+      onFileSelected?.(file);
+      setSelectedFileName(file?.name);
+    },
+    [onFileSelected],
+  );
+
+  const handleFileInputReset = useCallback(() => {
+    if (fileInputRef.current) {
+      onFileSelected?.(undefined);
+      fileInputRef.current.value = '';
+      setSelectedFileName(undefined);
+    }
+  }, [fileInputRef, onFileSelected]);
+
+  const handleFileInputClick = useCallback(() => {
+    fileInputRef.current?.click();
+  }, [fileInputRef]);
+
+  return (
+    <div className={classNames('sw-flex sw-items-center sw-gap-2', className)}>
+      {selectedFileName ? (
+        <>
+          <ButtonSecondary onClick={handleFileInputReset}>{clearLabel}</ButtonSecondary>
+          <Note>{selectedFileName}</Note>
+        </>
+      ) : (
+        <>
+          <ButtonSecondary onClick={handleFileInputClick}>{chooseLabel}</ButtonSecondary>
+          <Note>{noFileLabel}</Note>
+        </>
+      )}
+      <input
+        data-testid="file-input"
+        hidden
+        id={id}
+        name={name}
+        onChange={handleFileInputChange}
+        ref={fileInputRef}
+        required={required}
+        type="file"
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/input/__tests__/FileInput-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/FileInput-test.tsx
new file mode 100644 (file)
index 0000000..fcfb6d7
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 { fireEvent, screen } from '@testing-library/react';
+import { render } from '../../../helpers/testUtils';
+import { FCProps } from '../../../types/misc';
+import { FileInput } from '../FileInput';
+
+it('should correclty choose a file and reset it', async () => {
+  const file = new File([''], 'file.txt', { type: 'text/plain' });
+  const onFileSelected = jest.fn();
+  const { user } = setupWithProps({ onFileSelected });
+
+  expect(screen.getByRole('button')).toHaveTextContent('Choose');
+  expect(screen.getByText('No file selected')).toBeVisible();
+
+  await user.click(screen.getByRole('button'));
+  fireEvent.change(screen.getByTestId('file-input'), {
+    target: { files: [file] },
+  });
+  expect(onFileSelected).toHaveBeenCalledWith(file);
+  expect(screen.getByText('file.txt')).toBeVisible();
+  expect(screen.getByRole('button')).toHaveTextContent('Clear');
+
+  await user.click(screen.getByRole('button'));
+  expect(screen.getByText('No file selected')).toBeVisible();
+  expect(onFileSelected).toHaveBeenCalledWith(undefined);
+});
+
+function setupWithProps(props: Partial<FCProps<typeof FileInput>> = {}) {
+  return render(
+    <FileInput chooseLabel="Choose" clearLabel="Clear" noFileLabel="No file selected" {...props} />,
+  );
+}
index ad389e9f27f7a015609306a1674836d00e157556..ca90354aa44ce6a15fd70ee60193c5e14e34dfde 100644 (file)
@@ -21,6 +21,7 @@ export * from './Checkbox';
 export * from './DatePicker';
 export * from './DateRangePicker';
 export * from './DiscreetSelect';
+export * from './FileInput';
 export * from './FormField';
 export * from './InputField';
 export * from './InputMultiSelect';
index 712447b19c87020b39549e6454b39bf37460a65a..8283e354334723472abdc289671846c63cd6d226 100644 (file)
@@ -305,7 +305,18 @@ export default class QualityProfilesServiceMock {
   }
 
   handleGetImporters = () => {
-    return this.reply([]);
+    return this.reply([
+      {
+        key: 'sonar-importer-a',
+        name: 'Importer A',
+        languages: ['c'],
+      },
+      {
+        key: 'sonar-importer-b',
+        name: 'Importer B',
+        languages: ['c'],
+      },
+    ]);
   };
 
   handleCopyProfile = (fromKey: string, name: string): Promise<Profile> => {
index ebef45448cb1cac08c3c09241f1b45acda8d6765..4533c4883f34ee8921b48070cb15bdd0c2e94421 100644 (file)
@@ -23,7 +23,7 @@ import selectEvent from 'react-select-event';
 import QualityProfilesServiceMock from '../../../api/mocks/QualityProfilesServiceMock';
 import { mockPaging, mockRule } from '../../../helpers/testMocks';
 import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
-import { byRole } from '../../../helpers/testSelector';
+import { byRole, byText } from '../../../helpers/testSelector';
 import routes from '../routes';
 
 jest.mock('../../../api/quality-profiles');
@@ -93,14 +93,16 @@ const ui = {
   }),
   listLinkJavaQualityProfile: byRole('link', { name: 'java quality profile' }),
   returnToList: byRole('link', { name: 'quality_profiles.page' }),
-  languageSelect: byRole('combobox', { name: 'language field_required' }),
+  languageSelect: byRole('combobox', { name: 'language' }),
   profileExtendSelect: byRole('combobox', {
-    name: 'quality_profiles.creation.choose_parent_quality_profile field_required',
+    name: 'quality_profiles.creation.choose_parent_quality_profile',
   }),
   profileCopySelect: byRole('combobox', {
-    name: 'quality_profiles.creation.choose_copy_quality_profile field_required',
+    name: 'quality_profiles.creation.choose_copy_quality_profile',
   }),
-  nameCreatePopupInput: byRole('textbox', { name: 'name field_required' }),
+  nameCreatePopupInput: byRole('textbox', { name: 'name required' }),
+  importerA: byText('Importer A'),
+  importerB: byText('Importer B'),
   comparisonDiffTableHeading: (rulesQuantity: number, profileName: string) =>
     byRole('columnheader', {
       name: `quality_profiles.x_rules_only_in.${rulesQuantity}.${profileName}`,
@@ -253,6 +255,29 @@ describe('Create', () => {
 
     expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument();
   });
+
+  it('should render importers', async () => {
+    const user = userEvent.setup();
+    serviceMock.setAdmin();
+    renderQualityProfiles();
+
+    await act(async () => {
+      await user.click(await ui.createButton.find());
+    });
+    await user.click(ui.blankRadio.get());
+    await selectEvent.select(ui.languageSelect.get(), 'C');
+
+    expect(ui.importerA.get()).toBeInTheDocument();
+    expect(ui.importerB.get()).toBeInTheDocument();
+
+    await user.click(ui.copyRadio.get());
+    expect(ui.importerA.query()).not.toBeInTheDocument();
+    expect(ui.importerB.query()).not.toBeInTheDocument();
+
+    await user.click(ui.extendRadio.get());
+    expect(ui.importerA.query()).not.toBeInTheDocument();
+    expect(ui.importerB.query()).not.toBeInTheDocument();
+  });
 });
 
 it('should be able to restore a quality profile', async () => {
index c9662824a2c516156110661bd90744cd5293e835..9309079a24901c7aa898ff891654c06ecb1c7e74 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 { FlagMessage } from 'design-system';
+import {
+  ButtonPrimary,
+  FileInput,
+  FlagMessage,
+  FormField,
+  InputField,
+  LabelValueSelectOption,
+  LightLabel,
+  Modal,
+  Note,
+  PopupZLevel,
+  SearchSelectDropdown,
+  SelectionCard,
+  Spinner,
+} from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
+import { useRef } from 'react';
+import { useIntl } from 'react-intl';
+import { SingleValue } from 'react-select';
 import {
   changeProfileParent,
   copyProfile,
   createQualityProfile,
   getImporters,
 } from '../../../api/quality-profiles';
-import Modal from '../../../components/controls/Modal';
-import RadioCard from '../../../components/controls/RadioCard';
-import Select from '../../../components/controls/Select';
-import ValidationInput from '../../../components/controls/ValidationInput';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { Location } from '../../../components/hoc/withRouter';
-import CopyQualityProfileIcon from '../../../components/icons/CopyQualityProfileIcon';
-import ExtendQualityProfileIcon from '../../../components/icons/ExtendQualityProfileIcon';
-import NewQualityProfileIcon from '../../../components/icons/NewQualityProfileIcon';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import Spinner from '../../../components/ui/Spinner';
-import { translate } from '../../../helpers/l10n';
 import { parseAsOptionalString } from '../../../helpers/query';
 import { useProfileInheritanceQuery } from '../../../queries/quality-profiles';
 import { Profile, ProfileActionModals } from '../types';
@@ -52,6 +59,9 @@ interface Props {
 
 export default function CreateProfileForm(props: Readonly<Props>) {
   const { languages, profiles, onCreate } = props;
+
+  const intl = useIntl();
+
   const [importers, setImporters] = React.useState<
     Array<{ key: string; languages: string[]; name: string }>
   >([]);
@@ -67,6 +77,8 @@ export default function CreateProfileForm(props: Readonly<Props>) {
   const [isValidProfile, setIsValidProfile] = React.useState<boolean>();
   const [profile, setProfile] = React.useState<Profile>();
 
+  const backupForm = useRef<HTMLFormElement>(null);
+
   const fetchImporters = React.useCallback(async () => {
     setLoading(true);
     try {
@@ -98,8 +110,8 @@ export default function CreateProfileForm(props: Readonly<Props>) {
   );
 
   const handleLanguageChange = React.useCallback(
-    (option: { value: string }) => {
-      setLanguage(option.value);
+    (option: SingleValue<LabelValueSelectOption<string>>) => {
+      setLanguage(option?.value);
       setIsValidLanguage(true);
       setProfile(undefined);
       setIsValidProfile(false);
@@ -108,43 +120,40 @@ export default function CreateProfileForm(props: Readonly<Props>) {
   );
 
   const handleQualityProfileChange = React.useCallback(
-    (option: { value: Profile } | null) => {
+    (option: SingleValue<LabelValueSelectOption<Profile>>) => {
       setProfile(option?.value);
-      setIsValidProfile(option !== null);
+      setIsValidProfile(Boolean(option?.value));
     },
     [setProfile, setIsValidProfile],
   );
 
-  const handleFormSubmit = React.useCallback(
-    async (event: React.SyntheticEvent<HTMLFormElement>) => {
-      event.preventDefault();
-
-      setSubmitting(true);
-      const profileKey = profile?.key;
-      try {
-        if (action === ProfileActionModals.Copy && profileKey && name) {
-          const profile = await copyProfile(profileKey, name);
-          onCreate(profile);
-        } else if (action === ProfileActionModals.Extend) {
-          const { profile } = await createQualityProfile({ language, name });
-
-          const parentProfile = profiles.find((p) => p.key === profileKey);
-          if (parentProfile) {
-            await changeProfileParent(profile, parentProfile);
-          }
+  const handleFormSubmit = React.useCallback(async () => {
+    setSubmitting(true);
+    const profileKey = profile?.key;
+    try {
+      if (action === ProfileActionModals.Copy && profileKey && name) {
+        const profile = await copyProfile(profileKey, name);
+        onCreate(profile);
+      } else if (action === ProfileActionModals.Extend) {
+        const { profile } = await createQualityProfile({ language, name });
 
-          onCreate(profile);
-        } else {
-          const data = new FormData(event.currentTarget);
-          const { profile } = await createQualityProfile(data);
-          onCreate(profile);
+        const parentProfile = profiles.find((p) => p.key === profileKey);
+        if (parentProfile) {
+          await changeProfileParent(profile, parentProfile);
         }
-      } finally {
-        setSubmitting(false);
+
+        onCreate(profile);
+      } else {
+        const formData = new FormData(backupForm?.current ? backupForm.current : undefined);
+        formData.set('language', language ?? '');
+        formData.set('name', name);
+        const { profile } = await createQualityProfile(formData);
+        onCreate(profile);
       }
-    },
-    [setSubmitting, onCreate, profiles, action, language, name, profile],
-  );
+    } finally {
+      setSubmitting(false);
+    }
+  }, [setSubmitting, onCreate, profiles, action, language, name, profile]);
 
   React.useEffect(() => {
     fetchImporters();
@@ -175,7 +184,7 @@ export default function CreateProfileForm(props: Readonly<Props>) {
   const canSubmit =
     (action === undefined && isValidName && isValidLanguage) ||
     (action !== undefined && isValidLanguage && isValidName && isValidProfile);
-  const header = translate('quality_profiles.new_profile');
+  const header = intl.formatMessage({ id: 'quality_profiles.new_profile' });
 
   const languageQueryFilter = parseAsOptionalString(props.location.query.language);
   const selectedLanguage = language ?? languageQueryFilter;
@@ -186,7 +195,7 @@ export default function CreateProfileForm(props: Readonly<Props>) {
   const profilesForSelectedLanguage = profiles.filter((p) => p.language === selectedLanguage);
   const profileOptions = sortBy(profilesForSelectedLanguage, 'name').map((profile) => ({
     label: profile.isBuiltIn
-      ? `${profile.name} (${translate('quality_profiles.built_in')})`
+      ? `${profile.name} (${intl.formatMessage({ id: 'quality_profiles.built_in' })})`
       : profile.name,
     value: profile,
   }));
@@ -196,179 +205,162 @@ export default function CreateProfileForm(props: Readonly<Props>) {
     value: l.key,
   }));
 
-  return (
-    <Modal contentLabel={header} onRequestClose={props.onClose} size="medium">
-      <form id="create-profile-form" onSubmit={handleFormSubmit}>
-        <div className="modal-head">
-          <h2>{header}</h2>
-        </div>
+  function handleSearch<T>(
+    options: { label: string; value: T }[],
+    query: string,
+    cb: (options: LabelValueSelectOption<T>[]) => void,
+  ) {
+    cb(options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())));
+  }
 
-        {loading ? (
-          <div className="modal-body">
-            <Spinner />
+  return (
+    <Modal
+      headerTitle={header}
+      onClose={props.onClose}
+      primaryButton={
+        !loading && (
+          <ButtonPrimary
+            onClick={handleFormSubmit}
+            disabled={submitting || !canSubmit}
+            type="submit"
+          >
+            {intl.formatMessage({ id: 'create' })}
+          </ButtonPrimary>
+        )
+      }
+      secondaryButtonLabel={intl.formatMessage({ id: 'cancel' })}
+      body={
+        <>
+          <LightLabel>
+            {intl.formatMessage({ id: 'quality_profiles.chose_creation_type' })}
+          </LightLabel>
+          <div className="sw-mt-4 sw-flex sw-flex-col sw-gap-2">
+            <SelectionCard
+              selected={action === ProfileActionModals.Extend}
+              onClick={handleSelectExtend}
+              title={intl.formatMessage({ id: 'quality_profiles.creation_from_extend' })}
+            >
+              <p className="spacer-bottom">
+                {intl.formatMessage({ id: 'quality_profiles.creation_from_extend_description_1' })}
+              </p>
+              <p>
+                {intl.formatMessage({ id: 'quality_profiles.creation_from_extend_description_2' })}
+              </p>
+            </SelectionCard>
+            <SelectionCard
+              selected={action === ProfileActionModals.Copy}
+              onClick={handleSelectCopy}
+              title={intl.formatMessage({ id: 'quality_profiles.creation_from_copy' })}
+            >
+              <p className="spacer-bottom">
+                {intl.formatMessage({ id: 'quality_profiles.creation_from_copy_description_1' })}
+              </p>
+              <p>
+                {intl.formatMessage({ id: 'quality_profiles.creation_from_copy_description_2' })}
+              </p>
+            </SelectionCard>
+            <SelectionCard
+              selected={action === undefined}
+              onClick={handleSelectBlank}
+              title={intl.formatMessage({ id: 'quality_profiles.creation_from_blank' })}
+            >
+              {intl.formatMessage({ id: 'quality_profiles.creation_from_blank_description' })}
+            </SelectionCard>
           </div>
-        ) : (
-          <div className="modal-body modal-container">
-            <fieldset className="modal-field big-spacer-bottom">
-              <label className="spacer-top">
-                {translate('quality_profiles.chose_creation_type')}
-              </label>
-              <div className="display-flex-row spacer-top">
-                <RadioCard
-                  noRadio
-                  selected={action === ProfileActionModals.Extend}
-                  onClick={handleSelectExtend}
-                  title={<ExtendQualityProfileIcon size={64} />}
-                >
-                  <h3 className="spacer-bottom h4">
-                    {translate('quality_profiles.creation_from_extend')}
-                  </h3>
-                  <p className="spacer-bottom">
-                    {translate('quality_profiles.creation_from_extend_description_1')}
-                  </p>
-                  <p>{translate('quality_profiles.creation_from_extend_description_2')}</p>
-                </RadioCard>
-                <RadioCard
-                  noRadio
-                  selected={action === ProfileActionModals.Copy}
-                  onClick={handleSelectCopy}
-                  title={<CopyQualityProfileIcon size={64} />}
-                >
-                  <h3 className="spacer-bottom h4">
-                    {translate('quality_profiles.creation_from_copy')}
-                  </h3>
-                  <p className="spacer-bottom">
-                    {translate('quality_profiles.creation_from_copy_description_1')}
-                  </p>
-                  <p>{translate('quality_profiles.creation_from_copy_description_2')}</p>
-                </RadioCard>
-                <RadioCard
-                  noRadio
-                  onClick={handleSelectBlank}
-                  selected={action === undefined}
-                  title={<NewQualityProfileIcon size={64} />}
-                >
-                  <h3 className="spacer-bottom h4">
-                    {translate('quality_profiles.creation_from_blank')}
-                  </h3>
-                  <p>{translate('quality_profiles.creation_from_blank_description')}</p>
-                </RadioCard>
+          {!isLoading && showBuiltInWarning && (
+            <FlagMessage variant="info" className="sw-block sw-my-4">
+              <div className="sw-flex sw-flex-col">
+                {intl.formatMessage({
+                  id: 'quality_profiles.no_built_in_updates_warning.new_profile',
+                })}
+                <span className="sw-mt-1">
+                  {intl.formatMessage({
+                    id: 'quality_profiles.no_built_in_updates_warning.new_profile.2',
+                  })}
+                </span>
               </div>
-            </fieldset>
-
-            {!isLoading && showBuiltInWarning && (
-              <FlagMessage variant="info" className="sw-mb-4">
-                <div className="sw-flex sw-flex-col">
-                  {translate('quality_profiles.no_built_in_updates_warning.new_profile')}
-                  <span className="sw-mt-1">
-                    {translate('quality_profiles.no_built_in_updates_warning.new_profile.2')}
-                  </span>
-                </div>
-              </FlagMessage>
-            )}
-
-            <MandatoryFieldsExplanation className="modal-field" />
-
-            <ValidationInput
-              className="form-field"
-              labelHtmlFor="create-profile-language-input"
-              label={translate('language')}
-              required
-              isInvalid={isValidLanguage !== undefined && !isValidLanguage}
-              isValid={!!isValidLanguage}
-            >
-              <Select
+            </FlagMessage>
+          )}
+          <div className="sw-my-4">
+            <MandatoryFieldsExplanation />
+          </div>
+          <FormField label={intl.formatMessage({ id: 'language' })} required>
+            <SearchSelectDropdown
+              controlAriaLabel={intl.formatMessage({ id: 'language' })}
+              autoFocus
+              inputId="create-profile-language-input"
+              name="language"
+              onChange={handleLanguageChange}
+              defaultOptions={languagesOptions}
+              loadOptions={(inputValue, cb) => handleSearch(languagesOptions, inputValue, cb)}
+              value={languagesOptions.find((o) => o.value === selectedLanguage)}
+              zLevel={PopupZLevel.Global}
+            />
+          </FormField>
+          {action !== undefined && (
+            <FormField label={intl.formatMessage({ id: 'quality_profiles.parent' })} required>
+              <SearchSelectDropdown
+                controlAriaLabel={intl.formatMessage({
+                  id:
+                    action === ProfileActionModals.Copy
+                      ? 'quality_profiles.creation.choose_copy_quality_profile'
+                      : 'quality_profiles.creation.choose_parent_quality_profile',
+                })}
                 autoFocus
-                inputId="create-profile-language-input"
-                name="language"
-                isClearable={false}
-                onChange={handleLanguageChange}
-                options={languagesOptions}
+                inputId="create-profile-parent-input"
+                name="parentKey"
+                onChange={handleQualityProfileChange}
+                defaultOptions={profileOptions}
+                loadOptions={(inputValue, cb) => handleSearch(profileOptions, inputValue, cb)}
                 isSearchable
-                value={languagesOptions.filter((o) => o.value === selectedLanguage)}
+                value={profileOptions.find((o) => o.value === profile)}
               />
-            </ValidationInput>
-            {action !== undefined && (
-              <ValidationInput
-                className="form-field"
-                labelHtmlFor="create-profile-parent-input"
-                label={translate(
-                  action === ProfileActionModals.Copy
-                    ? 'quality_profiles.creation.choose_copy_quality_profile'
-                    : 'quality_profiles.creation.choose_parent_quality_profile',
-                )}
-                required
-                isInvalid={isValidProfile !== undefined && !isValidProfile}
-                isValid={!!isValidProfile}
-              >
-                <Select
-                  autoFocus
-                  inputId="create-profile-parent-input"
-                  name="parentKey"
-                  isClearable={false}
-                  onChange={handleQualityProfileChange}
-                  options={profileOptions}
-                  isSearchable
-                  value={profileOptions.filter((o) => o.value === profile)}
-                />
-              </ValidationInput>
-            )}
-            <ValidationInput
-              className="form-field"
-              labelHtmlFor="create-profile-name"
-              label={translate('name')}
-              error={translate('quality_profiles.name_invalid')}
+            </FormField>
+          )}
+          <FormField
+            htmlFor="create-profile-name"
+            label={intl.formatMessage({ id: 'name' })}
+            required
+          >
+            <InputField
+              autoFocus
+              id="create-profile-name"
+              maxLength={50}
+              name="name"
+              onChange={handleNameChange}
               required
-              isInvalid={isValidName !== undefined && !isValidName}
-              isValid={!!isValidName}
-            >
-              <input
-                autoFocus
-                id="create-profile-name"
-                maxLength={100}
-                name="name"
-                onChange={handleNameChange}
-                size={50}
-                type="text"
-                value={name}
-              />
-            </ValidationInput>
+              size="full"
+              type="text"
+              value={name}
+            />
+          </FormField>
 
-            {action === undefined &&
-              filteredImporters.map((importer) => (
-                <div
-                  className="modal-field spacer-bottom js-importer"
-                  data-key={importer.key}
+          {action === undefined && (
+            <form ref={backupForm}>
+              {filteredImporters.map((importer) => (
+                <FormField
                   key={importer.key}
+                  htmlFor={'create-profile-form-backup-' + importer.key}
+                  label={importer.name}
                 >
-                  <label htmlFor={'create-profile-form-backup-' + importer.key}>
-                    {importer.name}
-                  </label>
-                  <input
-                    id={'create-profile-form-backup-' + importer.key}
-                    name={'backup_' + importer.key}
-                    type="file"
+                  <FileInput
+                    id={`create-profile-form-backup-${importer.key}`}
+                    name={`backup_${importer.key}`}
+                    chooseLabel={intl.formatMessage({ id: 'choose_file' })}
+                    clearLabel={intl.formatMessage({ id: 'clear_file' })}
+                    noFileLabel={intl.formatMessage({ id: 'no_file_selected' })}
                   />
-                  <p className="note">
-                    {translate('quality_profiles.optional_configuration_file')}
-                  </p>
-                </div>
-              ))}
-          </div>
-        )}
-
-        <div className="modal-foot">
-          {(submitting || isLoading) && <i className="spinner spacer-right" />}
-          {!loading && (
-            <SubmitButton disabled={submitting || !canSubmit} id="create-profile-submit">
-              {translate('create')}
-            </SubmitButton>
+                  <Note>
+                    {intl.formatMessage({ id: 'quality_profiles.optional_configuration_file' })}
+                  </Note>
+                </FormField>
+              ))}{' '}
+            </form>
           )}
-          <ResetButtonLink id="create-profile-cancel" onClick={props.onClose}>
-            {translate('cancel')}
-          </ResetButtonLink>
-        </div>
-      </form>
-    </Modal>
+
+          <Spinner loading={submitting || isLoading} />
+        </>
+      }
+    />
   );
 }
index a83a184f419acc06080e08c83d9f1eaad6388b38..4941aab5cd117a88932ed8daa9303bec8227d2dd 100644 (file)
@@ -40,8 +40,10 @@ see_changelog=See Changelog
 changelog=Changelog
 change_verb=Change
 check_all=Check all
+choose_file=Choose file
 class=Class
 classes=Classes
+clear_file=Clear file
 close=Close
 closed=Closed
 code=Code
@@ -146,6 +148,7 @@ next=Next
 new_name=New name
 next_=next
 none=None
+no_file_selected=No file selected
 no_tags=No tags
 not_now=Not now
 or=Or