]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20156 Rename new code period to new code definition (#9110)
authorAndrey Luiz <andrey.luiz@sonarsource.com>
Mon, 21 Aug 2023 12:37:56 +0000 (14:37 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 24 Aug 2023 20:03:07 +0000 (20:03 +0000)
Co-authored-by: Ambroise C <ambroise.christea@sonarsource.com>
50 files changed:
server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts [deleted file]
server/sonar-web/src/main/js/api/newCodeDefinition.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/newCodePeriod.ts [deleted file]
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/styles.css [deleted file]
server/sonar-web/src/main/js/apps/projectBaseline/utils.ts [deleted file]
server/sonar-web/src/main/js/apps/projectNewCode/components/AppHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchAnalysisList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchAnalysisListRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchNewCodeDefinitionSettingModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingAnalysis.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/utils-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/routes.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectNewCode/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx [deleted file]
server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
server/sonar-web/src/main/js/components/new-code-definition/__tests__/NCDAutoUpdateMessage-test.tsx

diff --git a/server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts
new file mode 100644 (file)
index 0000000..147246c
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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 { cloneDeep } from 'lodash';
+import {
+  mockNewCodePeriod,
+  mockNewCodePeriodBranch,
+} from '../../helpers/mocks/new-code-definition';
+import {
+  NewCodeDefinition,
+  NewCodeDefinitionBranch,
+  NewCodeDefinitionType,
+} from '../../types/new-code-definition';
+import {
+  getNewCodeDefinition,
+  listBranchesNewCodeDefinition,
+  resetNewCodeDefinition,
+  setNewCodeDefinition,
+} from '../newCodeDefinition';
+
+jest.mock('../newCodeDefinition');
+export default class NewCodeDefinitionServiceMock {
+  #defaultNewCodePeriod = mockNewCodePeriod({ inherited: true });
+  #defaultListBranchesNewCode = [
+    mockNewCodePeriodBranch({ inherited: true, branchKey: 'main' }),
+    mockNewCodePeriodBranch({
+      branchKey: 'feature',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '1',
+    }),
+  ];
+
+  #newCodePeriod: NewCodeDefinition;
+  #listBranchesNewCode: NewCodeDefinitionBranch[];
+
+  constructor() {
+    this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
+    this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
+    jest.mocked(getNewCodeDefinition).mockImplementation(this.handleGetNewCodePeriod);
+    jest.mocked(setNewCodeDefinition).mockImplementation(this.handleSetNewCodePeriod);
+    jest.mocked(resetNewCodeDefinition).mockImplementation(this.handleResetNewCodePeriod);
+    jest
+      .mocked(listBranchesNewCodeDefinition)
+      .mockImplementation(this.handleListBranchesNewCodePeriod);
+  }
+
+  handleGetNewCodePeriod = () => {
+    return this.reply(this.#newCodePeriod);
+  };
+
+  handleSetNewCodePeriod = (data: {
+    project?: string;
+    branch?: string;
+    type: NewCodeDefinitionType;
+    value?: string;
+  }) => {
+    const { type, value, branch } = data;
+    if (branch) {
+      const branchNewCode = this.#listBranchesNewCode.find(
+        (bNew) => bNew.branchKey === branch
+      ) as NewCodeDefinitionBranch;
+      branchNewCode.type = type;
+      branchNewCode.value = value;
+    } else {
+      this.#newCodePeriod = mockNewCodePeriod({ type, value });
+    }
+
+    return this.reply(undefined);
+  };
+
+  handleResetNewCodePeriod = (data: { project?: string; branch?: string }) => {
+    const { branch } = data;
+    if (branch) {
+      const index = this.#listBranchesNewCode.findIndex((bNew) => bNew.branchKey === branch);
+      if (index >= 0) {
+        Object.assign(this.#listBranchesNewCode[index], cloneDeep(this.#defaultNewCodePeriod));
+      }
+    } else {
+      this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
+    }
+
+    return this.reply(undefined);
+  };
+
+  handleListBranchesNewCodePeriod = () => {
+    return this.reply({ newCodePeriods: this.#listBranchesNewCode });
+  };
+
+  setNewCodePeriod = (newCodePeriod: NewCodeDefinition) => {
+    this.#newCodePeriod = newCodePeriod;
+  };
+
+  setListBranchesNewCode = (listBranchesNewCode: NewCodeDefinitionBranch[]) => {
+    this.#listBranchesNewCode = listBranchesNewCode;
+  };
+
+  reset = () => {
+    this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
+    this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
+  };
+
+  reply<T>(response: T): Promise<T> {
+    return Promise.resolve(cloneDeep(response));
+  }
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts
deleted file mode 100644 (file)
index 0d429a7..0000000
+++ /dev/null
@@ -1,119 +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 { cloneDeep } from 'lodash';
-import {
-  mockNewCodePeriod,
-  mockNewCodePeriodBranch,
-} from '../../helpers/mocks/new-code-definition';
-import {
-  NewCodeDefinition,
-  NewCodeDefinitionBranch,
-  NewCodeDefinitionType,
-} from '../../types/new-code-definition';
-import {
-  getNewCodePeriod,
-  listBranchesNewCodePeriod,
-  resetNewCodePeriod,
-  setNewCodePeriod,
-} from '../newCodePeriod';
-
-jest.mock('../newCodePeriod');
-export default class NewCodePeriodsServiceMock {
-  #defaultNewCodePeriod = mockNewCodePeriod({ inherited: true });
-  #defaultListBranchesNewCode = [
-    mockNewCodePeriodBranch({ inherited: true, branchKey: 'main' }),
-    mockNewCodePeriodBranch({
-      branchKey: 'feature',
-      type: NewCodeDefinitionType.NumberOfDays,
-      value: '1',
-    }),
-  ];
-
-  #newCodePeriod: NewCodeDefinition;
-  #listBranchesNewCode: NewCodeDefinitionBranch[];
-
-  constructor() {
-    this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
-    this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
-    jest.mocked(getNewCodePeriod).mockImplementation(this.handleGetNewCodePeriod);
-    jest.mocked(setNewCodePeriod).mockImplementation(this.handleSetNewCodePeriod);
-    jest.mocked(resetNewCodePeriod).mockImplementation(this.handleResetNewCodePeriod);
-    jest.mocked(listBranchesNewCodePeriod).mockImplementation(this.handleListBranchesNewCodePeriod);
-  }
-
-  handleGetNewCodePeriod = () => {
-    return this.reply(this.#newCodePeriod);
-  };
-
-  handleSetNewCodePeriod = (data: {
-    project?: string;
-    branch?: string;
-    type: NewCodeDefinitionType;
-    value?: string;
-  }) => {
-    const { type, value, branch } = data;
-    if (branch) {
-      const branchNewCode = this.#listBranchesNewCode.find(
-        (bNew) => bNew.branchKey === branch
-      ) as NewCodeDefinitionBranch;
-      branchNewCode.type = type;
-      branchNewCode.value = value;
-    } else {
-      this.#newCodePeriod = mockNewCodePeriod({ type, value });
-    }
-
-    return this.reply(undefined);
-  };
-
-  handleResetNewCodePeriod = (data: { project?: string; branch?: string }) => {
-    const { branch } = data;
-    if (branch) {
-      const index = this.#listBranchesNewCode.findIndex((bNew) => bNew.branchKey === branch);
-      if (index >= 0) {
-        Object.assign(this.#listBranchesNewCode[index], cloneDeep(this.#defaultNewCodePeriod));
-      }
-    } else {
-      this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
-    }
-
-    return this.reply(undefined);
-  };
-
-  handleListBranchesNewCodePeriod = () => {
-    return this.reply({ newCodePeriods: this.#listBranchesNewCode });
-  };
-
-  setNewCodePeriod = (newCodePeriod: NewCodeDefinition) => {
-    this.#newCodePeriod = newCodePeriod;
-  };
-
-  setListBranchesNewCode = (listBranchesNewCode: NewCodeDefinitionBranch[]) => {
-    this.#listBranchesNewCode = listBranchesNewCode;
-  };
-
-  reset = () => {
-    this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
-    this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
-  };
-
-  reply<T>(response: T): Promise<T> {
-    return Promise.resolve(cloneDeep(response));
-  }
-}
diff --git a/server/sonar-web/src/main/js/api/newCodeDefinition.ts b/server/sonar-web/src/main/js/api/newCodeDefinition.ts
new file mode 100644 (file)
index 0000000..19d0e81
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 { throwGlobalError } from '../helpers/error';
+import { getJSON, post } from '../helpers/request';
+import {
+  NewCodeDefinition,
+  NewCodeDefinitionBranch,
+  NewCodeDefinitionType,
+} from '../types/new-code-definition';
+
+export function getNewCodeDefinition(data?: {
+  project?: string;
+  branch?: string;
+}): Promise<Omit<NewCodeDefinition, 'effectiveValue'>> {
+  return getJSON('/api/new_code_periods/show', data).catch(throwGlobalError);
+}
+
+export function setNewCodeDefinition(data: {
+  project?: string;
+  branch?: string;
+  type: NewCodeDefinitionType;
+  value?: string;
+}): Promise<void> {
+  return post('/api/new_code_periods/set', data).catch(throwGlobalError);
+}
+
+export function resetNewCodeDefinition(data: { project?: string; branch?: string }): Promise<void> {
+  return post('/api/new_code_periods/unset', data).catch(throwGlobalError);
+}
+
+export function listBranchesNewCodeDefinition(data: {
+  project: string;
+}): Promise<{ newCodePeriods: NewCodeDefinitionBranch[] }> {
+  return getJSON('/api/new_code_periods/list', data).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/api/newCodePeriod.ts b/server/sonar-web/src/main/js/api/newCodePeriod.ts
deleted file mode 100644 (file)
index ca56e5f..0000000
+++ /dev/null
@@ -1,52 +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 { throwGlobalError } from '../helpers/error';
-import { getJSON, post } from '../helpers/request';
-import {
-  NewCodeDefinition,
-  NewCodeDefinitionBranch,
-  NewCodeDefinitionType,
-} from '../types/new-code-definition';
-
-export function getNewCodePeriod(data?: {
-  project?: string;
-  branch?: string;
-}): Promise<Omit<NewCodeDefinition, 'effectiveValue'>> {
-  return getJSON('/api/new_code_periods/show', data).catch(throwGlobalError);
-}
-
-export function setNewCodePeriod(data: {
-  project?: string;
-  branch?: string;
-  type: NewCodeDefinitionType;
-  value?: string;
-}): Promise<void> {
-  return post('/api/new_code_periods/set', data).catch(throwGlobalError);
-}
-
-export function resetNewCodePeriod(data: { project?: string; branch?: string }): Promise<void> {
-  return post('/api/new_code_periods/unset', data).catch(throwGlobalError);
-}
-
-export function listBranchesNewCodePeriod(data: {
-  project: string;
-}): Promise<{ newCodePeriods: NewCodeDefinitionBranch[] }> {
-  return getJSON('/api/new_code_periods/list', data).catch(throwGlobalError);
-}
index 370f86af3fa723e9b8c1e78f9109d2ae2ae9220b..8b02cbeab11f717e7140c46a7560e9226fb3ee0b 100644 (file)
@@ -40,13 +40,13 @@ import overviewRoutes from '../../apps/overview/routes';
 import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
 import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
 import projectActivityRoutes from '../../apps/projectActivity/routes';
-import projectBaselineRoutes from '../../apps/projectBaseline/routes';
 import projectBranchesRoutes from '../../apps/projectBranches/routes';
 import ProjectDeletionApp from '../../apps/projectDeletion/App';
 import projectDumpRoutes from '../../apps/projectDump/routes';
 import projectInfoRoutes from '../../apps/projectInformation/routes';
 import ProjectKeyApp from '../../apps/projectKey/ProjectKeyApp';
 import ProjectLinksApp from '../../apps/projectLinks/ProjectLinksApp';
+import projectNewCodeDefinitionRoutes from '../../apps/projectNewCode/routes';
 import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
 import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes';
 import projectsRoutes from '../../apps/projects/routes';
@@ -127,7 +127,7 @@ function renderComponentRoutes() {
             element={<ProjectAdminPageExtension />}
           />
           {backgroundTasksRoutes()}
-          {projectBaselineRoutes()}
+          {projectNewCodeDefinitionRoutes()}
           {projectBranchesRoutes()}
           {projectDumpRoutes()}
           {settingsRoutes()}
index db9b1a3b2bad835e5408fe7247b258c470197fc9..747cc0a90052a4cd053f31355db4aa2caaeeb4cc 100644 (file)
@@ -25,7 +25,7 @@ import selectEvent from 'react-select-event';
 import { searchAzureRepositories } from '../../../../api/alm-integrations';
 import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
@@ -35,7 +35,7 @@ jest.mock('../../../../api/alm-settings');
 
 let almIntegrationHandler: AlmIntegrationsServiceMock;
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const ui = {
   azureCreateProjectButton: byText('onboarding.create_project.select_method.azure'),
@@ -54,7 +54,7 @@ beforeAll(() => {
   });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
index 460e9f73850180e22f4d659cba9256c5a7ca8851..1072dfe83cc9333b8e5008ebd6de237c6e809a1a 100644 (file)
@@ -25,7 +25,7 @@ import selectEvent from 'react-select-event';
 import { searchForBitbucketServerRepositories } from '../../../../api/alm-integrations';
 import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
@@ -35,7 +35,7 @@ jest.mock('../../../../api/alm-settings');
 
 let almIntegrationHandler: AlmIntegrationsServiceMock;
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const ui = {
   bitbucketServerCreateProjectButton: byText('onboarding.create_project.select_method.bitbucket'),
@@ -53,7 +53,7 @@ beforeAll(() => {
   });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
index 9cd94893473a1da680031f05b0eaaf54435f0685..8cf355084dab21252dc7220a2a11f9cdc3c01fc2 100644 (file)
@@ -25,7 +25,7 @@ import selectEvent from 'react-select-event';
 import { searchForBitbucketCloudRepositories } from '../../../../api/alm-integrations';
 import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
@@ -36,7 +36,7 @@ jest.mock('../../../../api/alm-settings');
 
 let almIntegrationHandler: AlmIntegrationsServiceMock;
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const ui = {
   bitbucketCloudCreateProjectButton: byText(
@@ -57,7 +57,7 @@ beforeAll(() => {
   });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
index bd2d47f5fbec71a9219c98523a0ffd54fb1f68b9..fc26be288345209eeb664f08267ee90d572e21cc 100644 (file)
@@ -22,7 +22,7 @@ import { screen } from '@testing-library/react';
 import * as React from 'react';
 import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { mockAppState } from '../../../../helpers/testMocks';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { AlmKeys } from '../../../../types/alm-settings';
@@ -33,7 +33,7 @@ jest.mock('../../../../api/alm-settings');
 
 let almIntegrationHandler: AlmIntegrationsServiceMock;
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const original = window.location;
 
@@ -44,7 +44,7 @@ beforeAll(() => {
   });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
index c9244c31a638a5d7b3787495639377ee4a9e2529..96243570673aedd7caaf2ff8db0173ebe69e6ef6 100644 (file)
@@ -25,7 +25,7 @@ import selectEvent from 'react-select-event';
 import { getGithubRepositories } from '../../../../api/alm-integrations';
 import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
@@ -37,7 +37,7 @@ const original = window.location;
 
 let almIntegrationHandler: AlmIntegrationsServiceMock;
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const ui = {
   githubCreateProjectButton: byText('onboarding.create_project.select_method.github'),
@@ -52,7 +52,7 @@ beforeAll(() => {
   });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
index 1fa6e4ee7517089b51ad9fdd5586706aa3298b49..43d9f8d11f08e9b2c0c51f2d9761ebeb1c664781 100644 (file)
@@ -24,7 +24,7 @@ import selectEvent from 'react-select-event';
 import { getGitlabProjects } from '../../../../api/alm-integrations';
 import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
@@ -34,7 +34,7 @@ jest.mock('../../../../api/alm-settings');
 
 let almIntegrationHandler: AlmIntegrationsServiceMock;
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const ui = {
   gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'),
@@ -54,7 +54,7 @@ beforeAll(() => {
   });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
index 8bf0af0edd295c433cd769bead04b02f603f9d6d..a0563c51d6a280939256e194a5f06a0df1222184 100644 (file)
@@ -21,8 +21,8 @@ import userEvent from '@testing-library/user-event';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import * as React from 'react';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
-import { getNewCodePeriod } from '../../../../api/newCodePeriod';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { getNewCodeDefinition } from '../../../../api/newCodeDefinition';
 import { mockProject } from '../../../../helpers/mocks/projects';
 import { mockAppState } from '../../../../helpers/testMocks';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
@@ -31,7 +31,7 @@ import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
 import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
 
 jest.mock('../../../../api/alm-settings');
-jest.mock('../../../../api/newCodePeriod');
+jest.mock('../../../../api/newCodeDefinition');
 jest.mock('../../../../api/project-management', () => ({
   setupManualProjectCreation: jest
     .fn()
@@ -89,7 +89,7 @@ async function fillFormAndNext(displayName: string, user: UserEvent) {
 }
 
 let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const original = window.location;
 
@@ -99,7 +99,7 @@ beforeAll(() => {
     value: { replace: jest.fn() },
   });
   almSettingsHandler = new AlmSettingsServiceMock();
-  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+  newCodePeriodHandler = new NewCodeDefinitionServiceMock();
 });
 
 beforeEach(() => {
@@ -122,7 +122,7 @@ it('should fill form and move to NCD selection', async () => {
 
 it('should select the global NCD when it is compliant', async () => {
   jest
-    .mocked(getNewCodePeriod)
+    .mocked(getNewCodeDefinition)
     .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '30' });
   const user = userEvent.setup();
   renderCreateProject();
@@ -140,7 +140,7 @@ it('should select the global NCD when it is compliant', async () => {
 
 it('global NCD option should be disabled if not compliant', async () => {
   jest
-    .mocked(getNewCodePeriod)
+    .mocked(getNewCodeDefinition)
     .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
   const user = userEvent.setup();
   renderCreateProject();
@@ -159,7 +159,7 @@ it.each([
   'should show warning message when global NCD is not compliant',
   async ({ canAdmin, message }) => {
     jest
-      .mocked(getNewCodePeriod)
+      .mocked(getNewCodeDefinition)
       .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
     const user = userEvent.setup();
     renderCreateProject({ appState: mockAppState({ canAdmin }) });
@@ -173,7 +173,7 @@ it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])(
   'should override the global NCD and pick a compliant NCD',
   async (option) => {
     jest
-      .mocked(getNewCodePeriod)
+      .mocked(getNewCodeDefinition)
       .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
     const user = userEvent.setup();
     renderCreateProject();
@@ -197,7 +197,7 @@ it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])(
 
 it('number of days ignores non-numeric inputs', async () => {
   jest
-    .mocked(getNewCodePeriod)
+    .mocked(getNewCodeDefinition)
     .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '60' });
   const user = userEvent.setup();
   renderCreateProject();
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx
deleted file mode 100644 (file)
index dbb6c20..0000000
+++ /dev/null
@@ -1,73 +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 { FormattedMessage } from 'react-intl';
-import DocLink from '../../../components/common/DocLink';
-import Link from '../../../components/common/Link';
-import { translate } from '../../../helpers/l10n';
-
-export interface AppHeaderProps {
-  canAdmin: boolean;
-}
-
-export default function AppHeader(props: AppHeaderProps) {
-  const { canAdmin } = props;
-
-  return (
-    <header className="page-header">
-      <h1 className="sw-mb-4">{translate('project_baseline.page')}</h1>
-      <p className="sw-mb-2">{translate('project_baseline.page.description')}</p>
-      <p className="sw-mb-2">{translate('settings.new_code_period.description1')}</p>
-      <p className="sw-mb-2">
-        {canAdmin && (
-          <FormattedMessage
-            defaultMessage={translate('project_baseline.page.description2')}
-            id="project_baseline.page.description2"
-            values={{
-              link: (
-                <Link to="/admin/settings?category=new_code_period">
-                  {translate('project_baseline.page.description2.link')}
-                </Link>
-              ),
-            }}
-          />
-        )}
-      </p>
-
-      <p className="sw-mb-2">
-        <FormattedMessage
-          defaultMessage={translate('settings.new_code_period.description3')}
-          id="settings.new_code_period.description3"
-          values={{
-            link: (
-              <DocLink to="/project-administration/defining-new-code/">
-                {translate('settings.new_code_period.description3.link')}
-              </DocLink>
-            ),
-          }}
-        />
-      </p>
-
-      <p className="sw-mt-4">
-        <strong>{translate('project_baseline.page.question')}</strong>
-      </p>
-    </header>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx
deleted file mode 100644 (file)
index d042123..0000000
+++ /dev/null
@@ -1,42 +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 { NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-export interface Props {
-  onSelect: (selection: NewCodeDefinitionType) => void;
-  selected: boolean;
-}
-
-export default function BaselineSettingAnalysis({ onSelect, selected }: Props) {
-  return (
-    <RadioCard
-      noRadio
-      disabled
-      onClick={() => onSelect(NewCodeDefinitionType.SpecificAnalysis)}
-      selected={selected}
-      title={translate('baseline.specific_analysis')}
-    >
-      <p className="big-spacer-bottom">{translate('baseline.specific_analysis.description')}</p>
-    </RadioCard>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
deleted file mode 100644 (file)
index 59f65f0..0000000
+++ /dev/null
@@ -1,136 +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 { SelectionCard } from 'design-system';
-import * as React from 'react';
-import { components, OptionProps } from 'react-select';
-import Select from '../../../components/controls/Select';
-import Tooltip from '../../../components/controls/Tooltip';
-import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-export interface BaselineSettingReferenceBranchProps {
-  branchList: BranchOption[];
-  className?: string;
-  disabled?: boolean;
-  onChangeReferenceBranch: (value: string) => void;
-  onSelect: (selection: NewCodeDefinitionType) => void;
-  referenceBranch: string;
-  selected: boolean;
-  settingLevel: 'project' | 'branch';
-}
-
-export interface BranchOption {
-  isDisabled?: boolean;
-  isInvalid?: boolean;
-  isMain: boolean;
-  label: string;
-  value: string;
-}
-
-function renderBranchOption(props: OptionProps<BranchOption, false>) {
-  const { data: option } = props;
-
-  return (
-    <components.Option {...props}>
-      {option.isInvalid ? (
-        <Tooltip
-          overlay={translateWithParameters(
-            'baseline.reference_branch.does_not_exist',
-            option.value
-          )}
-        >
-          <span>
-            {option.value} <AlertErrorIcon />
-          </span>
-        </Tooltip>
-      ) : (
-        <>
-          <span
-            title={
-              option.isDisabled
-                ? translate('baseline.reference_branch.cannot_be_itself')
-                : undefined
-            }
-          >
-            {option.value}
-          </span>
-          {option.isMain && (
-            <div className="badge spacer-left">{translate('branches.main_branch')}</div>
-          )}
-        </>
-      )}
-    </components.Option>
-  );
-}
-
-export default function BaselineSettingReferenceBranch(props: BaselineSettingReferenceBranchProps) {
-  const { branchList, className, disabled, referenceBranch, selected, settingLevel } = props;
-
-  const currentBranch = branchList.find((b) => b.value === referenceBranch) || {
-    label: referenceBranch,
-    value: referenceBranch,
-    isMain: false,
-    isInvalid: true,
-  };
-
-  return (
-    <SelectionCard
-      className={className}
-      disabled={disabled}
-      onClick={() => props.onSelect(NewCodeDefinitionType.ReferenceBranch)}
-      selected={selected}
-      title={translate('baseline.reference_branch')}
-    >
-      <>
-        <div>
-          <p className="sw-mb-3">{translate('baseline.reference_branch.description')}</p>
-          <p className="sw-mb-4">{translate('baseline.reference_branch.usecase')}</p>
-        </div>
-        {selected && (
-          <>
-            {settingLevel === 'project' && (
-              <p className="spacer-top">{translate('baseline.reference_branch.description2')}</p>
-            )}
-            <div className="big-spacer-top display-flex-column">
-              <MandatoryFieldsExplanation className="spacer-bottom" />
-              <label className="text-middle" htmlFor="reference_branch">
-                <strong>{translate('baseline.reference_branch.choose')}</strong>
-                <MandatoryFieldMarker />
-              </label>
-              <Select
-                className="little-spacer-top spacer-bottom"
-                options={branchList}
-                aria-label={translate('baseline.reference_branch.choose')}
-                onChange={(option: BranchOption) => props.onChangeReferenceBranch(option.value)}
-                value={currentBranch}
-                components={{
-                  Option: renderBranchOption,
-                }}
-              />
-            </div>
-          </>
-        )}
-      </>
-    </SelectionCard>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
deleted file mode 100644 (file)
index 74e0987..0000000
+++ /dev/null
@@ -1,160 +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 { subDays } from 'date-fns';
-import { throttle } from 'lodash';
-import * as React from 'react';
-import { getProjectActivity } from '../../../api/projectActivity';
-import { parseDate, toShortISO8601String } from '../../../helpers/dates';
-import { Analysis, ParsedAnalysis } from '../../../types/project-activity';
-import { Dict } from '../../../types/types';
-import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
-
-interface Props {
-  analysis: string;
-  branch: string;
-  component: string;
-  onSelectAnalysis: (analysis: ParsedAnalysis) => void;
-}
-
-interface State {
-  analyses: ParsedAnalysis[];
-  loading: boolean;
-  range: number;
-  scroll: number;
-}
-
-const STICKY_BADGE_SCROLL_OFFSET = 10;
-
-export default class BranchAnalysisList extends React.PureComponent<Props, State> {
-  mounted = false;
-  badges: Dict<HTMLDivElement> = {};
-  state: State = {
-    analyses: [],
-    loading: true,
-    range: 30,
-    scroll: 0,
-  };
-
-  constructor(props: Props) {
-    super(props);
-    this.updateScroll = throttle(this.updateScroll, 20);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchAnalyses(true);
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  scrollToSelected() {
-    document.querySelector('.branch-analysis.selected')?.scrollIntoView({
-      block: 'center',
-      behavior: 'smooth',
-    });
-  }
-
-  fetchAnalyses(initial = false) {
-    const { analysis, branch, component } = this.props;
-    const { range } = this.state;
-    this.setState({ loading: true });
-
-    return getProjectActivity({
-      branch,
-      project: component,
-      from: range ? toShortISO8601String(subDays(new Date(), range)) : undefined,
-    }).then((result: { analyses: Analysis[] }) => {
-      // If the selected analysis wasn't found in the default 30 days range, redo the search
-      if (initial && analysis && !result.analyses.find((a) => a.key === analysis)) {
-        this.handleRangeChange({ value: 0 });
-        return;
-      }
-
-      this.setState(
-        {
-          analyses: result.analyses.map((analysis) => ({
-            ...analysis,
-            date: parseDate(analysis.date),
-          })) as ParsedAnalysis[],
-          loading: false,
-        },
-        () => {
-          this.scrollToSelected();
-        }
-      );
-    });
-  }
-
-  handleScroll = (e: React.SyntheticEvent<HTMLDivElement>) => {
-    if (e.currentTarget) {
-      this.updateScroll(e.currentTarget.scrollTop);
-    }
-  };
-
-  updateScroll = (scroll: number) => {
-    this.setState({ scroll });
-  };
-
-  registerBadgeNode = (version: string) => (el: HTMLDivElement) => {
-    if (el) {
-      if (!el.getAttribute('originOffsetTop')) {
-        el.setAttribute('originOffsetTop', String(el.offsetTop));
-      }
-      this.badges[version] = el;
-    }
-  };
-
-  shouldStick = (version: string) => {
-    const badge = this.badges[version];
-    return (
-      !!badge &&
-      Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + STICKY_BADGE_SCROLL_OFFSET
-    );
-  };
-
-  handleRangeChange = ({ value }: { value: number }) => {
-    this.setState({ range: value }, () => {
-      this.fetchAnalyses().catch(() => {
-        /* noop */
-      });
-    });
-  };
-
-  render() {
-    const { analysis, onSelectAnalysis } = this.props;
-    const { analyses, loading, range } = this.state;
-
-    return (
-      <BranchAnalysisListRenderer
-        analyses={analyses}
-        handleRangeChange={this.handleRangeChange}
-        handleScroll={this.handleScroll}
-        loading={loading}
-        onSelectAnalysis={onSelectAnalysis}
-        range={range}
-        registerBadgeNode={this.registerBadgeNode}
-        selectedAnalysisKey={analysis}
-        shouldStick={this.shouldStick}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx
deleted file mode 100644 (file)
index 420f7f7..0000000
+++ /dev/null
@@ -1,207 +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 classNames from 'classnames';
-import * as React from 'react';
-import { injectIntl, IntlShape, WrappedComponentProps } from 'react-intl';
-import Radio from '../../../components/controls/Radio';
-import Select from '../../../components/controls/Select';
-import Tooltip from '../../../components/controls/Tooltip';
-import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
-import TimeFormatter from '../../../components/intl/TimeFormatter';
-import Spinner from '../../../components/ui/Spinner';
-import { parseDate, toShortISO8601String } from '../../../helpers/dates';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { ParsedAnalysis } from '../../../types/project-activity';
-import Events from '../../projectActivity/components/Events';
-import { getAnalysesByVersionByDay } from '../../projectActivity/utils';
-
-export interface BranchAnalysisListRendererProps {
-  analyses: ParsedAnalysis[];
-  handleRangeChange: ({ value }: { value: number }) => void;
-  handleScroll: (e: React.SyntheticEvent<HTMLDivElement>) => void;
-  loading: boolean;
-  onSelectAnalysis: (analysis: ParsedAnalysis) => void;
-  range: number;
-  registerBadgeNode: (version: string) => (el: HTMLDivElement) => void;
-  selectedAnalysisKey: string;
-  shouldStick: (version: string) => boolean;
-}
-
-function renderAnalysis(args: {
-  analysis: ParsedAnalysis;
-  isFirst: boolean;
-  onSelectAnalysis: (analysis: ParsedAnalysis) => void;
-  selectedAnalysisKey: string;
-  intl: IntlShape;
-}) {
-  const { analysis, isFirst, selectedAnalysisKey, intl } = args;
-  return (
-    <li
-      className={classNames('branch-analysis', {
-        selected: analysis.key === selectedAnalysisKey,
-      })}
-      data-date={parseDate(analysis.date).valueOf()}
-      key={analysis.key}
-    >
-      <div className="branch-analysis-time spacer-right">
-        <TimeFormatter date={parseDate(analysis.date)} long={false}>
-          {(formattedTime) => (
-            <time className="text-middle" dateTime={parseDate(analysis.date).toISOString()}>
-              {formattedTime}
-            </time>
-          )}
-        </TimeFormatter>
-      </div>
-
-      {analysis.events.length > 0 && (
-        <Events analysisKey={analysis.key} events={analysis.events} isFirst={isFirst} />
-      )}
-
-      <div className="analysis-selection-button">
-        <Radio
-          checked={analysis.key === selectedAnalysisKey}
-          ariaLabel={translateWithParameters(
-            'baseline.branch_analyses.analysis_for_x',
-            `${intl.formatDate(analysis.date, longFormatterOption)}, ${intl.formatTime(
-              analysis.date
-            )}`
-          )}
-          onCheck={() => {}}
-          value=""
-          disabled
-        />
-      </div>
-    </li>
-  );
-}
-
-function BranchAnalysisListRenderer(
-  props: BranchAnalysisListRendererProps & WrappedComponentProps
-) {
-  const { analyses, loading, range, selectedAnalysisKey, intl } = props;
-
-  const byVersionByDay = React.useMemo(
-    () =>
-      getAnalysesByVersionByDay(analyses, {
-        category: '',
-      }),
-    [analyses]
-  );
-
-  const hasFilteredData =
-    byVersionByDay.length > 1 ||
-    (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
-
-  const options = [
-    {
-      label: translate('baseline.branch_analyses.ranges.30days'),
-      value: 30,
-    },
-    {
-      label: translate('baseline.branch_analyses.ranges.allTime'),
-      value: 0,
-    },
-  ];
-
-  return (
-    <>
-      <div className="spacer-bottom">
-        <label htmlFor="branch-analysis-from-input" className="spacer-right">
-          {translate('baseline.analysis_from')}
-        </label>
-        <Select
-          blurInputOnSelect
-          inputId="branch-analysis-from-input"
-          className="input-medium spacer-left"
-          onChange={props.handleRangeChange}
-          options={options}
-          isSearchable={false}
-          value={options.filter((o) => o.value === range)}
-        />
-      </div>
-      <div className="branch-analysis-list-wrapper">
-        <div className="bordered branch-analysis-list" onScroll={props.handleScroll}>
-          <Spinner className="big-spacer-top" loading={loading} />
-
-          {!loading && !hasFilteredData ? (
-            <div className="big-spacer-top big-spacer-bottom strong">
-              {translate('baseline.no_analyses')}
-            </div>
-          ) : (
-            <ul>
-              {byVersionByDay.map((version, idx) => {
-                const days = Object.keys(version.byDay);
-                if (days.length <= 0) {
-                  return null;
-                }
-                return (
-                  <li key={version.key || 'noversion'}>
-                    {version.version && (
-                      <div
-                        className={classNames('branch-analysis-version-badge', {
-                          first: idx === 0,
-                          sticky: props.shouldStick(version.version),
-                        })}
-                        ref={props.registerBadgeNode(version.version)}
-                      >
-                        <Tooltip
-                          mouseEnterDelay={0.5}
-                          overlay={`${translate('version')} ${version.version}`}
-                        >
-                          <span className="badge">{version.version}</span>
-                        </Tooltip>
-                      </div>
-                    )}
-                    <ul className="branch-analysis-days-list">
-                      {days.map((day) => (
-                        <li
-                          className="branch-analysis-day"
-                          data-day={toShortISO8601String(Number(day))}
-                          key={day}
-                        >
-                          <div className="branch-analysis-date">
-                            <DateFormatter date={Number(day)} long />
-                          </div>
-                          <ul className="branch-analysis-analyses-list">
-                            {version.byDay[day]?.map((analysis) =>
-                              renderAnalysis({
-                                analysis,
-                                selectedAnalysisKey,
-                                isFirst: analyses[0].key === analysis.key,
-                                onSelectAnalysis: props.onSelectAnalysis,
-                                intl,
-                              })
-                            )}
-                          </ul>
-                        </li>
-                      ))}
-                    </ul>
-                  </li>
-                );
-              })}
-            </ul>
-          )}
-        </div>
-      </div>
-    </>
-  );
-}
-
-export default injectIntl(BranchAnalysisListRenderer);
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
deleted file mode 100644 (file)
index 67fe858..0000000
+++ /dev/null
@@ -1,231 +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 { noop } from 'lodash';
-import * as React from 'react';
-import { setNewCodePeriod } from '../../../api/newCodePeriod';
-import Modal from '../../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-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 Spinner from '../../../components/ui/Spinner';
-import { toISO8601WithOffsetString } from '../../../helpers/dates';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getNumberOfDaysDefaultValue } from '../../../helpers/new-code-definition';
-import { Branch, BranchWithNewCodePeriod } from '../../../types/branch-like';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-import { getSettingValue, validateSetting } from '../utils';
-import BaselineSettingAnalysis from './BaselineSettingAnalysis';
-import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
-import BranchAnalysisList from './BranchAnalysisList';
-
-interface Props {
-  branch: BranchWithNewCodePeriod;
-  branchList: Branch[];
-  component: string;
-  onClose: (branch?: string, newSetting?: NewCodeDefinition) => void;
-  inheritedSetting: NewCodeDefinition;
-  generalSetting: NewCodeDefinition;
-}
-
-interface State {
-  analysis: string;
-  analysisDate?: Date;
-  days: string;
-  isChanged: boolean;
-  referenceBranch: string;
-  saving: boolean;
-  selected?: NewCodeDefinitionType;
-}
-
-export default class BranchBaselineSettingModal extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-
-    const { branch, branchList, inheritedSetting, generalSetting } = props;
-    const otherBranches = branchList.filter((b) => b.name !== branch.name);
-    const defaultBranch = otherBranches.length > 0 ? otherBranches[0].name : '';
-
-    this.state = {
-      analysis: this.getValueFromProps(NewCodeDefinitionType.SpecificAnalysis) || '',
-      days:
-        this.getValueFromProps(NewCodeDefinitionType.NumberOfDays) ||
-        getNumberOfDaysDefaultValue(generalSetting, inheritedSetting),
-      isChanged: false,
-      referenceBranch:
-        this.getValueFromProps(NewCodeDefinitionType.ReferenceBranch) || defaultBranch,
-      saving: false,
-      selected: branch.newCodePeriod?.type,
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  getValueFromProps(type: NewCodeDefinitionType) {
-    return this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type === type
-      ? this.props.branch.newCodePeriod.value
-      : null;
-  }
-
-  branchToOption = (b: Branch) => ({
-    label: b.name,
-    value: b.name,
-    isMain: b.isMain,
-    isDisabled: b.name === this.props.branch.name, // cannot itself be used as a reference branch
-  });
-
-  handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
-    e.preventDefault();
-
-    const { branch, component } = this.props;
-    const { analysis, analysisDate, days, referenceBranch, selected: type } = this.state;
-
-    const value = getSettingValue({ type, analysis, days, referenceBranch });
-
-    if (type) {
-      this.setState({ saving: true });
-      setNewCodePeriod({
-        project: component,
-        type,
-        value,
-        branch: branch.name,
-      }).then(
-        () => {
-          if (this.mounted) {
-            this.setState({
-              saving: false,
-              isChanged: false,
-            });
-            this.props.onClose(branch.name, {
-              type,
-              value,
-              effectiveValue: analysisDate && toISO8601WithOffsetString(analysisDate),
-            });
-          }
-        },
-        () => {
-          if (this.mounted) {
-            this.setState({
-              saving: false,
-            });
-          }
-        }
-      );
-    }
-  };
-
-  requestClose = () => this.props.onClose();
-
-  handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
-
-  handleSelectReferenceBranch = (referenceBranch: string) =>
-    this.setState({ referenceBranch, isChanged: true });
-
-  handleSelectSetting = (selected: NewCodeDefinitionType) => {
-    this.setState((currentState) => ({ selected, isChanged: selected !== currentState.selected }));
-  };
-
-  render() {
-    const { branch, branchList } = this.props;
-    const { analysis, days, isChanged, referenceBranch, saving, selected } = this.state;
-
-    const header = translateWithParameters('baseline.new_code_period_for_branch_x', branch.name);
-
-    const currentSetting = branch.newCodePeriod?.type;
-    const currentSettingValue = branch.newCodePeriod?.value;
-
-    const isValid = validateSetting({
-      days,
-      referenceBranch,
-      selected,
-    });
-
-    return (
-      <Modal contentLabel={header} onRequestClose={this.requestClose} size="large">
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-        <form onSubmit={this.handleSubmit}>
-          <div className="modal-body modal-container branch-baseline-setting-modal">
-            <p className="sw-mb-3">{translate('baseline.new_code_period_for_branch_x.question')}</p>
-            <NewCodeDefinitionWarning
-              newCodeDefinitionType={currentSetting}
-              newCodeDefinitionValue={currentSettingValue}
-              isBranchSupportEnabled
-              level="branch"
-            />
-            <div className="display-flex-column huge-spacer-bottom sw-gap-4" role="radiogroup">
-              <NewCodeDefinitionPreviousVersionOption
-                isDefault={false}
-                onSelect={this.handleSelectSetting}
-                selected={selected === NewCodeDefinitionType.PreviousVersion}
-              />
-              <NewCodeDefinitionDaysOption
-                days={days}
-                isChanged={isChanged}
-                isValid={isValid}
-                onChangeDays={this.handleSelectDays}
-                onSelect={this.handleSelectSetting}
-                selected={selected === NewCodeDefinitionType.NumberOfDays}
-              />
-              <BaselineSettingReferenceBranch
-                branchList={branchList.map(this.branchToOption)}
-                onChangeReferenceBranch={this.handleSelectReferenceBranch}
-                onSelect={this.handleSelectSetting}
-                referenceBranch={referenceBranch}
-                selected={selected === NewCodeDefinitionType.ReferenceBranch}
-                settingLevel="branch"
-              />
-              {currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
-                <BaselineSettingAnalysis
-                  onSelect={noop}
-                  selected={selected === NewCodeDefinitionType.SpecificAnalysis}
-                />
-              )}
-            </div>
-            {selected === NewCodeDefinitionType.SpecificAnalysis && (
-              <BranchAnalysisList
-                analysis={analysis}
-                branch={branch.name}
-                component={this.props.component}
-                onSelectAnalysis={noop}
-              />
-            )}
-          </div>
-          <footer className="modal-foot">
-            <Spinner className="spacer-right" loading={saving} />
-            <SubmitButton disabled={!isChanged || saving || !isValid}>
-              {translate('save')}
-            </SubmitButton>
-            <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
-          </footer>
-        </form>
-      </Modal>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
deleted file mode 100644 (file)
index ff1aa3a..0000000
+++ /dev/null
@@ -1,185 +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 { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../api/newCodePeriod';
-import Spinner from '../../../components/ui/Spinner';
-import { isBranch, sortBranches } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
-import { DEFAULT_NEW_CODE_DEFINITION_TYPE } from '../../../helpers/new-code-definition';
-import { Branch, BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
-import { NewCodeDefinition } from '../../../types/new-code-definition';
-import { Component } from '../../../types/types';
-import BranchBaselineSettingModal from './BranchBaselineSettingModal';
-import BranchListRow from './BranchListRow';
-
-interface Props {
-  branchList: Branch[];
-  component: Component;
-  inheritedSetting: NewCodeDefinition;
-  generalSetting: NewCodeDefinition;
-}
-
-interface State {
-  branches: BranchWithNewCodePeriod[];
-  editedBranch?: BranchWithNewCodePeriod;
-  loading: boolean;
-}
-
-export default class BranchList extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    branches: [],
-    loading: true,
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchBranches();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.branchList !== this.props.branchList) {
-      this.fetchBranches();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  sortAndFilterBranches(branchLikes: BranchLike[] = []) {
-    return sortBranches(branchLikes.filter(isBranch));
-  }
-
-  fetchBranches() {
-    const project = this.props.component.key;
-    this.setState({ loading: true });
-
-    listBranchesNewCodePeriod({ project }).then(
-      (branchSettings) => {
-        const newCodePeriods = branchSettings.newCodePeriods
-          ? branchSettings.newCodePeriods.filter((ncp) => !ncp.inherited)
-          : [];
-
-        const branchesWithBaseline = this.props.branchList.map((b) => {
-          const newCodePeriod = newCodePeriods.find((ncp) => ncp.branchKey === b.name);
-          if (!newCodePeriod) {
-            return b;
-          }
-          const { type = DEFAULT_NEW_CODE_DEFINITION_TYPE, value, effectiveValue } = newCodePeriod;
-          return {
-            ...b,
-            newCodePeriod: { type, value, effectiveValue },
-          };
-        });
-
-        this.setState({ branches: branchesWithBaseline, loading: false });
-      },
-      () => {
-        this.setState({ loading: false });
-      }
-    );
-  }
-
-  updateBranchNewCodePeriod = (branch: string, newSetting: NewCodeDefinition | undefined) => {
-    const { branches } = this.state;
-
-    const updated = branches.find((b) => b.name === branch);
-    if (updated) {
-      updated.newCodePeriod = newSetting;
-    }
-    return branches.slice(0);
-  };
-
-  openEditModal = (branch: BranchWithNewCodePeriod) => {
-    this.setState({ editedBranch: branch });
-  };
-
-  closeEditModal = (branch?: string, newSetting?: NewCodeDefinition) => {
-    if (branch) {
-      this.setState({
-        branches: this.updateBranchNewCodePeriod(branch, newSetting),
-        editedBranch: undefined,
-      });
-    } else {
-      this.setState({ editedBranch: undefined });
-    }
-  };
-
-  resetToDefault = (branch: string) => {
-    return resetNewCodePeriod({
-      project: this.props.component.key,
-      branch,
-    }).then(() => {
-      this.setState({ branches: this.updateBranchNewCodePeriod(branch, undefined) });
-    });
-  };
-
-  render() {
-    const { branchList, inheritedSetting, generalSetting } = this.props;
-    const { branches, editedBranch, loading } = this.state;
-
-    if (branches.length < 1) {
-      return null;
-    }
-
-    if (loading) {
-      return <Spinner />;
-    }
-
-    return (
-      <>
-        <table className="data zebra">
-          <thead>
-            <tr>
-              <th>{translate('branch_list.branch')}</th>
-              <th className="nowrap huge-spacer-right">
-                {translate('branch_list.current_setting')}
-              </th>
-              <th className="thin nowrap">{translate('branch_list.actions')}</th>
-            </tr>
-          </thead>
-          <tbody>
-            {branches.map((branch) => (
-              <BranchListRow
-                branch={branch}
-                existingBranches={branchList.map((b) => b.name)}
-                inheritedSetting={inheritedSetting}
-                key={branch.name}
-                onOpenEditModal={this.openEditModal}
-                onResetToDefault={this.resetToDefault}
-              />
-            ))}
-          </tbody>
-        </table>
-        {editedBranch && (
-          <BranchBaselineSettingModal
-            branch={editedBranch}
-            branchList={branchList}
-            component={this.props.component.key}
-            onClose={this.closeEditModal}
-            inheritedSetting={inheritedSetting}
-            generalSetting={generalSetting}
-          />
-        )}
-      </>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
deleted file mode 100644 (file)
index 401c22e..0000000
+++ /dev/null
@@ -1,145 +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 ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-import Tooltip from '../../../components/controls/Tooltip';
-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/new-code-definition';
-import { BranchWithNewCodePeriod } from '../../../types/branch-like';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-export interface BranchListRowProps {
-  branch: BranchWithNewCodePeriod;
-  existingBranches: Array<string>;
-  inheritedSetting: NewCodeDefinition;
-  onOpenEditModal: (branch: BranchWithNewCodePeriod) => void;
-  onResetToDefault: (branchName: string) => void;
-}
-
-function renderNewCodePeriodSetting(newCodePeriod: NewCodeDefinition) {
-  switch (newCodePeriod.type) {
-    case NewCodeDefinitionType.SpecificAnalysis:
-      return (
-        <>
-          {`${translate('baseline.specific_analysis')}: `}
-          {newCodePeriod.effectiveValue ? (
-            <DateTimeFormatter date={newCodePeriod.effectiveValue} />
-          ) : (
-            '?'
-          )}
-        </>
-      );
-    case NewCodeDefinitionType.NumberOfDays:
-      return `${translate('new_code_definition.number_days')}: ${newCodePeriod.value}`;
-    case NewCodeDefinitionType.PreviousVersion:
-      return translate('new_code_definition.previous_version');
-    case NewCodeDefinitionType.ReferenceBranch:
-      return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
-    default:
-      return newCodePeriod.type;
-  }
-}
-
-function branchInheritsItselfAsReference(
-  branch: BranchWithNewCodePeriod,
-  inheritedSetting: NewCodeDefinition
-) {
-  return (
-    !branch.newCodePeriod &&
-    inheritedSetting.type === NewCodeDefinitionType.ReferenceBranch &&
-    branch.name === inheritedSetting.value
-  );
-}
-
-function referenceBranchDoesNotExist(
-  branch: BranchWithNewCodePeriod,
-  existingBranches: Array<string>
-) {
-  return (
-    branch.newCodePeriod &&
-    branch.newCodePeriod.value &&
-    branch.newCodePeriod.type === NewCodeDefinitionType.ReferenceBranch &&
-    !existingBranches.includes(branch.newCodePeriod.value)
-  );
-}
-
-export default function BranchListRow(props: BranchListRowProps) {
-  const { branch, existingBranches, inheritedSetting } = props;
-
-  let settingWarning: string | undefined;
-  if (branchInheritsItselfAsReference(branch, inheritedSetting)) {
-    settingWarning = translateWithParameters(
-      'baseline.reference_branch.invalid_branch_setting',
-      branch.name
-    );
-  } else if (referenceBranchDoesNotExist(branch, existingBranches)) {
-    settingWarning = translateWithParameters(
-      'baseline.reference_branch.does_not_exist',
-      branch.newCodePeriod?.value || ''
-    );
-  }
-
-  const isCompliant = isNewCodeDefinitionCompliant(inheritedSetting);
-
-  return (
-    <tr className={settingWarning ? 'branch-setting-warning' : ''}>
-      <td className="nowrap">
-        <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
-        {branch.name}
-        {branch.isMain && (
-          <div className="badge spacer-left">{translate('branches.main_branch')}</div>
-        )}
-      </td>
-      <td className="huge-spacer-right nowrap">
-        <Tooltip overlay={settingWarning}>
-          <span>
-            {settingWarning && <WarningIcon className="little-spacer-right" />}
-            {branch.newCodePeriod
-              ? renderNewCodePeriodSetting(branch.newCodePeriod)
-              : translate('branch_list.default_setting')}
-          </span>
-        </Tooltip>
-      </td>
-      <td className="text-right">
-        <ActionsDropdown
-          label={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
-        >
-          <ActionsDropdownItem onClick={() => props.onOpenEditModal(branch)}>
-            {translate('edit')}
-          </ActionsDropdownItem>
-          {branch.newCodePeriod && (
-            <ActionsDropdownItem
-              disabled={!isCompliant}
-              onClick={() => props.onResetToDefault(branch.name)}
-              tooltipOverlay={
-                isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
-              }
-            >
-              {translate('reset_to_default')}
-            </ActionsDropdownItem>
-          )}
-        </ActionsDropdown>
-      </td>
-    </tr>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
deleted file mode 100644 (file)
index 620c5d7..0000000
+++ /dev/null
@@ -1,350 +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 classNames from 'classnames';
-import { debounce } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
-import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
-import withAvailableFeatures, {
-  WithAvailableFeaturesProps,
-} from '../../../app/components/available-features/withAvailableFeatures';
-import withComponentContext from '../../../app/components/componentContext/withComponentContext';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
-import Spinner from '../../../components/ui/Spinner';
-import { isBranch, sortBranches } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
-import {
-  DEFAULT_NEW_CODE_DEFINITION_TYPE,
-  getNumberOfDaysDefaultValue,
-} from '../../../helpers/new-code-definition';
-import { withBranchLikes } from '../../../queries/branch';
-import { AppState } from '../../../types/appstate';
-import { Branch, BranchLike } from '../../../types/branch-like';
-import { Feature } from '../../../types/features';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-import { Component } from '../../../types/types';
-import '../styles.css';
-import { getSettingValue } from '../utils';
-import AppHeader from './AppHeader';
-import BranchList from './BranchList';
-import ProjectBaselineSelector from './ProjectBaselineSelector';
-
-interface Props extends WithAvailableFeaturesProps {
-  branchLike: Branch;
-  branchLikes: BranchLike[];
-  component: Component;
-  appState: AppState;
-}
-
-interface State {
-  analysis?: string;
-  branchList: Branch[];
-  currentSetting?: NewCodeDefinitionType;
-  currentSettingValue?: string;
-  days: string;
-  generalSetting?: NewCodeDefinition;
-  isChanged: boolean;
-  loading: boolean;
-  overrideGeneralSetting?: boolean;
-  referenceBranch?: string;
-  saving: boolean;
-  selected?: NewCodeDefinitionType;
-  success?: boolean;
-}
-
-class ProjectBaselineApp extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    branchList: [],
-    days: getNumberOfDaysDefaultValue(),
-    isChanged: false,
-    loading: true,
-    saving: false,
-  };
-
-  // We use debounce as we could have multiple save in less that 3sec.
-  resetSuccess = debounce(() => this.setState({ success: undefined }), 3000);
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchLeakPeriodSetting();
-    this.sortAndFilterBranches(this.props.branchLikes);
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.branchLikes !== this.props.branchLikes) {
-      this.sortAndFilterBranches(this.props.branchLikes);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  getUpdatedState(params: {
-    currentSetting?: NewCodeDefinitionType;
-    currentSettingValue?: string;
-    generalSetting: NewCodeDefinition;
-  }) {
-    const { currentSetting, currentSettingValue, generalSetting } = params;
-    const { referenceBranch } = this.state;
-
-    const defaultDays = getNumberOfDaysDefaultValue(generalSetting);
-
-    return {
-      loading: false,
-      currentSetting,
-      currentSettingValue,
-      generalSetting,
-      isChanged: false,
-      selected: currentSetting || generalSetting.type,
-      overrideGeneralSetting: Boolean(currentSetting),
-      days:
-        (currentSetting === NewCodeDefinitionType.NumberOfDays && currentSettingValue) ||
-        defaultDays,
-      analysis:
-        (currentSetting === NewCodeDefinitionType.SpecificAnalysis && currentSettingValue) || '',
-      referenceBranch:
-        (currentSetting === NewCodeDefinitionType.ReferenceBranch && currentSettingValue) ||
-        referenceBranch,
-    };
-  }
-
-  sortAndFilterBranches(branchLikes: BranchLike[] = []) {
-    const branchList = sortBranches(branchLikes.filter(isBranch));
-    this.setState({ branchList, referenceBranch: branchList[0]?.name });
-  }
-
-  fetchLeakPeriodSetting() {
-    const { branchLike, component } = this.props;
-
-    this.setState({ loading: true });
-
-    Promise.all([
-      getNewCodePeriod(),
-      getNewCodePeriod({
-        branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
-        project: component.key,
-      }),
-    ]).then(
-      ([generalSetting, setting]) => {
-        if (this.mounted) {
-          if (!generalSetting.type) {
-            generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE };
-          }
-          const currentSettingValue = setting.value;
-          const currentSetting = setting.inherited
-            ? undefined
-            : setting.type || DEFAULT_NEW_CODE_DEFINITION_TYPE;
-
-          this.setState(
-            this.getUpdatedState({
-              generalSetting,
-              currentSetting,
-              currentSettingValue,
-            })
-          );
-        }
-      },
-      () => {
-        this.setState({ loading: false });
-      }
-    );
-  }
-
-  resetSetting = () => {
-    this.setState({ saving: true });
-    resetNewCodePeriod({ project: this.props.component.key }).then(
-      () => {
-        this.setState({
-          saving: false,
-          currentSetting: undefined,
-          isChanged: false,
-          selected: undefined,
-          success: true,
-        });
-        this.resetSuccess();
-      },
-      () => {
-        this.setState({ saving: false });
-      }
-    );
-  };
-
-  handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
-
-  handleSelectReferenceBranch = (referenceBranch: string) => {
-    this.setState({ referenceBranch, isChanged: true });
-  };
-
-  handleCancel = () =>
-    this.setState(
-      ({
-        generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE },
-        currentSetting,
-        currentSettingValue,
-      }) => this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
-    );
-
-  handleSelectSetting = (selected?: NewCodeDefinitionType) => {
-    this.setState((currentState) => ({
-      selected,
-      isChanged: selected !== currentState.selected,
-    }));
-  };
-
-  handleToggleSpecificSetting = (overrideGeneralSetting: boolean) =>
-    this.setState((currentState) => ({
-      overrideGeneralSetting,
-      isChanged: currentState.overrideGeneralSetting !== overrideGeneralSetting,
-    }));
-
-  handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
-    e.preventDefault();
-
-    const { component } = this.props;
-    const { days, selected: type, referenceBranch, overrideGeneralSetting } = this.state;
-
-    if (!overrideGeneralSetting) {
-      this.resetSetting();
-      return;
-    }
-
-    const value = getSettingValue({ type, days, referenceBranch });
-
-    if (type) {
-      this.setState({ saving: true });
-      setNewCodePeriod({
-        project: component.key,
-        type,
-        value,
-      }).then(
-        () => {
-          this.setState({
-            saving: false,
-            currentSetting: type,
-            currentSettingValue: value || undefined,
-            isChanged: false,
-            success: true,
-          });
-          this.resetSuccess();
-        },
-        () => {
-          this.setState({ saving: false });
-        }
-      );
-    }
-  };
-
-  render() {
-    const { appState, component, branchLike } = this.props;
-    const {
-      analysis,
-      branchList,
-      currentSetting,
-      days,
-      generalSetting,
-      isChanged,
-      loading,
-      currentSettingValue,
-      overrideGeneralSetting,
-      referenceBranch,
-      saving,
-      selected,
-      success,
-    } = this.state;
-    const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport);
-
-    return (
-      <>
-        <Suggestions suggestions="project_baseline" />
-        <Helmet defer={false} title={translate('project_baseline.page')} />
-        <div className="page page-limited">
-          <AppHeader canAdmin={!!appState.canAdmin} />
-          <Spinner loading={loading} />
-
-          {!loading && (
-            <div className="panel-white project-baseline">
-              {branchSupportEnabled && <h2>{translate('project_baseline.default_setting')}</h2>}
-
-              {generalSetting && overrideGeneralSetting !== undefined && (
-                <ProjectBaselineSelector
-                  analysis={analysis}
-                  branch={branchLike}
-                  branchList={branchList}
-                  branchesEnabled={branchSupportEnabled}
-                  canAdmin={appState.canAdmin}
-                  component={component.key}
-                  currentSetting={currentSetting}
-                  currentSettingValue={currentSettingValue}
-                  days={days}
-                  generalSetting={generalSetting}
-                  isChanged={isChanged}
-                  onCancel={this.handleCancel}
-                  onSelectDays={this.handleSelectDays}
-                  onSelectReferenceBranch={this.handleSelectReferenceBranch}
-                  onSelectSetting={this.handleSelectSetting}
-                  onSubmit={this.handleSubmit}
-                  onToggleSpecificSetting={this.handleToggleSpecificSetting}
-                  overrideGeneralSetting={overrideGeneralSetting}
-                  referenceBranch={referenceBranch}
-                  saving={saving}
-                  selected={selected}
-                />
-              )}
-
-              <div className={classNames('spacer-top', { invisible: saving || !success })}>
-                <span className="text-success">
-                  <AlertSuccessIcon className="spacer-right" />
-                  {translate('settings.state.saved')}
-                </span>
-              </div>
-              {generalSetting && branchSupportEnabled && (
-                <div className="huge-spacer-top branch-baseline-selector">
-                  <hr />
-                  <h2>{translate('project_baseline.configure_branches')}</h2>
-                  <BranchList
-                    branchList={branchList}
-                    component={component}
-                    inheritedSetting={
-                      currentSetting
-                        ? {
-                            type: currentSetting,
-                            value: currentSettingValue,
-                          }
-                        : generalSetting
-                    }
-                    generalSetting={generalSetting}
-                  />
-                </div>
-              )}
-            </div>
-          )}
-        </div>
-      </>
-    );
-  }
-}
-
-export default withComponentContext(
-  withAvailableFeatures(withAppStateContext(withBranchLikes(ProjectBaselineApp)))
-);
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
deleted file mode 100644 (file)
index affb46b..0000000
+++ /dev/null
@@ -1,207 +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 classNames from 'classnames';
-import { RadioButton } from 'design-system';
-import { noop } from 'lodash';
-import * as React from 'react';
-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 Spinner from '../../../components/ui/Spinner';
-import { translate } from '../../../helpers/l10n';
-import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
-import { Branch } from '../../../types/branch-like';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-import { validateSetting } from '../utils';
-import BaselineSettingAnalysis from './BaselineSettingAnalysis';
-import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
-import BranchAnalysisList from './BranchAnalysisList';
-
-export interface ProjectBaselineSelectorProps {
-  analysis?: string;
-  branch?: Branch;
-  branchList: Branch[];
-  branchesEnabled?: boolean;
-  canAdmin: boolean | undefined;
-  component: string;
-  currentSetting?: NewCodeDefinitionType;
-  currentSettingValue?: string;
-  days: string;
-  generalSetting: NewCodeDefinition;
-  isChanged: boolean;
-  onCancel: () => void;
-  onSelectDays: (value: string) => void;
-  onSelectReferenceBranch: (value: string) => void;
-  onSelectSetting: (value?: NewCodeDefinitionType) => void;
-  onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
-  onToggleSpecificSetting: (selection: boolean) => void;
-  referenceBranch?: string;
-  saving: boolean;
-  selected?: NewCodeDefinitionType;
-  overrideGeneralSetting: boolean;
-}
-
-function branchToOption(b: Branch) {
-  return { label: b.name, value: b.name, isMain: b.isMain };
-}
-
-export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
-  const {
-    analysis,
-    branch,
-    branchList,
-    branchesEnabled,
-    canAdmin,
-    component,
-    currentSetting,
-    currentSettingValue,
-    days,
-    generalSetting,
-    isChanged,
-    overrideGeneralSetting,
-    referenceBranch,
-    saving,
-    selected,
-  } = props;
-
-  const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
-
-  const isValid = validateSetting({
-    days,
-    overrideGeneralSetting,
-    referenceBranch,
-    selected,
-  });
-
-  if (branch === undefined) {
-    return null;
-  }
-
-  return (
-    <form className="project-baseline-selector" onSubmit={props.onSubmit}>
-      <div className="big-spacer-top spacer-bottom" role="radiogroup">
-        <RadioButton
-          checked={!overrideGeneralSetting}
-          className="big-spacer-bottom"
-          disabled={!isGlobalNcdCompliant}
-          onCheck={() => props.onToggleSpecificSetting(false)}
-          value="general"
-        >
-          <Tooltip
-            overlay={
-              isGlobalNcdCompliant
-                ? null
-                : translate('project_baseline.compliance.warning.title.global')
-            }
-          >
-            <span>{translate('project_baseline.global_setting')}</span>
-          </Tooltip>
-        </RadioButton>
-
-        <div className="sw-ml-4">
-          <GlobalNewCodeDefinitionDescription
-            globalNcd={generalSetting}
-            isGlobalNcdCompliant={isGlobalNcdCompliant}
-            canAdmin={canAdmin}
-          />
-        </div>
-
-        <RadioButton
-          checked={overrideGeneralSetting}
-          className="huge-spacer-top"
-          onCheck={() => props.onToggleSpecificSetting(true)}
-          value="specific"
-        >
-          {translate('project_baseline.specific_setting')}
-        </RadioButton>
-      </div>
-
-      <div className="big-spacer-left big-spacer-right project-baseline-setting">
-        <NewCodeDefinitionWarning
-          newCodeDefinitionType={currentSetting}
-          newCodeDefinitionValue={currentSettingValue}
-          isBranchSupportEnabled={branchesEnabled}
-          level="project"
-        />
-        <div className="display-flex-column big-spacer-bottom sw-gap-4" role="radiogroup">
-          <NewCodeDefinitionPreviousVersionOption
-            disabled={!overrideGeneralSetting}
-            onSelect={props.onSelectSetting}
-            selected={overrideGeneralSetting && selected === NewCodeDefinitionType.PreviousVersion}
-          />
-          <NewCodeDefinitionDaysOption
-            days={days}
-            disabled={!overrideGeneralSetting}
-            isChanged={isChanged}
-            isValid={isValid}
-            onChangeDays={props.onSelectDays}
-            onSelect={props.onSelectSetting}
-            selected={overrideGeneralSetting && selected === NewCodeDefinitionType.NumberOfDays}
-          />
-          {branchesEnabled && (
-            <BaselineSettingReferenceBranch
-              branchList={branchList.map(branchToOption)}
-              disabled={!overrideGeneralSetting}
-              onChangeReferenceBranch={props.onSelectReferenceBranch}
-              onSelect={props.onSelectSetting}
-              referenceBranch={referenceBranch || ''}
-              selected={
-                overrideGeneralSetting && selected === NewCodeDefinitionType.ReferenceBranch
-              }
-              settingLevel="project"
-            />
-          )}
-          {!branchesEnabled && currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
-            <BaselineSettingAnalysis
-              onSelect={noop}
-              selected={
-                overrideGeneralSetting && selected === NewCodeDefinitionType.SpecificAnalysis
-              }
-            />
-          )}
-        </div>
-        {!branchesEnabled &&
-          overrideGeneralSetting &&
-          selected === NewCodeDefinitionType.SpecificAnalysis && (
-            <BranchAnalysisList
-              analysis={analysis || ''}
-              branch={branch.name}
-              component={component}
-              onSelectAnalysis={noop}
-            />
-          )}
-      </div>
-      <div className={classNames('big-spacer-top', { invisible: !isChanged })}>
-        <Alert variant="info" className="spacer-bottom">
-          {translate('baseline.next_analysis_notice')}
-        </Alert>
-        <Spinner className="spacer-right" loading={saving} />
-        <SubmitButton disabled={saving || !isValid || !isChanged}>{translate('save')}</SubmitButton>
-        <ResetButtonLink className="spacer-left" onClick={props.onCancel}>
-          {translate('cancel')}
-        </ResetButtonLink>
-      </div>
-    </form>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
deleted file mode 100644 (file)
index 8a43206..0000000
+++ /dev/null
@@ -1,395 +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 { within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { first, last } from 'lodash';
-import selectEvent from 'react-select-event';
-import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
-import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition';
-import { mockAppState } from '../../../../helpers/testMocks';
-import {
-  RenderContext,
-  renderAppWithComponentContext,
-} from '../../../../helpers/testReactTestingUtils';
-import { byRole, byText } from '../../../../helpers/testSelector';
-import { Feature } from '../../../../types/features';
-import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import routes from '../../routes';
-
-jest.mock('../../../../api/newCodePeriod');
-jest.mock('../../../../api/projectActivity');
-jest.mock('../../../../api/branches');
-
-const codePeriodsMock = new NewCodePeriodsServiceMock();
-const projectActivityMock = new ProjectActivityServiceMock();
-const branchHandler = new BranchesServiceMock();
-
-afterEach(() => {
-  branchHandler.reset();
-  codePeriodsMock.reset();
-  projectActivityMock.reset();
-});
-
-it('renders correctly without branch support feature', async () => {
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp();
-  await ui.appIsLoaded();
-
-  expect(await ui.generalSettingRadio.find()).toBeChecked();
-  expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
-
-  // User is not admin
-  expect(ui.generalSettingsLink.query()).not.toBeInTheDocument();
-
-  // Specific branch setting is not rendered without feature branch
-  expect(ui.branchListHeading.query()).not.toBeInTheDocument();
-  expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
-});
-
-it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
-  codePeriodsMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '99',
-    inherited: true,
-  });
-
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp();
-  await ui.appIsLoaded();
-
-  expect(await ui.generalSettingRadio.find()).toBeChecked();
-  expect(ui.generalSettingRadio.get()).toBeDisabled();
-  expect(ui.complianceWarning.get()).toBeVisible();
-});
-
-it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
-  codePeriodsMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '99',
-    inherited: true,
-  });
-
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
-  await ui.appIsLoaded();
-
-  expect(await ui.generalSettingRadio.find()).toBeChecked();
-  expect(ui.generalSettingRadio.get()).toBeDisabled();
-  expect(ui.complianceWarningAdmin.get()).toBeVisible();
-  expect(ui.complianceWarning.query()).not.toBeInTheDocument();
-});
-
-it('renders correctly with branch support feature', async () => {
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-    appState: mockAppState({ canAdmin: true }),
-  });
-  await ui.appIsLoaded();
-
-  expect(await ui.generalSettingRadio.find()).toBeChecked();
-  expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
-
-  // User is admin
-  expect(ui.generalSettingsLink.get()).toBeInTheDocument();
-
-  // Specific branch setting is rendered with feature support branch
-  expect(ui.branchListHeading.get()).toBeInTheDocument();
-  expect(ui.referenceBranchRadio.get()).toBeInTheDocument();
-});
-
-it('can set previous version specific setting', async () => {
-  const { ui, user } = getPageObjects();
-  renderProjectBaselineApp();
-  await ui.appIsLoaded();
-
-  expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
-  await ui.setPreviousVersionSetting();
-  expect(ui.previousVersionRadio.get()).toBeChecked();
-
-  // Save changes
-  await user.click(ui.saveButton.get());
-
-  expect(ui.saved.get()).toBeInTheDocument();
-
-  // Set general setting
-  await user.click(ui.generalSettingRadio.get());
-  expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
-  await user.click(ui.saveButton.get());
-  expect(ui.saved.get()).toBeInTheDocument();
-});
-
-it('can set number of days specific setting', async () => {
-  const { ui, user } = getPageObjects();
-  renderProjectBaselineApp();
-  await ui.appIsLoaded();
-
-  expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
-  await ui.setNumberDaysSetting('10');
-  expect(ui.numberDaysRadio.get()).toBeChecked();
-
-  // Reset to initial state
-  await user.click(ui.cancelButton.get());
-  expect(ui.generalSettingRadio.get()).toBeChecked();
-  expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
-
-  // Save changes
-  await ui.setNumberDaysSetting('10');
-  await user.click(ui.saveButton.get());
-
-  expect(ui.saved.get()).toBeInTheDocument();
-});
-
-it('can set reference branch specific setting', async () => {
-  const { ui, user } = getPageObjects();
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-  });
-  await ui.appIsLoaded();
-
-  expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
-  await ui.setReferenceBranchSetting('main');
-  expect(ui.referenceBranchRadio.get()).toBeChecked();
-
-  // Save changes
-  await user.click(ui.saveButton.get());
-
-  expect(ui.saved.get()).toBeInTheDocument();
-});
-
-it('cannot set specific analysis setting', async () => {
-  const { ui } = getPageObjects();
-  codePeriodsMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.SpecificAnalysis,
-    value: 'analysis_id',
-  });
-  renderProjectBaselineApp();
-  await ui.appIsLoaded();
-
-  expect(await ui.specificAnalysisRadio.find()).toBeChecked();
-  expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
-  expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
-
-  await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
-
-  expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
-  expect(ui.saveButton.get()).toBeDisabled();
-});
-
-it('renders correctly branch modal', async () => {
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-  });
-  await ui.appIsLoaded();
-
-  await ui.openBranchSettingModal('main');
-
-  expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
-});
-
-it('can set a previous version setting for branch', async () => {
-  const { ui, user } = getPageObjects();
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-  });
-  await ui.appIsLoaded();
-  await ui.setBranchPreviousVersionSetting('main');
-
-  expect(
-    within(byRole('table').get()).getByText('new_code_definition.previous_version')
-  ).toBeInTheDocument();
-
-  await user.click(await ui.branchActionsButton('main').find());
-
-  expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
-  await user.click(ui.resetToDefaultButton.get());
-
-  expect(
-    first(within(byRole('table').get()).getAllByText('branch_list.default_setting'))
-  ).toBeInTheDocument();
-});
-
-it('can set a number of days setting for branch', async () => {
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-  });
-  await ui.appIsLoaded();
-
-  await ui.setBranchNumberOfDaysSetting('main', '15');
-
-  expect(
-    within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
-  ).toBeInTheDocument();
-});
-
-it('cannot set a specific analysis setting for branch', async () => {
-  const { ui } = getPageObjects();
-  codePeriodsMock.setListBranchesNewCode([
-    mockNewCodePeriodBranch({
-      branchKey: 'main',
-      type: NewCodeDefinitionType.SpecificAnalysis,
-      value: 'analysis_id',
-    }),
-  ]);
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-  });
-  await ui.appIsLoaded();
-
-  await ui.openBranchSettingModal('main');
-
-  expect(ui.specificAnalysisRadio.get()).toBeChecked();
-  expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
-  expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
-
-  await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
-
-  expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
-  expect(last(ui.saveButton.getAll())).toBeDisabled();
-});
-
-it('can set a reference branch setting for branch', async () => {
-  const { ui } = getPageObjects();
-  renderProjectBaselineApp({
-    featureList: [Feature.BranchSupport],
-  });
-  await ui.appIsLoaded();
-
-  await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
-
-  expect(
-    byRole('table').byText('baseline.reference_branch: normal-branch').get()
-  ).toBeInTheDocument();
-});
-
-function renderProjectBaselineApp(context: RenderContext = {}, params?: string) {
-  return renderAppWithComponentContext(
-    'baseline',
-    routes,
-    {
-      ...context,
-      navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
-    },
-    {
-      component: mockComponent(),
-    }
-  );
-}
-
-function getPageObjects() {
-  const user = userEvent.setup();
-
-  const ui = {
-    pageHeading: byRole('heading', { name: 'project_baseline.page' }),
-    branchTableHeading: byText('branch_list.branch'),
-    branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
-    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: /new_code_definition.previous_version.description/,
-    }),
-    numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
-    numberDaysInput: byRole('spinbutton'),
-    referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
-    chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
-    specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
-    specificAnalysisWarning: byText('baseline.specific_analysis.compliance_warning.title'),
-    analysisFromSelect: byRole('combobox', { name: 'baseline.analysis_from' }),
-    analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
-    saveButton: byRole('button', { name: 'save' }),
-    cancelButton: byRole('button', { name: 'cancel' }),
-    branchActionsButton: (branch: string) =>
-      byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
-    editButton: byRole('button', { name: 'edit' }),
-    resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
-    saved: byText('settings.state.saved'),
-    complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
-    complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
-  };
-
-  async function appIsLoaded() {
-    expect(await ui.pageHeading.find()).toBeInTheDocument();
-  }
-
-  async function setPreviousVersionSetting() {
-    await user.click(ui.specificSettingRadio.get());
-    await user.click(ui.previousVersionRadio.get());
-  }
-
-  async function setBranchPreviousVersionSetting(branch: string) {
-    await openBranchSettingModal(branch);
-    await user.click(last(ui.previousVersionRadio.getAll()) as HTMLElement);
-    await user.click(last(ui.saveButton.getAll()) as HTMLElement);
-  }
-
-  async function setNumberDaysSetting(value: string) {
-    await user.click(ui.specificSettingRadio.get());
-    await user.click(ui.numberDaysRadio.get());
-    await user.clear(ui.numberDaysInput.get());
-    await user.type(ui.numberDaysInput.get(), value);
-  }
-
-  async function setBranchNumberOfDaysSetting(branch: string, value: string) {
-    await openBranchSettingModal(branch);
-    await user.click(last(ui.numberDaysRadio.getAll()) as HTMLElement);
-    await user.clear(ui.numberDaysInput.get());
-    await user.type(ui.numberDaysInput.get(), value);
-    await user.click(last(ui.saveButton.getAll()) as HTMLElement);
-  }
-
-  async function setReferenceBranchSetting(branch: string) {
-    await user.click(ui.specificSettingRadio.get());
-    await user.click(ui.referenceBranchRadio.get());
-    await selectEvent.select(ui.chooseBranchSelect.get(), branch);
-  }
-
-  async function setBranchReferenceToBranchSetting(branch: string, branchRef: string) {
-    await openBranchSettingModal(branch);
-    await user.click(last(ui.referenceBranchRadio.getAll()) as HTMLElement);
-    await selectEvent.select(ui.chooseBranchSelect.get(), branchRef);
-    await user.click(last(ui.saveButton.getAll()) as HTMLElement);
-  }
-
-  async function openBranchSettingModal(branch: string) {
-    await user.click(await ui.branchActionsButton(branch).find());
-    await user.click(ui.editButton.get());
-  }
-
-  return {
-    ui: {
-      ...ui,
-      appIsLoaded,
-      setNumberDaysSetting,
-      setPreviousVersionSetting,
-      setReferenceBranchSetting,
-      setBranchPreviousVersionSetting,
-      setBranchNumberOfDaysSetting,
-      setBranchReferenceToBranchSetting,
-      openBranchSettingModal,
-    },
-    user,
-  };
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts
deleted file mode 100644 (file)
index b0bd20f..0000000
+++ /dev/null
@@ -1,116 +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 { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import { getSettingValue, validateSetting } from '../../utils';
-
-describe('getSettingValue', () => {
-  const state = {
-    analysis: 'analysis',
-    days: '35',
-    referenceBranch: 'branch-4.2',
-  };
-
-  it('should work for Days', () => {
-    expect(getSettingValue({ ...state, type: NewCodeDefinitionType.NumberOfDays })).toBe(
-      state.days
-    );
-  });
-
-  it('should work for Analysis', () => {
-    expect(getSettingValue({ ...state, type: NewCodeDefinitionType.SpecificAnalysis })).toBe(
-      state.analysis
-    );
-  });
-
-  it('should work for Previous version', () => {
-    expect(
-      getSettingValue({ ...state, type: NewCodeDefinitionType.PreviousVersion })
-    ).toBeUndefined();
-  });
-
-  it('should work for Reference branch', () => {
-    expect(getSettingValue({ ...state, type: NewCodeDefinitionType.ReferenceBranch })).toBe(
-      state.referenceBranch
-    );
-  });
-});
-
-describe('validateSettings', () => {
-  it('should validate at branch level', () => {
-    expect(validateSetting({ days: '' })).toEqual(false);
-    expect(
-      validateSetting({
-        days: '12',
-        selected: NewCodeDefinitionType.NumberOfDays,
-      })
-    ).toEqual(true);
-    expect(
-      validateSetting({
-        days: 'nope',
-        selected: NewCodeDefinitionType.NumberOfDays,
-      })
-    ).toEqual(false);
-    expect(
-      validateSetting({
-        days: '',
-        selected: NewCodeDefinitionType.SpecificAnalysis,
-      })
-    ).toEqual(false);
-    expect(
-      validateSetting({
-        days: '',
-        referenceBranch: 'master',
-        selected: NewCodeDefinitionType.ReferenceBranch,
-      })
-    ).toEqual(true);
-    expect(
-      validateSetting({
-        days: '',
-        referenceBranch: '',
-        selected: NewCodeDefinitionType.ReferenceBranch,
-      })
-    ).toEqual(false);
-  });
-
-  it('should validate at project level', () => {
-    expect(validateSetting({ days: '', overrideGeneralSetting: false })).toEqual(true);
-    expect(
-      validateSetting({
-        selected: NewCodeDefinitionType.PreviousVersion,
-        days: '',
-        overrideGeneralSetting: true,
-      })
-    ).toEqual(true);
-    expect(
-      validateSetting({
-        selected: NewCodeDefinitionType.NumberOfDays,
-        days: '',
-        overrideGeneralSetting: true,
-      })
-    ).toEqual(false);
-    expect(
-      validateSetting({
-        selected: NewCodeDefinitionType.NumberOfDays,
-        days: '12',
-        overrideGeneralSetting: true,
-      })
-    ).toEqual(true);
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx
deleted file mode 100644 (file)
index 2235fe7..0000000
+++ /dev/null
@@ -1,26 +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 React from 'react';
-import { Route } from 'react-router-dom';
-import ProjectBaselineApp from './components/ProjectBaselineApp';
-
-const routes = () => <Route path="baseline" element={<ProjectBaselineApp />} />;
-
-export default routes;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/styles.css b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css
deleted file mode 100644 (file)
index 5e09ae4..0000000
+++ /dev/null
@@ -1,149 +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.
- */
-.project-baseline {
-  padding: calc(4 * var(--gridSize));
-}
-
-.project-baseline-setting {
-  display: flex;
-  flex-direction: column;
-  max-height: 60vh;
-  padding-top: 2px;
-}
-
-.branch-baseline-selector > hr {
-  margin: 0 calc(-4 * var(--gridSize)) calc(4 * var(--gridSize));
-}
-
-.branch-baseline-setting-modal {
-  min-height: 450px;
-  display: flex;
-  flex-direction: column;
-}
-
-.branch-analysis-list-wrapper {
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-  position: relative;
-  min-height: 200px;
-}
-
-.branch-analysis-list {
-  overflow-y: auto;
-  padding-left: 12px;
-  padding-right: 15px;
-  min-height: 50px;
-}
-
-.branch-analysis-list > ul {
-  padding-top: 18px;
-}
-
-.branch-analysis-date {
-  margin-bottom: 16px;
-  font-size: 15px;
-  font-weight: bold;
-}
-
-.branch-analysis-day {
-  margin-top: var(--gridSize);
-  margin-bottom: calc(3 * var(--gridSize));
-}
-
-.branch-analysis {
-  display: flex;
-  justify-content: space-between;
-  padding: var(--gridSize);
-  border-top: 1px solid var(--barBorderColor);
-  border-bottom: 1px solid var(--barBorderColor);
-  cursor: not-allowed;
-}
-
-.branch-analysis + .branch-analysis {
-  border-top: none;
-}
-
-.branch-analysis:hover {
-  background-color: var(--disableGrayBg);
-}
-
-.branch-analysis > .project-activity-events {
-  flex: 1 0 50%;
-}
-
-.branch-analysis-time {
-  width: 150px;
-}
-
-.branch-analysis-version-badge {
-  margin-left: -12px;
-  padding-top: var(--gridSize);
-  padding-bottom: var(--gridSize);
-  background-color: white;
-}
-
-.branch-analysis-version-badge.sticky + .branch-analysis-days-list {
-  padding-top: 36px;
-}
-
-.branch-analysis-version-badge.sticky,
-.branch-analysis-version-badge.first {
-  position: absolute;
-  top: 1px;
-  left: 13px;
-  right: 16px;
-  padding-top: calc(3 * var(--gridSize));
-  z-index: var(--belowNormalZIndex);
-}
-
-.branch-analysis-version-badge .badge {
-  max-width: 385px;
-  border-radius: 0 2px 2px 0;
-  font-weight: bold;
-  font-size: var(--smallFontSize);
-  letter-spacing: 0;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.branch-setting-warning {
-  background-color: var(--alertBackgroundWarning) !important;
-}
-
-.project-activity-event-icon.VERSION {
-  color: var(--blue);
-}
-
-.project-activity-event-icon.QUALITY_GATE {
-  color: var(--purple);
-}
-
-.project-activity-event-icon.QUALITY_PROFILE {
-  color: #cccccc;
-}
-
-.project-activity-event-icon.DEFINITION_CHANGE {
-  color: #33a759;
-}
-
-.project-activity-event-icon.OTHER {
-  color: #442d1b;
-}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
deleted file mode 100644 (file)
index 45c5c8e..0000000
+++ /dev/null
@@ -1,63 +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 { isNewCodeDefinitionCompliant } from '../../helpers/new-code-definition';
-import { NewCodeDefinitionType } from '../../types/new-code-definition';
-
-export function getSettingValue({
-  analysis,
-  days,
-  referenceBranch,
-  type,
-}: {
-  analysis?: string;
-  days?: string;
-  referenceBranch?: string;
-  type?: NewCodeDefinitionType;
-}) {
-  switch (type) {
-    case NewCodeDefinitionType.NumberOfDays:
-      return days;
-    case NewCodeDefinitionType.ReferenceBranch:
-      return referenceBranch;
-    case NewCodeDefinitionType.SpecificAnalysis:
-      return analysis;
-    default:
-      return undefined;
-  }
-}
-
-export function validateSetting(state: {
-  days: string;
-  overrideGeneralSetting?: boolean;
-  referenceBranch?: string;
-  selected?: NewCodeDefinitionType;
-}) {
-  const { days, overrideGeneralSetting, referenceBranch = '', selected } = state;
-
-  return (
-    overrideGeneralSetting === false ||
-    (!!selected &&
-      isNewCodeDefinitionCompliant({
-        type: selected,
-        value: days,
-      }) &&
-      (selected !== NewCodeDefinitionType.ReferenceBranch || referenceBranch.length > 0))
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/AppHeader.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/AppHeader.tsx
new file mode 100644 (file)
index 0000000..dbb6c20
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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 DocLink from '../../../components/common/DocLink';
+import Link from '../../../components/common/Link';
+import { translate } from '../../../helpers/l10n';
+
+export interface AppHeaderProps {
+  canAdmin: boolean;
+}
+
+export default function AppHeader(props: AppHeaderProps) {
+  const { canAdmin } = props;
+
+  return (
+    <header className="page-header">
+      <h1 className="sw-mb-4">{translate('project_baseline.page')}</h1>
+      <p className="sw-mb-2">{translate('project_baseline.page.description')}</p>
+      <p className="sw-mb-2">{translate('settings.new_code_period.description1')}</p>
+      <p className="sw-mb-2">
+        {canAdmin && (
+          <FormattedMessage
+            defaultMessage={translate('project_baseline.page.description2')}
+            id="project_baseline.page.description2"
+            values={{
+              link: (
+                <Link to="/admin/settings?category=new_code_period">
+                  {translate('project_baseline.page.description2.link')}
+                </Link>
+              ),
+            }}
+          />
+        )}
+      </p>
+
+      <p className="sw-mb-2">
+        <FormattedMessage
+          defaultMessage={translate('settings.new_code_period.description3')}
+          id="settings.new_code_period.description3"
+          values={{
+            link: (
+              <DocLink to="/project-administration/defining-new-code/">
+                {translate('settings.new_code_period.description3.link')}
+              </DocLink>
+            ),
+          }}
+        />
+      </p>
+
+      <p className="sw-mt-4">
+        <strong>{translate('project_baseline.page.question')}</strong>
+      </p>
+    </header>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchAnalysisList.tsx
new file mode 100644 (file)
index 0000000..74e0987
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * 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 { subDays } from 'date-fns';
+import { throttle } from 'lodash';
+import * as React from 'react';
+import { getProjectActivity } from '../../../api/projectActivity';
+import { parseDate, toShortISO8601String } from '../../../helpers/dates';
+import { Analysis, ParsedAnalysis } from '../../../types/project-activity';
+import { Dict } from '../../../types/types';
+import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
+
+interface Props {
+  analysis: string;
+  branch: string;
+  component: string;
+  onSelectAnalysis: (analysis: ParsedAnalysis) => void;
+}
+
+interface State {
+  analyses: ParsedAnalysis[];
+  loading: boolean;
+  range: number;
+  scroll: number;
+}
+
+const STICKY_BADGE_SCROLL_OFFSET = 10;
+
+export default class BranchAnalysisList extends React.PureComponent<Props, State> {
+  mounted = false;
+  badges: Dict<HTMLDivElement> = {};
+  state: State = {
+    analyses: [],
+    loading: true,
+    range: 30,
+    scroll: 0,
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.updateScroll = throttle(this.updateScroll, 20);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchAnalyses(true);
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  scrollToSelected() {
+    document.querySelector('.branch-analysis.selected')?.scrollIntoView({
+      block: 'center',
+      behavior: 'smooth',
+    });
+  }
+
+  fetchAnalyses(initial = false) {
+    const { analysis, branch, component } = this.props;
+    const { range } = this.state;
+    this.setState({ loading: true });
+
+    return getProjectActivity({
+      branch,
+      project: component,
+      from: range ? toShortISO8601String(subDays(new Date(), range)) : undefined,
+    }).then((result: { analyses: Analysis[] }) => {
+      // If the selected analysis wasn't found in the default 30 days range, redo the search
+      if (initial && analysis && !result.analyses.find((a) => a.key === analysis)) {
+        this.handleRangeChange({ value: 0 });
+        return;
+      }
+
+      this.setState(
+        {
+          analyses: result.analyses.map((analysis) => ({
+            ...analysis,
+            date: parseDate(analysis.date),
+          })) as ParsedAnalysis[],
+          loading: false,
+        },
+        () => {
+          this.scrollToSelected();
+        }
+      );
+    });
+  }
+
+  handleScroll = (e: React.SyntheticEvent<HTMLDivElement>) => {
+    if (e.currentTarget) {
+      this.updateScroll(e.currentTarget.scrollTop);
+    }
+  };
+
+  updateScroll = (scroll: number) => {
+    this.setState({ scroll });
+  };
+
+  registerBadgeNode = (version: string) => (el: HTMLDivElement) => {
+    if (el) {
+      if (!el.getAttribute('originOffsetTop')) {
+        el.setAttribute('originOffsetTop', String(el.offsetTop));
+      }
+      this.badges[version] = el;
+    }
+  };
+
+  shouldStick = (version: string) => {
+    const badge = this.badges[version];
+    return (
+      !!badge &&
+      Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + STICKY_BADGE_SCROLL_OFFSET
+    );
+  };
+
+  handleRangeChange = ({ value }: { value: number }) => {
+    this.setState({ range: value }, () => {
+      this.fetchAnalyses().catch(() => {
+        /* noop */
+      });
+    });
+  };
+
+  render() {
+    const { analysis, onSelectAnalysis } = this.props;
+    const { analyses, loading, range } = this.state;
+
+    return (
+      <BranchAnalysisListRenderer
+        analyses={analyses}
+        handleRangeChange={this.handleRangeChange}
+        handleScroll={this.handleScroll}
+        loading={loading}
+        onSelectAnalysis={onSelectAnalysis}
+        range={range}
+        registerBadgeNode={this.registerBadgeNode}
+        selectedAnalysisKey={analysis}
+        shouldStick={this.shouldStick}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchAnalysisListRenderer.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchAnalysisListRenderer.tsx
new file mode 100644 (file)
index 0000000..420f7f7
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import classNames from 'classnames';
+import * as React from 'react';
+import { injectIntl, IntlShape, WrappedComponentProps } from 'react-intl';
+import Radio from '../../../components/controls/Radio';
+import Select from '../../../components/controls/Select';
+import Tooltip from '../../../components/controls/Tooltip';
+import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
+import TimeFormatter from '../../../components/intl/TimeFormatter';
+import Spinner from '../../../components/ui/Spinner';
+import { parseDate, toShortISO8601String } from '../../../helpers/dates';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { ParsedAnalysis } from '../../../types/project-activity';
+import Events from '../../projectActivity/components/Events';
+import { getAnalysesByVersionByDay } from '../../projectActivity/utils';
+
+export interface BranchAnalysisListRendererProps {
+  analyses: ParsedAnalysis[];
+  handleRangeChange: ({ value }: { value: number }) => void;
+  handleScroll: (e: React.SyntheticEvent<HTMLDivElement>) => void;
+  loading: boolean;
+  onSelectAnalysis: (analysis: ParsedAnalysis) => void;
+  range: number;
+  registerBadgeNode: (version: string) => (el: HTMLDivElement) => void;
+  selectedAnalysisKey: string;
+  shouldStick: (version: string) => boolean;
+}
+
+function renderAnalysis(args: {
+  analysis: ParsedAnalysis;
+  isFirst: boolean;
+  onSelectAnalysis: (analysis: ParsedAnalysis) => void;
+  selectedAnalysisKey: string;
+  intl: IntlShape;
+}) {
+  const { analysis, isFirst, selectedAnalysisKey, intl } = args;
+  return (
+    <li
+      className={classNames('branch-analysis', {
+        selected: analysis.key === selectedAnalysisKey,
+      })}
+      data-date={parseDate(analysis.date).valueOf()}
+      key={analysis.key}
+    >
+      <div className="branch-analysis-time spacer-right">
+        <TimeFormatter date={parseDate(analysis.date)} long={false}>
+          {(formattedTime) => (
+            <time className="text-middle" dateTime={parseDate(analysis.date).toISOString()}>
+              {formattedTime}
+            </time>
+          )}
+        </TimeFormatter>
+      </div>
+
+      {analysis.events.length > 0 && (
+        <Events analysisKey={analysis.key} events={analysis.events} isFirst={isFirst} />
+      )}
+
+      <div className="analysis-selection-button">
+        <Radio
+          checked={analysis.key === selectedAnalysisKey}
+          ariaLabel={translateWithParameters(
+            'baseline.branch_analyses.analysis_for_x',
+            `${intl.formatDate(analysis.date, longFormatterOption)}, ${intl.formatTime(
+              analysis.date
+            )}`
+          )}
+          onCheck={() => {}}
+          value=""
+          disabled
+        />
+      </div>
+    </li>
+  );
+}
+
+function BranchAnalysisListRenderer(
+  props: BranchAnalysisListRendererProps & WrappedComponentProps
+) {
+  const { analyses, loading, range, selectedAnalysisKey, intl } = props;
+
+  const byVersionByDay = React.useMemo(
+    () =>
+      getAnalysesByVersionByDay(analyses, {
+        category: '',
+      }),
+    [analyses]
+  );
+
+  const hasFilteredData =
+    byVersionByDay.length > 1 ||
+    (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
+
+  const options = [
+    {
+      label: translate('baseline.branch_analyses.ranges.30days'),
+      value: 30,
+    },
+    {
+      label: translate('baseline.branch_analyses.ranges.allTime'),
+      value: 0,
+    },
+  ];
+
+  return (
+    <>
+      <div className="spacer-bottom">
+        <label htmlFor="branch-analysis-from-input" className="spacer-right">
+          {translate('baseline.analysis_from')}
+        </label>
+        <Select
+          blurInputOnSelect
+          inputId="branch-analysis-from-input"
+          className="input-medium spacer-left"
+          onChange={props.handleRangeChange}
+          options={options}
+          isSearchable={false}
+          value={options.filter((o) => o.value === range)}
+        />
+      </div>
+      <div className="branch-analysis-list-wrapper">
+        <div className="bordered branch-analysis-list" onScroll={props.handleScroll}>
+          <Spinner className="big-spacer-top" loading={loading} />
+
+          {!loading && !hasFilteredData ? (
+            <div className="big-spacer-top big-spacer-bottom strong">
+              {translate('baseline.no_analyses')}
+            </div>
+          ) : (
+            <ul>
+              {byVersionByDay.map((version, idx) => {
+                const days = Object.keys(version.byDay);
+                if (days.length <= 0) {
+                  return null;
+                }
+                return (
+                  <li key={version.key || 'noversion'}>
+                    {version.version && (
+                      <div
+                        className={classNames('branch-analysis-version-badge', {
+                          first: idx === 0,
+                          sticky: props.shouldStick(version.version),
+                        })}
+                        ref={props.registerBadgeNode(version.version)}
+                      >
+                        <Tooltip
+                          mouseEnterDelay={0.5}
+                          overlay={`${translate('version')} ${version.version}`}
+                        >
+                          <span className="badge">{version.version}</span>
+                        </Tooltip>
+                      </div>
+                    )}
+                    <ul className="branch-analysis-days-list">
+                      {days.map((day) => (
+                        <li
+                          className="branch-analysis-day"
+                          data-day={toShortISO8601String(Number(day))}
+                          key={day}
+                        >
+                          <div className="branch-analysis-date">
+                            <DateFormatter date={Number(day)} long />
+                          </div>
+                          <ul className="branch-analysis-analyses-list">
+                            {version.byDay[day]?.map((analysis) =>
+                              renderAnalysis({
+                                analysis,
+                                selectedAnalysisKey,
+                                isFirst: analyses[0].key === analysis.key,
+                                onSelectAnalysis: props.onSelectAnalysis,
+                                intl,
+                              })
+                            )}
+                          </ul>
+                        </li>
+                      ))}
+                    </ul>
+                  </li>
+                );
+              })}
+            </ul>
+          )}
+        </div>
+      </div>
+    </>
+  );
+}
+
+export default injectIntl(BranchAnalysisListRenderer);
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchList.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchList.tsx
new file mode 100644 (file)
index 0000000..87b0744
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * 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 {
+  listBranchesNewCodeDefinition,
+  resetNewCodeDefinition,
+} from '../../../api/newCodeDefinition';
+import Spinner from '../../../components/ui/Spinner';
+import { isBranch, sortBranches } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { DEFAULT_NEW_CODE_DEFINITION_TYPE } from '../../../helpers/new-code-definition';
+import { Branch, BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { NewCodeDefinition } from '../../../types/new-code-definition';
+import { Component } from '../../../types/types';
+import BranchListRow from './BranchListRow';
+import BranchNewCodeDefinitionSettingModal from './BranchNewCodeDefinitionSettingModal';
+
+interface Props {
+  branchList: Branch[];
+  component: Component;
+  inheritedSetting: NewCodeDefinition;
+  generalSetting: NewCodeDefinition;
+}
+
+interface State {
+  branches: BranchWithNewCodePeriod[];
+  editedBranch?: BranchWithNewCodePeriod;
+  loading: boolean;
+}
+
+export default class BranchList extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = {
+    branches: [],
+    loading: true,
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchBranches();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.branchList !== this.props.branchList) {
+      this.fetchBranches();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  sortAndFilterBranches(branchLikes: BranchLike[] = []) {
+    return sortBranches(branchLikes.filter(isBranch));
+  }
+
+  fetchBranches() {
+    const project = this.props.component.key;
+    this.setState({ loading: true });
+
+    listBranchesNewCodeDefinition({ project }).then(
+      (branchSettings) => {
+        const newCodePeriods = branchSettings.newCodePeriods
+          ? branchSettings.newCodePeriods.filter((ncp) => !ncp.inherited)
+          : [];
+
+        const branchesWithBaseline = this.props.branchList.map((b) => {
+          const newCodePeriod = newCodePeriods.find((ncp) => ncp.branchKey === b.name);
+          if (!newCodePeriod) {
+            return b;
+          }
+          const { type = DEFAULT_NEW_CODE_DEFINITION_TYPE, value, effectiveValue } = newCodePeriod;
+          return {
+            ...b,
+            newCodePeriod: { type, value, effectiveValue },
+          };
+        });
+
+        this.setState({ branches: branchesWithBaseline, loading: false });
+      },
+      () => {
+        this.setState({ loading: false });
+      }
+    );
+  }
+
+  updateBranchNewCodePeriod = (branch: string, newSetting: NewCodeDefinition | undefined) => {
+    const { branches } = this.state;
+
+    const updated = branches.find((b) => b.name === branch);
+    if (updated) {
+      updated.newCodePeriod = newSetting;
+    }
+    return branches.slice(0);
+  };
+
+  openEditModal = (branch: BranchWithNewCodePeriod) => {
+    this.setState({ editedBranch: branch });
+  };
+
+  closeEditModal = (branch?: string, newSetting?: NewCodeDefinition) => {
+    if (branch) {
+      this.setState({
+        branches: this.updateBranchNewCodePeriod(branch, newSetting),
+        editedBranch: undefined,
+      });
+    } else {
+      this.setState({ editedBranch: undefined });
+    }
+  };
+
+  resetToDefault = (branch: string) => {
+    return resetNewCodeDefinition({
+      project: this.props.component.key,
+      branch,
+    }).then(() => {
+      this.setState({ branches: this.updateBranchNewCodePeriod(branch, undefined) });
+    });
+  };
+
+  render() {
+    const { branchList, inheritedSetting, generalSetting } = this.props;
+    const { branches, editedBranch, loading } = this.state;
+
+    if (branches.length < 1) {
+      return null;
+    }
+
+    if (loading) {
+      return <Spinner />;
+    }
+
+    return (
+      <>
+        <table className="data zebra">
+          <thead>
+            <tr>
+              <th>{translate('branch_list.branch')}</th>
+              <th className="nowrap huge-spacer-right">
+                {translate('branch_list.current_setting')}
+              </th>
+              <th className="thin nowrap">{translate('branch_list.actions')}</th>
+            </tr>
+          </thead>
+          <tbody>
+            {branches.map((branch) => (
+              <BranchListRow
+                branch={branch}
+                existingBranches={branchList.map((b) => b.name)}
+                inheritedSetting={inheritedSetting}
+                key={branch.name}
+                onOpenEditModal={this.openEditModal}
+                onResetToDefault={this.resetToDefault}
+              />
+            ))}
+          </tbody>
+        </table>
+        {editedBranch && (
+          <BranchNewCodeDefinitionSettingModal
+            branch={editedBranch}
+            branchList={branchList}
+            component={this.props.component.key}
+            onClose={this.closeEditModal}
+            inheritedSetting={inheritedSetting}
+            generalSetting={generalSetting}
+          />
+        )}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx
new file mode 100644 (file)
index 0000000..401c22e
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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 ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
+import Tooltip from '../../../components/controls/Tooltip';
+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/new-code-definition';
+import { BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+export interface BranchListRowProps {
+  branch: BranchWithNewCodePeriod;
+  existingBranches: Array<string>;
+  inheritedSetting: NewCodeDefinition;
+  onOpenEditModal: (branch: BranchWithNewCodePeriod) => void;
+  onResetToDefault: (branchName: string) => void;
+}
+
+function renderNewCodePeriodSetting(newCodePeriod: NewCodeDefinition) {
+  switch (newCodePeriod.type) {
+    case NewCodeDefinitionType.SpecificAnalysis:
+      return (
+        <>
+          {`${translate('baseline.specific_analysis')}: `}
+          {newCodePeriod.effectiveValue ? (
+            <DateTimeFormatter date={newCodePeriod.effectiveValue} />
+          ) : (
+            '?'
+          )}
+        </>
+      );
+    case NewCodeDefinitionType.NumberOfDays:
+      return `${translate('new_code_definition.number_days')}: ${newCodePeriod.value}`;
+    case NewCodeDefinitionType.PreviousVersion:
+      return translate('new_code_definition.previous_version');
+    case NewCodeDefinitionType.ReferenceBranch:
+      return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
+    default:
+      return newCodePeriod.type;
+  }
+}
+
+function branchInheritsItselfAsReference(
+  branch: BranchWithNewCodePeriod,
+  inheritedSetting: NewCodeDefinition
+) {
+  return (
+    !branch.newCodePeriod &&
+    inheritedSetting.type === NewCodeDefinitionType.ReferenceBranch &&
+    branch.name === inheritedSetting.value
+  );
+}
+
+function referenceBranchDoesNotExist(
+  branch: BranchWithNewCodePeriod,
+  existingBranches: Array<string>
+) {
+  return (
+    branch.newCodePeriod &&
+    branch.newCodePeriod.value &&
+    branch.newCodePeriod.type === NewCodeDefinitionType.ReferenceBranch &&
+    !existingBranches.includes(branch.newCodePeriod.value)
+  );
+}
+
+export default function BranchListRow(props: BranchListRowProps) {
+  const { branch, existingBranches, inheritedSetting } = props;
+
+  let settingWarning: string | undefined;
+  if (branchInheritsItselfAsReference(branch, inheritedSetting)) {
+    settingWarning = translateWithParameters(
+      'baseline.reference_branch.invalid_branch_setting',
+      branch.name
+    );
+  } else if (referenceBranchDoesNotExist(branch, existingBranches)) {
+    settingWarning = translateWithParameters(
+      'baseline.reference_branch.does_not_exist',
+      branch.newCodePeriod?.value || ''
+    );
+  }
+
+  const isCompliant = isNewCodeDefinitionCompliant(inheritedSetting);
+
+  return (
+    <tr className={settingWarning ? 'branch-setting-warning' : ''}>
+      <td className="nowrap">
+        <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
+        {branch.name}
+        {branch.isMain && (
+          <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+        )}
+      </td>
+      <td className="huge-spacer-right nowrap">
+        <Tooltip overlay={settingWarning}>
+          <span>
+            {settingWarning && <WarningIcon className="little-spacer-right" />}
+            {branch.newCodePeriod
+              ? renderNewCodePeriodSetting(branch.newCodePeriod)
+              : translate('branch_list.default_setting')}
+          </span>
+        </Tooltip>
+      </td>
+      <td className="text-right">
+        <ActionsDropdown
+          label={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
+        >
+          <ActionsDropdownItem onClick={() => props.onOpenEditModal(branch)}>
+            {translate('edit')}
+          </ActionsDropdownItem>
+          {branch.newCodePeriod && (
+            <ActionsDropdownItem
+              disabled={!isCompliant}
+              onClick={() => props.onResetToDefault(branch.name)}
+              tooltipOverlay={
+                isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
+              }
+            >
+              {translate('reset_to_default')}
+            </ActionsDropdownItem>
+          )}
+        </ActionsDropdown>
+      </td>
+    </tr>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchNewCodeDefinitionSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchNewCodeDefinitionSettingModal.tsx
new file mode 100644 (file)
index 0000000..06d4c05
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ * 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 { setNewCodeDefinition } from '../../../api/newCodeDefinition';
+import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+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 Spinner from '../../../components/ui/Spinner';
+import { toISO8601WithOffsetString } from '../../../helpers/dates';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { getNumberOfDaysDefaultValue } from '../../../helpers/new-code-definition';
+import { Branch, BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+import { getSettingValue, validateSetting } from '../utils';
+import BranchAnalysisList from './BranchAnalysisList';
+import NewCodeDefinitionSettingAnalysis from './NewCodeDefinitionSettingAnalysis';
+import NewCodeDefinitionSettingReferenceBranch from './NewCodeDefinitionSettingReferenceBranch';
+
+interface Props {
+  branch: BranchWithNewCodePeriod;
+  branchList: Branch[];
+  component: string;
+  onClose: (branch?: string, newSetting?: NewCodeDefinition) => void;
+  inheritedSetting: NewCodeDefinition;
+  generalSetting: NewCodeDefinition;
+}
+
+interface State {
+  analysis: string;
+  analysisDate?: Date;
+  days: string;
+  isChanged: boolean;
+  referenceBranch: string;
+  saving: boolean;
+  selected?: NewCodeDefinitionType;
+}
+
+export default class BranchNewCodeDefinitionSettingModal extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+
+    const { branch, branchList, inheritedSetting, generalSetting } = props;
+    const otherBranches = branchList.filter((b) => b.name !== branch.name);
+    const defaultBranch = otherBranches.length > 0 ? otherBranches[0].name : '';
+
+    this.state = {
+      analysis: this.getValueFromProps(NewCodeDefinitionType.SpecificAnalysis) || '',
+      days:
+        this.getValueFromProps(NewCodeDefinitionType.NumberOfDays) ||
+        getNumberOfDaysDefaultValue(generalSetting, inheritedSetting),
+      isChanged: false,
+      referenceBranch:
+        this.getValueFromProps(NewCodeDefinitionType.ReferenceBranch) || defaultBranch,
+      saving: false,
+      selected: branch.newCodePeriod?.type,
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  getValueFromProps(type: NewCodeDefinitionType) {
+    return this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type === type
+      ? this.props.branch.newCodePeriod.value
+      : null;
+  }
+
+  branchToOption = (b: Branch) => ({
+    label: b.name,
+    value: b.name,
+    isMain: b.isMain,
+    isDisabled: b.name === this.props.branch.name, // cannot itself be used as a reference branch
+  });
+
+  handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+    e.preventDefault();
+
+    const { branch, component } = this.props;
+    const { analysis, analysisDate, days, referenceBranch, selected: type } = this.state;
+
+    const value = getSettingValue({ type, analysis, days, referenceBranch });
+
+    if (type) {
+      this.setState({ saving: true });
+      setNewCodeDefinition({
+        project: component,
+        type,
+        value,
+        branch: branch.name,
+      }).then(
+        () => {
+          if (this.mounted) {
+            this.setState({
+              saving: false,
+              isChanged: false,
+            });
+            this.props.onClose(branch.name, {
+              type,
+              value,
+              effectiveValue: analysisDate && toISO8601WithOffsetString(analysisDate),
+            });
+          }
+        },
+        () => {
+          if (this.mounted) {
+            this.setState({
+              saving: false,
+            });
+          }
+        }
+      );
+    }
+  };
+
+  requestClose = () => this.props.onClose();
+
+  handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
+
+  handleSelectReferenceBranch = (referenceBranch: string) =>
+    this.setState({ referenceBranch, isChanged: true });
+
+  handleSelectSetting = (selected: NewCodeDefinitionType) => {
+    this.setState((currentState) => ({ selected, isChanged: selected !== currentState.selected }));
+  };
+
+  render() {
+    const { branch, branchList } = this.props;
+    const { analysis, days, isChanged, referenceBranch, saving, selected } = this.state;
+
+    const header = translateWithParameters('baseline.new_code_period_for_branch_x', branch.name);
+
+    const currentSetting = branch.newCodePeriod?.type;
+    const currentSettingValue = branch.newCodePeriod?.value;
+
+    const isValid = validateSetting({
+      days,
+      referenceBranch,
+      selected,
+    });
+
+    return (
+      <Modal contentLabel={header} onRequestClose={this.requestClose} size="large">
+        <header className="modal-head">
+          <h2>{header}</h2>
+        </header>
+        <form onSubmit={this.handleSubmit}>
+          <div className="modal-body modal-container branch-baseline-setting-modal">
+            <p className="sw-mb-3">{translate('baseline.new_code_period_for_branch_x.question')}</p>
+            <NewCodeDefinitionWarning
+              newCodeDefinitionType={currentSetting}
+              newCodeDefinitionValue={currentSettingValue}
+              isBranchSupportEnabled
+              level="branch"
+            />
+            <div className="display-flex-column huge-spacer-bottom sw-gap-4" role="radiogroup">
+              <NewCodeDefinitionPreviousVersionOption
+                isDefault={false}
+                onSelect={this.handleSelectSetting}
+                selected={selected === NewCodeDefinitionType.PreviousVersion}
+              />
+              <NewCodeDefinitionDaysOption
+                days={days}
+                isChanged={isChanged}
+                isValid={isValid}
+                onChangeDays={this.handleSelectDays}
+                onSelect={this.handleSelectSetting}
+                selected={selected === NewCodeDefinitionType.NumberOfDays}
+              />
+              <NewCodeDefinitionSettingReferenceBranch
+                branchList={branchList.map(this.branchToOption)}
+                onChangeReferenceBranch={this.handleSelectReferenceBranch}
+                onSelect={this.handleSelectSetting}
+                referenceBranch={referenceBranch}
+                selected={selected === NewCodeDefinitionType.ReferenceBranch}
+                settingLevel="branch"
+              />
+              {currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
+                <NewCodeDefinitionSettingAnalysis
+                  onSelect={noop}
+                  selected={selected === NewCodeDefinitionType.SpecificAnalysis}
+                />
+              )}
+            </div>
+            {selected === NewCodeDefinitionType.SpecificAnalysis && (
+              <BranchAnalysisList
+                analysis={analysis}
+                branch={branch.name}
+                component={this.props.component}
+                onSelectAnalysis={noop}
+              />
+            )}
+          </div>
+          <footer className="modal-foot">
+            <Spinner className="spacer-right" loading={saving} />
+            <SubmitButton disabled={!isChanged || saving || !isValid}>
+              {translate('save')}
+            </SubmitButton>
+            <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
+          </footer>
+        </form>
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingAnalysis.tsx
new file mode 100644 (file)
index 0000000..027db43
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 { NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+export interface Props {
+  onSelect: (selection: NewCodeDefinitionType) => void;
+  selected: boolean;
+}
+
+export default function NewCodeDefinitionSettingAnalysis({ onSelect, selected }: Props) {
+  return (
+    <RadioCard
+      noRadio
+      disabled
+      onClick={() => onSelect(NewCodeDefinitionType.SpecificAnalysis)}
+      selected={selected}
+      title={translate('baseline.specific_analysis')}
+    >
+      <p className="big-spacer-bottom">{translate('baseline.specific_analysis.description')}</p>
+    </RadioCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/NewCodeDefinitionSettingReferenceBranch.tsx
new file mode 100644 (file)
index 0000000..3d0aa3a
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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 { SelectionCard } from 'design-system';
+import * as React from 'react';
+import { components, OptionProps } from 'react-select';
+import Select from '../../../components/controls/Select';
+import Tooltip from '../../../components/controls/Tooltip';
+import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
+import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
+import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+export interface BaselineSettingReferenceBranchProps {
+  branchList: BranchOption[];
+  className?: string;
+  disabled?: boolean;
+  onChangeReferenceBranch: (value: string) => void;
+  onSelect: (selection: NewCodeDefinitionType) => void;
+  referenceBranch: string;
+  selected: boolean;
+  settingLevel: 'project' | 'branch';
+}
+
+export interface BranchOption {
+  isDisabled?: boolean;
+  isInvalid?: boolean;
+  isMain: boolean;
+  label: string;
+  value: string;
+}
+
+function renderBranchOption(props: OptionProps<BranchOption, false>) {
+  const { data: option } = props;
+
+  return (
+    <components.Option {...props}>
+      {option.isInvalid ? (
+        <Tooltip
+          overlay={translateWithParameters(
+            'baseline.reference_branch.does_not_exist',
+            option.value
+          )}
+        >
+          <span>
+            {option.value} <AlertErrorIcon />
+          </span>
+        </Tooltip>
+      ) : (
+        <>
+          <span
+            title={
+              option.isDisabled
+                ? translate('baseline.reference_branch.cannot_be_itself')
+                : undefined
+            }
+          >
+            {option.value}
+          </span>
+          {option.isMain && (
+            <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+          )}
+        </>
+      )}
+    </components.Option>
+  );
+}
+
+export default function NewCodeDefinitionSettingReferenceBranch(
+  props: BaselineSettingReferenceBranchProps
+) {
+  const { branchList, className, disabled, referenceBranch, selected, settingLevel } = props;
+
+  const currentBranch = branchList.find((b) => b.value === referenceBranch) || {
+    label: referenceBranch,
+    value: referenceBranch,
+    isMain: false,
+    isInvalid: true,
+  };
+
+  return (
+    <SelectionCard
+      className={className}
+      disabled={disabled}
+      onClick={() => props.onSelect(NewCodeDefinitionType.ReferenceBranch)}
+      selected={selected}
+      title={translate('baseline.reference_branch')}
+    >
+      <>
+        <div>
+          <p className="sw-mb-3">{translate('baseline.reference_branch.description')}</p>
+          <p className="sw-mb-4">{translate('baseline.reference_branch.usecase')}</p>
+        </div>
+        {selected && (
+          <>
+            {settingLevel === 'project' && (
+              <p className="spacer-top">{translate('baseline.reference_branch.description2')}</p>
+            )}
+            <div className="big-spacer-top display-flex-column">
+              <MandatoryFieldsExplanation className="spacer-bottom" />
+              <label className="text-middle" htmlFor="reference_branch">
+                <strong>{translate('baseline.reference_branch.choose')}</strong>
+                <MandatoryFieldMarker />
+              </label>
+              <Select
+                className="little-spacer-top spacer-bottom"
+                options={branchList}
+                aria-label={translate('baseline.reference_branch.choose')}
+                onChange={(option: BranchOption) => props.onChangeReferenceBranch(option.value)}
+                value={currentBranch}
+                components={{
+                  Option: renderBranchOption,
+                }}
+              />
+            </div>
+          </>
+        )}
+      </>
+    </SelectionCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx
new file mode 100644 (file)
index 0000000..92c1cbb
--- /dev/null
@@ -0,0 +1,354 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import classNames from 'classnames';
+import { debounce } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import {
+  getNewCodeDefinition,
+  resetNewCodeDefinition,
+  setNewCodeDefinition,
+} from '../../../api/newCodeDefinition';
+import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import withAvailableFeatures, {
+  WithAvailableFeaturesProps,
+} from '../../../app/components/available-features/withAvailableFeatures';
+import withComponentContext from '../../../app/components/componentContext/withComponentContext';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import Spinner from '../../../components/ui/Spinner';
+import { isBranch, sortBranches } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import {
+  DEFAULT_NEW_CODE_DEFINITION_TYPE,
+  getNumberOfDaysDefaultValue,
+} from '../../../helpers/new-code-definition';
+import { withBranchLikes } from '../../../queries/branch';
+import { AppState } from '../../../types/appstate';
+import { Branch, BranchLike } from '../../../types/branch-like';
+import { Feature } from '../../../types/features';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+import { Component } from '../../../types/types';
+import '../styles.css';
+import { getSettingValue } from '../utils';
+import AppHeader from './AppHeader';
+import BranchList from './BranchList';
+import ProjectNewCodeDefinitionSelector from './ProjectNewCodeDefinitionSelector';
+
+interface Props extends WithAvailableFeaturesProps {
+  branchLike: Branch;
+  branchLikes: BranchLike[];
+  component: Component;
+  appState: AppState;
+}
+
+interface State {
+  analysis?: string;
+  branchList: Branch[];
+  currentSetting?: NewCodeDefinitionType;
+  currentSettingValue?: string;
+  days: string;
+  generalSetting?: NewCodeDefinition;
+  isChanged: boolean;
+  loading: boolean;
+  overrideGeneralSetting?: boolean;
+  referenceBranch?: string;
+  saving: boolean;
+  selected?: NewCodeDefinitionType;
+  success?: boolean;
+}
+
+class ProjectNewCodeDefinitionApp extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = {
+    branchList: [],
+    days: getNumberOfDaysDefaultValue(),
+    isChanged: false,
+    loading: true,
+    saving: false,
+  };
+
+  // We use debounce as we could have multiple save in less that 3sec.
+  resetSuccess = debounce(() => this.setState({ success: undefined }), 3000);
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchLeakPeriodSetting();
+    this.sortAndFilterBranches(this.props.branchLikes);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.branchLikes !== this.props.branchLikes) {
+      this.sortAndFilterBranches(this.props.branchLikes);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  getUpdatedState(params: {
+    currentSetting?: NewCodeDefinitionType;
+    currentSettingValue?: string;
+    generalSetting: NewCodeDefinition;
+  }) {
+    const { currentSetting, currentSettingValue, generalSetting } = params;
+    const { referenceBranch } = this.state;
+
+    const defaultDays = getNumberOfDaysDefaultValue(generalSetting);
+
+    return {
+      loading: false,
+      currentSetting,
+      currentSettingValue,
+      generalSetting,
+      isChanged: false,
+      selected: currentSetting || generalSetting.type,
+      overrideGeneralSetting: Boolean(currentSetting),
+      days:
+        (currentSetting === NewCodeDefinitionType.NumberOfDays && currentSettingValue) ||
+        defaultDays,
+      analysis:
+        (currentSetting === NewCodeDefinitionType.SpecificAnalysis && currentSettingValue) || '',
+      referenceBranch:
+        (currentSetting === NewCodeDefinitionType.ReferenceBranch && currentSettingValue) ||
+        referenceBranch,
+    };
+  }
+
+  sortAndFilterBranches(branchLikes: BranchLike[] = []) {
+    const branchList = sortBranches(branchLikes.filter(isBranch));
+    this.setState({ branchList, referenceBranch: branchList[0]?.name });
+  }
+
+  fetchLeakPeriodSetting() {
+    const { branchLike, component } = this.props;
+
+    this.setState({ loading: true });
+
+    Promise.all([
+      getNewCodeDefinition(),
+      getNewCodeDefinition({
+        branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
+        project: component.key,
+      }),
+    ]).then(
+      ([generalSetting, setting]) => {
+        if (this.mounted) {
+          if (!generalSetting.type) {
+            generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE };
+          }
+          const currentSettingValue = setting.value;
+          const currentSetting = setting.inherited
+            ? undefined
+            : setting.type || DEFAULT_NEW_CODE_DEFINITION_TYPE;
+
+          this.setState(
+            this.getUpdatedState({
+              generalSetting,
+              currentSetting,
+              currentSettingValue,
+            })
+          );
+        }
+      },
+      () => {
+        this.setState({ loading: false });
+      }
+    );
+  }
+
+  resetSetting = () => {
+    this.setState({ saving: true });
+    resetNewCodeDefinition({ project: this.props.component.key }).then(
+      () => {
+        this.setState({
+          saving: false,
+          currentSetting: undefined,
+          isChanged: false,
+          selected: undefined,
+          success: true,
+        });
+        this.resetSuccess();
+      },
+      () => {
+        this.setState({ saving: false });
+      }
+    );
+  };
+
+  handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
+
+  handleSelectReferenceBranch = (referenceBranch: string) => {
+    this.setState({ referenceBranch, isChanged: true });
+  };
+
+  handleCancel = () =>
+    this.setState(
+      ({
+        generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE },
+        currentSetting,
+        currentSettingValue,
+      }) => this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
+    );
+
+  handleSelectSetting = (selected?: NewCodeDefinitionType) => {
+    this.setState((currentState) => ({
+      selected,
+      isChanged: selected !== currentState.selected,
+    }));
+  };
+
+  handleToggleSpecificSetting = (overrideGeneralSetting: boolean) =>
+    this.setState((currentState) => ({
+      overrideGeneralSetting,
+      isChanged: currentState.overrideGeneralSetting !== overrideGeneralSetting,
+    }));
+
+  handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+    e.preventDefault();
+
+    const { component } = this.props;
+    const { days, selected: type, referenceBranch, overrideGeneralSetting } = this.state;
+
+    if (!overrideGeneralSetting) {
+      this.resetSetting();
+      return;
+    }
+
+    const value = getSettingValue({ type, days, referenceBranch });
+
+    if (type) {
+      this.setState({ saving: true });
+      setNewCodeDefinition({
+        project: component.key,
+        type,
+        value,
+      }).then(
+        () => {
+          this.setState({
+            saving: false,
+            currentSetting: type,
+            currentSettingValue: value || undefined,
+            isChanged: false,
+            success: true,
+          });
+          this.resetSuccess();
+        },
+        () => {
+          this.setState({ saving: false });
+        }
+      );
+    }
+  };
+
+  render() {
+    const { appState, component, branchLike } = this.props;
+    const {
+      analysis,
+      branchList,
+      currentSetting,
+      days,
+      generalSetting,
+      isChanged,
+      loading,
+      currentSettingValue,
+      overrideGeneralSetting,
+      referenceBranch,
+      saving,
+      selected,
+      success,
+    } = this.state;
+    const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport);
+
+    return (
+      <>
+        <Suggestions suggestions="project_baseline" />
+        <Helmet defer={false} title={translate('project_baseline.page')} />
+        <div className="page page-limited">
+          <AppHeader canAdmin={!!appState.canAdmin} />
+          <Spinner loading={loading} />
+
+          {!loading && (
+            <div className="panel-white project-baseline">
+              {branchSupportEnabled && <h2>{translate('project_baseline.default_setting')}</h2>}
+
+              {generalSetting && overrideGeneralSetting !== undefined && (
+                <ProjectNewCodeDefinitionSelector
+                  analysis={analysis}
+                  branch={branchLike}
+                  branchList={branchList}
+                  branchesEnabled={branchSupportEnabled}
+                  canAdmin={appState.canAdmin}
+                  component={component.key}
+                  currentSetting={currentSetting}
+                  currentSettingValue={currentSettingValue}
+                  days={days}
+                  generalSetting={generalSetting}
+                  isChanged={isChanged}
+                  onCancel={this.handleCancel}
+                  onSelectDays={this.handleSelectDays}
+                  onSelectReferenceBranch={this.handleSelectReferenceBranch}
+                  onSelectSetting={this.handleSelectSetting}
+                  onSubmit={this.handleSubmit}
+                  onToggleSpecificSetting={this.handleToggleSpecificSetting}
+                  overrideGeneralSetting={overrideGeneralSetting}
+                  referenceBranch={referenceBranch}
+                  saving={saving}
+                  selected={selected}
+                />
+              )}
+
+              <div className={classNames('spacer-top', { invisible: saving || !success })}>
+                <span className="text-success">
+                  <AlertSuccessIcon className="spacer-right" />
+                  {translate('settings.state.saved')}
+                </span>
+              </div>
+              {generalSetting && branchSupportEnabled && (
+                <div className="huge-spacer-top branch-baseline-selector">
+                  <hr />
+                  <h2>{translate('project_baseline.configure_branches')}</h2>
+                  <BranchList
+                    branchList={branchList}
+                    component={component}
+                    inheritedSetting={
+                      currentSetting
+                        ? {
+                            type: currentSetting,
+                            value: currentSettingValue,
+                          }
+                        : generalSetting
+                    }
+                    generalSetting={generalSetting}
+                  />
+                </div>
+              )}
+            </div>
+          )}
+        </div>
+      </>
+    );
+  }
+}
+
+export default withComponentContext(
+  withAvailableFeatures(withAppStateContext(withBranchLikes(ProjectNewCodeDefinitionApp)))
+);
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx
new file mode 100644 (file)
index 0000000..204f410
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import classNames from 'classnames';
+import { RadioButton } from 'design-system';
+import { noop } from 'lodash';
+import * as React from 'react';
+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 Spinner from '../../../components/ui/Spinner';
+import { translate } from '../../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
+import { Branch } from '../../../types/branch-like';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+import { validateSetting } from '../utils';
+import BranchAnalysisList from './BranchAnalysisList';
+import NewCodeDefinitionSettingAnalysis from './NewCodeDefinitionSettingAnalysis';
+import NewCodeDefinitionSettingReferenceBranch from './NewCodeDefinitionSettingReferenceBranch';
+
+export interface ProjectBaselineSelectorProps {
+  analysis?: string;
+  branch?: Branch;
+  branchList: Branch[];
+  branchesEnabled?: boolean;
+  canAdmin: boolean | undefined;
+  component: string;
+  currentSetting?: NewCodeDefinitionType;
+  currentSettingValue?: string;
+  days: string;
+  generalSetting: NewCodeDefinition;
+  isChanged: boolean;
+  onCancel: () => void;
+  onSelectDays: (value: string) => void;
+  onSelectReferenceBranch: (value: string) => void;
+  onSelectSetting: (value?: NewCodeDefinitionType) => void;
+  onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
+  onToggleSpecificSetting: (selection: boolean) => void;
+  referenceBranch?: string;
+  saving: boolean;
+  selected?: NewCodeDefinitionType;
+  overrideGeneralSetting: boolean;
+}
+
+function branchToOption(b: Branch) {
+  return { label: b.name, value: b.name, isMain: b.isMain };
+}
+
+export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineSelectorProps) {
+  const {
+    analysis,
+    branch,
+    branchList,
+    branchesEnabled,
+    canAdmin,
+    component,
+    currentSetting,
+    currentSettingValue,
+    days,
+    generalSetting,
+    isChanged,
+    overrideGeneralSetting,
+    referenceBranch,
+    saving,
+    selected,
+  } = props;
+
+  const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
+
+  const isValid = validateSetting({
+    days,
+    overrideGeneralSetting,
+    referenceBranch,
+    selected,
+  });
+
+  if (branch === undefined) {
+    return null;
+  }
+
+  return (
+    <form className="project-baseline-selector" onSubmit={props.onSubmit}>
+      <div className="big-spacer-top spacer-bottom" role="radiogroup">
+        <RadioButton
+          checked={!overrideGeneralSetting}
+          className="big-spacer-bottom"
+          disabled={!isGlobalNcdCompliant}
+          onCheck={() => props.onToggleSpecificSetting(false)}
+          value="general"
+        >
+          <Tooltip
+            overlay={
+              isGlobalNcdCompliant
+                ? null
+                : translate('project_baseline.compliance.warning.title.global')
+            }
+          >
+            <span>{translate('project_baseline.global_setting')}</span>
+          </Tooltip>
+        </RadioButton>
+
+        <div className="sw-ml-4">
+          <GlobalNewCodeDefinitionDescription
+            globalNcd={generalSetting}
+            isGlobalNcdCompliant={isGlobalNcdCompliant}
+            canAdmin={canAdmin}
+          />
+        </div>
+
+        <RadioButton
+          checked={overrideGeneralSetting}
+          className="huge-spacer-top"
+          onCheck={() => props.onToggleSpecificSetting(true)}
+          value="specific"
+        >
+          {translate('project_baseline.specific_setting')}
+        </RadioButton>
+      </div>
+
+      <div className="big-spacer-left big-spacer-right project-baseline-setting">
+        <NewCodeDefinitionWarning
+          newCodeDefinitionType={currentSetting}
+          newCodeDefinitionValue={currentSettingValue}
+          isBranchSupportEnabled={branchesEnabled}
+          level="project"
+        />
+        <div className="display-flex-column big-spacer-bottom sw-gap-4" role="radiogroup">
+          <NewCodeDefinitionPreviousVersionOption
+            disabled={!overrideGeneralSetting}
+            onSelect={props.onSelectSetting}
+            selected={overrideGeneralSetting && selected === NewCodeDefinitionType.PreviousVersion}
+          />
+          <NewCodeDefinitionDaysOption
+            days={days}
+            disabled={!overrideGeneralSetting}
+            isChanged={isChanged}
+            isValid={isValid}
+            onChangeDays={props.onSelectDays}
+            onSelect={props.onSelectSetting}
+            selected={overrideGeneralSetting && selected === NewCodeDefinitionType.NumberOfDays}
+          />
+          {branchesEnabled && (
+            <NewCodeDefinitionSettingReferenceBranch
+              branchList={branchList.map(branchToOption)}
+              disabled={!overrideGeneralSetting}
+              onChangeReferenceBranch={props.onSelectReferenceBranch}
+              onSelect={props.onSelectSetting}
+              referenceBranch={referenceBranch || ''}
+              selected={
+                overrideGeneralSetting && selected === NewCodeDefinitionType.ReferenceBranch
+              }
+              settingLevel="project"
+            />
+          )}
+          {!branchesEnabled && currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
+            <NewCodeDefinitionSettingAnalysis
+              onSelect={noop}
+              selected={
+                overrideGeneralSetting && selected === NewCodeDefinitionType.SpecificAnalysis
+              }
+            />
+          )}
+        </div>
+        {!branchesEnabled &&
+          overrideGeneralSetting &&
+          selected === NewCodeDefinitionType.SpecificAnalysis && (
+            <BranchAnalysisList
+              analysis={analysis || ''}
+              branch={branch.name}
+              component={component}
+              onSelectAnalysis={noop}
+            />
+          )}
+      </div>
+      <div className={classNames('big-spacer-top', { invisible: !isChanged })}>
+        <Alert variant="info" className="spacer-bottom">
+          {translate('baseline.next_analysis_notice')}
+        </Alert>
+        <Spinner className="spacer-right" loading={saving} />
+        <SubmitButton disabled={saving || !isValid || !isChanged}>{translate('save')}</SubmitButton>
+        <ResetButtonLink className="spacer-left" onClick={props.onCancel}>
+          {translate('cancel')}
+        </ResetButtonLink>
+      </div>
+    </form>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx
new file mode 100644 (file)
index 0000000..3a3a609
--- /dev/null
@@ -0,0 +1,395 @@
+/*
+ * 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 { within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { first, last } from 'lodash';
+import selectEvent from 'react-select-event';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition';
+import { mockAppState } from '../../../../helpers/testMocks';
+import {
+  RenderContext,
+  renderAppWithComponentContext,
+} from '../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../helpers/testSelector';
+import { Feature } from '../../../../types/features';
+import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
+import routes from '../../routes';
+
+jest.mock('../../../../api/newCodeDefinition');
+jest.mock('../../../../api/projectActivity');
+jest.mock('../../../../api/branches');
+
+const codePeriodsMock = new NewCodeDefinitionServiceMock();
+const projectActivityMock = new ProjectActivityServiceMock();
+const branchHandler = new BranchesServiceMock();
+
+afterEach(() => {
+  branchHandler.reset();
+  codePeriodsMock.reset();
+  projectActivityMock.reset();
+});
+
+it('renders correctly without branch support feature', async () => {
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp();
+  await ui.appIsLoaded();
+
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
+  expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
+
+  // User is not admin
+  expect(ui.generalSettingsLink.query()).not.toBeInTheDocument();
+
+  // Specific branch setting is not rendered without feature branch
+  expect(ui.branchListHeading.query()).not.toBeInTheDocument();
+  expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
+});
+
+it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
+  codePeriodsMock.setNewCodePeriod({
+    type: NewCodeDefinitionType.NumberOfDays,
+    value: '99',
+    inherited: true,
+  });
+
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp();
+  await ui.appIsLoaded();
+
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
+  expect(ui.generalSettingRadio.get()).toBeDisabled();
+  expect(ui.complianceWarning.get()).toBeVisible();
+});
+
+it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
+  codePeriodsMock.setNewCodePeriod({
+    type: NewCodeDefinitionType.NumberOfDays,
+    value: '99',
+    inherited: true,
+  });
+
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
+  await ui.appIsLoaded();
+
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
+  expect(ui.generalSettingRadio.get()).toBeDisabled();
+  expect(ui.complianceWarningAdmin.get()).toBeVisible();
+  expect(ui.complianceWarning.query()).not.toBeInTheDocument();
+});
+
+it('renders correctly with branch support feature', async () => {
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+    appState: mockAppState({ canAdmin: true }),
+  });
+  await ui.appIsLoaded();
+
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
+  expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
+
+  // User is admin
+  expect(ui.generalSettingsLink.get()).toBeInTheDocument();
+
+  // Specific branch setting is rendered with feature support branch
+  expect(ui.branchListHeading.get()).toBeInTheDocument();
+  expect(ui.referenceBranchRadio.get()).toBeInTheDocument();
+});
+
+it('can set previous version specific setting', async () => {
+  const { ui, user } = getPageObjects();
+  renderProjectBaselineApp();
+  await ui.appIsLoaded();
+
+  expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
+  await ui.setPreviousVersionSetting();
+  expect(ui.previousVersionRadio.get()).toBeChecked();
+
+  // Save changes
+  await user.click(ui.saveButton.get());
+
+  expect(ui.saved.get()).toBeInTheDocument();
+
+  // Set general setting
+  await user.click(ui.generalSettingRadio.get());
+  expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
+  await user.click(ui.saveButton.get());
+  expect(ui.saved.get()).toBeInTheDocument();
+});
+
+it('can set number of days specific setting', async () => {
+  const { ui, user } = getPageObjects();
+  renderProjectBaselineApp();
+  await ui.appIsLoaded();
+
+  expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
+  await ui.setNumberDaysSetting('10');
+  expect(ui.numberDaysRadio.get()).toBeChecked();
+
+  // Reset to initial state
+  await user.click(ui.cancelButton.get());
+  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
+
+  // Save changes
+  await ui.setNumberDaysSetting('10');
+  await user.click(ui.saveButton.get());
+
+  expect(ui.saved.get()).toBeInTheDocument();
+});
+
+it('can set reference branch specific setting', async () => {
+  const { ui, user } = getPageObjects();
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+  });
+  await ui.appIsLoaded();
+
+  expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
+  await ui.setReferenceBranchSetting('main');
+  expect(ui.referenceBranchRadio.get()).toBeChecked();
+
+  // Save changes
+  await user.click(ui.saveButton.get());
+
+  expect(ui.saved.get()).toBeInTheDocument();
+});
+
+it('cannot set specific analysis setting', async () => {
+  const { ui } = getPageObjects();
+  codePeriodsMock.setNewCodePeriod({
+    type: NewCodeDefinitionType.SpecificAnalysis,
+    value: 'analysis_id',
+  });
+  renderProjectBaselineApp();
+  await ui.appIsLoaded();
+
+  expect(await ui.specificAnalysisRadio.find()).toBeChecked();
+  expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
+  expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
+
+  await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
+
+  expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
+  expect(ui.saveButton.get()).toBeDisabled();
+});
+
+it('renders correctly branch modal', async () => {
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+  });
+  await ui.appIsLoaded();
+
+  await ui.openBranchSettingModal('main');
+
+  expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
+});
+
+it('can set a previous version setting for branch', async () => {
+  const { ui, user } = getPageObjects();
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+  });
+  await ui.appIsLoaded();
+  await ui.setBranchPreviousVersionSetting('main');
+
+  expect(
+    within(byRole('table').get()).getByText('new_code_definition.previous_version')
+  ).toBeInTheDocument();
+
+  await user.click(await ui.branchActionsButton('main').find());
+
+  expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
+  await user.click(ui.resetToDefaultButton.get());
+
+  expect(
+    first(within(byRole('table').get()).getAllByText('branch_list.default_setting'))
+  ).toBeInTheDocument();
+});
+
+it('can set a number of days setting for branch', async () => {
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+  });
+  await ui.appIsLoaded();
+
+  await ui.setBranchNumberOfDaysSetting('main', '15');
+
+  expect(
+    within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
+  ).toBeInTheDocument();
+});
+
+it('cannot set a specific analysis setting for branch', async () => {
+  const { ui } = getPageObjects();
+  codePeriodsMock.setListBranchesNewCode([
+    mockNewCodePeriodBranch({
+      branchKey: 'main',
+      type: NewCodeDefinitionType.SpecificAnalysis,
+      value: 'analysis_id',
+    }),
+  ]);
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+  });
+  await ui.appIsLoaded();
+
+  await ui.openBranchSettingModal('main');
+
+  expect(ui.specificAnalysisRadio.get()).toBeChecked();
+  expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
+  expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
+
+  await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
+
+  expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
+  expect(last(ui.saveButton.getAll())).toBeDisabled();
+});
+
+it('can set a reference branch setting for branch', async () => {
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp({
+    featureList: [Feature.BranchSupport],
+  });
+  await ui.appIsLoaded();
+
+  await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
+
+  expect(
+    byRole('table').byText('baseline.reference_branch: normal-branch').get()
+  ).toBeInTheDocument();
+});
+
+function renderProjectBaselineApp(context: RenderContext = {}, params?: string) {
+  return renderAppWithComponentContext(
+    'baseline',
+    routes,
+    {
+      ...context,
+      navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
+    },
+    {
+      component: mockComponent(),
+    }
+  );
+}
+
+function getPageObjects() {
+  const user = userEvent.setup();
+
+  const ui = {
+    pageHeading: byRole('heading', { name: 'project_baseline.page' }),
+    branchTableHeading: byText('branch_list.branch'),
+    branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
+    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: /new_code_definition.previous_version.description/,
+    }),
+    numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
+    numberDaysInput: byRole('spinbutton'),
+    referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
+    chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
+    specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
+    specificAnalysisWarning: byText('baseline.specific_analysis.compliance_warning.title'),
+    analysisFromSelect: byRole('combobox', { name: 'baseline.analysis_from' }),
+    analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
+    saveButton: byRole('button', { name: 'save' }),
+    cancelButton: byRole('button', { name: 'cancel' }),
+    branchActionsButton: (branch: string) =>
+      byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
+    editButton: byRole('button', { name: 'edit' }),
+    resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
+    saved: byText('settings.state.saved'),
+    complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
+    complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
+  };
+
+  async function appIsLoaded() {
+    expect(await ui.pageHeading.find()).toBeInTheDocument();
+  }
+
+  async function setPreviousVersionSetting() {
+    await user.click(ui.specificSettingRadio.get());
+    await user.click(ui.previousVersionRadio.get());
+  }
+
+  async function setBranchPreviousVersionSetting(branch: string) {
+    await openBranchSettingModal(branch);
+    await user.click(last(ui.previousVersionRadio.getAll()) as HTMLElement);
+    await user.click(last(ui.saveButton.getAll()) as HTMLElement);
+  }
+
+  async function setNumberDaysSetting(value: string) {
+    await user.click(ui.specificSettingRadio.get());
+    await user.click(ui.numberDaysRadio.get());
+    await user.clear(ui.numberDaysInput.get());
+    await user.type(ui.numberDaysInput.get(), value);
+  }
+
+  async function setBranchNumberOfDaysSetting(branch: string, value: string) {
+    await openBranchSettingModal(branch);
+    await user.click(last(ui.numberDaysRadio.getAll()) as HTMLElement);
+    await user.clear(ui.numberDaysInput.get());
+    await user.type(ui.numberDaysInput.get(), value);
+    await user.click(last(ui.saveButton.getAll()) as HTMLElement);
+  }
+
+  async function setReferenceBranchSetting(branch: string) {
+    await user.click(ui.specificSettingRadio.get());
+    await user.click(ui.referenceBranchRadio.get());
+    await selectEvent.select(ui.chooseBranchSelect.get(), branch);
+  }
+
+  async function setBranchReferenceToBranchSetting(branch: string, branchRef: string) {
+    await openBranchSettingModal(branch);
+    await user.click(last(ui.referenceBranchRadio.getAll()) as HTMLElement);
+    await selectEvent.select(ui.chooseBranchSelect.get(), branchRef);
+    await user.click(last(ui.saveButton.getAll()) as HTMLElement);
+  }
+
+  async function openBranchSettingModal(branch: string) {
+    await user.click(await ui.branchActionsButton(branch).find());
+    await user.click(ui.editButton.get());
+  }
+
+  return {
+    ui: {
+      ...ui,
+      appIsLoaded,
+      setNumberDaysSetting,
+      setPreviousVersionSetting,
+      setReferenceBranchSetting,
+      setBranchPreviousVersionSetting,
+      setBranchNumberOfDaysSetting,
+      setBranchReferenceToBranchSetting,
+      openBranchSettingModal,
+    },
+    user,
+  };
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/utils-test.ts
new file mode 100644 (file)
index 0000000..b0bd20f
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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 { NewCodeDefinitionType } from '../../../../types/new-code-definition';
+import { getSettingValue, validateSetting } from '../../utils';
+
+describe('getSettingValue', () => {
+  const state = {
+    analysis: 'analysis',
+    days: '35',
+    referenceBranch: 'branch-4.2',
+  };
+
+  it('should work for Days', () => {
+    expect(getSettingValue({ ...state, type: NewCodeDefinitionType.NumberOfDays })).toBe(
+      state.days
+    );
+  });
+
+  it('should work for Analysis', () => {
+    expect(getSettingValue({ ...state, type: NewCodeDefinitionType.SpecificAnalysis })).toBe(
+      state.analysis
+    );
+  });
+
+  it('should work for Previous version', () => {
+    expect(
+      getSettingValue({ ...state, type: NewCodeDefinitionType.PreviousVersion })
+    ).toBeUndefined();
+  });
+
+  it('should work for Reference branch', () => {
+    expect(getSettingValue({ ...state, type: NewCodeDefinitionType.ReferenceBranch })).toBe(
+      state.referenceBranch
+    );
+  });
+});
+
+describe('validateSettings', () => {
+  it('should validate at branch level', () => {
+    expect(validateSetting({ days: '' })).toEqual(false);
+    expect(
+      validateSetting({
+        days: '12',
+        selected: NewCodeDefinitionType.NumberOfDays,
+      })
+    ).toEqual(true);
+    expect(
+      validateSetting({
+        days: 'nope',
+        selected: NewCodeDefinitionType.NumberOfDays,
+      })
+    ).toEqual(false);
+    expect(
+      validateSetting({
+        days: '',
+        selected: NewCodeDefinitionType.SpecificAnalysis,
+      })
+    ).toEqual(false);
+    expect(
+      validateSetting({
+        days: '',
+        referenceBranch: 'master',
+        selected: NewCodeDefinitionType.ReferenceBranch,
+      })
+    ).toEqual(true);
+    expect(
+      validateSetting({
+        days: '',
+        referenceBranch: '',
+        selected: NewCodeDefinitionType.ReferenceBranch,
+      })
+    ).toEqual(false);
+  });
+
+  it('should validate at project level', () => {
+    expect(validateSetting({ days: '', overrideGeneralSetting: false })).toEqual(true);
+    expect(
+      validateSetting({
+        selected: NewCodeDefinitionType.PreviousVersion,
+        days: '',
+        overrideGeneralSetting: true,
+      })
+    ).toEqual(true);
+    expect(
+      validateSetting({
+        selected: NewCodeDefinitionType.NumberOfDays,
+        days: '',
+        overrideGeneralSetting: true,
+      })
+    ).toEqual(false);
+    expect(
+      validateSetting({
+        selected: NewCodeDefinitionType.NumberOfDays,
+        days: '12',
+        overrideGeneralSetting: true,
+      })
+    ).toEqual(true);
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/routes.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/routes.tsx
new file mode 100644 (file)
index 0000000..f6aa61e
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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 React from 'react';
+import { Route } from 'react-router-dom';
+import ProjectNewCodeDefinitionApp from './components/ProjectNewCodeDefinitionApp';
+
+const routes = () => <Route path="baseline" element={<ProjectNewCodeDefinitionApp />} />;
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/styles.css b/server/sonar-web/src/main/js/apps/projectNewCode/styles.css
new file mode 100644 (file)
index 0000000..5e09ae4
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+.project-baseline {
+  padding: calc(4 * var(--gridSize));
+}
+
+.project-baseline-setting {
+  display: flex;
+  flex-direction: column;
+  max-height: 60vh;
+  padding-top: 2px;
+}
+
+.branch-baseline-selector > hr {
+  margin: 0 calc(-4 * var(--gridSize)) calc(4 * var(--gridSize));
+}
+
+.branch-baseline-setting-modal {
+  min-height: 450px;
+  display: flex;
+  flex-direction: column;
+}
+
+.branch-analysis-list-wrapper {
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  position: relative;
+  min-height: 200px;
+}
+
+.branch-analysis-list {
+  overflow-y: auto;
+  padding-left: 12px;
+  padding-right: 15px;
+  min-height: 50px;
+}
+
+.branch-analysis-list > ul {
+  padding-top: 18px;
+}
+
+.branch-analysis-date {
+  margin-bottom: 16px;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+.branch-analysis-day {
+  margin-top: var(--gridSize);
+  margin-bottom: calc(3 * var(--gridSize));
+}
+
+.branch-analysis {
+  display: flex;
+  justify-content: space-between;
+  padding: var(--gridSize);
+  border-top: 1px solid var(--barBorderColor);
+  border-bottom: 1px solid var(--barBorderColor);
+  cursor: not-allowed;
+}
+
+.branch-analysis + .branch-analysis {
+  border-top: none;
+}
+
+.branch-analysis:hover {
+  background-color: var(--disableGrayBg);
+}
+
+.branch-analysis > .project-activity-events {
+  flex: 1 0 50%;
+}
+
+.branch-analysis-time {
+  width: 150px;
+}
+
+.branch-analysis-version-badge {
+  margin-left: -12px;
+  padding-top: var(--gridSize);
+  padding-bottom: var(--gridSize);
+  background-color: white;
+}
+
+.branch-analysis-version-badge.sticky + .branch-analysis-days-list {
+  padding-top: 36px;
+}
+
+.branch-analysis-version-badge.sticky,
+.branch-analysis-version-badge.first {
+  position: absolute;
+  top: 1px;
+  left: 13px;
+  right: 16px;
+  padding-top: calc(3 * var(--gridSize));
+  z-index: var(--belowNormalZIndex);
+}
+
+.branch-analysis-version-badge .badge {
+  max-width: 385px;
+  border-radius: 0 2px 2px 0;
+  font-weight: bold;
+  font-size: var(--smallFontSize);
+  letter-spacing: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.branch-setting-warning {
+  background-color: var(--alertBackgroundWarning) !important;
+}
+
+.project-activity-event-icon.VERSION {
+  color: var(--blue);
+}
+
+.project-activity-event-icon.QUALITY_GATE {
+  color: var(--purple);
+}
+
+.project-activity-event-icon.QUALITY_PROFILE {
+  color: #cccccc;
+}
+
+.project-activity-event-icon.DEFINITION_CHANGE {
+  color: #33a759;
+}
+
+.project-activity-event-icon.OTHER {
+  color: #442d1b;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/utils.ts b/server/sonar-web/src/main/js/apps/projectNewCode/utils.ts
new file mode 100644 (file)
index 0000000..45c5c8e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 { isNewCodeDefinitionCompliant } from '../../helpers/new-code-definition';
+import { NewCodeDefinitionType } from '../../types/new-code-definition';
+
+export function getSettingValue({
+  analysis,
+  days,
+  referenceBranch,
+  type,
+}: {
+  analysis?: string;
+  days?: string;
+  referenceBranch?: string;
+  type?: NewCodeDefinitionType;
+}) {
+  switch (type) {
+    case NewCodeDefinitionType.NumberOfDays:
+      return days;
+    case NewCodeDefinitionType.ReferenceBranch:
+      return referenceBranch;
+    case NewCodeDefinitionType.SpecificAnalysis:
+      return analysis;
+    default:
+      return undefined;
+  }
+}
+
+export function validateSetting(state: {
+  days: string;
+  overrideGeneralSetting?: boolean;
+  referenceBranch?: string;
+  selected?: NewCodeDefinitionType;
+}) {
+  const { days, overrideGeneralSetting, referenceBranch = '', selected } = state;
+
+  return (
+    overrideGeneralSetting === false ||
+    (!!selected &&
+      isNewCodeDefinitionCompliant({
+        type: selected,
+        value: days,
+      }) &&
+      (selected !== NewCodeDefinitionType.ReferenceBranch || referenceBranch.length > 0))
+  );
+}
index 26a657948c965388f900a10be36dbbd76c219b4a..a5489fc67c3bee1ee2fc8bb613e889f635d08529 100644 (file)
@@ -29,11 +29,11 @@ import {
   NEW_CODE_PERIOD_CATEGORY,
   PULL_REQUEST_DECORATION_BINDING_CATEGORY,
 } from '../constants';
-import AlmIntegration from './almIntegration/AlmIntegration';
 import { AnalysisScope } from './AnalysisScope';
-import Authentication from './authentication/Authentication';
 import Languages from './Languages';
-import NewCodePeriod from './NewCodePeriod';
+import NewCodeDefinition from './NewCodeDefinition';
+import AlmIntegration from './almIntegration/AlmIntegration';
+import Authentication from './authentication/Authentication';
 import PullRequestDecorationBinding from './pullRequestDecorationBinding/PRDecorationBinding';
 
 export interface AdditionalCategoryComponentProps {
@@ -110,7 +110,7 @@ function getLanguagesComponent(props: AdditionalCategoryComponentProps) {
 }
 
 function getNewCodePeriodComponent() {
-  return <NewCodePeriod />;
+  return <NewCodeDefinition />;
 }
 
 function getAnalysisScopeComponent(props: AdditionalCategoryComponentProps) {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx
new file mode 100644 (file)
index 0000000..de5639a
--- /dev/null
@@ -0,0 +1,264 @@
+/*
+ * 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 { getNewCodeDefinition, setNewCodeDefinition } from '../../../api/newCodeDefinition';
+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 Spinner from '../../../components/ui/Spinner';
+import { translate } from '../../../helpers/l10n';
+import {
+  getNumberOfDaysDefaultValue,
+  isNewCodeDefinitionCompliant,
+} from '../../../helpers/new-code-definition';
+import { NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+interface State {
+  currentSetting?: NewCodeDefinitionType;
+  days: string;
+  previousNonCompliantValue?: string;
+  ncdUpdatedAt?: number;
+  loading: boolean;
+  currentSettingValue?: string;
+  isChanged: boolean;
+  saving: boolean;
+  selected?: NewCodeDefinitionType;
+  success: boolean;
+}
+
+export default class NewCodeDefinition extends React.PureComponent<{}, State> {
+  mounted = false;
+  state: State = {
+    loading: true,
+    days: getNumberOfDaysDefaultValue(),
+    isChanged: false,
+    saving: false,
+    success: false,
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchNewCodePeriodSetting();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchNewCodePeriodSetting() {
+    getNewCodeDefinition()
+      .then(({ type, value, previousNonCompliantValue, updatedAt }) => {
+        this.setState(({ days }) => ({
+          currentSetting: type,
+          days: type === NewCodeDefinitionType.NumberOfDays ? String(value) : days,
+          loading: false,
+          currentSettingValue: value,
+          selected: type,
+          previousNonCompliantValue,
+          ncdUpdatedAt: updatedAt,
+        }));
+      })
+      .catch(() => {
+        this.setState({ loading: false });
+      });
+  }
+
+  onSelectDays = (days: string) => {
+    this.setState({ days, success: false, isChanged: true });
+  };
+
+  onSelectSetting = (selected: NewCodeDefinitionType) => {
+    this.setState((currentState) => ({
+      selected,
+      success: false,
+      isChanged: selected !== currentState.selected,
+    }));
+  };
+
+  onCancel = () => {
+    this.setState(({ currentSetting, currentSettingValue, days }) => ({
+      isChanged: false,
+      selected: currentSetting,
+      days:
+        currentSetting === NewCodeDefinitionType.NumberOfDays ? String(currentSettingValue) : days,
+    }));
+  };
+
+  onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+    e.preventDefault();
+
+    const { days, selected } = this.state;
+
+    const type = selected;
+    const value = type === NewCodeDefinitionType.NumberOfDays ? days : undefined;
+
+    this.setState({ saving: true, success: false });
+    setNewCodeDefinition({
+      type: type as NewCodeDefinitionType,
+      value,
+    }).then(
+      () => {
+        if (this.mounted) {
+          this.setState({
+            saving: false,
+            currentSetting: type,
+            currentSettingValue: value || undefined,
+            isChanged: false,
+            success: true,
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({
+            saving: false,
+          });
+        }
+      }
+    );
+  };
+
+  render() {
+    const {
+      currentSetting,
+      days,
+      previousNonCompliantValue,
+      ncdUpdatedAt,
+      loading,
+      isChanged,
+      currentSettingValue,
+      saving,
+      selected,
+      success,
+    } = this.state;
+
+    const isValid =
+      selected !== NewCodeDefinitionType.NumberOfDays ||
+      isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: days });
+
+    return (
+      <>
+        <h2
+          className="settings-sub-category-name settings-definition-name"
+          title={translate('settings.new_code_period.title')}
+        >
+          {translate('settings.new_code_period.title')}
+        </h2>
+
+        <ul className="settings-sub-categories-list">
+          <li>
+            <ul className="settings-definitions-list">
+              <li>
+                <div className="settings-definition">
+                  <div className="settings-definition-left">
+                    <div className="small">
+                      <p className="sw-mb-2">
+                        {translate('settings.new_code_period.description0')}
+                      </p>
+                      <p className="sw-mb-2">
+                        {translate('settings.new_code_period.description1')}
+                      </p>
+                      <p className="sw-mb-2">
+                        {translate('settings.new_code_period.description2')}
+                      </p>
+
+                      <p className="sw-mb-2">
+                        <FormattedMessage
+                          defaultMessage={translate('settings.new_code_period.description3')}
+                          id="settings.new_code_period.description3"
+                          values={{
+                            link: (
+                              <DocLink to="/project-administration/defining-new-code/">
+                                {translate('settings.new_code_period.description3.link')}
+                              </DocLink>
+                            ),
+                          }}
+                        />
+                      </p>
+
+                      <p className="sw-mt-4">
+                        <strong>{translate('settings.new_code_period.question')}</strong>
+                      </p>
+                    </div>
+                  </div>
+
+                  <div className="settings-definition-right">
+                    <Spinner loading={loading}>
+                      <form onSubmit={this.onSubmit}>
+                        <NewCodeDefinitionPreviousVersionOption
+                          isDefault
+                          onSelect={this.onSelectSetting}
+                          selected={selected === NewCodeDefinitionType.PreviousVersion}
+                        />
+                        <NewCodeDefinitionDaysOption
+                          className="spacer-top sw-mb-4"
+                          days={days}
+                          previousNonCompliantValue={previousNonCompliantValue}
+                          updatedAt={ncdUpdatedAt}
+                          isChanged={isChanged}
+                          isValid={isValid}
+                          onChangeDays={this.onSelectDays}
+                          onSelect={this.onSelectSetting}
+                          selected={selected === NewCodeDefinitionType.NumberOfDays}
+                        />
+                        <NewCodeDefinitionWarning
+                          newCodeDefinitionType={currentSetting}
+                          newCodeDefinitionValue={currentSettingValue}
+                          isBranchSupportEnabled={undefined}
+                          level="global"
+                        />
+                        {isChanged && (
+                          <div className="big-spacer-top">
+                            <p className="spacer-bottom">
+                              {translate('baseline.next_analysis_notice')}
+                            </p>
+                            <Spinner className="spacer-right" loading={saving} />
+                            <SubmitButton disabled={saving || !isValid}>
+                              {translate('save')}
+                            </SubmitButton>
+                            <ResetButtonLink className="spacer-left" onClick={this.onCancel}>
+                              {translate('cancel')}
+                            </ResetButtonLink>
+                          </div>
+                        )}
+                        {!saving && !loading && success && (
+                          <div className="big-spacer-top">
+                            <span className="text-success">
+                              <AlertSuccessIcon className="spacer-right" />
+                              {translate('settings.state.saved')}
+                            </span>
+                          </div>
+                        )}
+                      </form>
+                    </Spinner>
+                  </div>
+                </div>
+              </li>
+            </ul>
+          </li>
+        </ul>
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx
deleted file mode 100644 (file)
index 76a6815..0000000
+++ /dev/null
@@ -1,264 +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 { FormattedMessage } from 'react-intl';
-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 Spinner from '../../../components/ui/Spinner';
-import { translate } from '../../../helpers/l10n';
-import {
-  getNumberOfDaysDefaultValue,
-  isNewCodeDefinitionCompliant,
-} from '../../../helpers/new-code-definition';
-import { NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-interface State {
-  currentSetting?: NewCodeDefinitionType;
-  days: string;
-  previousNonCompliantValue?: string;
-  ncdUpdatedAt?: number;
-  loading: boolean;
-  currentSettingValue?: string;
-  isChanged: boolean;
-  saving: boolean;
-  selected?: NewCodeDefinitionType;
-  success: boolean;
-}
-
-export default class NewCodePeriod extends React.PureComponent<{}, State> {
-  mounted = false;
-  state: State = {
-    loading: true,
-    days: getNumberOfDaysDefaultValue(),
-    isChanged: false,
-    saving: false,
-    success: false,
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchNewCodePeriodSetting();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchNewCodePeriodSetting() {
-    getNewCodePeriod()
-      .then(({ type, value, previousNonCompliantValue, updatedAt }) => {
-        this.setState(({ days }) => ({
-          currentSetting: type,
-          days: type === NewCodeDefinitionType.NumberOfDays ? String(value) : days,
-          loading: false,
-          currentSettingValue: value,
-          selected: type,
-          previousNonCompliantValue,
-          ncdUpdatedAt: updatedAt,
-        }));
-      })
-      .catch(() => {
-        this.setState({ loading: false });
-      });
-  }
-
-  onSelectDays = (days: string) => {
-    this.setState({ days, success: false, isChanged: true });
-  };
-
-  onSelectSetting = (selected: NewCodeDefinitionType) => {
-    this.setState((currentState) => ({
-      selected,
-      success: false,
-      isChanged: selected !== currentState.selected,
-    }));
-  };
-
-  onCancel = () => {
-    this.setState(({ currentSetting, currentSettingValue, days }) => ({
-      isChanged: false,
-      selected: currentSetting,
-      days:
-        currentSetting === NewCodeDefinitionType.NumberOfDays ? String(currentSettingValue) : days,
-    }));
-  };
-
-  onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
-    e.preventDefault();
-
-    const { days, selected } = this.state;
-
-    const type = selected;
-    const value = type === NewCodeDefinitionType.NumberOfDays ? days : undefined;
-
-    this.setState({ saving: true, success: false });
-    setNewCodePeriod({
-      type: type as NewCodeDefinitionType,
-      value,
-    }).then(
-      () => {
-        if (this.mounted) {
-          this.setState({
-            saving: false,
-            currentSetting: type,
-            currentSettingValue: value || undefined,
-            isChanged: false,
-            success: true,
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({
-            saving: false,
-          });
-        }
-      }
-    );
-  };
-
-  render() {
-    const {
-      currentSetting,
-      days,
-      previousNonCompliantValue,
-      ncdUpdatedAt,
-      loading,
-      isChanged,
-      currentSettingValue,
-      saving,
-      selected,
-      success,
-    } = this.state;
-
-    const isValid =
-      selected !== NewCodeDefinitionType.NumberOfDays ||
-      isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: days });
-
-    return (
-      <>
-        <h2
-          className="settings-sub-category-name settings-definition-name"
-          title={translate('settings.new_code_period.title')}
-        >
-          {translate('settings.new_code_period.title')}
-        </h2>
-
-        <ul className="settings-sub-categories-list">
-          <li>
-            <ul className="settings-definitions-list">
-              <li>
-                <div className="settings-definition">
-                  <div className="settings-definition-left">
-                    <div className="small">
-                      <p className="sw-mb-2">
-                        {translate('settings.new_code_period.description0')}
-                      </p>
-                      <p className="sw-mb-2">
-                        {translate('settings.new_code_period.description1')}
-                      </p>
-                      <p className="sw-mb-2">
-                        {translate('settings.new_code_period.description2')}
-                      </p>
-
-                      <p className="sw-mb-2">
-                        <FormattedMessage
-                          defaultMessage={translate('settings.new_code_period.description3')}
-                          id="settings.new_code_period.description3"
-                          values={{
-                            link: (
-                              <DocLink to="/project-administration/defining-new-code/">
-                                {translate('settings.new_code_period.description3.link')}
-                              </DocLink>
-                            ),
-                          }}
-                        />
-                      </p>
-
-                      <p className="sw-mt-4">
-                        <strong>{translate('settings.new_code_period.question')}</strong>
-                      </p>
-                    </div>
-                  </div>
-
-                  <div className="settings-definition-right">
-                    <Spinner loading={loading}>
-                      <form onSubmit={this.onSubmit}>
-                        <NewCodeDefinitionPreviousVersionOption
-                          isDefault
-                          onSelect={this.onSelectSetting}
-                          selected={selected === NewCodeDefinitionType.PreviousVersion}
-                        />
-                        <NewCodeDefinitionDaysOption
-                          className="spacer-top sw-mb-4"
-                          days={days}
-                          previousNonCompliantValue={previousNonCompliantValue}
-                          updatedAt={ncdUpdatedAt}
-                          isChanged={isChanged}
-                          isValid={isValid}
-                          onChangeDays={this.onSelectDays}
-                          onSelect={this.onSelectSetting}
-                          selected={selected === NewCodeDefinitionType.NumberOfDays}
-                        />
-                        <NewCodeDefinitionWarning
-                          newCodeDefinitionType={currentSetting}
-                          newCodeDefinitionValue={currentSettingValue}
-                          isBranchSupportEnabled={undefined}
-                          level="global"
-                        />
-                        {isChanged && (
-                          <div className="big-spacer-top">
-                            <p className="spacer-bottom">
-                              {translate('baseline.next_analysis_notice')}
-                            </p>
-                            <Spinner className="spacer-right" loading={saving} />
-                            <SubmitButton disabled={saving || !isValid}>
-                              {translate('save')}
-                            </SubmitButton>
-                            <ResetButtonLink className="spacer-left" onClick={this.onCancel}>
-                              {translate('cancel')}
-                            </ResetButtonLink>
-                          </div>
-                        )}
-                        {!saving && !loading && success && (
-                          <div className="big-spacer-top">
-                            <span className="text-success">
-                              <AlertSuccessIcon className="spacer-right" />
-                              {translate('settings.state.saved')}
-                            </span>
-                          </div>
-                        )}
-                      </form>
-                    </Spinner>
-                  </div>
-                </div>
-              </li>
-            </ul>
-          </li>
-        </ul>
-      </>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx
new file mode 100644 (file)
index 0000000..422a084
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * 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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { MessageTypes } from '../../../../api/messages';
+import MessagesServiceMock from '../../../../api/mocks/MessagesServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
+import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
+import NewCodeDefinition from '../NewCodeDefinition';
+
+let newCodeMock: NewCodeDefinitionServiceMock;
+let messagesMock: MessagesServiceMock;
+
+beforeAll(() => {
+  newCodeMock = new NewCodeDefinitionServiceMock();
+  messagesMock = new MessagesServiceMock();
+});
+
+afterEach(() => {
+  newCodeMock.reset();
+  messagesMock.reset();
+});
+
+const ui = {
+  newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }),
+  savedMsg: byText('settings.state.saved'),
+  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('spinbutton') /* spinbutton is the default role for a number input */,
+  saveButton: byRole('button', { name: 'save' }),
+  cancelButton: byRole('button', { name: 'cancel' }),
+  ncdAutoUpdateMessage: byText(/new_code_definition.auto_update.global.page.message/),
+  ncdAutoUpdateMessageDismiss: byLabelText('alert.dismiss'),
+};
+
+it('renders and behaves as expected', async () => {
+  const user = userEvent.setup();
+  renderNewCodePeriod();
+
+  expect(await ui.newCodeTitle.find()).toBeInTheDocument();
+  // Previous version should be checked by default
+  expect(ui.prevVersionRadio.get()).toBeChecked();
+
+  // Can select number of days
+  await user.click(ui.daysNumberRadio.get());
+  expect(ui.daysNumberRadio.get()).toBeChecked();
+
+  // Save should be disabled for zero
+  expect(ui.daysInput.get()).toHaveValue(30);
+  await user.clear(ui.daysInput.get());
+  await user.type(ui.daysInput.get(), '0');
+  expect(await ui.saveButton.find()).toBeDisabled();
+
+  // Save should not appear at all for NaN
+  await user.clear(ui.daysInput.get());
+  await user.type(ui.daysInput.get(), 'asdas');
+  expect(ui.saveButton.query()).toBeDisabled();
+
+  // Save enabled for valid days number
+  await user.clear(ui.daysInput.get());
+  await user.type(ui.daysInput.get(), '10');
+  expect(ui.saveButton.get()).toBeEnabled();
+
+  // Can cancel action
+  await user.click(ui.cancelButton.get());
+  expect(ui.prevVersionRadio.get()).toBeChecked();
+
+  // Can save change
+  await user.click(ui.daysNumberRadio.get());
+  await user.clear(ui.daysInput.get());
+  await user.type(ui.daysInput.get(), '10');
+  await user.click(ui.saveButton.get());
+  expect(ui.savedMsg.get()).toBeInTheDocument();
+
+  await user.click(ui.prevVersionRadio.get());
+  await user.click(ui.cancelButton.get());
+  await user.click(ui.prevVersionRadio.get());
+  await user.click(ui.saveButton.get());
+  expect(ui.savedMsg.get()).toBeInTheDocument();
+});
+
+it('renders and behaves properly when the current value is not compliant', async () => {
+  const user = userEvent.setup();
+  newCodeMock.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays, value: '91' });
+  renderNewCodePeriod();
+
+  expect(await ui.newCodeTitle.find()).toBeInTheDocument();
+  expect(ui.daysNumberRadio.get()).toBeChecked();
+  expect(ui.daysInput.get()).toHaveValue(91);
+
+  // Should warn about non compliant value
+  expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument();
+
+  await user.clear(ui.daysInput.get());
+  await user.type(ui.daysInput.get(), '92');
+
+  expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument();
+});
+
+it('displays information message when NCD is automatically updated', async () => {
+  newCodeMock.setNewCodePeriod({
+    type: NewCodeDefinitionType.NumberOfDays,
+    value: '90',
+    previousNonCompliantValue: '120',
+    updatedAt: 1692279521904,
+  });
+  renderNewCodePeriod();
+
+  expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
+});
+
+it('dismisses information message when NCD is automatically updated', async () => {
+  newCodeMock.setNewCodePeriod({
+    type: NewCodeDefinitionType.NumberOfDays,
+    value: '90',
+    previousNonCompliantValue: '120',
+    updatedAt: 1692279521904,
+  });
+  renderNewCodePeriod();
+
+  expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
+
+  const user = userEvent.setup();
+  await user.click(ui.ncdAutoUpdateMessageDismiss.get());
+
+  expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
+});
+
+it('does not display information message when NCD is automatically updated if message is already dismissed', () => {
+  newCodeMock.setNewCodePeriod({
+    type: NewCodeDefinitionType.NumberOfDays,
+    value: '90',
+    previousNonCompliantValue: '120',
+    updatedAt: 1692279521904,
+  });
+  messagesMock.setMessageDismissed({ messageType: MessageTypes.GlobalNcdPage90 });
+  renderNewCodePeriod();
+
+  expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
+});
+
+function renderNewCodePeriod() {
+  return renderComponent(<NewCodeDefinition />);
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
deleted file mode 100644 (file)
index 8682daf..0000000
+++ /dev/null
@@ -1,165 +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 { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { MessageTypes } from '../../../../api/messages';
-import MessagesServiceMock from '../../../../api/mocks/MessagesServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
-import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import NewCodePeriod from '../NewCodePeriod';
-
-let newCodeMock: NewCodePeriodsServiceMock;
-let messagesMock: MessagesServiceMock;
-
-beforeAll(() => {
-  newCodeMock = new NewCodePeriodsServiceMock();
-  messagesMock = new MessagesServiceMock();
-});
-
-afterEach(() => {
-  newCodeMock.reset();
-  messagesMock.reset();
-});
-
-const ui = {
-  newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }),
-  savedMsg: byText('settings.state.saved'),
-  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('spinbutton') /* spinbutton is the default role for a number input */,
-  saveButton: byRole('button', { name: 'save' }),
-  cancelButton: byRole('button', { name: 'cancel' }),
-  ncdAutoUpdateMessage: byText(/new_code_definition.auto_update.global.page.message/),
-  ncdAutoUpdateMessageDismiss: byLabelText('alert.dismiss'),
-};
-
-it('renders and behaves as expected', async () => {
-  const user = userEvent.setup();
-  renderNewCodePeriod();
-
-  expect(await ui.newCodeTitle.find()).toBeInTheDocument();
-  // Previous version should be checked by default
-  expect(ui.prevVersionRadio.get()).toBeChecked();
-
-  // Can select number of days
-  await user.click(ui.daysNumberRadio.get());
-  expect(ui.daysNumberRadio.get()).toBeChecked();
-
-  // Save should be disabled for zero
-  expect(ui.daysInput.get()).toHaveValue(30);
-  await user.clear(ui.daysInput.get());
-  await user.type(ui.daysInput.get(), '0');
-  expect(await ui.saveButton.find()).toBeDisabled();
-
-  // Save should not appear at all for NaN
-  await user.clear(ui.daysInput.get());
-  await user.type(ui.daysInput.get(), 'asdas');
-  expect(ui.saveButton.query()).toBeDisabled();
-
-  // Save enabled for valid days number
-  await user.clear(ui.daysInput.get());
-  await user.type(ui.daysInput.get(), '10');
-  expect(ui.saveButton.get()).toBeEnabled();
-
-  // Can cancel action
-  await user.click(ui.cancelButton.get());
-  expect(ui.prevVersionRadio.get()).toBeChecked();
-
-  // Can save change
-  await user.click(ui.daysNumberRadio.get());
-  await user.clear(ui.daysInput.get());
-  await user.type(ui.daysInput.get(), '10');
-  await user.click(ui.saveButton.get());
-  expect(ui.savedMsg.get()).toBeInTheDocument();
-
-  await user.click(ui.prevVersionRadio.get());
-  await user.click(ui.cancelButton.get());
-  await user.click(ui.prevVersionRadio.get());
-  await user.click(ui.saveButton.get());
-  expect(ui.savedMsg.get()).toBeInTheDocument();
-});
-
-it('renders and behaves properly when the current value is not compliant', async () => {
-  const user = userEvent.setup();
-  newCodeMock.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays, value: '91' });
-  renderNewCodePeriod();
-
-  expect(await ui.newCodeTitle.find()).toBeInTheDocument();
-  expect(ui.daysNumberRadio.get()).toBeChecked();
-  expect(ui.daysInput.get()).toHaveValue(91);
-
-  // Should warn about non compliant value
-  expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument();
-
-  await user.clear(ui.daysInput.get());
-  await user.type(ui.daysInput.get(), '92');
-
-  expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument();
-});
-
-it('displays information message when NCD is automatically updated', async () => {
-  newCodeMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '90',
-    previousNonCompliantValue: '120',
-    updatedAt: 1692279521904,
-  });
-  renderNewCodePeriod();
-
-  expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
-});
-
-it('dismisses information message when NCD is automatically updated', async () => {
-  newCodeMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '90',
-    previousNonCompliantValue: '120',
-    updatedAt: 1692279521904,
-  });
-  renderNewCodePeriod();
-
-  expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
-
-  const user = userEvent.setup();
-  await user.click(ui.ncdAutoUpdateMessageDismiss.get());
-
-  expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
-});
-
-it('does not display information message when NCD is automatically updated if message is already dismissed', () => {
-  newCodeMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '90',
-    previousNonCompliantValue: '120',
-    updatedAt: 1692279521904,
-  });
-  messagesMock.setMessageDismissed({ messageType: MessageTypes.GlobalNcdPage90 });
-  renderNewCodePeriod();
-
-  expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
-});
-
-function renderNewCodePeriod() {
-  return renderComponent(<NewCodePeriod />);
-}
index cecc314dc8622af6b229d0f1d6670d2d888a091e..e9386faeb7bfed3ad6af57228f1338dd5a6eebda 100644 (file)
@@ -20,7 +20,7 @@
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { FormattedMessage, useIntl } from 'react-intl';
 import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages';
-import { getNewCodePeriod } from '../../api/newCodePeriod';
+import { getNewCodeDefinition } from '../../api/newCodeDefinition';
 import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
 import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
 import { NEW_CODE_PERIOD_CATEGORY } from '../../apps/settings/constants';
@@ -80,7 +80,7 @@ function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
 
   useEffect(() => {
     async function fetchNewCodeDefinition() {
-      const newCodeDefinition = await getNewCodePeriod(
+      const newCodeDefinition = await getNewCodeDefinition(
         component && {
           project: component.key,
         }
index 47dd557065b050fa9b9682fe8e5ee51e2569c27f..2b2b7587c0f84224af4ada078921bbaf53c8e920 100644 (file)
@@ -27,7 +27,7 @@ import {
 } from 'design-system';
 import { noop } from 'lodash';
 import * as React from 'react';
-import { getNewCodePeriod } from '../../api/newCodePeriod';
+import { getNewCodeDefinition } from '../../api/newCodeDefinition';
 import { translate } from '../../helpers/l10n';
 import {
   getNumberOfDaysDefaultValue,
@@ -88,7 +88,7 @@ export default function NewCodeDefinitionSelector(props: Props) {
 
   React.useEffect(() => {
     function fetchGlobalNcd() {
-      getNewCodePeriod().then(setGlobalNcd, noop);
+      getNewCodeDefinition().then(setGlobalNcd, noop);
     }
 
     fetchGlobalNcd();
index fc022703eeb4e727168f03f897a19e0689d2a9d0..d8d89f1694311e0821d502fe420cce6922bb3aa3 100644 (file)
@@ -23,7 +23,7 @@ import React from 'react';
 import { Route } from 'react-router-dom';
 import { MessageTypes } from '../../../api/messages';
 import MessagesServiceMock from '../../../api/mocks/MessagesServiceMock';
-import NewCodePeriodsServiceMock from '../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../api/mocks/NewCodeDefinitionServiceMock';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
@@ -32,7 +32,7 @@ import { NewCodeDefinitionType } from '../../../types/new-code-definition';
 import { Component } from '../../../types/types';
 import NCDAutoUpdateMessage from '../NCDAutoUpdateMessage';
 
-let newCodeDefinitionMock: NewCodePeriodsServiceMock;
+let newCodeDefinitionMock: NewCodeDefinitionServiceMock;
 let messagesMock: MessagesServiceMock;
 
 describe('Global NCD update notification banner', () => {
@@ -54,7 +54,7 @@ describe('Global NCD update notification banner', () => {
   }
 
   beforeAll(() => {
-    newCodeDefinitionMock = new NewCodePeriodsServiceMock();
+    newCodeDefinitionMock = new NewCodeDefinitionServiceMock();
     messagesMock = new MessagesServiceMock();
   });
 
@@ -143,7 +143,7 @@ describe('Project NCD update notification banner', () => {
   }
 
   beforeAll(() => {
-    newCodeDefinitionMock = new NewCodePeriodsServiceMock();
+    newCodeDefinitionMock = new NewCodeDefinitionServiceMock();
     messagesMock = new MessagesServiceMock();
   });