.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);
};
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);
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;
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;
</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));
}
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';
</div>
<div className={classNames({ 'sw-hidden': !isProjectSetupDone })}>
<NewCodeDefinitionSelection
- canAdmin={Boolean(appState.canAdmin)}
router={router}
createProjectFnRef={this.createProjectFnRef}
/>
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';
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'),
};
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)
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>();
/>
</p>
- <NewCodeDefinitionSelector canAdmin={canAdmin} onNcdChanged={selectDefinition} />
+ <NewCodeDefinitionSelector onNcdChanged={selectDefinition} />
<div className="sw-mt-10 sw-mb-8">
<ButtonPrimary
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';
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,
<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}
* 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';
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';
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(
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';
branch?: Branch;
branchList: Branch[];
branchesEnabled?: boolean;
- canAdmin: boolean | undefined;
component: string;
newCodeDefinitionType?: NewCodeDefinitionType;
newCodeDefinitionValue?: string;
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;
branch,
branchList,
branchesEnabled,
- canAdmin,
component,
newCodeDefinitionType,
newCodeDefinitionValue,
selectedNewCodeDefinitionType,
} = props;
- const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(globalNewCodeDefinition);
-
const isValid = validateSetting({
numberOfDays: days,
overrideGlobalNewCodeDefinition,
<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
</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}
disabled={!overrideGlobalNewCodeDefinition}
onChangeReferenceBranch={props.onSelectReferenceBranch}
onSelect={props.onSelectSetting}
- referenceBranch={referenceBranch || ''}
+ referenceBranch={referenceBranch ?? ''}
selected={
overrideGlobalNewCodeDefinition &&
selectedNewCodeDefinitionType === NewCodeDefinitionType.ReferenceBranch
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>
);
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({
// 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 () => {
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 () => {
// 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();
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'),
};
* 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';
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>
+ </>
+ );
}
* 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';
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());
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 () => {
* 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;
}
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>
);
}
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';
} 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();
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
}, [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;
}
--- /dev/null
+/*
+ * 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')}:
+ <DocLink to="/project-administration/defining-new-code/">
+ {translate('baseline.specific_analysis.compliance_warning.link')}
+ </DocLink>
+ </p>
+ </Alert>
+ );
+}
<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" />}
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);
<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
+++ /dev/null
-/*
- * 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')}:
- <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')}:
- <DocLink to="/project-administration/defining-new-code/">
- {translate('baseline.number_days.compliance_warning.link')}
- </DocLink>
- </p>
- </Alert>
- );
- }
-
- return null;
-}
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
);
--- /dev/null
+/*
+ * 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),
+ });
+ },
+ });
+}
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
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.