@@ -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); |
@@ -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)); |
@@ -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} | |||
/> |
@@ -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) |
@@ -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 |
@@ -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} |
@@ -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( |
@@ -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> | |||
); |
@@ -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'), | |||
}; |
@@ -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> | |||
</> | |||
); | |||
} |
@@ -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 () => { |
@@ -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> | |||
); | |||
} |
@@ -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; | |||
} | |||
@@ -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')}: | |||
<DocLink to="/project-administration/defining-new-code/"> | |||
{translate('baseline.specific_analysis.compliance_warning.link')} | |||
</DocLink> | |||
</p> | |||
</Alert> | |||
); | |||
} |
@@ -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" />} |
@@ -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 |
@@ -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')}: | |||
<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; | |||
} |
@@ -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 | |||
); |
@@ -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), | |||
}); | |||
}, | |||
}); | |||
} |
@@ -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. |