]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19453 New code definition is made part of the manual project creation (#8486)
authorAndrey Luiz <andrey.luiz@sonarsource.com>
Fri, 9 Jun 2023 13:58:56 +0000 (15:58 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 14 Jun 2023 09:51:06 +0000 (09:51 +0000)
20 files changed:
server/sonar-web/src/main/js/api/components.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 [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/types.ts
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 60d38ed488c64bc4ce09cfa12c171a0b0874a124..7c05e3ec95c2b6a40418204e3f9561a3142839d9 100644 (file)
@@ -97,11 +97,27 @@ export function deletePortfolio(portfolio: string): Promise<void | Response> {
   return post('/api/views/delete', { key: portfolio }).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 createProject(data: {
   name: string;
   project: string;
   mainBranch: string;
   visibility?: Visibility;
+  newCodeDefinitionType?: string;
+  newCodeDefinitionValue?: string;
 }): Promise<{ project: ProjectBase }> {
   return postJSON('/api/projects/create', data).catch(throwGlobalError);
 }
index 53d317ad3afec7c89d2f21a24705a734c40a4fbf..379b9a2b27b89e56116913ca70a9fb5bd1b38c02 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 { 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 (file)
index 0000000..06bb6cb
--- /dev/null
@@ -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} />);
+}
index 98a05291a2ad99eece98da71b709432b927c0f19..8bcecd0dd45eeed53243190e78e340d6f7da0585 100644 (file)
 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} />
   );
 }
index f2c681e616487ed75155b34cfa590d256e7babfd..ef344ed928d9d1180e455f628fa72b0552b98f87 100644 (file)
@@ -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 && (
index f8c9fc9b05401ee552f9487f54780572c0e3a73c..ddd7c3700a468172cb49880d5d9c28fd17086954 100644 (file)
@@ -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 }>;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx
deleted file mode 100644 (file)
index b92f1de..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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 * as React from 'react';
-import RadioCard from '../../../components/controls/RadioCard';
-import ValidationInput, {
-  ValidationInputErrorPlacement,
-} from '../../../components/controls/ValidationInput';
-import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { MAX_NUMBER_OF_DAYS, MIN_NUMBER_OF_DAYS } from '../../../helpers/periods';
-import { NewCodePeriodSettingType } from '../../../types/types';
-
-export interface Props {
-  className?: string;
-  days: string;
-  disabled?: boolean;
-  isChanged: boolean;
-  isValid: boolean;
-  onChangeDays: (value: string) => void;
-  onSelect: (selection: NewCodePeriodSettingType) => void;
-  selected: boolean;
-}
-
-export default function BaselineSettingDays(props: Props) {
-  const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props;
-
-  return (
-    <RadioCard
-      className={className}
-      disabled={disabled}
-      onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)}
-      selected={selected}
-      title={translate('baseline.number_days')}
-    >
-      <>
-        <div>
-          <p className="sw-mb-3">{translate('baseline.number_days.description')}</p>
-          <p className="sw-mb-4">{translate('baseline.number_days.usecase')}</p>
-        </div>
-        {selected && (
-          <>
-            <MandatoryFieldsExplanation />
-
-            <ValidationInput
-              labelHtmlFor="baseline_number_of_days"
-              isInvalid={!isValid}
-              isValid={isChanged && isValid}
-              errorPlacement={ValidationInputErrorPlacement.Bottom}
-              error={translateWithParameters(
-                'baseline.number_days.invalid',
-                MIN_NUMBER_OF_DAYS,
-                MAX_NUMBER_OF_DAYS
-              )}
-              label={translate('baseline.specify_days')}
-              required
-            >
-              <input
-                id="baseline_number_of_days"
-                onChange={(e) => onChangeDays(e.currentTarget.value)}
-                type="text"
-                value={days}
-              />
-            </ValidationInput>
-          </>
-        )}
-      </>
-    </RadioCard>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx
deleted file mode 100644 (file)
index f2e2656..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 * as React from 'react';
-import RadioCard from '../../../components/controls/RadioCard';
-import { translate } from '../../../helpers/l10n';
-import { NewCodePeriodSettingType } from '../../../types/types';
-
-export interface Props {
-  disabled?: boolean;
-  isDefault?: boolean;
-  onSelect: (selection: NewCodePeriodSettingType) => void;
-  selected: boolean;
-}
-
-export default function BaselineSettingPreviousVersion(props: Props) {
-  const { disabled, isDefault, onSelect, selected } = props;
-  return (
-    <RadioCard
-      disabled={disabled}
-      onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)}
-      selected={selected}
-      title={
-        translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '')
-      }
-    >
-      <div>
-        <p>{translate('baseline.previous_version.description')}</p>
-        <p className="sw-mt-3">{translate('baseline.previous_version.usecase')}</p>
-      </div>
-    </RadioCard>
-  );
-}
index 5dcca14156dca00f2a00d06d635449e66deecf99..c60deb34b089e1f20fe36f47c8278e562ce1ba39 100644 (file)
  */
 import * as React from 'react';
 import { setNewCodePeriod } from '../../../api/newCodePeriod';
