Browse Source

SONAR-20249 Hide NCD banner on save (#9186)

tags/10.3.0.82913
Ambroise C 9 months ago
parent
commit
a3e95e5161
20 changed files with 530 additions and 945 deletions
  1. 14
    9
      server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts
  2. 24
    4
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  3. 1
    2
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  4. 0
    60
      server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
  5. 2
    3
      server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
  6. 4
    8
      server/sonar-web/src/main/js/apps/projectNewCode/components/BranchNewCodeDefinitionSettingModal.tsx
  7. 162
    308
      server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx
  8. 19
    37
      server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx
  9. 11
    44
      server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx
  10. 128
    226
      server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx
  11. 3
    22
      server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx
  12. 6
    50
      server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
  13. 29
    31
      server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx
  14. 43
    0
      server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx
  15. 4
    2
      server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
  16. 5
    27
      server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
  17. 0
    94
      server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionWarning.tsx
  18. 1
    2
      server/sonar-web/src/main/js/helpers/new-code-definition.ts
  19. 73
    0
      server/sonar-web/src/main/js/queries/newCodeDefinition.ts
  20. 1
    16
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 14
- 9
server/sonar-web/src/main/js/api/mocks/NewCodeDefinitionServiceMock.ts View 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);

+ 24
- 4
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View 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));

+ 1
- 2
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View 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}
/>

+ 0
- 60
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx View 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)

+ 2
- 3
server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx View 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

+ 4
- 8
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchNewCodeDefinitionSettingModal.tsx View 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}

+ 162
- 308
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx View File

@@ -17,22 +17,14 @@
* 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(

+ 19
- 37
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionSelector.tsx View 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>
);

+ 11
- 44
server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx View 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'),
};

+ 128
- 226
server/sonar-web/src/main/js/apps/settings/components/NewCodeDefinition.tsx View File

@@ -17,15 +17,13 @@
* 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>
</>
);
}

+ 3
- 22
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodeDefinition-it.tsx View 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 () => {

+ 6
- 50
server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx View File

@@ -17,23 +17,15 @@
* 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>
);
}

+ 29
- 31
server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx View File

@@ -20,11 +20,11 @@
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;
}


+ 43
- 0
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionAnalysisWarning.tsx View File

@@ -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>
);
}

+ 4
- 2
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx View 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" />}

+ 5
- 27
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx View 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

+ 0
- 94
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionWarning.tsx View File

@@ -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;
}

+ 1
- 2
server/sonar-web/src/main/js/helpers/new-code-definition.ts View 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
);

+ 73
- 0
server/sonar-web/src/main/js/queries/newCodeDefinition.ts View File

@@ -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),
});
},
});
}

+ 1
- 16
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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.

Loading…
Cancel
Save