]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20249 Hide NCD banner on save (#9186)
authorAmbroise C <ambroise.christea@sonarsource.com>
Fri, 1 Sep 2023 12:18:58 +0000 (14:18 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 1 Sep 2023 20:03:03 +0000 (20:03 +0000)
20 files changed:
server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchNewCodeDefinitionSettingModal.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx
server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx
server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionWarning.tsx [deleted file]
server/sonar-web/src/main/js/helpers/new-code-definition.ts
server/sonar-web/src/main/js/queries/newCodeDefinition.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 147246c309e390059cc19758d05cb784662e7d85..167ee04638b67d2a8430a78fc0084ea5b435968f 100644 (file)
@@ -60,7 +60,13 @@ export default class NewCodeDefinitionServiceMock {
       .mockImplementation(this.handleListBranchesNewCodePeriod);
   }
 
-  handleGetNewCodePeriod = () => {
+  handleGetNewCodePeriod = (data?: { branch?: string; project?: string }) => {
+    if (data?.branch !== undefined) {
+      return this.reply(
+        this.#listBranchesNewCode.find((b) => b.branchKey === data?.branch) as NewCodeDefinition
+      );
+    }
+
     return this.reply(this.#newCodePeriod);
   };
 
@@ -70,15 +76,14 @@ export default class NewCodeDefinitionServiceMock {
     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;
+    const { project, type, value, branch } = data;
+    if (project !== undefined && branch !== undefined) {
+      this.#listBranchesNewCode = this.#listBranchesNewCode.filter((b) => b.branchKey !== branch);
+      this.#listBranchesNewCode.push(
+        mockNewCodePeriodBranch({ type, value, branchKey: branch, projectKey: project })
+      );
     } else {
-      this.#newCodePeriod = mockNewCodePeriod({ type, value });
+      this.#newCodePeriod = mockNewCodePeriod({ projectKey: project, type, value });
     }
 
     return this.reply(undefined);
index 151f5c72ab0c4d21e96b0f6462b9230ba8ca6046..48cc565af1dbc4baa9cc974b0fa018e49fb907e6 100644 (file)
@@ -21,17 +21,24 @@ import { TopBar } from 'design-system';
 import * as React from 'react';
 import NCDAutoUpdateMessage from '../../../../components/new-code-definition/NCDAutoUpdateMessage';
 import { translate } from '../../../../helpers/l10n';
+import { withBranchLikes } from '../../../../queries/branch';
 import { ProjectAlmBindingConfigurationErrors } from '../../../../types/alm-settings';
+import { Branch } from '../../../../types/branch-like';
 import { ComponentQualifier } from '../../../../types/component';
+import { Feature } from '../../../../types/features';
 import { Task } from '../../../../types/tasks';
 import { Component } from '../../../../types/types';
 import RecentHistory from '../../RecentHistory';
+import withAvailableFeatures, {
+  WithAvailableFeaturesProps,
+} from '../../available-features/withAvailableFeatures';
 import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif';
 import Header from './Header';
 import HeaderMeta from './HeaderMeta';
 import Menu from './Menu';
 
-export interface ComponentNavProps {
+export interface ComponentNavProps extends WithAvailableFeaturesProps {
+  branchLike?: Branch;
   component: Component;
   currentTask?: Task;
   isInProgress?: boolean;
@@ -39,8 +46,16 @@ export interface ComponentNavProps {
   projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
 }
 
-export default function ComponentNav(props: ComponentNavProps) {
-  const { component, currentTask, isInProgress, isPending, projectBindingErrors } = props;
+function ComponentNav(props: ComponentNavProps) {
+  const {
+    branchLike,
+    component,
+    currentTask,
+    hasFeature,
+    isInProgress,
+    isPending,
+    projectBindingErrors,
+  } = props;
 
   React.useEffect(() => {
     const { breadcrumbs, key, name } = component;
@@ -70,10 +85,15 @@ export default function ComponentNav(props: ComponentNavProps) {
         </div>
         <Menu component={component} isInProgress={isInProgress} isPending={isPending} />
       </TopBar>
-      <NCDAutoUpdateMessage component={component} />
+      <NCDAutoUpdateMessage
+        branchName={hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name}
+        component={component}
+      />
       {projectBindingErrors !== undefined && (
         <ComponentNavProjectBindingErrorNotif component={component} />
       )}
     </>
   );
 }
+
+export default withAvailableFeatures(withBranchLikes(ComponentNav));
index 2ece6c731e830115aaf628a154e5ea8950220048..a6cb0f013fd1d64fdd44756b9b0379a5e40da23c 100644 (file)
@@ -272,7 +272,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
   }
 
   render() {
-    const { appState, location, router } = this.props;
+    const { location, router } = this.props;
     const { creatingAlmDefinition } = this.state;
     const mode: CreateProjectModes | undefined = location.query?.mode;
     const isProjectSetupDone = location.query?.setncd === 'true';
@@ -295,7 +295,6 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
           </div>
           <div className={classNames({ 'sw-hidden': !isProjectSetupDone })}>
             <NewCodeDefinitionSelection
-              canAdmin={Boolean(appState.canAdmin)}
               router={router}
               createProjectFnRef={this.createProjectFnRef}
             />
index a0563c51d6a280939256e194a5f06a0df1222184..874dc3cc1a1b057fe3bebc3728d3c7228be66840 100644 (file)
@@ -24,7 +24,6 @@ import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock
 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';
 import { byRole, byText } from '../../../../helpers/testSelector';
 import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
@@ -73,8 +72,6 @@ const ui = {
     name: /new_code_definition.number_days.specify_days/,
   }),
   ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'),
-  ncdWarningTextAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
-  ncdWarningText: byText('new_code_definition.compliance.warning.explanation'),
   projectDashboardText: byText('/dashboard?id=foo'),
 };
 
@@ -138,63 +135,6 @@ it('should select the global NCD when it is compliant', async () => {
   expect(ui.projectCreateButton.get()).toBeEnabled();
 });
 
-it('global NCD option should be disabled if not compliant', async () => {
-  jest
-    .mocked(getNewCodeDefinition)
-    .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
-  const user = userEvent.setup();
-  renderCreateProject();
-  await fillFormAndNext('test', user);
-
-  expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
-  expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
-  expect(ui.inheritGlobalNcdRadio.get()).toBeDisabled();
-  expect(ui.projectCreateButton.get()).toBeDisabled();
-});
-
-it.each([
-  { canAdmin: true, message: ui.ncdWarningTextAdmin },
-  { canAdmin: false, message: ui.ncdWarningText },
-])(
-  'should show warning message when global NCD is not compliant',
-  async ({ canAdmin, message }) => {
-    jest
-      .mocked(getNewCodeDefinition)
-      .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
-    const user = userEvent.setup();
-    renderCreateProject({ appState: mockAppState({ canAdmin }) });
-    await fillFormAndNext('test', user);
-
-    expect(message.get()).toBeInTheDocument();
-  }
-);
-
-it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])(
-  'should override the global NCD and pick a compliant NCD',
-  async (option) => {
-    jest
-      .mocked(getNewCodeDefinition)
-      .mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
-    const user = userEvent.setup();
-    renderCreateProject();
-    await fillFormAndNext('test', user);
-
-    expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
-    expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
-    expect(ui.inheritGlobalNcdRadio.get()).toBeDisabled();
-    expect(ui.projectCreateButton.get()).toBeDisabled();
-    expect(ui.overrideNcdRadio.get()).toBeEnabled();
-    expect(option.get()).toHaveClass('disabled');
-
-    await user.click(ui.overrideNcdRadio.get());
-    expect(option.get()).not.toHaveClass('disabled');
-
-    await user.click(option.get());
-
-    expect(ui.projectCreateButton.get()).toBeEnabled();
-  }
-);
-
 it('number of days ignores non-numeric inputs', async () => {
   jest
     .mocked(getNewCodeDefinition)
index 003be840a138c341daab726e059b2ca8c01776c4..76f1c49061d2672b45a4f304bf95762199f0c5c9 100644 (file)
@@ -30,13 +30,12 @@ import { NewCodeDefinitiondWithCompliance } from '../../../../types/new-code-def
 import { CreateProjectApiCallback } from '../types';
 
 interface Props {
-  canAdmin: boolean;
   createProjectFnRef: CreateProjectApiCallback | null;
   router: Router;
 }
 
 export default function NewCodeDefinitionSelection(props: Props) {
-  const { canAdmin, createProjectFnRef, router } = props;
+  const { createProjectFnRef, router } = props;
 
   const [submitting, setSubmitting] = React.useState(false);
   const [selectedDefinition, selectDefinition] = React.useState<NewCodeDefinitiondWithCompliance>();
@@ -75,7 +74,7 @@ export default function NewCodeDefinitionSelection(props: Props) {
         />
       </p>
 
-      <NewCodeDefinitionSelector canAdmin={canAdmin} onNcdChanged={selectDefinition} />
+      <NewCodeDefinitionSelector onNcdChanged={selectDefinition} />
 
       <div className="sw-mt-10 sw-mb-8">
         <ButtonPrimary
index d5b8bc266e6587e2d3c13a76372d067064e16309..8da03078843138b7e5d9f64665d558fc516268e9 100644 (file)
@@ -22,9 +22,9 @@ import * as React from 'react';
 import { setNewCodeDefinition } from '../../../api/newCodeDefinition';
 import Modal from '../../../components/controls/Modal';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import NewCodeDefinitionAnalysisWarning from '../../../components/new-code-definition/NewCodeDefinitionAnalysisWarning';
 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 { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils';
 import Spinner from '../../../components/ui/Spinner';
 import { toISO8601WithOffsetString } from '../../../helpers/dates';
@@ -168,7 +168,6 @@ export default class BranchNewCodeDefinitionSettingModal extends React.PureCompo
     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({
       numberOfDays: days,
@@ -184,12 +183,9 @@ export default class BranchNewCodeDefinitionSettingModal extends React.PureCompo
         <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={NewCodeDefinitionLevels.Branch}
-            />
+            {currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
+              <NewCodeDefinitionAnalysisWarning />
+            )}
             <div className="display-flex-column huge-spacer-bottom sw-gap-4" role="radiogroup">
               <NewCodeDefinitionPreviousVersionOption
                 isDefault={false}
index ad353f95ca305d2931bba43ede5db6a210764d7d..3e30064ba50be5d25b3c4b4a3f621129f1ed9821 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import classNames from 'classnames';
-import { debounce } from 'lodash';
-import * as React from 'react';
+import React, { useCallback, useEffect, useMemo, useState } 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';
@@ -41,10 +33,14 @@ import {
   getNumberOfDaysDefaultValue,
 } from '../../../helpers/new-code-definition';
 import { withBranchLikes } from '../../../queries/branch';
+import {
+  useNewCodeDefinitionMutation,
+  useNewCodeDefinitionQuery,
+} from '../../../queries/newCodeDefinition';
 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 { NewCodeDefinitionType } from '../../../types/new-code-definition';
 import { Component } from '../../../types/types';
 import '../styles.css';
 import { getSettingValue } from '../utils';
@@ -52,334 +48,192 @@ import AppHeader from './AppHeader';
 import BranchList from './BranchList';
 import ProjectNewCodeDefinitionSelector from './ProjectNewCodeDefinitionSelector';
 
-interface Props extends WithAvailableFeaturesProps {
+interface ProjectNewCodeDefinitionAppProps extends WithAvailableFeaturesProps {
   branchLike: Branch;
   branchLikes: BranchLike[];
   component: Component;
   appState: AppState;
 }
 
-interface State {
-  analysis?: string;
-  branchList: Branch[];
-  newCodeDefinitionType?: NewCodeDefinitionType;
-  newCodeDefinitionValue?: string;
-  previousNonCompliantValue?: string;
-  projectNcdUpdatedAt?: number;
-  numberOfDays: string;
-  globalNewCodeDefinition?: NewCodeDefinition;
-  isChanged: boolean;
-  loading: boolean;
-  overrideGlobalNewCodeDefinition?: boolean;
-  referenceBranch?: string;
-  saving: boolean;
-  selectedNewCodeDefinitionType?: NewCodeDefinitionType;
-  success?: boolean;
-}
-
-class ProjectNewCodeDefinitionApp extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    branchList: [],
-    numberOfDays: 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);
+function ProjectNewCodeDefinitionApp(props: ProjectNewCodeDefinitionAppProps) {
+  const { appState, component, branchLike, branchLikes, hasFeature } = props;
+
+  const [isSpecificNewCodeDefinition, setIsSpecificNewCodeDefinition] = useState<boolean>();
+  const [numberOfDays, setNumberOfDays] = useState(getNumberOfDaysDefaultValue());
+  const [referenceBranch, setReferenceBranch] = useState<string | undefined>(undefined);
+  const [specificAnalysis, setSpecificAnalysis] = useState<string | undefined>(undefined);
+  const [selectedNewCodeDefinitionType, setSelectedNewCodeDefinitionType] =
+    useState<NewCodeDefinitionType>(DEFAULT_NEW_CODE_DEFINITION_TYPE);
+
+  const {
+    data: globalNewCodeDefinition = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE },
+    isLoading: isGlobalNCDLoading,
+  } = useNewCodeDefinitionQuery();
+  const { data: projectNewCodeDefinition, isLoading: isProjectNCDLoading } =
+    useNewCodeDefinitionQuery({
+      branchName: hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
+      projectKey: component.key,
+    });
+  const { isLoading: isSaving, mutate: postNewCodeDefinition } = useNewCodeDefinitionMutation();
+
+  const branchList = useMemo(() => {
+    return sortBranches(branchLikes.filter(isBranch));
+  }, [branchLikes]);
+  const isFormTouched = useMemo(() => {
+    if (isSpecificNewCodeDefinition === undefined) {
+      return false;
+    }
+    if (isSpecificNewCodeDefinition !== !projectNewCodeDefinition?.inherited) {
+      return true;
     }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  getUpdatedState(params: {
-    newCodeDefinitionType?: NewCodeDefinitionType;
-    newCodeDefinitionValue?: string;
-    globalNewCodeDefinition: NewCodeDefinition;
-    previousNonCompliantValue?: string;
-    projectNcdUpdatedAt?: number;
-  }) {
-    const {
-      newCodeDefinitionType,
-      newCodeDefinitionValue,
-      globalNewCodeDefinition,
-      previousNonCompliantValue,
-      projectNcdUpdatedAt,
-    } = params;
-    const { referenceBranch } = this.state;
-
-    const defaultDays = getNumberOfDaysDefaultValue(globalNewCodeDefinition);
-
-    return {
-      loading: false,
-      newCodeDefinitionType,
-      newCodeDefinitionValue,
-      previousNonCompliantValue,
-      projectNcdUpdatedAt,
-      globalNewCodeDefinition,
-      isChanged: false,
-      selectedNewCodeDefinitionType: newCodeDefinitionType ?? globalNewCodeDefinition.type,
-      overrideGlobalNewCodeDefinition: Boolean(newCodeDefinitionType),
-      numberOfDays:
-        (newCodeDefinitionType === NewCodeDefinitionType.NumberOfDays && newCodeDefinitionValue) ||
-        defaultDays,
-      analysis:
-        (newCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis &&
-          newCodeDefinitionValue) ||
-        '',
-      referenceBranch:
-        (newCodeDefinitionType === NewCodeDefinitionType.ReferenceBranch &&
-          newCodeDefinitionValue) ||
-        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 });
+    if (!isSpecificNewCodeDefinition) {
+      return false;
+    }
 
-    Promise.all([
-      getNewCodeDefinition(),
-      getNewCodeDefinition({
-        branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
-        project: component.key,
-      }),
-    ]).then(
-      ([globalNewCodeDefinition, setting]) => {
-        if (this.mounted) {
-          if (!globalNewCodeDefinition.type) {
-            globalNewCodeDefinition = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE };
-          }
-          const newCodeDefinitionValue = setting.value;
-          const newCodeDefinitionType = setting.inherited
-            ? undefined
-            : setting.type || DEFAULT_NEW_CODE_DEFINITION_TYPE;
+    if (selectedNewCodeDefinitionType !== projectNewCodeDefinition?.type) {
+      return true;
+    }
 
-          this.setState(
-            this.getUpdatedState({
-              globalNewCodeDefinition,
-              newCodeDefinitionType,
-              newCodeDefinitionValue,
-              previousNonCompliantValue: setting.previousNonCompliantValue,
-              projectNcdUpdatedAt: setting.updatedAt,
-            })
-          );
-        }
-      },
-      () => {
-        this.setState({ loading: false });
-      }
+    switch (selectedNewCodeDefinitionType) {
+      case NewCodeDefinitionType.NumberOfDays:
+        return numberOfDays !== String(projectNewCodeDefinition?.value);
+      case NewCodeDefinitionType.ReferenceBranch:
+        return referenceBranch !== projectNewCodeDefinition?.value;
+      case NewCodeDefinitionType.SpecificAnalysis:
+        return specificAnalysis !== projectNewCodeDefinition?.value;
+      default:
+        return false;
+    }
+  }, [
+    isSpecificNewCodeDefinition,
+    numberOfDays,
+    projectNewCodeDefinition,
+    referenceBranch,
+    selectedNewCodeDefinitionType,
+    specificAnalysis,
+  ]);
+
+  const defaultReferenceBranch = branchList[0]?.name;
+  const isLoading = isGlobalNCDLoading || isProjectNCDLoading;
+  const branchSupportEnabled = hasFeature(Feature.BranchSupport);
+
+  const resetStatesFromProjectNewCodeDefinition = useCallback(() => {
+    setIsSpecificNewCodeDefinition(
+      projectNewCodeDefinition === undefined ? undefined : !projectNewCodeDefinition.inherited
     );
-  }
-
-  resetSetting = () => {
-    this.setState({ saving: true });
-    resetNewCodeDefinition({ project: this.props.component.key }).then(
-      () => {
-        this.setState({
-          saving: false,
-          newCodeDefinitionType: undefined,
-          isChanged: false,
-          selectedNewCodeDefinitionType: undefined,
-          success: true,
-        });
-        this.resetSuccess();
-      },
-      () => {
-        this.setState({ saving: false });
-      }
+    setSelectedNewCodeDefinitionType(
+      projectNewCodeDefinition?.type ?? DEFAULT_NEW_CODE_DEFINITION_TYPE
     );
-  };
-
-  handleSelectDays = (days: string) => this.setState({ numberOfDays: days, isChanged: true });
-
-  handleSelectReferenceBranch = (referenceBranch: string) => {
-    this.setState({ referenceBranch, isChanged: true });
-  };
-
-  handleCancel = () =>
-    this.setState(
-      ({
-        globalNewCodeDefinition = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE },
-        newCodeDefinitionType,
-        newCodeDefinitionValue,
-      }) =>
-        this.getUpdatedState({
-          globalNewCodeDefinition,
-          newCodeDefinitionType,
-          newCodeDefinitionValue,
-        })
+    setNumberOfDays(getNumberOfDaysDefaultValue(globalNewCodeDefinition, projectNewCodeDefinition));
+    setReferenceBranch(
+      projectNewCodeDefinition?.type === NewCodeDefinitionType.ReferenceBranch
+        ? projectNewCodeDefinition.value
+        : defaultReferenceBranch
     );
-
-  handleSelectSetting = (selectedNewCodeDefinitionType?: NewCodeDefinitionType) => {
-    this.setState((currentState) => ({
-      selectedNewCodeDefinitionType,
-      isChanged: selectedNewCodeDefinitionType !== currentState.selectedNewCodeDefinitionType,
-    }));
+    setSpecificAnalysis(
+      projectNewCodeDefinition?.type === NewCodeDefinitionType.SpecificAnalysis
+        ? projectNewCodeDefinition.value
+        : undefined
+    );
+  }, [defaultReferenceBranch, globalNewCodeDefinition, projectNewCodeDefinition]);
+
+  const onResetNewCodeDefinition = () => {
+    postNewCodeDefinition({
+      branch: hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
+      project: component.key,
+      type: undefined,
+    });
   };
 
-  handleToggleSpecificSetting = (overrideGlobalNewCodeDefinition: boolean) =>
-    this.setState((currentState) => ({
-      overrideGlobalNewCodeDefinition,
-      isChanged: currentState.overrideGlobalNewCodeDefinition !== overrideGlobalNewCodeDefinition,
-    }));
-
-  handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+  const onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
     e.preventDefault();
 
-    const { component } = this.props;
-    const {
-      numberOfDays,
-      selectedNewCodeDefinitionType: type,
-      referenceBranch,
-      overrideGlobalNewCodeDefinition,
-    } = this.state;
-
-    if (!overrideGlobalNewCodeDefinition) {
-      this.resetSetting();
+    if (!isSpecificNewCodeDefinition) {
+      onResetNewCodeDefinition();
       return;
     }
 
-    const value = getSettingValue({ type, numberOfDays, referenceBranch });
+    const value = getSettingValue({
+      type: selectedNewCodeDefinitionType,
+      numberOfDays,
+      referenceBranch,
+    });
 
-    if (type) {
-      this.setState({ saving: true });
-      setNewCodeDefinition({
+    if (selectedNewCodeDefinitionType) {
+      postNewCodeDefinition({
+        branch: hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
         project: component.key,
-        type,
+        type: selectedNewCodeDefinitionType,
         value,
-      }).then(
-        () => {
-          this.setState({
-            saving: false,
-            newCodeDefinitionType: type,
-            newCodeDefinitionValue: value || undefined,
-            previousNonCompliantValue: undefined,
-            projectNcdUpdatedAt: Date.now(),
-            isChanged: false,
-            success: true,
-          });
-          this.resetSuccess();
-        },
-        () => {
-          this.setState({ saving: false });
-        }
-      );
+      });
     }
   };
 
-  render() {
-    const { appState, component, branchLike } = this.props;
-    const {
-      analysis,
-      branchList,
-      newCodeDefinitionType,
-      numberOfDays,
-      previousNonCompliantValue,
-      projectNcdUpdatedAt,
-      globalNewCodeDefinition,
-      isChanged,
-      loading,
-      newCodeDefinitionValue,
-      overrideGlobalNewCodeDefinition,
-      referenceBranch,
-      saving,
-      selectedNewCodeDefinitionType,
-      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>}
-
-              {globalNewCodeDefinition && overrideGlobalNewCodeDefinition !== undefined && (
-                <ProjectNewCodeDefinitionSelector
-                  analysis={analysis}
-                  branch={branchLike}
+  useEffect(() => {
+    setReferenceBranch(defaultReferenceBranch);
+  }, [defaultReferenceBranch]);
+
+  useEffect(() => {
+    resetStatesFromProjectNewCodeDefinition();
+  }, [resetStatesFromProjectNewCodeDefinition]);
+
+  return (
+    <>
+      <Suggestions suggestions="project_baseline" />
+      <Helmet defer={false} title={translate('project_baseline.page')} />
+      <div className="page page-limited">
+        <AppHeader canAdmin={!!appState.canAdmin} />
+        <Spinner loading={isLoading} />
+
+        {!isLoading && (
+          <div className="panel-white project-baseline">
+            {branchSupportEnabled && <h2>{translate('project_baseline.default_setting')}</h2>}
+
+            {globalNewCodeDefinition && isSpecificNewCodeDefinition !== undefined && (
+              <ProjectNewCodeDefinitionSelector
+                analysis={specificAnalysis}
+                branch={branchLike}
+                branchList={branchList}
+                branchesEnabled={branchSupportEnabled}
+                component={component.key}
+                newCodeDefinitionType={projectNewCodeDefinition?.type}
+                newCodeDefinitionValue={projectNewCodeDefinition?.value}
+                days={numberOfDays}
+                previousNonCompliantValue={projectNewCodeDefinition?.previousNonCompliantValue}
+                projectNcdUpdatedAt={projectNewCodeDefinition?.updatedAt}
+                globalNewCodeDefinition={globalNewCodeDefinition}
+                isChanged={isFormTouched}
+                onCancel={resetStatesFromProjectNewCodeDefinition}
+                onSelectDays={setNumberOfDays}
+                onSelectReferenceBranch={setReferenceBranch}
+                onSelectSetting={setSelectedNewCodeDefinitionType}
+                onSubmit={onSubmit}
+                onToggleSpecificSetting={setIsSpecificNewCodeDefinition}
+                overrideGlobalNewCodeDefinition={isSpecificNewCodeDefinition}
+                referenceBranch={referenceBranch}
+                saving={isSaving}
+                selectedNewCodeDefinitionType={selectedNewCodeDefinitionType}
+              />
+            )}
+
+            {globalNewCodeDefinition && branchSupportEnabled && (
+              <div className="huge-spacer-top branch-baseline-selector">
+                <hr />
+                <h2>{translate('project_baseline.configure_branches')}</h2>
+                <BranchList
                   branchList={branchList}
-                  branchesEnabled={branchSupportEnabled}
-                  canAdmin={appState.canAdmin}
-                  component={component.key}
-                  newCodeDefinitionType={newCodeDefinitionType}
-                  newCodeDefinitionValue={newCodeDefinitionValue}
-                  days={numberOfDays}
-                  previousNonCompliantValue={previousNonCompliantValue}
-                  projectNcdUpdatedAt={projectNcdUpdatedAt}
+                  component={component}
+                  inheritedSetting={projectNewCodeDefinition ?? globalNewCodeDefinition}
                   globalNewCodeDefinition={globalNewCodeDefinition}
-                  isChanged={isChanged}
-                  onCancel={this.handleCancel}
-                  onSelectDays={this.handleSelectDays}
-                  onSelectReferenceBranch={this.handleSelectReferenceBranch}
-                  onSelectSetting={this.handleSelectSetting}
-                  onSubmit={this.handleSubmit}
-                  onToggleSpecificSetting={this.handleToggleSpecificSetting}
-                  overrideGlobalNewCodeDefinition={overrideGlobalNewCodeDefinition}
-                  referenceBranch={referenceBranch}
-                  saving={saving}
-                  selectedNewCodeDefinitionType={selectedNewCodeDefinitionType}
                 />
-              )}
-
-              <div className={classNames('spacer-top', { invisible: saving || !success })}>
-                <span className="text-success">
-                  <AlertSuccessIcon className="spacer-right" />
-                  {translate('settings.state.saved')}
-                </span>
               </div>
-              {globalNewCodeDefinition && branchSupportEnabled && (
-                <div className="huge-spacer-top branch-baseline-selector">
-                  <hr />
-                  <h2>{translate('project_baseline.configure_branches')}</h2>
-                  <BranchList
-                    branchList={branchList}
-                    component={component}
-                    inheritedSetting={
-                      newCodeDefinitionType
-                        ? {
-                            type: newCodeDefinitionType,
-                            value: newCodeDefinitionValue,
-                          }
-                        : globalNewCodeDefinition
-                    }
-                    globalNewCodeDefinition={globalNewCodeDefinition}
-                  />
-                </div>
-              )}
-            </div>
-          )}
-        </div>
-      </>
-    );
-  }
+            )}
+          </div>
+        )}
+      </div>
+    </>
+  );
 }
 
 export default withComponentContext(
index 0aac108102e447d7831f1ee0a9f10d60fad5302a..c1f729164412d25eed1db53b3cb91133ccbd3b08 100644 (file)
@@ -21,17 +21,15 @@ 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 NewCodeDefinitionAnalysisWarning from '../../../components/new-code-definition/NewCodeDefinitionAnalysisWarning';
 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 { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils';
 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';
@@ -44,7 +42,6 @@ export interface ProjectBaselineSelectorProps {
   branch?: Branch;
   branchList: Branch[];
   branchesEnabled?: boolean;
-  canAdmin: boolean | undefined;
   component: string;
   newCodeDefinitionType?: NewCodeDefinitionType;
   newCodeDefinitionValue?: string;
@@ -56,7 +53,7 @@ export interface ProjectBaselineSelectorProps {
   onCancel: () => void;
   onSelectDays: (value: string) => void;
   onSelectReferenceBranch: (value: string) => void;
-  onSelectSetting: (value?: NewCodeDefinitionType) => void;
+  onSelectSetting: (value: NewCodeDefinitionType) => void;
   onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
   onToggleSpecificSetting: (selection: boolean) => void;
   referenceBranch?: string;
@@ -75,7 +72,6 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS
     branch,
     branchList,
     branchesEnabled,
-    canAdmin,
     component,
     newCodeDefinitionType,
     newCodeDefinitionValue,
@@ -90,8 +86,6 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS
     selectedNewCodeDefinitionType,
   } = props;
 
-  const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(globalNewCodeDefinition);
-
   const isValid = validateSetting({
     numberOfDays: days,
     overrideGlobalNewCodeDefinition,
@@ -109,27 +103,14 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS
         <RadioButton
           checked={!overrideGlobalNewCodeDefinition}
           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>
+          <span>{translate('project_baseline.global_setting')}</span>
         </RadioButton>
 
         <div className="sw-ml-4">
-          <GlobalNewCodeDefinitionDescription
-            globalNcd={globalNewCodeDefinition}
-            isGlobalNcdCompliant={isGlobalNcdCompliant}
-            canAdmin={canAdmin}
-          />
+          <GlobalNewCodeDefinitionDescription globalNcd={globalNewCodeDefinition} />
         </div>
 
         <RadioButton
@@ -143,12 +124,9 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS
       </div>
 
       <div className="big-spacer-left big-spacer-right project-baseline-setting">
-        <NewCodeDefinitionWarning
-          newCodeDefinitionType={newCodeDefinitionType}
-          newCodeDefinitionValue={newCodeDefinitionValue}
-          isBranchSupportEnabled={branchesEnabled}
-          level={NewCodeDefinitionLevels.Project}
-        />
+        {newCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis && (
+          <NewCodeDefinitionAnalysisWarning />
+        )}
         <div className="display-flex-column big-spacer-bottom sw-gap-4" role="radiogroup">
           <NewCodeDefinitionPreviousVersionOption
             disabled={!overrideGlobalNewCodeDefinition}
@@ -184,7 +162,7 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS
               disabled={!overrideGlobalNewCodeDefinition}
               onChangeReferenceBranch={props.onSelectReferenceBranch}
               onSelect={props.onSelectSetting}
-              referenceBranch={referenceBranch || ''}
+              referenceBranch={referenceBranch ?? ''}
               selected={
                 overrideGlobalNewCodeDefinition &&
                 selectedNewCodeDefinitionType === NewCodeDefinitionType.ReferenceBranch
@@ -206,22 +184,26 @@ export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineS
           overrideGlobalNewCodeDefinition &&
           selectedNewCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis && (
             <BranchAnalysisList
-              analysis={analysis || ''}
+              analysis={analysis ?? ''}
               branch={branch.name}
               component={component}
               onSelectAnalysis={noop}
             />
           )}
       </div>
-      <div className={classNames('big-spacer-top', { invisible: !isChanged })}>
-        <Alert variant="info" className="spacer-bottom">
+      <div className="big-spacer-top">
+        <Alert variant="info" className={classNames('spacer-bottom', { invisible: !isChanged })}>
           {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>
+        {!saving && (
+          <>
+            <SubmitButton disabled={!isValid || !isChanged}>{translate('save')}</SubmitButton>
+            <ResetButtonLink className="spacer-left" disabled={!isChanged} onClick={props.onCancel}>
+              {translate('cancel')}
+            </ResetButtonLink>
+          </>
+        )}
       </div>
     </form>
   );
index 084e134f393cecdc517a1844e3c1e855b95f601a..8acca30f39e0b4d3eddefb8a1e57417d3b04a299 100644 (file)
@@ -70,39 +70,6 @@ it('renders correctly without branch support feature', async () => {
   expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
 });
 
-it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
-  newCodeDefinitionMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '99',
-    inherited: true,
-  });
-
-  const { ui } = getPageObjects();
-  renderProjectNewCodeDefinitionApp();
-  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 () => {
-  newCodeDefinitionMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.NumberOfDays,
-    value: '99',
-    inherited: true,
-  });
-
-  const { ui } = getPageObjects();
-  renderProjectNewCodeDefinitionApp({ 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();
   renderProjectNewCodeDefinitionApp({
@@ -134,13 +101,13 @@ it('can set previous version specific setting', async () => {
   // Save changes
   await user.click(ui.saveButton.get());
 
-  expect(ui.saved.get()).toBeInTheDocument();
+  expect(ui.saveButton.get()).toBeDisabled();
 
   // 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();
+  expect(ui.saveButton.get()).toBeDisabled();
 });
 
 it('can set number of days specific setting', async () => {
@@ -161,7 +128,7 @@ it('can set number of days specific setting', async () => {
   await ui.setNumberDaysSetting('10');
   await user.click(ui.saveButton.get());
 
-  expect(ui.saved.get()).toBeInTheDocument();
+  expect(ui.saveButton.get()).toBeDisabled();
 });
 
 it('can set reference branch specific setting', async () => {
@@ -178,15 +145,18 @@ it('can set reference branch specific setting', async () => {
   // Save changes
   await user.click(ui.saveButton.get());
 
-  expect(ui.saved.get()).toBeInTheDocument();
+  expect(ui.saveButton.get()).toBeDisabled();
 });
 
 it('cannot set specific analysis setting', async () => {
   const { ui } = getPageObjects();
-  newCodeDefinitionMock.setNewCodePeriod({
-    type: NewCodeDefinitionType.SpecificAnalysis,
-    value: 'analysis_id',
-  });
+  newCodeDefinitionMock.setListBranchesNewCode([
+    mockNewCodePeriodBranch({
+      branchKey: 'main',
+      type: NewCodeDefinitionType.SpecificAnalysis,
+      value: 'analysis_id',
+    }),
+  ]);
   renderProjectNewCodeDefinitionApp();
   await ui.appIsLoaded();
 
@@ -430,9 +400,6 @@ function getPageObjects() {
       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'),
     branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/),
     dismissButton: byLabelText('alert.dismiss'),
   };
index a2144e436b7ce41ca632aa13e03c69a210323381..527e0a5e58e0d24a7573796cb06179032ed0e004 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import * as React from 'react';
+import classNames from 'classnames';
+import React, { useCallback, useEffect } 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 { NewCodeDefinitionLevels } from '../../../components/new-code-definition/utils';
 import Spinner from '../../../components/ui/Spinner';
 import { translate } from '../../../helpers/l10n';
@@ -33,245 +31,149 @@ import {
   getNumberOfDaysDefaultValue,
   isNewCodeDefinitionCompliant,
 } from '../../../helpers/new-code-definition';
+import {
+  useNewCodeDefinitionMutation,
+  useNewCodeDefinitionQuery,
+} from '../../../queries/newCodeDefinition';
 import { NewCodeDefinitionType } from '../../../types/new-code-definition';
 
-interface State {
-  currentSetting?: NewCodeDefinitionType;
-  days: string;
-  previousNonCompliantValue?: string;
-  ncdUpdatedAt?: number;
-  loading: boolean;
-  currentSettingValue?: string;
-  isChanged: boolean;
-  projectKey?: string;
-  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();
-  }
+export default function NewCodeDefinition() {
+  const [numberOfDays, setNumberOfDays] = React.useState(getNumberOfDaysDefaultValue());
+  const [selectedNewCodeDefinitionType, setSelectedNewCodeDefinitionType] = React.useState<
+    NewCodeDefinitionType | undefined
+  >(undefined);
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchNewCodePeriodSetting() {
-    getNewCodeDefinition()
-      .then(({ type, value, previousNonCompliantValue, projectKey, updatedAt }) => {
-        this.setState(({ days }) => ({
-          currentSetting: type,
-          days: type === NewCodeDefinitionType.NumberOfDays ? String(value) : days,
-          loading: false,
-          currentSettingValue: value,
-          selected: type,
-          previousNonCompliantValue,
-          projectKey,
-          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,
-    }));
-  };
+  const { data: newCodeDefinition, isLoading } = useNewCodeDefinitionQuery();
+  const { isLoading: isSaving, mutate: postNewCodeDefinition } = useNewCodeDefinitionMutation();
 
-  onCancel = () => {
-    this.setState(({ currentSetting, currentSettingValue, days }) => ({
-      isChanged: false,
-      selected: currentSetting,
-      days:
-        currentSetting === NewCodeDefinitionType.NumberOfDays ? String(currentSettingValue) : days,
-    }));
-  };
+  const resetNewCodeDefinition = useCallback(() => {
+    setSelectedNewCodeDefinitionType(newCodeDefinition?.type);
+    setNumberOfDays(getNumberOfDaysDefaultValue(newCodeDefinition));
+  }, [newCodeDefinition]);
 
-  onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+  const onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
     e.preventDefault();
 
-    const { days, selected } = this.state;
-
-    const type = selected;
-    const value = type === NewCodeDefinitionType.NumberOfDays ? days : undefined;
+    const type = selectedNewCodeDefinitionType;
+    const value = type === NewCodeDefinitionType.NumberOfDays ? numberOfDays : undefined;
 
-    this.setState({ saving: true, success: false });
-    setNewCodeDefinition({
-      type: type as NewCodeDefinitionType,
+    postNewCodeDefinition({
+      type,
       value,
-    }).then(
-      () => {
-        if (this.mounted) {
-          this.setState({
-            saving: false,
-            currentSetting: type,
-            currentSettingValue: value || undefined,
-            previousNonCompliantValue: undefined,
-            ncdUpdatedAt: Date.now(),
-            isChanged: false,
-            success: true,
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({
-            saving: false,
-          });
-        }
-      }
-    );
+    });
   };
 
-  render() {
-    const {
-      currentSetting,
-      days,
-      previousNonCompliantValue,
-      ncdUpdatedAt,
-      loading,
-      isChanged,
-      currentSettingValue,
-      projectKey,
-      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>
+  useEffect(() => {
+    resetNewCodeDefinition();
+  }, [resetNewCodeDefinition]);
+
+  const isValid =
+    selectedNewCodeDefinitionType !== NewCodeDefinitionType.NumberOfDays ||
+    isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: numberOfDays });
+
+  const isFormTouched =
+    selectedNewCodeDefinitionType === NewCodeDefinitionType.NumberOfDays
+      ? numberOfDays !== newCodeDefinition?.value
+      : selectedNewCodeDefinitionType !== newCodeDefinition?.type;
+
+  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}
-                          currentDaysValue={
-                            currentSetting === NewCodeDefinitionType.NumberOfDays
-                              ? currentSettingValue
-                              : undefined
-                          }
-                          previousNonCompliantValue={previousNonCompliantValue}
-                          projectKey={projectKey}
-                          updatedAt={ncdUpdatedAt}
-                          isChanged={isChanged}
-                          isValid={isValid}
-                          onChangeDays={this.onSelectDays}
-                          onSelect={this.onSelectSetting}
-                          selected={selected === NewCodeDefinitionType.NumberOfDays}
-                          settingLevel={NewCodeDefinitionLevels.Global}
-                        />
-                        <NewCodeDefinitionWarning
-                          newCodeDefinitionType={currentSetting}
-                          newCodeDefinitionValue={currentSettingValue}
-                          isBranchSupportEnabled={undefined}
-                          level={NewCodeDefinitionLevels.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}>
+                <div className="settings-definition-right">
+                  <Spinner loading={isLoading}>
+                    <form onSubmit={onSubmit}>
+                      <NewCodeDefinitionPreviousVersionOption
+                        isDefault
+                        onSelect={setSelectedNewCodeDefinitionType}
+                        selected={
+                          selectedNewCodeDefinitionType === NewCodeDefinitionType.PreviousVersion
+                        }
+                      />
+                      <NewCodeDefinitionDaysOption
+                        className="spacer-top sw-mb-4"
+                        days={numberOfDays}
+                        currentDaysValue={
+                          newCodeDefinition?.type === NewCodeDefinitionType.NumberOfDays
+                            ? newCodeDefinition?.value
+                            : undefined
+                        }
+                        previousNonCompliantValue={newCodeDefinition?.previousNonCompliantValue}
+                        projectKey={newCodeDefinition?.projectKey}
+                        updatedAt={newCodeDefinition?.updatedAt}
+                        isChanged={isFormTouched}
+                        isValid={isValid}
+                        onChangeDays={setNumberOfDays}
+                        onSelect={setSelectedNewCodeDefinitionType}
+                        selected={
+                          selectedNewCodeDefinitionType === NewCodeDefinitionType.NumberOfDays
+                        }
+                        settingLevel={NewCodeDefinitionLevels.Global}
+                      />
+                      <div className="big-spacer-top">
+                        <p className={classNames('spacer-bottom', { invisible: !isFormTouched })}>
+                          {translate('baseline.next_analysis_notice')}
+                        </p>
+                        <Spinner className="spacer-right" loading={isSaving} />
+                        {!isSaving && (
+                          <>
+                            <SubmitButton disabled={!isFormTouched || !isValid}>
                               {translate('save')}
                             </SubmitButton>
-                            <ResetButtonLink className="spacer-left" onClick={this.onCancel}>
+                            <ResetButtonLink
+                              className="spacer-left"
+                              disabled={!isFormTouched}
+                              onClick={resetNewCodeDefinition}
+                            >
                               {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>
+                    </form>
+                  </Spinner>
                 </div>
-              </li>
-            </ul>
-          </li>
-        </ul>
-      </>
-    );
-  }
+              </div>
+            </li>
+          </ul>
+        </li>
+      </ul>
+    </>
+  );
 }
index def197bba026fadc086b1f2333da913d3ef97d19..2d4621dadc0566e1640a39dbd7354b6a150a8af0 100644 (file)
@@ -17,7 +17,6 @@
  * 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';
@@ -60,7 +59,7 @@ it('renders and behaves as expected', async () => {
 
   expect(await ui.newCodeTitle.find()).toBeInTheDocument();
   // Previous version should be checked by default
-  expect(ui.prevVersionRadio.get()).toBeChecked();
+  expect(await ui.prevVersionRadio.find()).toBeChecked();
 
   // Can select number of days
   await user.click(ui.daysNumberRadio.get());
@@ -91,31 +90,13 @@ it('renders and behaves as expected', async () => {
   await user.clear(ui.daysInput.get());
   await user.type(ui.daysInput.get(), '10');
   await user.click(ui.saveButton.get());
-  expect(ui.savedMsg.get()).toBeInTheDocument();
+  expect(ui.saveButton.get()).toBeDisabled();
 
   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();
+  expect(ui.saveButton.get()).toBeDisabled();
 });
 
 it('displays information message when NCD is automatically updated', async () => {
index 133e3e0b71164eee6d92a7f7ba376d8310b8ba0e..22958df2264851daf08a401740c0a5ad9740f389 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { FlagMessage, Link } from 'design-system';
 import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { NewCodeDefinition, NewCodeDefinitionType } from '../../types/new-code-definition';
 
 interface Props {
   globalNcd: NewCodeDefinition;
-  isGlobalNcdCompliant: boolean;
-  canAdmin?: boolean;
 }
 
-export default function GlobalNewCodeDefinitionDescription({
-  globalNcd,
-  isGlobalNcdCompliant,
-  canAdmin,
-}: Props) {
+export default function GlobalNewCodeDefinitionDescription({ globalNcd }: Props) {
   let setting: string;
   let description: string;
   let useCase: string;
@@ -51,46 +43,10 @@ export default function GlobalNewCodeDefinitionDescription({
   }
 
   return (
-    <>
-      <div className="sw-flex sw-flex-col sw-gap-2 sw-max-w-[800px]">
-        <strong className="sw-font-bold">{setting}</strong>
-        {isGlobalNcdCompliant && (
-          <>
-            <span>{description}</span>
-            <span>{useCase}</span>
-          </>
-        )}
-      </div>
-      {!isGlobalNcdCompliant && (
-        <FlagMessage variant="warning" className="sw-mt-4 sw-max-w-[800px]">
-          <span>
-            <p className="sw-mb-2 sw-font-bold">
-              {translate('new_code_definition.compliance.warning.title.global')}
-            </p>
-            <p className="sw-mb-2">
-              {canAdmin ? (
-                <FormattedMessage
-                  id="new_code_definition.compliance.warning.explanation.admin"
-                  defaultMessage={translate(
-                    'new_code_definition.compliance.warning.explanation.admin'
-                  )}
-                  values={{
-                    link: (
-                      <Link to="/admin/settings?category=new_code_period">
-                        {translate(
-                          'new_code_definition.compliance.warning.explanation.action.admin.link'
-                        )}
-                      </Link>
-                    ),
-                  }}
-                />
-              ) : (
-                translate('new_code_definition.compliance.warning.explanation')
-              )}
-            </p>
-          </span>
-        </FlagMessage>
-      )}
-    </>
+    <div className="sw-flex sw-flex-col sw-gap-2 sw-max-w-[800px]">
+      <strong className="sw-font-bold">{setting}</strong>
+      <span>{description}</span>
+      <span>{useCase}</span>
+    </div>
   );
 }
index ecb71e828939033cb7d8a451ec70e62aa95ef5fb..f4cd199ae0afa56131fe22e155ef6e2e65463588 100644 (file)
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { FormattedMessage, useIntl } from 'react-intl';
 import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages';
-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';
 import { queryToSearch } from '../../helpers/urls';
+import { useNewCodeDefinitionQuery } from '../../queries/newCodeDefinition';
 import { Component } from '../../types/types';
 import Link from '../common/Link';
 import DismissableAlertComponent from '../ui/DismissableAlertComponent';
@@ -35,11 +35,12 @@ import {
 } from './utils';
 
 interface NCDAutoUpdateMessageProps extends Pick<CurrentUserContextInterface, 'currentUser'> {
+  branchName?: string;
   component?: Component;
 }
 
 function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
-  const { component, currentUser } = props;
+  const { branchName, component, currentUser } = props;
   const isGlobalBanner = component === undefined;
   const intl = useIntl();
 
@@ -47,10 +48,14 @@ function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
   const [previouslyNonCompliantNewCodeDefinition, setPreviouslyNonCompliantNewCodeDefinition] =
     useState<PreviouslyNonCompliantNCD | undefined>(undefined);
 
-  const isAdmin = useMemo(
-    () => isGlobalOrProjectAdmin(currentUser, component),
-    [component, currentUser]
-  );
+  const isAdmin = isGlobalOrProjectAdmin(currentUser, component);
+
+  const { data: newCodeDefinition } = useNewCodeDefinitionQuery({
+    branchName,
+    enabled: isAdmin,
+    projectKey: component?.key,
+  });
+
   const ncdReviewLinkTo = useMemo(
     () =>
       isGlobalBanner
@@ -79,37 +84,30 @@ function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
   }, [component, isGlobalBanner]);
 
   useEffect(() => {
-    async function fetchNewCodeDefinition() {
-      const newCodeDefinition = await getNewCodeDefinition(
-        component && {
-          project: component.key,
-        }
+    async function updateMessageStatus() {
+      const messageStatus = await checkMessageDismissed(
+        isGlobalBanner
+          ? {
+              messageType: MessageTypes.GlobalNcd90,
+            }
+          : {
+              messageType: MessageTypes.ProjectNcd90,
+              projectKey: component.key,
+            }
       );
 
-      if (isPreviouslyNonCompliantDaysNCD(newCodeDefinition)) {
-        setPreviouslyNonCompliantNewCodeDefinition(newCodeDefinition);
-
-        const messageStatus = await checkMessageDismissed(
-          isGlobalBanner
-            ? {
-                messageType: MessageTypes.GlobalNcd90,
-              }
-            : {
-                messageType: MessageTypes.ProjectNcd90,
-                projectKey: component.key,
-              }
-        );
-
-        setDismissed(messageStatus.dismissed);
-      }
+      setDismissed(messageStatus.dismissed);
     }
 
-    if (isAdmin) {
-      fetchNewCodeDefinition();
+    if (newCodeDefinition && isPreviouslyNonCompliantDaysNCD(newCodeDefinition)) {
+      setPreviouslyNonCompliantNewCodeDefinition(newCodeDefinition);
+      updateMessageStatus();
+    } else {
+      setPreviouslyNonCompliantNewCodeDefinition(undefined);
     }
-  }, [isAdmin, component, isGlobalBanner]);
+  }, [component?.key, isGlobalBanner, newCodeDefinition]);
 
-  if (!isAdmin || dismissed || !previouslyNonCompliantNewCodeDefinition) {
+  if (dismissed || !previouslyNonCompliantNewCodeDefinition) {
     return null;
   }
 
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx
new file mode 100644 (file)
index 0000000..44b8302
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import DocLink from '../common/DocLink';
+import { Alert } from '../ui/Alert';
+
+export default function NewCodeDefinitionAnalysisWarning() {
+  return (
+    <Alert variant="warning" className="sw-mb-4 sw-max-w-[800px]">
+      <p className="sw-mb-2 sw-font-bold">
+        {translate('baseline.specific_analysis.compliance_warning.title')}
+      </p>
+      <p className="sw-mb-2">
+        {translate('baseline.specific_analysis.compliance_warning.explanation')}
+      </p>
+      <p>
+        {translate('learn_more')}:&nbsp;
+        <DocLink to="/project-administration/defining-new-code/">
+          {translate('baseline.specific_analysis.compliance_warning.link')}
+        </DocLink>
+      </p>
+    </Alert>
+  );
+}
index de6016a23b949cbe72e2552c42b501ff3c1ccd98..fad3f8e72820fe01e1fd0ab32530693d39ffa7c6 100644 (file)
@@ -125,11 +125,13 @@ export default function NewCodeDefinitionDaysOption(props: Props) {
               <div className="sw-my-2 sw-flex sw-items-center">
                 <InputField
                   id="baseline_number_of_days"
-                  type="number"
-                  required
                   isInvalid={!isValid}
                   isValid={isChanged && isValid}
+                  max={NUMBER_OF_DAYS_MAX_VALUE}
+                  min={NUMBER_OF_DAYS_MIN_VALUE}
                   onChange={(e) => onChangeDays(e.currentTarget.value)}
+                  required
+                  type="number"
                   value={days}
                 />
                 {!isValid && <FlagErrorIcon className="sw-ml-2" />}
index 59f0d4eee6cefaba558e5e9f02d4209960c9b60b..329a84c2d53200f306378664a0e8bbc7c9d8ccdd 100644 (file)
@@ -38,30 +38,23 @@ import {
   NewCodeDefinitionType,
   NewCodeDefinitiondWithCompliance,
 } from '../../types/new-code-definition';
-import Tooltip from '../controls/Tooltip';
 import GlobalNewCodeDefinitionDescription from './GlobalNewCodeDefinitionDescription';
 import NewCodeDefinitionDaysOption from './NewCodeDefinitionDaysOption';
 import NewCodeDefinitionPreviousVersionOption from './NewCodeDefinitionPreviousVersionOption';
 import { NewCodeDefinitionLevels } from './utils';
 
 interface Props {
-  canAdmin: boolean | undefined;
   onNcdChanged: (ncd: NewCodeDefinitiondWithCompliance) => void;
 }
 
 export default function NewCodeDefinitionSelector(props: Props) {
-  const { canAdmin, onNcdChanged } = props;
+  const { onNcdChanged } = props;
 
   const [globalNcd, setGlobalNcd] = React.useState<NewCodeDefinition | null>(null);
   const [selectedNcdType, setSelectedNcdType] = React.useState<NewCodeDefinitionType | null>(null);
   const [days, setDays] = React.useState<string>('');
   const [isChanged, setIsChanged] = React.useState<boolean>(false);
 
-  const isGlobalNcdCompliant = React.useMemo(
-    () => Boolean(globalNcd && isNewCodeDefinitionCompliant(globalNcd)),
-    [globalNcd]
-  );
-
   React.useEffect(() => {
     const numberOfDays = getNumberOfDaysDefaultValue(globalNcd);
     setDays(numberOfDays);
@@ -115,34 +108,19 @@ export default function NewCodeDefinitionSelector(props: Props) {
         <RadioButton
           aria-label={translate('new_code_definition.global_setting')}
           checked={selectedNcdType === NewCodeDefinitionType.Inherited}
-          disabled={!isGlobalNcdCompliant}
           onCheck={() => handleNcdChanged(NewCodeDefinitionType.Inherited)}
           value="general"
         >
-          <Tooltip
-            overlay={
-              isGlobalNcdCompliant
-                ? null
-                : translate('new_code_definition.compliance.warning.title.global')
-            }
-          >
-            <span className="sw-font-semibold">
-              {translate('new_code_definition.global_setting')}
-            </span>
-          </Tooltip>
+          <span className="sw-font-semibold">
+            {translate('new_code_definition.global_setting')}
+          </span>
         </RadioButton>
 
         <StyledGlobalSettingWrapper
           className="sw-mt-4 sw-ml-6"
           selected={selectedNcdType === NewCodeDefinitionType.Inherited}
         >
-          {globalNcd && (
-            <GlobalNewCodeDefinitionDescription
-              globalNcd={globalNcd}
-              isGlobalNcdCompliant={isGlobalNcdCompliant}
-              canAdmin={canAdmin}
-            />
-          )}
+          {globalNcd && <GlobalNewCodeDefinitionDescription globalNcd={globalNcd} />}
         </StyledGlobalSettingWrapper>
 
         <RadioButton
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionWarning.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionWarning.tsx
deleted file mode 100644 (file)
index 1a97822..0000000
+++ /dev/null
@@ -1,94 +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 { translate } from '../../helpers/l10n';
-import { isNewCodeDefinitionCompliant } from '../../helpers/new-code-definition';
-import { NewCodeDefinitionType } from '../../types/new-code-definition';
-import DocLink from '../common/DocLink';
-import { Alert } from '../ui/Alert';
-import { NewCodeDefinitionLevels } from './utils';
-
-export interface NewCodeDefinitionWarningProps {
-  newCodeDefinitionType: NewCodeDefinitionType | undefined;
-  newCodeDefinitionValue: string | undefined;
-  isBranchSupportEnabled: boolean | undefined;
-  level: Exclude<NewCodeDefinitionLevels, NewCodeDefinitionLevels.NewProject>;
-}
-
-export default function NewCodeDefinitionWarning({
-  newCodeDefinitionType,
-  newCodeDefinitionValue,
-  isBranchSupportEnabled,
-  level,
-}: NewCodeDefinitionWarningProps) {
-  if (
-    newCodeDefinitionType === undefined ||
-    isNewCodeDefinitionCompliant({ type: newCodeDefinitionType, value: newCodeDefinitionValue })
-  ) {
-    return null;
-  }
-
-  if (newCodeDefinitionType === NewCodeDefinitionType.SpecificAnalysis) {
-    return (
-      <Alert variant="warning" className="sw-mb-4 sw-max-w-[800px]">
-        <p className="sw-mb-2 sw-font-bold">
-          {translate('baseline.specific_analysis.compliance_warning.title')}
-        </p>
-        <p className="sw-mb-2">
-          {translate('baseline.specific_analysis.compliance_warning.explanation')}
-        </p>
-        <p>
-          {translate('learn_more')}:&nbsp;
-          <DocLink to="/project-administration/defining-new-code/">
-            {translate('baseline.specific_analysis.compliance_warning.link')}
-          </DocLink>
-        </p>
-      </Alert>
-    );
-  }
-
-  if (newCodeDefinitionType === NewCodeDefinitionType.NumberOfDays) {
-    return (
-      <Alert variant="warning" className="sw-mb-4 sw-max-w-[800px]">
-        <p className="sw-mb-2 sw-font-bold">
-          {translate('baseline.number_days.compliance_warning.title')}
-        </p>
-        <p className="sw-mb-2">
-          {translate(
-            `baseline.number_days.compliance_warning.content.${level}${
-              isBranchSupportEnabled && level === NewCodeDefinitionLevels.Project
-                ? '.with_branch_support'
-                : ''
-            }`
-          )}
-        </p>
-        <p>
-          {translate('learn_more')}:&nbsp;
-          <DocLink to="/project-administration/defining-new-code/">
-            {translate('baseline.number_days.compliance_warning.link')}
-          </DocLink>
-        </p>
-      </Alert>
-    );
-  }
-
-  return null;
-}
index d02b8742c620759742be8aa2077ac640efd60e80..9f724a3f9d4fb6f85f3abb86790e301770deed9a 100644 (file)
@@ -32,8 +32,7 @@ export function isNewCodeDefinitionCompliant(newCodePeriod: NewCodeDefinition) {
         return false;
       }
       return (
-        !/\D/.test(newCodePeriod.value) &&
-        Number.isInteger(+newCodePeriod.value) &&
+        /^\d+$/.test(newCodePeriod.value) &&
         NUMBER_OF_DAYS_MIN_VALUE <= +newCodePeriod.value &&
         +newCodePeriod.value <= NUMBER_OF_DAYS_MAX_VALUE
       );
diff --git a/server/sonar-web/src/main/js/queries/newCodeDefinition.ts b/server/sonar-web/src/main/js/queries/newCodeDefinition.ts
new file mode 100644 (file)
index 0000000..9dcdea5
--- /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.
+ */
+// React-query component for new code definition
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import {
+  getNewCodeDefinition,
+  resetNewCodeDefinition,
+  setNewCodeDefinition,
+} from '../api/newCodeDefinition';
+import { NewCodeDefinitionType } from '../types/new-code-definition';
+
+function getNewCodeDefinitionQueryKey(projectKey?: string, branchName?: string) {
+  return ['new-code-definition', { projectKey, branchName }];
+}
+
+export function useNewCodeDefinitionQuery(params?: {
+  branchName?: string;
+  enabled?: boolean;
+  projectKey?: string;
+}) {
+  return useQuery(
+    getNewCodeDefinitionQueryKey(params?.projectKey, params?.branchName),
+    () => getNewCodeDefinition({ branch: params?.branchName, project: params?.projectKey }),
+    { enabled: params?.enabled ?? true, refetchOnWindowFocus: false }
+  );
+}
+
+export function useNewCodeDefinitionMutation() {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (newCodeDefinition: {
+      project?: string;
+      branch?: string;
+      type?: NewCodeDefinitionType;
+      value?: string;
+    }) => {
+      const { branch, project, type, value } = newCodeDefinition;
+
+      if (type === undefined) {
+        return resetNewCodeDefinition({
+          branch,
+          project,
+        });
+      }
+
+      return setNewCodeDefinition({ branch, project, type, value });
+    },
+    onSuccess(_, { branch, project }) {
+      queryClient.invalidateQueries({
+        queryKey: getNewCodeDefinitionQueryKey(project, branch),
+      });
+    },
+  });
+}
index e78def3287548b6ec6dd77d7bd6a734ba72d4663..e3daa4badecd2e9f398396acced48f1a35d11063 100644 (file)
@@ -655,17 +655,7 @@ project_baseline.specific_setting=Define a specific setting for this project
 project_baseline.configure_branches=Set a specific setting for a branch
 
 project_baseline.compliance.warning.title.project=Your project new code definition is not compliant with the Clean as You Code methodology
-project_baseline.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology
-project_baseline.compliance.warning.explanation=Please ask an administrator to update the global new code definition before switching back to it.
-project_baseline.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it.
-project_baseline.warning.explanation.action.admin.link=General Settings > New Code
-
-baseline.number_days.compliance_warning.title=Your new code definition is not compliant with the Clean as You Code methodology
-baseline.number_days.compliance_warning.content.global=We recommend that you update this new code definition so that new projects and existing projects that do not use a specific New Code definition benefit from the Clean as You Code methodology by default.
-baseline.number_days.compliance_warning.content.project=We recommend that you update this new code definition so that your project benefits from the Clean as You Code methodology.
-baseline.number_days.compliance_warning.content.project.with_branch_support=We recommend that you update this new code definition so that new branches and existing branches that do not use a specific New Code definition benefit from the Clean as You Code methodology by default.
-baseline.number_days.compliance_warning.content.branch=We recommend that you update this new code definition so that your branch benefits from the Clean as You Code methodology.
-baseline.number_days.compliance_warning.link=Defining New Code
+
 baseline.specific_analysis=Specific analysis
 baseline.specific_analysis.description=Choose an analysis as the baseline for the new code.
 baseline.specific_analysis.compliance_warning.title=Choosing the "Specific analysis" option from the SonarQube UI is not compliant with the Clean as You Code methodology
@@ -3955,11 +3945,6 @@ new_code_definition.question=Choose the baseline for new code for this project
 new_code_definition.global_setting=Use the global setting
 new_code_definition.specific_setting=Define a specific setting for this project
 
-new_code_definition.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology
-new_code_definition.compliance.warning.explanation=Please ask an administrator to update the global new code definition before you can use it for your project.
-new_code_definition.compliance.warning.explanation.admin=Please update the global new code definition under {link} before you can use it for your project.
-new_code_definition.compliance.warning.explanation.action.admin.link=General Settings > New Code
-
 new_code_definition.previous_version=Previous version
 new_code_definition.previous_version.usecase=Recommended for projects following regular versions or releases.
 new_code_definition.previous_version.description=Any code that has changed since the previous version is considered new code.