-import Modal from '../../../components/controls/Modal';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import Modal from '../../../components/controls/Modal';
+import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption';
 import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { toISO8601WithOffsetString } from '../../../helpers/dates';
@@ -30,8 +32,6 @@ import { ParsedAnalysis } from '../../../types/project-activity';
 import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types';
 import { getSettingValue, validateSetting } from '../utils';
 import BaselineSettingAnalysis from './BaselineSettingAnalysis';
-import BaselineSettingDays from './BaselineSettingDays';
-import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
 import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
 import BranchAnalysisList from './BranchAnalysisList';
 
@@ -170,12 +170,12 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
               level="branch"
             />
             <div className="display-flex-row huge-spacer-bottom" role="radiogroup">
-              <BaselineSettingPreviousVersion
+              <NewCodeDefinitionPreviousVersionOption
                 isDefault={false}
                 onSelect={this.handleSelectSetting}
                 selected={selected === NewCodePeriodSettingType.PREVIOUS_VERSION}
               />
-              <BaselineSettingDays
+              <NewCodeDefinitionDaysOption
                 days={days}
                 isChanged={isChanged}
                 isValid={isValid}
index a386e12a2d1f4c3e5a170baa265b8fb3b41c6227..39197e19dd25f4cb9a664001164e660cca99f61d 100644 (file)
@@ -24,9 +24,9 @@ import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
 import WarningIcon from '../../../components/icons/WarningIcon';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
 import { BranchWithNewCodePeriod } from '../../../types/branch-like';
 import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types';
-import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
 
 export interface BranchListRowProps {
   branch: BranchWithNewCodePeriod;
@@ -50,9 +50,9 @@ function renderNewCodePeriodSetting(newCodePeriod: NewCodePeriod) {
         </>
       );
     case NewCodePeriodSettingType.NUMBER_OF_DAYS:
-      return `${translate('baseline.number_days')}: ${newCodePeriod.value}`;
+      return `${translate('new_code_definition.number_days')}: ${newCodePeriod.value}`;
     case NewCodePeriodSettingType.PREVIOUS_VERSION:
-      return translate('baseline.previous_version');
+      return translate('new_code_definition.previous_version');
     case NewCodePeriodSettingType.REFERENCE_BRANCH:
       return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
     default:
index 06503b77976015722695d7b81a6b6ee50ca734a0..f1104622cfeadd216cf83406b8a2607a51e46738 100644 (file)
  */
 import classNames from 'classnames';
 import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import Link from '../../../components/common/Link';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Radio from '../../../components/controls/Radio';
 import Tooltip from '../../../components/controls/Tooltip';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import GlobalNewCodeDefinitionDescription from '../../../components/new-code-definition/GlobalNewCodeDefinitionDescription';
+import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption';
 import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning';
 import { Alert } from '../../../components/ui/Alert';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
 import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
 import { Branch } from '../../../types/branch-like';
 import { ParsedAnalysis } from '../../../types/project-activity';
 import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types';
 import { validateSetting } from '../utils';
 import BaselineSettingAnalysis from './BaselineSettingAnalysis';
-import BaselineSettingDays from './BaselineSettingDays';
-import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
 import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
 import BranchAnalysisList from './BranchAnalysisList';
 
@@ -63,33 +62,6 @@ export interface ProjectBaselineSelectorProps {
   overrideGeneralSetting: boolean;
 }
 
-function renderGeneralSetting(generalSetting: NewCodePeriod) {
-  let setting: string;
-  let description: string;
-  let useCase: string;
-  if (generalSetting.type === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
-    setting = `${translate('baseline.number_days')} (${translateWithParameters(
-      'duration.days',
-      generalSetting.value || '?'
-    )})`;
-    description = translate('baseline.number_days.description');
-    useCase = translate('baseline.number_days.usecase');
-  } else {
-    setting = translate('baseline.previous_version');
-    description = translate('baseline.previous_version.description');
-    useCase = translate('baseline.previous_version.usecase');
-  }
-
-  return (
-    <div className="general-setting display-flex-start">
-      <span className="sw-font-bold flex-0">{setting}:&nbsp;</span>
-      <span>
-        {description} {useCase}
-      </span>
-    </div>
-  );
-}
-
 function branchToOption(b: Branch) {
   return { label: b.name, value: b.name, isMain: b.isMain };
 }
@@ -112,7 +84,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
     selected,
   } = props;
 
