]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21023 Review field input and validation in the local project creation
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 3 Jan 2024 10:28:34 +0000 (11:28 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 16 Jan 2024 20:02:43 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/project-management.ts
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 279d78a19b851cdf0e28c2b35becafb17f411edf..f684e4b3750b16dc4eb5a0db9f1876eb81d99fc9 100644 (file)
@@ -83,20 +83,6 @@ export function createProject(data: {
   return postJSON('/api/projects/create', data).catch(throwGlobalError);
 }
 
-export function setupManualProjectCreation(data: {
-  name: string;
-  project: string;
-  mainBranch: string;
-  visibility?: Visibility;
-}) {
-  return (newCodeDefinitionType?: string, newCodeDefinitionValue?: string) =>
-    createProject({
-      ...data,
-      newCodeDefinitionType,
-      newCodeDefinitionValue,
-    });
-}
-
 export function changeProjectDefaultVisibility(
   projectVisibility: Visibility,
 ): Promise<void | Response> {
index 515ee92366fce84348e53a392e14647f1d31cbcc..973e39baa86a6071f0f600cb2a0b78b689da4b96 100644 (file)
@@ -58,6 +58,7 @@ interface State {
   loading: boolean;
   creatingAlmDefinition?: AlmKeys;
   importProjects?: ImportProjectParam;
+  redirectTo: string;
 }
 
 const PROJECT_MODE_FOR_ALM_KEY = {
@@ -125,6 +126,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
     githubSettings: [],
     gitlabSettings: [],
     loading: true,
+    redirectTo: this.props.location.state?.from || '/projects',
   };
 
   componentDidMount() {
@@ -228,6 +230,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
       githubSettings,
       gitlabSettings,
       loading,
+      redirectTo,
     } = this.state;
     const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport);
 
@@ -297,6 +300,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
           <ManualProjectCreate
             branchesEnabled={branchSupportEnabled}
             onProjectSetupDone={this.handleProjectSetupDone}
+            onClose={() => this.props.router.push({ pathname: redirectTo })}
           />
         );
       }
