aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/create
diff options
context:
space:
mode:
authorAndrey Luiz <andrey.luiz@sonarsource.com>2023-06-09 15:58:56 +0200
committersonartech <sonartech@sonarsource.com>2023-06-14 09:51:06 +0000
commit200ef3a99587a0679748e14d8889e61ebc1ef3d1 (patch)
treec06c092756225312e0c19c420bf7fee298936775 /server/sonar-web/src/main/js/apps/create
parent451a379605676df360745519038b5ae2770a00ea (diff)
downloadsonarqube-200ef3a99587a0679748e14d8889e61ebc1ef3d1.tar.gz
sonarqube-200ef3a99587a0679748e14d8889e61ebc1ef3d1.zip
SONAR-19453 New code definition is made part of the manual project creation (#8486)
Diffstat (limited to 'server/sonar-web/src/main/js/apps/create')
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx248
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/types.ts8
5 files changed, 375 insertions, 72 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index 53d317ad3af..379b9a2b27b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -17,30 +17,37 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { noop } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import { FormattedMessage } from 'react-intl';
import { getAlmSettings } from '../../../api/alm-settings';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import DocLink from '../../../components/common/DocLink';
+import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import NewCodeDefinitionSelector from '../../../components/new-code-definition/NewCodeDefinitionSelector';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { AppState } from '../../../types/appstate';
import { Feature } from '../../../types/features';
+import { NewCodePeriodWithCompliance } from '../../../types/types';
import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm';
import AzureProjectCreate from './Azure/AzureProjectCreate';
import BitbucketCloudProjectCreate from './BitbucketCloud/BitbucketCloudProjectCreate';
import BitbucketProjectCreate from './BitbucketServer/BitbucketProjectCreate';
+import CreateProjectPageHeader from './components/CreateProjectPageHeader';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './Github/GitHubProjectCreate';
import GitlabProjectCreate from './Gitlab/GitlabProjectCreate';
import ManualProjectCreate from './manual/ManualProjectCreate';
import './style.css';
-import { CreateProjectModes } from './types';
+import { CreateProjectApiCallback, CreateProjectModes } from './types';
export interface CreateProjectPageProps extends WithAvailableFeaturesProps {
appState: AppState;
@@ -55,7 +62,9 @@ interface State {
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
+ isProjectSetupDone: boolean;
creatingAlmDefinition?: AlmKeys;
+ selectedNcd: NewCodePeriodWithCompliance | null;
}
const PROJECT_MODE_FOR_ALM_KEY = {
@@ -68,6 +77,8 @@ const PROJECT_MODE_FOR_ALM_KEY = {
export class CreateProjectPage extends React.PureComponent<CreateProjectPageProps, State> {
mounted = false;
+ createProjectFnRef: CreateProjectApiCallback | null = null;
+
state: State = {
azureSettings: [],
bitbucketSettings: [],
@@ -75,6 +86,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
githubSettings: [],
gitlabSettings: [],
loading: true,
+ isProjectSetupDone: false,
+ selectedNcd: null,
};
componentDidMount() {
@@ -124,6 +137,21 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
this.props.router.push(getProjectUrl(projectKey));
};
+ handleManualProjectCreate = () => {
+ const { selectedNcd } = this.state;
+ if (this.createProjectFnRef && selectedNcd) {
+ this.createProjectFnRef(selectedNcd.type, selectedNcd.value).then(
+ ({ project }) => this.handleProjectCreate(project.key),
+ noop
+ );
+ }
+ };
+
+ handleProjectSetupDone = (createProject: CreateProjectApiCallback) => {
+ this.createProjectFnRef = createProject;
+ this.setState({ isProjectSetupDone: true });
+ };
+
handleOnCancelCreation = () => {
this.setState({ creatingAlmDefinition: undefined });
};
@@ -146,6 +174,16 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
}
};
+ handleNcdChanged = (ncd: NewCodePeriodWithCompliance) => {
+ this.setState({
+ selectedNcd: ncd,
+ });
+ };
+
+ handleGoBack = () => {
+ this.setState({ isProjectSetupDone: false });
+ };
+
renderProjectCreation(mode?: CreateProjectModes) {
const {
appState: { canAdmin },
@@ -227,7 +265,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
return (
<ManualProjectCreate
branchesEnabled={branchSupportEnabled}
- onProjectCreate={this.handleProjectCreate}
+ onProjectSetupDone={this.handleProjectSetupDone}
/>
);
}
@@ -251,9 +289,59 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
}
}
+ renderNcdSelection() {
+ const { appState } = this.props;
+ const { selectedNcd } = this.state;
+
+ return (
+ <div id="project-ncd-selection">
+ <CreateProjectPageHeader
+ title={translate('onboarding.create_project.new_code_definition.title')}
+ />
+
+ <h1 className="sw-mb-4">{translate('onboarding.create_project.new_code_definition')}</h1>
+ <p className="sw-mb-2">
+ {translate('onboarding.create_project.new_code_definition.description')}
+ </p>
+ <p className="sw-mb-2">
+ {translate('onboarding.create_project.new_code_definition.description1')}
+ </p>
+
+ <p className="sw-mb-2">
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_project.new_code_definition.description2')}
+ id="onboarding.create_project.new_code_definition.description2"
+ values={{
+ link: (
+ <DocLink to="/project-administration/defining-new-code/">
+ {translate('onboarding.create_project.new_code_definition.description2.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+
+ <NewCodeDefinitionSelector
+ canAdmin={appState.canAdmin}
+ onNcdChanged={this.handleNcdChanged}
+ />
+
+ <div className="sw-flex sw-flex-row sw-gap-2 sw-mt-4">
+ <ButtonLink onClick={this.handleGoBack}>{translate('back')}</ButtonLink>
+ <SubmitButton
+ onClick={this.handleManualProjectCreate}
+ disabled={!selectedNcd?.isCompliant}
+ >
+ {translate('onboarding.create_project.new_code_definition.create_project')}
+ </SubmitButton>
+ </div>
+ </div>
+ );
+ }
+
render() {
const { location } = this.props;
- const { creatingAlmDefinition } = this.state;
+ const { creatingAlmDefinition, isProjectSetupDone } = this.state;
const mode: CreateProjectModes | undefined = location.query?.mode;
return (
@@ -261,7 +349,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
<Helmet title={translate('onboarding.create_project.select_method')} titleTemplate="%s" />
<A11ySkipTarget anchor="create_project_main" />
<div className="page page-limited huge-spacer-bottom position-relative" id="create-project">
- {this.renderProjectCreation(mode)}
+ {isProjectSetupDone ? this.renderNcdSelection() : this.renderProjectCreation(mode)}
+
{creatingAlmDefinition && (
<AlmBindingDefinitionForm
alm={creatingAlmDefinition}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
new file mode 100644
index 00000000000..06bb6cb7c3e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
@@ -0,0 +1,248 @@
+/*
+ * 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 userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import * as React from 'react';
+import { byRole, byText } from 'testing-library-selector';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
+import { getNewCodePeriod } from '../../../../api/newCodePeriod';
+import { mockProject } from '../../../../helpers/mocks/projects';
+import { mockAppState } from '../../../../helpers/testMocks';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { NewCodePeriodSettingType } from '../../../../types/types';
+import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+
+jest.mock('../../../../api/alm-settings');
+jest.mock('../../../../api/newCodePeriod');
+jest.mock('../../../../api/components', () => ({
+ ...jest.requireActual('../../../../api/components'),
+ setupManualProjectCreation: jest
+ .fn()
+ .mockReturnValue(() => Promise.resolve({ project: mockProject() })),
+ doesComponentExists: jest
+ .fn()
+ .mockImplementation(({ component }) => Promise.resolve(component === 'exists')),
+}));
+jest.mock('../../../../api/settings', () => ({
+ getValue: jest.fn().mockResolvedValue({ value: 'main' }),
+}));
+
+const ui = {
+ manualCreateProjectOption: byText('onboarding.create_project.select_method.manual'),
+ manualProjectHeader: byText('onboarding.create_project.setup_manually'),
+ displayNameField: byRole('textbox', {
+ name: /onboarding.create_project.display_name/,
+ }),
+ projectNextButton: byRole('button', { name: 'next' }),
+ newCodeDefinitionHeader: byText('onboarding.create_project.new_code_definition.title'),
+ newCodeDefinitionBackButton: byRole('button', { name: 'back' }),
+ inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
+ projectCreateButton: byRole('button', {
+ name: 'onboarding.create_project.new_code_definition.create_project',
+ }),
+ overrideNcdRadio: byRole('radio', { name: 'new_code_definition.specific_setting' }),
+ ncdOptionPreviousVersionRadio: byRole('radio', {
+ name: /new_code_definition.previous_version/,
+ }),
+ ncdOptionRefBranchRadio: byRole('radio', {
+ name: /new_code_definition.reference_branch/,
+ }),
+ ncdOptionDaysRadio: byRole('radio', {
+ name: /new_code_definition.number_days/,
+ }),
+ ncdOptionDaysInput: byRole('textbox', {
+ name: /new_code_definition.number_days.specify_days/,
+ }),
+ ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'),
+ ncdWarningTextAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
+ ncdWarningText: byText('new_code_definition.compliance.warning.explanation'),
+ projectDashboardText: byText('/dashboard?id=foo'),
+};
+
+async function fillFormAndNext(displayName: string, user: UserEvent) {
+ await user.click(ui.manualCreateProjectOption.get());
+
+ expect(ui.manualProjectHeader.get()).toBeInTheDocument();
+
+ await user.click(ui.displayNameField.get());
+ await user.keyboard(displayName);
+
+ expect(ui.projectNextButton.get()).toBeEnabled();
+ await user.click(ui.projectNextButton.get());
+}
+
+let almSettingsHandler: AlmSettingsServiceMock;
+let newCodePeriodHandler: NewCodePeriodsServiceMock;
+
+beforeAll(() => {
+ almSettingsHandler = new AlmSettingsServiceMock();
+ newCodePeriodHandler = new NewCodePeriodsServiceMock();
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ almSettingsHandler.reset();
+ newCodePeriodHandler.reset();
+});
+
+it('should fill form and move to NCD selection and back', async () => {
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+
+ expect(ui.newCodeDefinitionBackButton.get()).toBeInTheDocument();
+ await user.click(ui.newCodeDefinitionBackButton.get());
+
+ expect(ui.manualProjectHeader.get()).toBeInTheDocument();
+
+ // TODO this must work at some point
+ // expect(ui.displayNameField.get()).toHaveValue('test');
+});
+
+it('should select the global NCD when it is compliant', async () => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '30' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeEnabled();
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+
+ await user.click(ui.inheritGlobalNcdRadio.get());
+
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+});
+
+it('global NCD option should be disabled if not compliant', async () => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '96' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toHaveClass('disabled');
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+});
+
+it.each([
+ { canAdmin: true, message: ui.ncdWarningTextAdmin },
+ { canAdmin: false, message: ui.ncdWarningText },
+])(
+ 'should show warning message when global NCD is not compliant',
+ async ({ canAdmin, message }) => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '96' });
+ const user = userEvent.setup();
+ renderCreateProject({ appState: mockAppState({ canAdmin }) });
+ await fillFormAndNext('test', user);
+
+ expect(message.get()).toBeInTheDocument();
+ }
+);
+
+it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])(
+ 'should override the global NCD and pick a compliant NCD',
+ async (option) => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '96' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toHaveClass('disabled');
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+ expect(ui.overrideNcdRadio.get()).not.toHaveClass('disabled');
+ expect(option.get()).toHaveClass('disabled');
+
+ await user.click(ui.overrideNcdRadio.get());
+ expect(option.get()).not.toHaveClass('disabled');
+
+ await user.click(option.get());
+
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+ }
+);
+
+it('number of days should show error message if value is not a number', async () => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '60' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+ expect(ui.overrideNcdRadio.get()).not.toHaveClass('disabled');
+ expect(ui.ncdOptionDaysRadio.get()).toHaveClass('disabled');
+
+ await user.click(ui.overrideNcdRadio.get());
+ expect(ui.ncdOptionDaysRadio.get()).not.toHaveClass('disabled');
+
+ await user.click(ui.ncdOptionDaysRadio.get());
+
+ expect(ui.ncdOptionDaysInput.get()).toBeInTheDocument();
+ expect(ui.ncdOptionDaysInput.get()).toHaveValue('30');
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+
+ await user.click(ui.ncdOptionDaysInput.get());
+ await user.keyboard('abc');
+
+ expect(ui.ncdOptionDaysInputError.get()).toBeInTheDocument();
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+
+ await user.clear(ui.ncdOptionDaysInput.get());
+ await user.click(ui.ncdOptionDaysInput.get());
+ await user.keyboard('30');
+
+ expect(ui.ncdOptionDaysInputError.query()).not.toBeInTheDocument();
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+});
+
+it('the project onboarding page should be displayed when the project is created', async () => {
+ newCodePeriodHandler.setNewCodePeriod({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('testing', user);
+
+ await user.click(ui.overrideNcdRadio.get());
+
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+ await user.click(ui.projectCreateButton.get());
+
+ expect(await ui.projectDashboardText.find()).toBeInTheDocument();
+});
+
+function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
+ renderApp('project/create', <CreateProjectPage {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
index 98a05291a2a..8bcecd0dd45 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
@@ -20,13 +20,17 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import { createProject, doesComponentExists } from '../../../../api/components';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import { byRole } from 'testing-library-selector';
+import { doesComponentExists } from '../../../../api/components';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import ManualProjectCreate from '../manual/ManualProjectCreate';
+const ui = {
+ nextButton: byRole('button', { name: 'next' }),
+};
+
jest.mock('../../../../api/components', () => ({
- createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } }),
+ setupManualProjectCreation: jest.fn(),
doesComponentExists: jest
.fn()
.mockImplementation(({ component }) => Promise.resolve(component === 'exists')),
@@ -36,15 +40,8 @@ jest.mock('../../../../api/settings', () => ({
getValue: jest.fn().mockResolvedValue({ value: 'main' }),
}));
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
-
-beforeAll(() => {
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
-});
-
beforeEach(() => {
jest.clearAllMocks();
- newCodePeriodHandler.reset();
});
it('should show branch information', async () => {
@@ -68,7 +65,7 @@ it('should validate form input', async () => {
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.project_key field_required' })
).toHaveValue('test');
- expect(screen.getByRole('button', { name: 'set_up' })).toBeEnabled();
+ expect(ui.nextButton.get()).toBeEnabled();
// Sanitize the key
await user.click(
@@ -93,7 +90,7 @@ it('should validate form input', async () => {
expect(
screen.getByText('onboarding.create_project.display_name.error.empty')
).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'set_up' })).toBeDisabled();
+ expect(ui.nextButton.get()).toBeDisabled();
// Only key
await user.click(
@@ -142,8 +139,8 @@ it('should validate form input', async () => {
it('should submit form input', async () => {
const user = userEvent.setup();
- const onProjectCreate = jest.fn();
- renderManualProjectCreate({ onProjectCreate });
+ const onProjectSetupDone = jest.fn();
+ renderManualProjectCreate({ onProjectSetupDone });
// All input valid
await user.click(
@@ -152,38 +149,14 @@ it('should submit form input', async () => {
})
);
await user.keyboard('test');
- await user.click(screen.getByRole('button', { name: 'set_up' }));
- expect(createProject).toHaveBeenCalledWith({
- name: 'test',
- project: 'test',
- mainBranch: 'main',
- });
- expect(onProjectCreate).toHaveBeenCalled();
-});
-
-it('should handle create failure', async () => {
- const user = userEvent.setup();
- (createProject as jest.Mock).mockRejectedValueOnce({});
- const onProjectCreate = jest.fn();
- renderManualProjectCreate({ onProjectCreate });
-
- // All input valid
- await user.click(
- await screen.findByRole('textbox', {
- name: 'onboarding.create_project.display_name field_required',
- })
- );
- await user.keyboard('test');
- await user.click(screen.getByRole('button', { name: 'set_up' }));
-
- expect(onProjectCreate).not.toHaveBeenCalled();
+ await user.click(ui.nextButton.get());
+ expect(onProjectSetupDone).toHaveBeenCalled();
});
it('should handle component exists failure', async () => {
const user = userEvent.setup();
- (doesComponentExists as jest.Mock).mockRejectedValueOnce({});
- const onProjectCreate = jest.fn();
- renderManualProjectCreate({ onProjectCreate });
+ jest.mocked(doesComponentExists).mockRejectedValueOnce({});
+ renderManualProjectCreate();
// All input valid
await user.click(
@@ -199,6 +172,6 @@ it('should handle component exists failure', async () => {
function renderManualProjectCreate(props: Partial<ManualProjectCreate['props']> = {}) {
renderComponent(
- <ManualProjectCreate branchesEnabled={false} onProjectCreate={jest.fn()} {...props} />
+ <ManualProjectCreate branchesEnabled={false} onProjectSetupDone={jest.fn()} {...props} />
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
index f2c681e6164..ef344ed928d 100644
--- a/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
@@ -21,26 +21,25 @@ import classNames from 'classnames';
import { debounce, isEmpty } from 'lodash';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { createProject, doesComponentExists } from '../../../../api/components';
+import { doesComponentExists, setupManualProjectCreation } from '../../../../api/components';
import { getValue } from '../../../../api/settings';
import DocLink from '../../../../components/common/DocLink';
import ProjectKeyInput from '../../../../components/common/ProjectKeyInput';
import ValidationInput from '../../../../components/controls/ValidationInput';
import { SubmitButton } from '../../../../components/controls/buttons';
import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import MandatoryFieldsExplanation from '../../../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../../../helpers/l10n';
import { PROJECT_KEY_INVALID_CHARACTERS, validateProjectKey } from '../../../../helpers/projects';
import { ProjectKeyValidationResult } from '../../../../types/component';
import { GlobalSettingKeys } from '../../../../types/settings';
import CreateProjectPageHeader from '../components/CreateProjectPageHeader';
-import InstanceNewCodeDefinitionComplianceWarning from '../components/InstanceNewCodeDefinitionComplianceWarning';
import { PROJECT_NAME_MAX_LEN } from '../constants';
+import { CreateProjectApiCallback } from '../types';
interface Props {
branchesEnabled: boolean;
- onProjectCreate: (projectKey: string) => void;
+ onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
}
interface State {
@@ -54,7 +53,6 @@ interface State {
mainBranchName: string;
mainBranchNameError?: string;
mainBranchNameTouched: boolean;
- submitting: boolean;
}
const DEBOUNCE_DELAY = 250;
@@ -69,7 +67,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
this.state = {
projectKey: '',
projectName: '',
- submitting: false,
projectKeyTouched: false,
projectNameTouched: false,
mainBranchName: 'main',
@@ -132,18 +129,12 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
event.preventDefault();
const { projectKey, projectName, mainBranchName } = this.state;
if (this.canSubmit(this.state)) {
- this.setState({ submitting: true });
- createProject({
- project: projectKey,
- name: (projectName || projectKey).trim(),
- mainBranch: mainBranchName,
- }).then(
- ({ project }) => this.props.onProjectCreate(project.key),
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- }
- }
+ this.props.onProjectSetupDone(
+ setupManualProjectCreation({
+ project: projectKey,
+ name: (projectName || projectKey).trim(),
+ mainBranch: mainBranchName,
+ })
);
}
};
@@ -221,7 +212,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
mainBranchName,
mainBranchNameError,
mainBranchNameTouched,
- submitting,
} = this.state;
const { branchesEnabled } = this.props;
@@ -235,8 +225,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
<>
<CreateProjectPageHeader title={translate('onboarding.create_project.setup_manually')} />
- <InstanceNewCodeDefinitionComplianceWarning />
-
<form id="create-project-manual" onSubmit={this.handleFormSubmit}>
<MandatoryFieldsExplanation className="big-spacer-bottom" />
@@ -308,10 +296,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
/>
</ValidationInput>
- <SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
- {translate('set_up')}
- </SubmitButton>
- <DeferredSpinner className="spacer-left" loading={submitting} />
+ <SubmitButton disabled={!this.canSubmit(this.state)}>{translate('next')}</SubmitButton>
</form>
{branchesEnabled && (
diff --git a/server/sonar-web/src/main/js/apps/create/project/types.ts b/server/sonar-web/src/main/js/apps/create/project/types.ts
index f8c9fc9b054..ddd7c3700a4 100644
--- a/server/sonar-web/src/main/js/apps/create/project/types.ts
+++ b/server/sonar-web/src/main/js/apps/create/project/types.ts
@@ -17,6 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ProjectBase } from '../../../api/components';
+import { NewCodePeriodSettingType } from '../../../types/types';
+
export enum CreateProjectModes {
Manual = 'manual',
AzureDevOps = 'azure',
@@ -25,3 +28,8 @@ export enum CreateProjectModes {
GitHub = 'github',
GitLab = 'gitlab',
}
+
+export type CreateProjectApiCallback = (
+ newCodeDefinitionType?: NewCodePeriodSettingType,
+ newCodeDefinitionValue?: string
+) => Promise<{ project: ProjectBase }>;