-  const isGeneralSettingCompliant = isNewCodeDefinitionCompliant(generalSetting);
+  const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
 
   const { isChanged, isValid } = validateSetting({
     analysis,
@@ -130,13 +102,13 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
         <Radio
           checked={!overrideGeneralSetting}
           className="big-spacer-bottom"
-          disabled={!isGeneralSettingCompliant}
+          disabled={!isGlobalNcdCompliant}
           onCheck={() => props.onToggleSpecificSetting(false)}
           value="general"
         >
           <Tooltip
             overlay={
-              isGeneralSettingCompliant
+              isGlobalNcdCompliant
                 ? null
                 : translate('project_baseline.compliance.warning.title.global')
             }
@@ -145,34 +117,12 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
           </Tooltip>
         </Radio>
 
-        <div className="big-spacer-left">
-          {!isGeneralSettingCompliant && (
-            <Alert variant="warning" className="sw-mb-4 sw-max-w-[800px]">
-              <p className="sw-mb-2 sw-font-bold">
-                {translate('project_baseline.compliance.warning.title.global')}
-              </p>
-              <p className="sw-mb-2">
-                {canAdmin ? (
-                  <FormattedMessage
-                    id="project_baseline.compliance.warning.explanation.admin"
-                    defaultMessage={translate(
-                      'project_baseline.compliance.warning.explanation.admin'
-                    )}
-                    values={{
-                      link: (
-                        <Link to="/admin/settings?category=new_code_period">
-                          {translate('project_baseline.warning.explanation.action.admin.link')}
-                        </Link>
-                      ),
-                    }}
-                  />
-                ) : (
-                  translate('project_baseline.compliance.warning.explanation')
-                )}
-              </p>
-            </Alert>
-          )}
-          {renderGeneralSetting(generalSetting)}
+        <div className="sw-ml-4">
+          <GlobalNewCodeDefinitionDescription
+            globalNcd={generalSetting}
+            isGlobalNcdCompliant={isGlobalNcdCompliant}
+            canAdmin={canAdmin}
+          />
         </div>
 
         <Radio
@@ -193,14 +143,14 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
           level="project"
         />
         <div className="display-flex-row big-spacer-bottom" role="radiogroup">
-          <BaselineSettingPreviousVersion
+          <NewCodeDefinitionPreviousVersionOption
             disabled={!overrideGeneralSetting}
             onSelect={props.onSelectSetting}
             selected={
               overrideGeneralSetting && selected === NewCodePeriodSettingType.PREVIOUS_VERSION
             }
           />
-          <BaselineSettingDays
+          <NewCodeDefinitionDaysOption
             days={days}
             disabled={!overrideGeneralSetting}
             isChanged={isChanged}
index 6cf0cc88d26ddf6f38c8c584c8670f5b2e98ec5c..c51eb64863a518d8ab09d85fa6bfc0b63b07a89e 100644 (file)
@@ -29,8 +29,8 @@ import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-period';
 import { mockAppState } from '../../../../helpers/testMocks';
 import {
-  RenderContext,
   renderAppWithComponentContext,
+  RenderContext,
 } from '../../../../helpers/testReactTestingUtils';
 import { Feature } from '../../../../types/features';
 import { NewCodePeriodSettingType } from '../../../../types/types';
@@ -213,7 +213,9 @@ it('can set a previous version setting for branch', async () => {
   await ui.appIsLoaded();
   await ui.setBranchPreviousVersionSetting('main');
 
-  expect(within(byRole('table').get()).getByText('baseline.previous_version')).toBeInTheDocument();
+  expect(
+    within(byRole('table').get()).getByText('new_code_definition.previous_version')
+  ).toBeInTheDocument();
 
   await user.click(await ui.branchActionsButton('main').find());
 
@@ -234,7 +236,9 @@ it('can set a number of days setting for branch', async () => {
 
   await ui.setBranchNumberOfDaysSetting('main', '15');
 
-  expect(within(byRole('table').get()).getByText('baseline.number_days: 15')).toBeInTheDocument();
+  expect(
+    within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
+  ).toBeInTheDocument();
 });
 
 it('cannot set a specific analysis setting for branch', async () => {
@@ -295,8 +299,10 @@ function getPageObjects() {
     generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }),
     generalSettingRadio: byRole('radio', { name: 'project_baseline.global_setting' }),
     specificSettingRadio: byRole('radio', { name: 'project_baseline.specific_setting' }),
-    previousVersionRadio: byRole('radio', { name: /baseline.previous_version.description/ }),
-    numberDaysRadio: byRole('radio', { name: /baseline.number_days.description/ }),
+    previousVersionRadio: byRole('radio', {
+      name: /new_code_definition.previous_version.description/,
+    }),
+    numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
     numberDaysInput: byRole('textbox'),
     referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
     chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
@@ -311,8 +317,8 @@ function getPageObjects() {
     editButton: byRole('button', { name: 'edit' }),
     resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
     saved: byText('settings.state.saved'),
-    complianceWarningAdmin: byText('project_baseline.compliance.warning.explanation.admin'),
-    complianceWarning: byText('project_baseline.compliance.warning.explanation'),
+    complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
+    complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
   };
 
   async function appIsLoaded() {
index aea830a6d97ebab25f798365dcfeb66682047b12..96498f2fd8a1639b67160cf2233afb95796a4b2e 100644 (file)
@@ -23,13 +23,13 @@ import { getNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
 import DocLink from '../../../components/common/DocLink';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption';
 import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { translate } from '../../../helpers/l10n';
 import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
 import { NewCodePeriodSettingType } from '../../../types/types';
-import BaselineSettingDays from '../../projectBaseline/components/BaselineSettingDays';
-import BaselineSettingPreviousVersion from '../../projectBaseline/components/BaselineSettingPreviousVersion';
 
 interface State {
   currentSetting?: NewCodePeriodSettingType;
@@ -184,12 +184,12 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
                   <div className="settings-definition-right">
                     <DeferredSpinner loading={loading} timeout={500}>
                       <form onSubmit={this.onSubmit}>
-                        <BaselineSettingPreviousVersion
+                        <NewCodeDefinitionPreviousVersionOption
                           isDefault
                           onSelect={this.onSelectSetting}
                           selected={selected === NewCodePeriodSettingType.PREVIOUS_VERSION}
                         />
-                        <BaselineSettingDays
+                        <NewCodeDefinitionDaysOption
                           className="spacer-top sw-mb-4"
                           days={days}
                           isChanged={isChanged}
index 640829ede7079b9a35cbd8ff5523548c93b9388d..406b219b4706e94deb1510ffcdcab62786b1d42a 100644 (file)
@@ -39,9 +39,9 @@ afterEach(() => {
 const ui = {
   newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }),
   savedMsg: byText('settings.state.saved'),
-  prevVersionRadio: byRole('radio', { name: /baseline.previous_version/ }),
-  daysNumberRadio: byRole('radio', { name: /baseline.number_days/ }),
-  daysNumberErrorMessage: byText('baseline.number_days.invalid', { exact: false }),
+  prevVersionRadio: byRole('radio', { name: /new_code_definition.previous_version/ }),
+  daysNumberRadio: byRole('radio', { name: /new_code_definition.number_days/ }),
+  daysNumberErrorMessage: byText('new_code_definition.number_days.invalid', { exact: false }),
   daysInput: byRole('textbox'),
   saveButton: byRole('button', { name: 'save' }),
   cancelButton: byRole('button', { name: 'cancel' }),
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
new file mode 100644 (file)
index 0000000..2751b70
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * 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 * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { NewCodePeriod, NewCodePeriodSettingType } from '../../types/types';
+import Link from '../common/Link';
+import { Alert } from '../ui/Alert';
+
+interface Props {
+  globalNcd: NewCodePeriod;
+  isGlobalNcdCompliant: boolean;
+  canAdmin?: boolean;
+}
+
+export default function GlobalNewCodeDefinitionDescription({
+  globalNcd,
+  isGlobalNcdCompliant,
+  canAdmin,
+}: Props) {
+  let setting: string;
+  let description: string;
+  let useCase: string;
+  if (globalNcd.type === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
+    setting = `${translate('new_code_definition.number_days')} (${translateWithParameters(
+      'duration.days',
+      globalNcd.value ?? '?'
+    )})`;
+    description = translate('new_code_definition.number_days.description');
+    useCase = translate('new_code_definition.number_days.usecase');
+  } else {
+    setting = translate('new_code_definition.previous_version');
+    description = translate('new_code_definition.previous_version.description');
+    useCase = translate('new_code_definition.previous_version.usecase');
+  }
+
+  return (
+    <>
+      <div className="general-setting display-flex-start">
+        <span className="sw-font-bold flex-0">{setting}:&nbsp;</span>
+        <span>
+          {description} {useCase}
+        </span>
+      </div>
+      {!isGlobalNcdCompliant && (
+        <Alert variant="warning" className="sw-mt-4 sw-max-w-[800px]">
+          <p className="sw-mb-2 sw-font-bold">
+            {translate('new_code_definition.compliance.warning.title.global')}
+          </p>
+          <p className="sw-mb-2">
+            {canAdmin ? (
+              <FormattedMessage
+                id="new_code_definition.compliance.warning.explanation.admin"
+                defaultMessage={translate(
+                  'new_code_definition.compliance.warning.explanation.admin'
+                )}
+                values={{
+                  link: (
+                    <Link to="/admin/settings?category=new_code_period">
+                      {translate(
+                        'new_code_definition.compliance.warning.explanation.action.admin.link'
+                      )}
+                    </Link>
+                  ),
+                }}
+              />
+            ) : (
+              translate('new_code_definition.compliance.warning.explanation')
+            )}
+          </p>
+        </Alert>
+      )}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
new file mode 100644 (file)
index 0000000..e648230
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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 * as React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { MAX_NUMBER_OF_DAYS, MIN_NUMBER_OF_DAYS } from '../../helpers/periods';
+import { NewCodePeriodSettingType } from '../../types/types';
+import RadioCard from '../controls/RadioCard';
+import ValidationInput, { ValidationInputErrorPlacement } from '../controls/ValidationInput';
+import MandatoryFieldsExplanation from '../ui/MandatoryFieldsExplanation';
+
+export interface Props {
+  className?: string;
+  days: string;
+  disabled?: boolean;
+  isChanged: boolean;
+  isValid: boolean;
+  onChangeDays: (value: string) => void;
+  onSelect: (selection: NewCodePeriodSettingType) => void;
+  selected: boolean;
+}
+
+export default function NewCodeDefinitionDaysOption(props: Props) {
+  const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props;
+
+  return (
+    <RadioCard
+      className={className}
+      disabled={disabled}
+      onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)}
+      selected={selected}
+      title={translate('new_code_definition.number_days')}
+    >
+      <>
+        <div>
+          <p className="sw-mb-3">{translate('new_code_definition.number_days.description')}</p>
+          <p className="sw-mb-4">{translate('new_code_definition.number_days.usecase')}</p>
+        </div>
+        {selected && (
+          <>
+            <MandatoryFieldsExplanation />
+
+            <ValidationInput
+              labelHtmlFor="baseline_number_of_days"
+              isInvalid={!isValid}
+              isValid={isChanged && isValid}
+              errorPlacement={ValidationInputErrorPlacement.Bottom}
+              error={translateWithParameters(
+                'new_code_definition.number_days.invalid',
+                MIN_NUMBER_OF_DAYS,
+                MAX_NUMBER_OF_DAYS
+              )}
+              label={translate('new_code_definition.number_days.specify_days')}
+              required
+            >
+              <input
+                id="baseline_number_of_days"
+                onChange={(e) => onChangeDays(e.currentTarget.value)}
+                type="text"
+                value={days}
+              />
+            </ValidationInput>
+          </>
+        )}
+      </>
+    </RadioCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx
new file mode 100644 (file)
index 0000000..263dbc0
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { NewCodePeriodSettingType } from '../../types/types';
+import RadioCard from '../controls/RadioCard';
+
+interface Props {
+  disabled?: boolean;
+  isDefault?: boolean;
+  onSelect: (selection: NewCodePeriodSettingType) => void;
+  selected: boolean;
+}
+
+export default function NewCodeDefinitionPreviousVersionOption({
+  disabled,
+  isDefault,
+  onSelect,
+  selected,
+}: Props) {
+  return (
+    <RadioCard
+      disabled={disabled}
+      onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)}
+      selected={selected}
+      title={
+        translate('new_code_definition.previous_version') +
+        (isDefault ? ` (${translate('default')})` : '')
+      }
+    >
+      <div>
+        <p>{translate('new_code_definition.previous_version.description')}</p>
+        <p className="sw-mt-3">{translate('new_code_definition.previous_version.usecase')}</p>
+      </div>
+    </RadioCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
new file mode 100644 (file)
index 0000000..a6d8887
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * 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 { noop } from 'lodash';
+import * as React from 'react';
+import { useEffect } from 'react';
+import { getNewCodePeriod } from '../../api/newCodePeriod';
+import { translate } from '../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../helpers/periods';
+import {
+  NewCodePeriod,
+  NewCodePeriodSettingType,
+  NewCodePeriodWithCompliance,
+} from '../../types/types';
+import Radio from '../controls/Radio';
+import RadioCard from '../controls/RadioCard';
+import Tooltip from '../controls/Tooltip';
+import { Alert } from '../ui/Alert';
+import GlobalNewCodeDefinitionDescription from './GlobalNewCodeDefinitionDescription';
+import NewCodeDefinitionDaysOption from './NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from './NewCodeDefinitionPreviousVersionOption';
+
+interface Props {
+  canAdmin: boolean | undefined;
+  onNcdChanged: (ncd: NewCodePeriodWithCompliance) => void;
+}
+
+const INITIAL_DAYS = '30';
+
+export default function NewCodeDefinitionSelector(props: Props) {
+  const { canAdmin, onNcdChanged } = props;
+
+  const [globalNcd, setGlobalNcd] = React.useState<NewCodePeriod | null>(null);
+  const [selectedNcdType, setSelectedNcdType] = React.useState<NewCodePeriodSettingType | null>(
+    null
+  );
+  const [days, setDays] = React.useState<string>(INITIAL_DAYS);
+
+  const iGlobalNcdCompliant = React.useMemo(
+    () => Boolean(globalNcd && isNewCodeDefinitionCompliant(globalNcd)),
+    [globalNcd]
+  );
+
+  const isChanged = React.useMemo(
+    () => selectedNcdType === NewCodePeriodSettingType.NUMBER_OF_DAYS && days !== INITIAL_DAYS,
+    [selectedNcdType, days]
+  );
+
+  const isCompliant = React.useMemo(
+    () =>
+      !!selectedNcdType &&
+      isNewCodeDefinitionCompliant({
+        type: selectedNcdType,
+        value: days,
+      }),
+    [selectedNcdType, days]
+  );
+
+  useEffect(() => {
+    function fetchGlobalNcd() {
+      getNewCodePeriod().then(setGlobalNcd, noop);
+    }
+
+    fetchGlobalNcd();
+  }, []);
+
+  useEffect(() => {
+    if (selectedNcdType) {
+      const type =
+        selectedNcdType === NewCodePeriodSettingType.INHERITED ? undefined : selectedNcdType;
+      const value = selectedNcdType === NewCodePeriodSettingType.NUMBER_OF_DAYS ? days : undefined;
+      onNcdChanged({ isCompliant, type, value });
+    }
+  }, [selectedNcdType, days, isCompliant, onNcdChanged]);
+
+  return (
+    <>
+      <p className="sw-mt-10">
+        <strong>{translate('new_code_definition.question')}</strong>
+      </p>
+      <div className="big-spacer-top spacer-bottom" role="radiogroup">
+        <Radio
+          ariaLabel={translate('new_code_definition.global_setting')}
+          checked={selectedNcdType === NewCodePeriodSettingType.INHERITED}
+          className="big-spacer-bottom"
+          disabled={!iGlobalNcdCompliant}
+          onCheck={() => setSelectedNcdType(NewCodePeriodSettingType.INHERITED)}
+          value="general"
+        >
+          <Tooltip
+            overlay={
+              iGlobalNcdCompliant
+                ? null
+                : translate('new_code_definition.compliance.warning.title.global')
+            }
+          >
+            <span>{translate('new_code_definition.global_setting')}</span>
+          </Tooltip>
+        </Radio>
+
+        <div className="sw-ml-4">
+          {globalNcd && (
+            <GlobalNewCodeDefinitionDescription
+              globalNcd={globalNcd}
+              isGlobalNcdCompliant={iGlobalNcdCompliant}
+              canAdmin={canAdmin}
+            />
+          )}
+        </div>
+
+        <Radio
+          ariaLabel={translate('new_code_definition.specific_setting')}
+          checked={Boolean(
+            selectedNcdType && selectedNcdType !== NewCodePeriodSettingType.INHERITED
+          )}
+          className="huge-spacer-top"
+          onCheck={() => setSelectedNcdType(NewCodePeriodSettingType.PREVIOUS_VERSION)}
+          value="specific"
+        >
+          {translate('new_code_definition.specific_setting')}
+        </Radio>
+      </div>
+
+      <div className="big-spacer-left big-spacer-right project-baseline-setting">
+        <div className="display-flex-row big-spacer-bottom" role="radiogroup">
+          <NewCodeDefinitionPreviousVersionOption
+            disabled={Boolean(
+              !selectedNcdType || selectedNcdType === NewCodePeriodSettingType.INHERITED
+            )}
+            onSelect={setSelectedNcdType}
+            selected={selectedNcdType === NewCodePeriodSettingType.PREVIOUS_VERSION}
+          />
+
+          <NewCodeDefinitionDaysOption
+            days={days}
+            disabled={Boolean(
+              !selectedNcdType || selectedNcdType === NewCodePeriodSettingType.INHERITED
+            )}
+            isChanged={isChanged}
+            isValid={isCompliant}
+            onChangeDays={setDays}
+            onSelect={setSelectedNcdType}
+            selected={selectedNcdType === NewCodePeriodSettingType.NUMBER_OF_DAYS}
+          />
+
+          <RadioCard
+            disabled={Boolean(
+              !selectedNcdType || selectedNcdType === NewCodePeriodSettingType.INHERITED
+            )}
+            onClick={() => setSelectedNcdType(NewCodePeriodSettingType.REFERENCE_BRANCH)}
+            selected={selectedNcdType === NewCodePeriodSettingType.REFERENCE_BRANCH}
+            title={translate('new_code_definition.reference_branch')}
+          >
+            <div>
+              <p className="sw-mb-3">
+                {translate('new_code_definition.reference_branch.description')}
+              </p>
+              <p className="sw-mb-4">{translate('new_code_definition.reference_branch.usecase')}</p>
+              {selectedNcdType === NewCodePeriodSettingType.REFERENCE_BRANCH && (
+                <Alert variant="info">
+                  {translate('new_code_definition.reference_branch.notice')}
+                </Alert>
+              )}
+            </div>
+          </RadioCard>
+        </div>
+      </div>
+    </>
+  );
+}
index cd5fcc34769d87d9d7659426206b289a8ddcbb23..f31661e0c7f551e4774a6da44e44ea31b03a51b9 100644 (file)
@@ -404,6 +404,12 @@ export interface NewCodePeriod {
   inherited?: boolean;
 }
 
+export interface NewCodePeriodWithCompliance {
+  type?: NewCodePeriodSettingType;
+  value?: string;
+  isCompliant: boolean;
+}
+
 export interface NewCodePeriodBranch extends NewCodePeriod {
   projectKey: string;
   branchKey: string;
@@ -414,6 +420,7 @@ export enum NewCodePeriodSettingType {
   NUMBER_OF_DAYS = 'NUMBER_OF_DAYS',
   SPECIFIC_ANALYSIS = 'SPECIFIC_ANALYSIS',
   REFERENCE_BRANCH = 'REFERENCE_BRANCH',
+  INHERITED = 'INHERITED',
 }
 
 export interface Paging {
@@ -617,6 +624,7 @@ export interface Snippet {
 export interface SnippetGroup extends SnippetsByComponent {
   locations: FlowLocation[];
 }
+
 export interface SnippetsByComponent {
   component: SourceViewerFile;
   sources: { [line: number]: SourceLine };
index f90095377ee1021f7cf946b2cac31139830ad03d..7bcc51b0e611e2e97085336030a822682dd950b6 100644 (file)
@@ -136,6 +136,7 @@ name=Name
 navigation=Navigation
 never=Never
 new=New
+next=Next
 new_name=New name
 next_=next
 none=None
@@ -645,13 +646,6 @@ project_baseline.compliance.warning.explanation=Please ask an administrator to u
 project_baseline.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it.
 project_baseline.warning.explanation.action.admin.link=General Settings > New Code
 
-baseline.previous_version=Previous version
-baseline.previous_version.usecase=Recommended for projects following regular versions or releases.
-baseline.previous_version.description=Any code that has changed since the previous version is considered new code.
-baseline.number_days=Number of days
-baseline.number_days.usecase=Recommended for projects following continuous delivery.
-baseline.number_days.description=Any code that has changed in the last x days is considered new code. If no action is taken on a new issue after x days, this issue will become part of the overall code.
-baseline.number_days.invalid=Please provide a whole number between {0} and {1}
 baseline.number_days.compliance_warning.title=Your new code definition is not compliant with the Clean as You Code methodology
 baseline.number_days.compliance_warning.content.global=We recommend that you update this new code definition so that new projects and existing projects that do not use a specific New Code definition benefit from the Clean as You Code methodology by default.
 baseline.number_days.compliance_warning.content.project=We recommend that you update this new code definition so that your project benefits from the Clean as You Code methodology.
@@ -669,7 +663,6 @@ baseline.reference_branch.description=Choose a branch as the baseline for the ne
 baseline.reference_branch.usecase=Recommended for projects using feature branches.
 baseline.reference_branch.description2=The branch you select as the reference branch will need its own new code definition to prevent it from using itself as a reference.
 
-baseline.specify_days=Specify a number of days
 baseline.last_analysis_before=Last analysis before
 baseline.next_analysis_notice=Changes will take effect after the next analysis
 
@@ -3751,6 +3744,35 @@ footer.version_x=Version {0}
 footer.web_api=Web API
 
 
+#------------------------------------------------------------------------------
+#
+# NEW CODE DEFINITION
+#
+#------------------------------------------------------------------------------
+new_code_definition.question=What should be the baseline for new code for this project?
+new_code_definition.global_setting=Use the global setting
+new_code_definition.specific_setting=Define a specific setting for this project
+
+new_code_definition.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology
+new_code_definition.compliance.warning.explanation=Please ask an administrator to update the global new code definition before switching back to it.
+new_code_definition.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it.
+new_code_definition.compliance.warning.explanation.action.admin.link=General Settings > New Code
+
+new_code_definition.previous_version=Previous version
+new_code_definition.previous_version.usecase=Recommended for projects following regular versions or releases.
+new_code_definition.previous_version.description=Any code that has changed since the previous version is considered new code.
+
+new_code_definition.number_days=Number of days
+new_code_definition.number_days.specify_days=Specify a number of days
+new_code_definition.number_days.usecase=Recommended for projects following continuous delivery.
+new_code_definition.number_days.description=Any code that has changed in the last x days is considered new code. If no action is taken on a new issue after x days, this issue will become part of the overall code.
+new_code_definition.number_days.invalid=Please provide a whole number between {0} and {1}
+
+new_code_definition.reference_branch=Reference branch
+new_code_definition.reference_branch.description=Choose a branch as the baseline for the new code.
+new_code_definition.reference_branch.usecase=Recommended for projects using feature branches.
+new_code_definition.reference_branch.notice=The main branch will be set as the reference branch when the project is created. You will be able to choose another branch as the reference branch when your project will have more branches.
+
 #------------------------------------------------------------------------------
 #
 # ONBOARDING
@@ -3890,6 +3912,14 @@ onboarding.create_project.gitlab.title=Gitlab project onboarding
 onboarding.create_project.gitlab.no_projects=No projects could be fetched from Gitlab. Contact your system administrator, or {link}.
 onboarding.create_project.gitlab.link=See on GitLab
 
+onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
+onboarding.create_project.new_code_definition=New Code
+onboarding.create_project.new_code_definition.description=The new code definition sets which part of your code will be considered new code.
+onboarding.create_project.new_code_definition.description1=This helps you focus attention on the most recent changes to your project, enabling you to follow the Clean as You Code methodology.
+onboarding.create_project.new_code_definition.description2=Learn more: {link}
+onboarding.create_project.new_code_definition.description2.link=Defining New Code
+onboarding.create_project.new_code_definition.create_project=Create project
+
 onboarding.create_project.new_code_option.warning.title=Your global new code definition is not compliant with the Clean as You Code methodology
 onboarding.create_project.new_code_option.warning.explanation=New projects use the global new code definition by default. {action} so that new projects benefit from the Clean as You Code methodology by default.
 onboarding.create_project.new_code_option.warning.explanation.action=We recommend that you ask an administrator of this SonarQube instance to update the global new code definition