@@ -321,7 +325,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
 
   render() {
     const { location } = this.props;
-    const { creatingAlmDefinition, importProjects } = this.state;
+    const { creatingAlmDefinition, importProjects, redirectTo } = this.state;
     const mode: CreateProjectModes | undefined = location.query?.mode;
     const isProjectSetupDone = location.query?.setncd === 'true';
     const gridLayoutStyle = mode ? 'sw-col-start-2 sw-col-span-10' : 'sw-col-span-12';
@@ -342,7 +346,11 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
             {this.renderProjectCreation(mode)}
           </div>
           {importProjects !== undefined && isProjectSetupDone && (
-            <NewCodeDefinitionSelection importProjects={importProjects} />
+            <NewCodeDefinitionSelection
+              importProjects={importProjects}
+              onClose={() => this.props.router.push({ pathname: redirectTo })}
+              redirectTo={redirectTo}
+            />
           )}
 
           {creatingAlmDefinition && (
index aedc1bd3ebcdefd2f2805434150d94828ebbf4be..bb6f76a3e91828b28e3750b1d312fbdfe467fce7 100644 (file)
  */
 import userEvent from '@testing-library/user-event';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
-import * as React from 'react';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
 import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { ProjectsServiceMock } from '../../../../api/mocks/ProjectsServiceMock';
 import { getNewCodeDefinition } from '../../../../api/newCodeDefinition';
 import { mockProject } from '../../../../helpers/mocks/projects';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { mockAppState, mockCurrentUser } from '../../../../helpers/testMocks';
+import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
 import { byRole, byText } from '../../../../helpers/testSelector';
 import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import { Permissions } from '../../../../types/permissions';
+import routes from '../../../projects/routes';
 
+jest.mock('../../../../api/measures');
+jest.mock('../../../../api/favorites');
 jest.mock('../../../../api/alm-settings');
 jest.mock('../../../../api/newCodeDefinition');
 jest.mock('../../../../api/project-management', () => ({
@@ -36,6 +40,8 @@ jest.mock('../../../../api/project-management', () => ({
 }));
 jest.mock('../../../../api/components', () => ({
   ...jest.requireActual('../../../../api/components'),
+  searchProjects: jest.fn(),
+  getScannableProjects: jest.fn(),
   doesComponentExists: jest
     .fn()
     .mockImplementation(({ component }) => Promise.resolve(component === 'exists')),
@@ -51,11 +57,18 @@ const ui = {
     name: /onboarding.create_project.display_name/,
   }),
   projectNextButton: byRole('button', { name: 'next' }),
+  newCodeDefinitionSection: byRole('region', {
+    name: 'onboarding.create_project.new_code_definition.title',
+  }),
   newCodeDefinitionHeader: byText('onboarding.create_x_project.new_code_definition.title1'),
   inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
   projectCreateButton: byRole('button', {
     name: 'onboarding.create_project.new_code_definition.create_x_projects1',
   }),
+  cancelButton: byRole('button', { name: 'cancel' }),
+  closeButton: byRole('button', { name: 'clear' }),
+  createProjectsButton: byRole('button', { name: 'projects.add' }),
+  createLocalProject: byRole('menuitem', { name: 'my_account.add_project.manual' }),
   overrideNcdRadio: byRole('radio', { name: 'new_code_definition.specific_setting' }),
   ncdOptionPreviousVersionRadio: byRole('radio', {
     name: /new_code_definition.previous_version/,
@@ -71,6 +84,7 @@ const ui = {
   }),
   ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'),
   projectDashboardText: byText('/dashboard?id=foo'),
+  projectsPageTitle: byRole('heading', { name: 'projects.page' }),
 };
 
 async function fillFormAndNext(displayName: string, user: UserEvent) {
@@ -85,6 +99,7 @@ async function fillFormAndNext(displayName: string, user: UserEvent) {
 
 let almSettingsHandler: AlmSettingsServiceMock;
 let newCodePeriodHandler: NewCodeDefinitionServiceMock;
+let projectHandler: ProjectsServiceMock;
 
 const original = window.location;
 
@@ -95,12 +110,14 @@ beforeAll(() => {
   });
   almSettingsHandler = new AlmSettingsServiceMock();
   newCodePeriodHandler = new NewCodeDefinitionServiceMock();
+  projectHandler = new ProjectsServiceMock();
 });
 
 beforeEach(() => {
   jest.clearAllMocks();
   almSettingsHandler.reset();
   newCodePeriodHandler.reset();
+  projectHandler.reset();
 });
 
 afterAll(() => {
@@ -175,8 +192,48 @@ it('the project onboarding page should be displayed when the project is created'
   expect(await ui.projectDashboardText.find()).toBeInTheDocument();
 });
 
-function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
-  renderApp('project/create', <CreateProjectPage {...props} />, {
-    navigateTo: 'project/create?mode=manual',
+it('validate the provate key field', async () => {
+  const user = userEvent.setup();
+  renderCreateProject();
+  expect(ui.manualProjectHeader.get()).toBeInTheDocument();
+
+  await user.click(ui.displayNameField.get());
+  await user.keyboard('exists');
+
+  expect(ui.projectNextButton.get()).toBeDisabled();
+  await user.click(ui.projectNextButton.get());
+});
+
+it('should navigate back to the Projects page when clicking cancel or close', async () => {
+  newCodePeriodHandler.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays });
+  const user = userEvent.setup();
+  renderCreateProject();
+
+  await user.click(ui.cancelButton.get());
+  expect(await ui.projectsPageTitle.find()).toBeInTheDocument();
+
+  await user.click(ui.createProjectsButton.get());
+  await user.click(await ui.createLocalProject.find());
+
+  await user.click(ui.closeButton.get());
+  expect(await ui.projectsPageTitle.find()).toBeInTheDocument();
+
+  await user.click(ui.createProjectsButton.get());
+  await user.click(await ui.createLocalProject.find());
+
+  expect(await ui.manualProjectHeader.find()).toBeInTheDocument();
+  await fillFormAndNext('testing', user);
+  expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+
+  await user.click(await ui.newCodeDefinitionSection.byRole('button', { name: 'clear' }).find());
+  expect(await ui.projectsPageTitle.find()).toBeInTheDocument();
+});
+
+function renderCreateProject() {
+  renderAppRoutes('projects/create?mode=manual', routes, {
+    currentUser: mockCurrentUser({
+      permissions: { global: [Permissions.ProjectCreation] },
+    }),
+    appState: mockAppState({ canAdmin: true }),
   });
 }
index bddb70d8edfc3e173e64d329e8ac01f1faf6f8a3..2e2e033ef7620339164d011bf70a68ea5e9d605a 100644 (file)
@@ -27,12 +27,10 @@ import ManualProjectCreate from '../manual/ManualProjectCreate';
 
 const ui = {
   nextButton: byRole('button', { name: 'next' }),
+  cancelButton: byRole('button', { name: 'cancel' }),
+  closeButton: byRole('button', { name: 'clear' }),
 };
 
-jest.mock('../../../../api/project-management', () => ({
-  setupManualProjectCreation: jest.fn(),
-}));
-
 jest.mock('../../../../api/components', () => ({
   doesComponentExists: jest
     .fn()
@@ -162,8 +160,13 @@ it('should handle component exists failure', async () => {
   ).toHaveValue('test');
 });
 
-function renderManualProjectCreate(props: Partial<ManualProjectCreate['props']> = {}) {
+function renderManualProjectCreate(props: Partial<Parameters<typeof ManualProjectCreate>[0]> = {}) {
   renderComponent(
-    <ManualProjectCreate branchesEnabled={false} onProjectSetupDone={jest.fn()} {...props} />,
+    <ManualProjectCreate
+      branchesEnabled={false}
+      onProjectSetupDone={jest.fn()}
+      onClose={jest.fn()}
+      {...props}
+    />,
   );
 }
index 6ed492fc3ed53ab96759610b93148808a51a8d02..e35f0e6907fcd0e5105df239d41901acc1cf12df 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 { ButtonPrimary, ButtonSecondary, FlagMessage, Link, Spinner, Title } from 'design-system';
+import {
+  ButtonPrimary,
+  ButtonSecondary,
+  CloseIcon,
+  FlagMessage,
+  InteractiveIcon,
+  Link,
+  Spinner,
+  Title,
+  addGlobalErrorMessage,
+  addGlobalSuccessMessage,
+} from 'design-system';
 import { omit } from 'lodash';
 import * as React from 'react';
 import { useEffect } from 'react';
@@ -25,7 +36,6 @@ import { FormattedMessage, useIntl } from 'react-intl';
 import { useNavigate, unstable_usePrompt as usePrompt } from 'react-router-dom';
 import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector';
 import { useDocUrl } from '../../../../helpers/docs';
-import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../../helpers/globalMessages';
 import { translate } from '../../../../helpers/l10n';
 import { getProjectUrl, queryToSearch } from '../../../../helpers/urls';
 import {
@@ -42,10 +52,12 @@ const listener = (event: BeforeUnloadEvent) => {
 
 interface Props {
   importProjects: ImportProjectParam;
+  onClose: () => void;
+  redirectTo: string;
 }
 
 export default function NewCodeDefinitionSelection(props: Props) {
-  const { importProjects } = props;
+  const { importProjects, redirectTo, onClose } = props;
 
   const [selectedDefinition, selectDefinition] = React.useState<NewCodeDefinitiondWithCompliance>();
   const [failedImports, setFailedImports] = React.useState<number>(0);
@@ -64,6 +76,21 @@ export default function NewCodeDefinitionSelection(props: Props) {
   const isMultipleProjects = projectCount > 1;
 
   useEffect(() => {
+    const redirect = (projectCount: number) => {
+      if (projectCount === 1 && data) {
+        if (redirectTo === '/projects') {
+          navigate(getProjectUrl(data.project.key));
+        } else {
+          onClose();
+        }
+      } else {
+        navigate({
+          pathname: '/projects',
+          search: queryToSearch({ sort: '-creation_date' }),
+        });
+      }
+    };
+
     if (mutateCount > 0 || isIdle) {
       return;
     }
@@ -80,30 +107,43 @@ export default function NewCodeDefinitionSelection(props: Props) {
     }
 
     if (projectCount > failedImports) {
-      addGlobalSuccessMessage(
-        intl.formatMessage(
-          { id: 'onboarding.create_project.success' },
-          {
-            count: projectCount - failedImports,
-          },
-        ),
-      );
-
-      if (projectCount === 1) {
-        if (data) {
-          navigate(getProjectUrl(data.project.key));
-        }
-      } else {
-        navigate({
-          pathname: '/projects',
-          search: queryToSearch({ sort: '-creation_date' }),
-        });
+      if (redirectTo === '/projects') {
+        addGlobalSuccessMessage(
+          intl.formatMessage(
+            { id: 'onboarding.create_project.success' },
+            {
+              count: projectCount - failedImports,
+            },
+          ),
+        );
+      } else if (data) {
+        addGlobalSuccessMessage(
+          <FormattedMessage
+            defaultMessage={translate('onboarding.create_project.success.admin')}
+            id="onboarding.create_project.success.admin"
+            values={{
+              project_link: <Link to={getProjectUrl(data.project.key)}>{data.project.name}</Link>,
+            }}
+          />,
+        );
       }
+      redirect(projectCount);
     }
 
     reset();
     setFailedImports(0);
-  }, [data, projectCount, failedImports, mutateCount, reset, intl, navigate, isIdle]);
+  }, [
+    data,
+    projectCount,
+    failedImports,
+    mutateCount,
+    reset,
+    intl,
+    navigate,
+    isIdle,
+    redirectTo,
+    onClose,
+  ]);
 
   React.useEffect(() => {
     if (isImporting) {
@@ -133,7 +173,24 @@ export default function NewCodeDefinitionSelection(props: Props) {
   };
 
   return (
-    <div id="project-ncd-selection" className="sw-body-sm">
+    <section
+      aria-label={translate('onboarding.create_project.new_code_definition.title')}
+      id="project-ncd-selection"
+      className="sw-body-sm"
+    >
+      <div className="sw-flex sw-justify-between">
+        <FormattedMessage
+          id="onboarding.create_project.manual.step2"
+          defaultMessage={translate('onboarding.create_project.manual.step2')}
+        />
+        <InteractiveIcon
+          Icon={CloseIcon}
+          aria-label={intl.formatMessage({ id: 'clear' })}
+          currentColor
+          onClick={onClose}
+          size="small"
+        />
+      </div>
       <Title>
         <FormattedMessage
           defaultMessage={translate('onboarding.create_x_project.new_code_definition.title')}
@@ -199,6 +256,6 @@ export default function NewCodeDefinitionSelection(props: Props) {
           </FlagMessage>
         )}
       </div>
-    </div>
+    </section>
   );
 }
index 727bccee2e6b92998225531ccae82fa8175a77d4..0df7e73c4694b410f4a0b6e0dee337500d96adb5 100644 (file)
 import classNames from 'classnames';
 import {
   ButtonPrimary,
+  ButtonSecondary,
+  CloseIcon,
   FlagErrorIcon,
   FlagMessage,
   FlagSuccessIcon,
   FormField,
   InputField,
+  InteractiveIcon,
   Link,
   Note,
+  TextError,
   Title,
 } from 'design-system';
 import { debounce, isEmpty } from 'lodash';
 import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
 import { doesComponentExists } from '../../../../api/components';
 import { getValue } from '../../../../api/settings';
 import { useDocUrl } from '../../../../helpers/docs';
@@ -46,6 +50,7 @@ import { CreateProjectModes } from '../types';
 interface Props {
   branchesEnabled: boolean;
   onProjectSetupDone: (importProjects: ImportProjectParam) => void;
+  onClose: () => void;
 }
 
 interface State {
@@ -53,7 +58,7 @@ interface State {
   projectNameError?: boolean;
   projectNameTouched: boolean;
   projectKey: string;
-  projectKeyError?: boolean;
+  projectKeyError?: 'DUPLICATE_KEY' | 'WRONG_FORMAT';
   projectKeyTouched: boolean;
   validatingProjectKey: boolean;
   mainBranchName: string;
@@ -65,60 +70,96 @@ const DEBOUNCE_DELAY = 250;
 
 type ValidState = State & Required<Pick<State, 'projectKey' | 'projectName'>>;
 
-export default class ManualProjectCreate extends React.PureComponent<Props, State> {
-  mounted = false;
+export default function ManualProjectCreate(props: Readonly<Props>) {
+  const [project, setProject] = React.useState<State>({
+    projectKey: '',
+    projectName: '',
+    projectKeyTouched: false,
+    projectNameTouched: false,
+    mainBranchName: 'main',
+    mainBranchNameTouched: false,
+    validatingProjectKey: false,
+  });
+  const intl = useIntl();
+  const docUrl = useDocUrl();
 
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      projectKey: '',
-      projectName: '',
-      projectKeyTouched: false,
-      projectNameTouched: false,
-      mainBranchName: 'main',
-      mainBranchNameTouched: false,
-      validatingProjectKey: false,
-    };
-    this.checkFreeKey = debounce(this.checkFreeKey, DEBOUNCE_DELAY);
-  }
+  const checkFreeKey = React.useCallback(
+    debounce((key: string) => {
+      setProject((prevProject) => ({ ...prevProject, validatingProjectKey: true }));
 
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchMainBranchName();
-  }
+      doesComponentExists({ component: key })
+        .then((alreadyExist) => {
+          setProject((prevProject) => {
+            if (key === prevProject.projectKey) {
+              return {
+                ...prevProject,
+                projectKeyError: alreadyExist ? 'DUPLICATE_KEY' : undefined,
+                validatingProjectKey: false,
+              };
+            }
+            return prevProject;
+          });
+        })
+        .catch(() => {
+          setProject((prevProject) => {
+            if (key === prevProject.projectKey) {
+              return {
+                ...prevProject,
+                projectKeyError: undefined,
+                validatingProjectKey: false,
+              };
+            }
+            return prevProject;
+          });
+        });
+    }, DEBOUNCE_DELAY),
+    [],
+  );
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const handleProjectKeyChange = React.useCallback(
+    (projectKey: string, fromUI = false) => {
+      const projectKeyError = validateKey(projectKey);
 
-  fetchMainBranchName = async () => {
-    const mainBranchName = await getValue({ key: GlobalSettingKeys.MainBranchName });
+      setProject((prevProject) => ({
+        ...prevProject,
+        projectKey,
+        projectKeyError,
+        projectKeyTouched: fromUI,
+      }));
 
-    if (this.mounted && mainBranchName.value !== undefined) {
-      this.setState({ mainBranchName: mainBranchName.value });
+      if (projectKeyError === undefined) {
+        checkFreeKey(projectKey);
+      }
+    },
+    [checkFreeKey],
+  );
+
+  React.useEffect(() => {
+    async function fetchMainBranchName() {
+      const { value: mainBranchName } = await getValue({ key: GlobalSettingKeys.MainBranchName });
+
+      if (mainBranchName !== undefined) {
+        setProject((prevProject) => ({
+          ...prevProject,
+          mainBranchName,
+        }));
+      }
     }
-  };
 
-  checkFreeKey = (key: string) => {
-    this.setState({ validatingProjectKey: true });
+    fetchMainBranchName();
+  }, []);
 
-    doesComponentExists({ component: key })
-      .then((alreadyExist) => {
-        if (this.mounted && key === this.state.projectKey) {
-          this.setState({
-            projectKeyError: alreadyExist ? true : undefined,
-            validatingProjectKey: false,
-          });
-        }
-      })
-      .catch(() => {
-        if (this.mounted && key === this.state.projectKey) {
-          this.setState({ projectKeyError: undefined, validatingProjectKey: false });
-        }
-      });
-  };
+  React.useEffect(() => {
+    if (!project.projectKeyTouched) {
+      const sanitizedProjectKey = project.projectName
+        .trim()
+        .replace(PROJECT_KEY_INVALID_CHARACTERS, '-');
 
-  canSubmit(state: State): state is ValidState {
+      handleProjectKeyChange(sanitizedProjectKey);
+    }
+  }, [project.projectName, project.projectKeyTouched, handleProjectKeyChange]);
+
+  const canSubmit = (state: State): state is ValidState => {
     const { projectKey, projectKeyError, projectName, projectNameError, mainBranchName } = state;
     return Boolean(
       projectKeyError === undefined &&
@@ -127,13 +168,13 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
         !isEmpty(projectName) &&
         !isEmpty(mainBranchName),
     );
-  }
+  };
 
-  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+  const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
-    const { projectKey, projectName, mainBranchName } = this.state;
-    if (this.canSubmit(this.state)) {
-      this.props.onProjectSetupDone({
+    const { projectKey, projectName, mainBranchName } = project;
+    if (canSubmit(project)) {
+      props.onProjectSetupDone({
         creationMode: CreateProjectModes.Manual,
         projects: [
           {
@@ -146,100 +187,97 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
     }
   };
 
-  handleProjectKeyChange = (projectKey: string, fromUI = false) => {
-    const projectKeyError = this.validateKey(projectKey);
-
-    this.setState({
-      projectKey,
-      projectKeyError,
-      projectKeyTouched: fromUI,
+  const handleProjectNameChange = (projectName: string, fromUI = false) => {
+    setProject({
+      ...project,
+      projectName,
+      projectNameError: validateName(projectName),
+      projectNameTouched: fromUI,
     });
-
-    if (projectKeyError === undefined) {
-      this.checkFreeKey(projectKey);
-    }
   };
 
-  handleProjectNameChange = (projectName: string, fromUI = false) => {
-    this.setState(
-      {
-        projectName,
-        projectNameError: this.validateName(projectName),
-        projectNameTouched: fromUI,
-      },
-      () => {
-        if (!this.state.projectKeyTouched) {
-          const sanitizedProjectKey = this.state.projectName
-            .trim()
-            .replace(PROJECT_KEY_INVALID_CHARACTERS, '-');
-          this.handleProjectKeyChange(sanitizedProjectKey);
-        }
-      },
-    );
-  };
-
-  handleBranchNameChange = (mainBranchName: string, fromUI = false) => {
-    this.setState({
+  const handleBranchNameChange = (mainBranchName: string, fromUI = false) => {
+    setProject({
+      ...project,
       mainBranchName,
-      mainBranchNameError: this.validateMainBranchName(mainBranchName),
+      mainBranchNameError: validateMainBranchName(mainBranchName),
       mainBranchNameTouched: fromUI,
     });
   };
 
-  validateKey = (projectKey: string) => {
+  const validateKey = (projectKey: string) => {
     const result = validateProjectKey(projectKey);
-    return result === ProjectKeyValidationResult.Valid ? undefined : true;
+    if (result !== ProjectKeyValidationResult.Valid) {
+      return 'WRONG_FORMAT';
+    }
+    return undefined;
   };
 
-  validateName = (projectName: string) => {
+  const validateName = (projectName: string) => {
     if (isEmpty(projectName)) {
       return true;
     }
     return undefined;
   };
 
-  validateMainBranchName = (mainBranchName: string) => {
+  const validateMainBranchName = (mainBranchName: string) => {
     if (isEmpty(mainBranchName)) {
       return true;
     }
     return undefined;
   };
 
-  render() {
-    const {
-      projectKey,
-      projectKeyError,
-      projectKeyTouched,
-      projectName,
-      projectNameError,
-      projectNameTouched,
-      validatingProjectKey,
-      mainBranchName,
-      mainBranchNameError,
-      mainBranchNameTouched,
-    } = this.state;
-    const { branchesEnabled } = this.props;
+  const {
+    projectKey,
+    projectKeyError,
+    projectKeyTouched,
+    projectName,
+    projectNameError,
+    projectNameTouched,
+    validatingProjectKey,
+    mainBranchName,
+    mainBranchNameError,
+    mainBranchNameTouched,
+  } = project;
+  const { branchesEnabled } = props;
 
-    const touched = Boolean(projectKeyTouched || projectNameTouched);
-    const projectNameIsInvalid = projectNameTouched && projectNameError !== undefined;
-    const projectNameIsValid = projectNameTouched && projectNameError === undefined;
-    const projectKeyIsInvalid = touched && projectKeyError !== undefined;
-    const projectKeyIsValid = touched && !validatingProjectKey && projectKeyError === undefined;
-    const mainBranchNameIsValid = mainBranchNameTouched && mainBranchNameError === undefined;
-    const mainBranchNameIsInvalid = mainBranchNameTouched && mainBranchNameError !== undefined;
+  const touched = Boolean(projectKeyTouched || projectNameTouched);
+  const projectNameIsInvalid = projectNameTouched && projectNameError !== undefined;
+  const projectNameIsValid = projectNameTouched && projectNameError === undefined;
+  const projectKeyIsInvalid = touched && projectKeyError !== undefined;
+  const projectKeyIsValid = touched && !validatingProjectKey && projectKeyError === undefined;
+  const mainBranchNameIsValid = mainBranchNameTouched && mainBranchNameError === undefined;
+  const mainBranchNameIsInvalid = mainBranchNameTouched && mainBranchNameError !== undefined;
 
-    return (
-      <div className="sw-max-w-[50%]">
-        <Title>{translate('onboarding.create_project.manual.title')}</Title>
-        {branchesEnabled && (
-          <FlagMessage className="sw-my-4" variant="info">
-            {translate('onboarding.create_project.pr_decoration.information')}
-          </FlagMessage>
-        )}
+  return (
+    <section
+      aria-label={translate('onboarding.create_project.manual.title')}
+      className="sw-body-sm"
+    >
+      <div className="sw-flex sw-justify-between">
+        <FormattedMessage
+          id="onboarding.create_project.manual.step1"
+          defaultMessage={translate('onboarding.create_project.manual.step1')}
+        />
+        <InteractiveIcon
+          Icon={CloseIcon}
+          aria-label={intl.formatMessage({ id: 'clear' })}
+          currentColor
+          onClick={props.onClose}
+          size="small"
+        />
+      </div>
+      <Title>{translate('onboarding.create_project.manual.title')}</Title>
+      {branchesEnabled && (
+        <FlagMessage className="sw-my-4" variant="info">
+          {translate('onboarding.create_project.pr_decoration.information')}
+        </FlagMessage>
+      )}
+      <div className="sw-max-w-[50%] sw-mt-2">
         <form
           id="create-project-manual"
           className="sw-flex-col sw-body-sm"
-          onSubmit={this.handleFormSubmit}
+          onSubmit={handleFormSubmit}
         >
           <FormField
             htmlFor="project-name"
@@ -255,7 +293,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
                 id="project-name"
                 maxLength={PROJECT_NAME_MAX_LEN}
                 minLength={1}
-                onChange={(e) => this.handleProjectNameChange(e.currentTarget.value, true)}
+                onChange={(e) => handleProjectNameChange(e.currentTarget.value, true)}
                 type="text"
                 value={projectName}
                 autoFocus
@@ -284,7 +322,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
                 size="large"
                 id="project-key"
                 minLength={1}
-                onChange={(e) => this.handleProjectKeyChange(e.currentTarget.value, true)}
+                onChange={(e) => handleProjectKeyChange(e.currentTarget.value, true)}
                 type="text"
                 value={projectKey}
                 isInvalid={projectKeyIsInvalid}
@@ -294,8 +332,16 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
               {projectKeyIsInvalid && <FlagErrorIcon className="sw-ml-2" />}
               {projectKeyIsValid && <FlagSuccessIcon className="sw-ml-2" />}
             </div>
-            <Note className="sw-mt-2">
-              {translate('onboarding.create_project.project_key.description')}
+            <Note className="sw-flex-col sw-mt-2">
+              {projectKeyError === 'DUPLICATE_KEY' && (
+                <TextError
+                  text={translate('onboarding.create_project.project_key.duplicate_key')}
+                />
+              )}
+              {!isEmpty(projectKey) && projectKeyError === 'WRONG_FORMAT' && (
+                <TextError text={translate('onboarding.create_project.project_key.wrong_format')} />
+              )}
+              <p>{translate('onboarding.create_project.project_key.description')}</p>
             </Note>
           </FormField>
 
@@ -312,7 +358,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
                 size="large"
                 id="main-branch-name"
                 minLength={1}
-                onChange={(e) => this.handleBranchNameChange(e.currentTarget.value, true)}
+                onChange={(e) => handleBranchNameChange(e.currentTarget.value, true)}
                 type="text"
                 value={mainBranchName}
                 isInvalid={mainBranchNameIsInvalid}
@@ -323,37 +369,28 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
               {mainBranchNameIsValid && <FlagSuccessIcon className="sw-ml-2" />}
             </div>
             <Note className="sw-mt-2">
-              <FormattedMessageWithDocLink />
+              <FormattedMessage
+                id="onboarding.create_project.main_branch_name.description"
+                defaultMessage={translate('onboarding.create_project.main_branch_name.description')}
+                values={{
+                  learn_more: (
+                    <Link to={docUrl('/analyzing-source-code/branches/branch-analysis')}>
+                      {translate('learn_more')}
+                    </Link>
+                  ),
+                }}
+              />
             </Note>
           </FormField>
 
-          <ButtonPrimary
-            type="submit"
-            className="sw-mt-4 sw-mb-4"
-            disabled={!this.canSubmit(this.state)}
-          >
+          <ButtonSecondary className="sw-mt-4 sw-mr-4" onClick={props.onClose} type="button">
+            {intl.formatMessage({ id: 'cancel' })}
+          </ButtonSecondary>
+          <ButtonPrimary className="sw-mt-4" type="submit" disabled={!canSubmit(project)}>
             {translate('next')}
           </ButtonPrimary>
         </form>
       </div>
-    );
-  }
-}
-
-function FormattedMessageWithDocLink() {
-  const docUrl = useDocUrl();
-
-  return (
-    <FormattedMessage
-      id="onboarding.create_project.main_branch_name.description"
-      defaultMessage={translate('onboarding.create_project.main_branch_name.description')}
-      values={{
-        learn_more: (
-          <Link to={docUrl('/analyzing-source-code/branches/branch-analysis')}>
-            {translate('learn_more')}
-          </Link>
-        ),
-      }}
-    />
+    </section>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
deleted file mode 100644 (file)
index 20b7d2f..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { FormattedMessage } from 'react-intl';
-import { createProject } from '../../api/project-management';
-import { getValue } from '../../api/settings';
-import Link from '../../components/common/Link';
-import VisibilitySelector from '../../components/common/VisibilitySelector';
-import Modal from '../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
-import { Alert } from '../../components/ui/Alert';
-import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation';
-import { translate } from '../../helpers/l10n';
-import { getProjectUrl } from '../../helpers/urls';
-import { Visibility } from '../../types/component';
-import { GlobalSettingKeys } from '../../types/settings';
-
-interface Props {
-  defaultProjectVisibility?: Visibility;
-  onClose: () => void;
-  onProjectCreated: () => void;
-}
-
-interface State {
-  createdProject?: { key: string; name: string };
-  key: string;
-  loading: boolean;
-  name: string;
-  visibility?: Visibility;
-  // add index declaration to be able to do `this.setState({ [name]: value });`
-  [x: string]: any;
-  mainBranchName: string;
-}
-
-export default class CreateProjectForm extends React.PureComponent<Props, State> {
-  closeButton?: HTMLElement | null;
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      key: '',
-      loading: false,
-      name: '',
-      visibility: props.defaultProjectVisibility,
-      mainBranchName: 'main',
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchMainBranchName();
-  }
-
-  componentDidUpdate() {
-    // wrap with `setTimeout` because of https://github.com/reactjs/react-modal/issues/338
-    setTimeout(() => {
-      if (this.closeButton) {
-        this.closeButton.focus();
-      }
-    }, 0);
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchMainBranchName = async () => {
-    const mainBranchName = await getValue({ key: GlobalSettingKeys.MainBranchName });
-
-    if (this.mounted && mainBranchName.value !== undefined) {
-      this.setState({ mainBranchName: mainBranchName.value });
-    }
-  };
-
-  handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    const { name, value } = event.currentTarget;
-    this.setState({ [name]: value });
-  };
-
-  handleVisibilityChange = (visibility: Visibility) => {
-    this.setState({ visibility });
-  };
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    const { name, key, mainBranchName, visibility } = this.state;
-
-    const data = {
-      name,
-      project: key,
-      mainBranch: mainBranchName,
-      visibility,
-    };
-
-    this.setState({ loading: true });
-    createProject(data).then(
-      (response) => {
-        if (this.mounted) {
-          this.setState({ createdProject: response.project, loading: false });
-          this.props.onProjectCreated();
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      },
-    );
-  };
-
-  render() {
-    const { defaultProjectVisibility } = this.props;
-    const { createdProject } = this.state;
-    const header = translate('qualifiers.create.TRK');
-
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        {createdProject ? (
-          <div>
-            <header className="modal-head">
-              <h2>{header}</h2>
-            </header>
-
-            <div className="modal-body">
-              <Alert variant="success">
-                <FormattedMessage
-                  defaultMessage={translate(
-                    'projects_management.project_has_been_successfully_created',
-                  )}
-                  id="projects_management.project_has_been_successfully_created"
-                  values={{
-                    project: (
-                      <Link to={getProjectUrl(createdProject.key)}>{createdProject.name}</Link>
-                    ),
-                  }}
-                />
-              </Alert>
-            </div>
-
-            <footer className="modal-foot">
-              <ResetButtonLink
-                id="create-project-close"
-                innerRef={(node) => (this.closeButton = node)}
-                onClick={this.props.onClose}
-              >
-                {translate('close')}
-              </ResetButtonLink>
-            </footer>
-          </div>
-        ) : (
-          <form id="create-project-form" onSubmit={this.handleFormSubmit}>
-            <header className="modal-head">
-              <h2>{header}</h2>
-            </header>
-
-            <div className="modal-body">
-              <MandatoryFieldsExplanation className="modal-field" />
-              <div className="modal-field">
-                <label htmlFor="create-project-name">
-                  {translate('onboarding.create_project.display_name')}
-                  <MandatoryFieldMarker />
-                </label>
-                <input
-                  autoFocus
-                  id="create-project-name"
-                  maxLength={2000}
-                  name="name"
-                  onChange={this.handleInputChange}
-                  required
-                  type="text"
-                  value={this.state.name}
-                />
-              </div>
-              <div className="modal-field">
-                <label htmlFor="create-project-key">
-                  {translate('onboarding.create_project.project_key')}
-                  <MandatoryFieldMarker />
-                </label>
-                <input
-                  id="create-project-key"
-                  maxLength={400}
-                  name="key"
-                  onChange={this.handleInputChange}
-                  required
-                  type="text"
-                  value={this.state.key}
-                />
-              </div>
-              <div className="modal-field">
-                <label htmlFor="create-project-main-branch-name">
-                  {translate('onboarding.create_project.main_branch_name')}
-                  <MandatoryFieldMarker />
-                </label>
-                <input
-                  id="create-project-main-branch-name"
-                  maxLength={400}
-                  name="mainBranchName"
-                  onChange={this.handleInputChange}
-                  required
-                  type="text"
-                  value={this.state.mainBranchName}
-                />
-              </div>
-              <div className="modal-field">
-                <label>{translate('visibility')}</label>
-                <VisibilitySelector
-                  canTurnToPrivate={defaultProjectVisibility !== undefined}
-                  className="little-spacer-top"
-                  onChange={this.handleVisibilityChange}
-                  visibility={this.state.visibility}
-                />
-              </div>
-            </div>
-
-            <footer className="modal-foot">
-              {this.state.loading && <i className="spinner spacer-right" />}
-              <SubmitButton disabled={this.state.loading} id="create-project-submit">
-                {translate('create')}
-              </SubmitButton>
-              <ResetButtonLink id="create-project-cancel" onClick={this.props.onClose}>
-                {translate('cancel')}
-              </ResetButtonLink>
-            </footer>
-          </form>
-        )}
-      </Modal>
-    );
-  }
-}
index f426a8611cd2536686080ade94f9d21c103ed25a..b9076134e046772776908ee00f86ef1f45e9f594 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
 import { Button, EditButton } from '../../components/controls/buttons';
 import { translate } from '../../helpers/l10n';
 import { Visibility } from '../../types/component';
@@ -27,12 +28,13 @@ import ChangeDefaultVisibilityForm from './ChangeDefaultVisibilityForm';
 export interface Props {
   defaultProjectVisibility?: Visibility;
   hasProvisionPermission?: boolean;
-  onProjectCreate: () => void;
   onChangeDefaultProjectVisibility: (visibility: Visibility) => void;
 }
 
 export default function Header(props: Readonly<Props>) {
   const [visibilityForm, setVisibilityForm] = useState(false);
+  const navigate = useNavigate();
+  const location = useLocation();
 
   const { defaultProjectVisibility, hasProvisionPermission } = props;
 
@@ -56,7 +58,14 @@ export default function Header(props: Readonly<Props>) {
         </span>
 
         {hasProvisionPermission && (
-          <Button id="create-project" onClick={props.onProjectCreate}>
+          <Button
+            id="create-project"
+            onClick={() =>
+              navigate('/projects/create?mode=manual', {
+                state: { from: location.pathname },
+              })
+            }
+          >
             {translate('qualifiers.create.TRK')}
           </Button>
         )}
index cd0078f0bf2099102c1f11b42c1f340947d9c788..baf0a3bccdab6a2bc22a7cd5a91d16dc53fdfb22 100644 (file)
@@ -37,7 +37,6 @@ import { Visibility } from '../../types/component';
 import { Permissions } from '../../types/permissions';
 import { SettingsKey } from '../../types/settings';
 import { LoggedInUser } from '../../types/users';
-import CreateProjectForm from './CreateProjectForm';
 import Header from './Header';
 import Projects from './Projects';
 import Search from './Search';
@@ -48,7 +47,6 @@ export interface Props {
 
 interface State {
   analyzedBefore?: Date;
-  createProjectForm: boolean;
   defaultProjectVisibility?: Visibility;
   page: number;
   projects: Project[];
@@ -70,7 +68,6 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
     this.state = {
-      createProjectForm: false,
       ready: false,
       projects: [],
       provisioned: false,
@@ -201,14 +198,6 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
     this.setState({ selection: [] });
   };
 
-  openCreateProjectForm = () => {
-    this.setState({ createProjectForm: true });
-  };
-
-  closeCreateProjectForm = () => {
-    this.setState({ createProjectForm: false });
-  };
-
   render() {
     const { currentUser } = this.props;
     const { defaultProjectVisibility } = this.state;
@@ -221,7 +210,6 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
           defaultProjectVisibility={defaultProjectVisibility}
           hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
           onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
-          onProjectCreate={this.openCreateProjectForm}
         />
 
         <Search
@@ -259,14 +247,6 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
           ready={this.state.ready}
           total={this.state.total}
         />
-
-        {this.state.createProjectForm && (
-          <CreateProjectForm
-            defaultProjectVisibility={defaultProjectVisibility}
-            onClose={this.closeCreateProjectForm}
-            onProjectCreated={this.requestProjects}
-          />
-        )}
       </main>
     );
   }
index 9bf4f787426e60821e2b21358cc8ad2a7b0301c8..16a2141c423a8de40a5ac32da26caea2238037d0 100644 (file)
@@ -101,7 +101,15 @@ const ui = {
   createProject: byRole('button', {
     name: 'qualifiers.create.TRK',
   }),
-
+  manualProjectHeader: byText('onboarding.create_project.manual.title'),
+  displayNameField: byRole('textbox', {
+    name: /onboarding.create_project.display_name/,
+  }),
+  projectNextButton: byRole('button', { name: 'next' }),
+  newCodeDefinitionHeader: byText('onboarding.create_x_project.new_code_definition.title1'),
+  projectCreateButton: byRole('button', {
+    name: 'onboarding.create_project.new_code_definition.create_x_projects1',
+  }),
   visibilityFilter: byRole('combobox', { name: 'projects_management.filter_by_visibility' }),
   qualifierFilter: byRole('combobox', { name: 'projects_management.filter_by_component' }),
   analysisDateFilter: byPlaceholderText('last_analysis_before'),
@@ -402,39 +410,14 @@ it('should load more and change the filter without caching old pages', async ()
 });
 
 it('should create project', async () => {
-  settingsHandler.set(GlobalSettingKeys.MainBranchName, 'main');
   const user = userEvent.setup();
+  settingsHandler.set(GlobalSettingKeys.MainBranchName, 'main');
   renderProjectManagementApp({}, { permissions: { global: [Permissions.ProjectCreation] } });
   await waitFor(() => expect(ui.row.getAll()).toHaveLength(5));
-  await user.click(await ui.createProject.find());
-  expect(ui.createDialog.get()).toBeInTheDocument();
-  expect(ui.createDialog.by(ui.privateVisibility).get()).not.toBeChecked();
-  await user.click(ui.createDialog.by(ui.privateVisibility).get());
-  expect(ui.createDialog.by(ui.privateVisibility).get()).not.toBeChecked();
-  await user.click(ui.createDialog.by(ui.cancel).get());
-
-  expect(await ui.defaultVisibility.find()).toBeInTheDocument();
-  expect(ui.defaultVisibility.get()).toHaveTextContent('—');
-  await user.click(ui.editDefaultVisibility.get());
-  expect(await ui.changeDefaultVisibilityDialog.find()).toBeInTheDocument();
-  expect(ui.defaultVisibilityWarning.get()).not.toHaveTextContent('.github');
-  await user.click(ui.changeDefaultVisibilityDialog.by(ui.visibilityPublicRadio).get());
-  await user.click(ui.changeDefaultVisibilityDialog.by(ui.submitDefaultVisibilityChange).get());
-  expect(ui.changeDefaultVisibilityDialog.query()).not.toBeInTheDocument();
-  expect(ui.defaultVisibility.get()).toHaveTextContent('visibility.public');
-
-  await user.click(await ui.createProject.find());
-  expect(ui.createDialog.get()).toBeInTheDocument();
-  await user.click(ui.createDialog.by(ui.privateVisibility).get());
-  expect(ui.createDialog.by(ui.privateVisibility).get()).toBeChecked();
-  await user.type(ui.createDialog.by(ui.displayNameInput).get(), 'a Test');
-  await user.type(ui.createDialog.by(ui.projectKeyInput).get(), 'test');
-  expect(ui.createDialog.by(ui.mainBranchNameInput).get()).toHaveValue('main');
-  await user.click(ui.createDialog.by(ui.create).get());
-  expect(ui.createDialog.by(ui.successMsg).get()).toBeInTheDocument();
-  await user.click(ui.createDialog.by(ui.close).get());
-  expect(ui.row.getAll()).toHaveLength(6);
-  expect(ui.row.getAll()[1]).toHaveTextContent('qualifier.TRKa Testvisibility.privatetest—');
+
+  await user.click(ui.createProject.get());
+
+  expect(byText('/projects/create?mode=manual').get()).toBeInTheDocument();
 });
 
 it('should edit permissions of single project', async () => {
index 83bfd80c7ee99f722146940492b817c54030d5b0..b280d8d0824c87bfa947ee306d0055b9cae023bc 100644 (file)
@@ -4207,6 +4207,8 @@ onboarding.project_analysis.header=Analyze your project
 onboarding.project_analysis.description=We initialized your project on SonarQube, now it's up to you to launch analyses!
 onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to integrating with Pipelines
 
+onboarding.create_project.manual.step1=1 of 2
+onboarding.create_project.manual.step2=2 of 2
 onboarding.create_project.manual.title=Create a local project
 onboarding.create_project.select_method=How do you want to create your project?
 onboarding.create_project.select_method.manually=Are you just testing or have an advanced use-case? Create a local project.
@@ -4229,6 +4231,8 @@ onboarding.create_project.import_select_method.gitlab=Import from GitLab
 onboarding.create_project.alm_not_configured=Contact your admin to set up the global configuration allowing you to import project from this DevOps Platform
 onboarding.create_project.check_alm_supported=Checking if available
 onboarding.create_project.project_key=Project key
+onboarding.create_project.project_key.duplicate_key=The project key name is already taken.
+onboarding.create_project.project_key.wrong_format=The provided value doesn't match the expected format.
 onboarding.create_project.project_key.description=The project key is a unique identifier for your project. It may contain up to 400 characters. Allowed characters are alphanumeric, '-' (dash), '_' (underscore), '.' (period) and ':' (colon), with at least one non-digit.
 onboarding.create_project.project_key.error.empty=You must provide at least one character.
 onboarding.create_project.project_key.error.too_long=The provided key is too long.
@@ -4332,11 +4336,13 @@ onboarding.create_project.import_in_progress={count} of {total} projects importe
 
 onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
 onboarding.create_x_project.new_code_definition.title=Set up {count, plural, one {project} other {# projects}} for Clean as You Code
+onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
 onboarding.create_project.new_code_definition.description=The new code definition sets which part of your code will be considered new code. This helps you focus attention on the most recent changes to your project, enabling you to follow the Clean as You Code methodology. Learn more: {link}
 onboarding.create_project.new_code_definition.description.link=Defining New Code
 onboarding.create_project.new_code_definition.create_x_projects=Create {count, plural, one {project} other {# projects}}
 onboarding.create_projects.new_code_definition.change_info=You can change this setting for each project individually at any time in the project administration settings.
 onboarding.create_project.success=Your {count, plural, one {project has} other {# projects have}} been created.
+onboarding.create_project.success.admin=Project {project_link} has been successfully created.
 onboarding.create_project.failure=Import of {count, plural, one {# project} other {# projects}} failed.
 
 onboarding.token.header=Provide a token