aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2023-08-07 17:34:23 +0200
committersonartech <sonartech@sonarsource.com>2023-08-14 20:02:57 +0000
commit3f570b118fa9a68469a34bb98f25367050f21025 (patch)
treee4cba8c1d56a44f4a08705bd37f77d36d40cd355 /server
parent08ef818d25713fcbd1e5c87f30521324b8f9e74c (diff)
downloadsonarqube-3f570b118fa9a68469a34bb98f25367050f21025.tar.gz
sonarqube-3f570b118fa9a68469a34bb98f25367050f21025.zip
SONAR-20086 Migrate new code period setup page
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/design-system/src/components/SelectionCard.tsx6
-rw-r--r--server/sonar-web/design-system/src/components/input/InputField.tsx6
-rw-r--r--server/sonar-web/design-system/src/theme/light.ts1
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx120
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx94
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx59
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx53
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx122
15 files changed, 279 insertions, 246 deletions
diff --git a/server/sonar-web/design-system/src/components/SelectionCard.tsx b/server/sonar-web/design-system/src/components/SelectionCard.tsx
index 9f14e1df853..088432fc90b 100644
--- a/server/sonar-web/design-system/src/components/SelectionCard.tsx
+++ b/server/sonar-web/design-system/src/components/SelectionCard.tsx
@@ -70,9 +70,10 @@ export function SelectionCard(props: SelectionCardProps) {
},
className
)}
- onClick={isActionable && !disabled ? onClick : undefined}
+ onClick={isActionable && !disabled && !selected ? onClick : undefined}
role={isActionable ? 'radio' : 'presentation'}
tabIndex={disabled ? -1 : 0}
+ type="button"
>
<StyledContent>
{isActionable && (
@@ -110,6 +111,7 @@ const StyledButton = styled.button`
background-color: ${themeColor('backgroundSecondary')};
border: ${themeBorder('default', 'selectionCardBorder')};
+ color: inherit;
&:focus {
outline: none;
@@ -139,6 +141,7 @@ const StyledButton = styled.button`
${tw`sw-cursor-not-allowed`}
background-color: ${themeColor('selectionCardDisabled')};
+ color: ${themeColor('selectionCardDisabledText')};
border: ${themeBorder('default', 'selectionCardBorderDisabled')};
}
`;
@@ -170,6 +173,7 @@ const StyledLabel = styled.label`
${tw`sw-body-sm-highlight`}
color: ${themeColor('selectionCardHeader')};
+ cursor: inherit;
.disabled & {
color: ${themeContrast('selectionCardDisabled')};
diff --git a/server/sonar-web/design-system/src/components/input/InputField.tsx b/server/sonar-web/design-system/src/components/input/InputField.tsx
index ec3ddad5a4f..781c5243751 100644
--- a/server/sonar-web/design-system/src/components/input/InputField.tsx
+++ b/server/sonar-web/design-system/src/components/input/InputField.tsx
@@ -134,6 +134,12 @@ const StyledInput = styled.input`
${baseStyle}
${tw`sw-h-control`}
}
+
+ input[type='number']& {
+ ${getInputVariant}
+ ${baseStyle}
+ ${tw`sw-h-control`}
+ }
`;
const StyledTextArea = styled.textarea`
diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts
index b9176bdac41..a8eaa23912e 100644
--- a/server/sonar-web/design-system/src/theme/light.ts
+++ b/server/sonar-web/design-system/src/theme/light.ts
@@ -505,6 +505,7 @@ export const lightTheme = {
// selection card
selectionCardHeader: secondary.darker,
selectionCardDisabled: secondary.light,
+ selectionCardDisabledText: secondary.dark,
selectionCardBorder: COLORS.blueGrey[100],
selectionCardBorderHover: COLORS.indigo[200],
selectionCardBorderSelected: primary.light,
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index 51c69701112..664bc914ae6 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -21,25 +21,17 @@ import classNames from 'classnames';
import { LargeCenteredLayout } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
-import { FormattedMessage } from 'react-intl';
import { getAlmSettings } from '../../../api/alm-settings';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import DocLink from '../../../components/common/DocLink';
-import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import NewCodeDefinitionSelector from '../../../components/new-code-definition/NewCodeDefinitionSelector';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { addGlobalSuccessMessage } from '../../../helpers/globalMessages';
import { translate } from '../../../helpers/l10n';
-import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { AppState } from '../../../types/appstate';
import { Feature } from '../../../types/features';
-import { NewCodeDefinitiondWithCompliance } from '../../../types/new-code-definition';
import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm';
import AzureProjectCreate from './Azure/AzureProjectCreate';
import BitbucketCloudProjectCreate from './BitbucketCloud/BitbucketCloudProjectCreate';
@@ -47,7 +39,7 @@ import BitbucketProjectCreate from './BitbucketServer/BitbucketProjectCreate';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './Github/GitHubProjectCreate';
import GitlabProjectCreate from './Gitlab/GitlabProjectCreate';
-import CreateProjectPageHeader from './components/CreateProjectPageHeader';
+import NewCodeDefinitionSelection from './components/NewCodeDefinitionSelection';
import ManualProjectCreate from './manual/ManualProjectCreate';
import './style.css';
import { CreateProjectApiCallback, CreateProjectModes } from './types';
@@ -65,10 +57,7 @@ interface State {
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
- isProjectSetupDone: boolean;
creatingAlmDefinition?: AlmKeys;
- selectedNcd: NewCodeDefinitiondWithCompliance | null;
- submitting: boolean;
}
const PROJECT_MODE_FOR_ALM_KEY = {
@@ -90,13 +79,12 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
githubSettings: [],
gitlabSettings: [],
loading: true,
- isProjectSetupDone: false,
- selectedNcd: null,
- submitting: false,
};
componentDidMount() {
this.mounted = true;
+
+ this.cleanQueryParameters();
this.fetchAlmBindings();
}
@@ -104,6 +92,18 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
this.mounted = false;
}
+ cleanQueryParameters() {
+ const { location, router } = this.props;
+
+ if (location.query?.setncd === 'true' && this.createProjectFnRef === null) {
+ // Timeout is required to force the refresh of the URL
+ setTimeout(() => {
+ location.query.setncd = undefined;
+ router.replace(location);
+ }, 0);
+ }
+ }
+
fetchAlmBindings = () => {
this.setState({ loading: true });
return getAlmSettings()
@@ -138,22 +138,12 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
this.setState({ creatingAlmDefinition: alm });
};
- handleProjectCreation = async () => {
- const { selectedNcd } = this.state;
- if (this.createProjectFnRef && selectedNcd) {
- this.setState({ submitting: true });
-
- const { project } = await this.createProjectFnRef(selectedNcd.type, selectedNcd.value);
- this.props.router.push(getProjectUrl(project.key));
-
- addGlobalSuccessMessage(translate('onboarding.create_project.success'));
- this.setState({ submitting: false });
- }
- };
-
handleProjectSetupDone = (createProject: CreateProjectApiCallback) => {
+ const { location, router } = this.props;
this.createProjectFnRef = createProject;
- this.setState({ isProjectSetupDone: true });
+
+ location.query.setncd = 'true';
+ router.push(location);
};
handleOnCancelCreation = () => {
@@ -178,16 +168,6 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
}
};
- handleNcdChanged = (ncd: NewCodeDefinitiondWithCompliance) => {
- this.setState({
- selectedNcd: ncd,
- });
- };
-
- handleGoBack = () => {
- this.setState({ isProjectSetupDone: false });
- };
-
renderProjectCreation(mode?: CreateProjectModes) {
const {
appState: { canAdmin },
@@ -292,61 +272,11 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
}
}
- renderNcdSelection() {
- const { appState } = this.props;
- const { selectedNcd, submitting } = this.state;
-
- return (
- <div id="project-ncd-selection">
- <CreateProjectPageHeader
- title={translate('onboarding.create_project.new_code_definition.title')}
- />
-
- <h1 className="sw-mb-4">{translate('onboarding.create_project.new_code_definition')}</h1>
- <p className="sw-mb-2">
- {translate('onboarding.create_project.new_code_definition.description')}
- </p>
- <p className="sw-mb-2">
- {translate('onboarding.create_project.new_code_definition.description1')}
- </p>
-
- <p className="sw-mb-2">
- <FormattedMessage
- defaultMessage={translate('onboarding.create_project.new_code_definition.description2')}
- id="onboarding.create_project.new_code_definition.description2"
- values={{
- link: (
- <DocLink to="/project-administration/defining-new-code/">
- {translate('onboarding.create_project.new_code_definition.description2.link')}
- </DocLink>
- ),
- }}
- />
- </p>
-
- <NewCodeDefinitionSelector
- canAdmin={appState.canAdmin}
- onNcdChanged={this.handleNcdChanged}
- />
-
- <div className="sw-flex sw-flex-row sw-gap-4 sw-mt-4">
- <ButtonLink onClick={this.handleGoBack}>{translate('back')}</ButtonLink>
- <SubmitButton
- onClick={this.handleProjectCreation}
- disabled={!selectedNcd?.isCompliant || submitting}
- >
- {translate('onboarding.create_project.new_code_definition.create_project')}
- <DeferredSpinner className="spacer-left" loading={submitting} />
- </SubmitButton>
- </div>
- </div>
- );
- }
-
render() {
- const { location } = this.props;
- const { creatingAlmDefinition, isProjectSetupDone } = this.state;
+ const { appState, location, router } = this.props;
+ const { creatingAlmDefinition } = this.state;
const mode: CreateProjectModes | undefined = location.query?.mode;
+ const isProjectSetupDone = location.query?.setncd === 'true';
return (
<LargeCenteredLayout className="sw-pt-8">
@@ -358,7 +288,11 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
{this.renderProjectCreation(mode)}
</div>
<div className={classNames({ 'sw-hidden': !isProjectSetupDone })}>
- {this.renderNcdSelection()}
+ <NewCodeDefinitionSelection
+ canAdmin={Boolean(appState.canAdmin)}
+ router={router}
+ createProjectFnRef={this.createProjectFnRef}
+ />
</div>
{creatingAlmDefinition && (
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
index d1cc3348d03..5a3a2a1064c 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
@@ -55,7 +55,6 @@ const ui = {
}),
projectNextButton: byRole('button', { name: 'next' }),
newCodeDefinitionHeader: byText('onboarding.create_project.new_code_definition.title'),
- newCodeDefinitionBackButton: byRole('button', { name: 'back' }),
inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
projectCreateButton: byRole('button', {
name: 'onboarding.create_project.new_code_definition.create_project',
@@ -70,7 +69,7 @@ const ui = {
ncdOptionDaysRadio: byRole('radio', {
name: /new_code_definition.number_days/,
}),
- ncdOptionDaysInput: byRole('textbox', {
+ ncdOptionDaysInput: byRole('spinbutton', {
name: /new_code_definition.number_days.specify_days/,
}),
ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'),
@@ -113,20 +112,12 @@ afterAll(() => {
Object.defineProperty(window, 'location', { configurable: true, value: original });
});
-it('should fill form and move to NCD selection and back', async () => {
+it('should fill form and move to NCD selection', async () => {
const user = userEvent.setup();
renderCreateProject();
await fillFormAndNext('test', user);
expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
-
- expect(ui.newCodeDefinitionBackButton.get()).toBeInTheDocument();
- await user.click(ui.newCodeDefinitionBackButton.get());
-
- expect(ui.manualProjectHeader.get()).toBeInTheDocument();
-
- // TODO this must work at some point
- // expect(ui.displayNameField.get()).toHaveValue('test');
});
it('should select the global NCD when it is compliant', async () => {
@@ -204,7 +195,7 @@ it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])(
}
);
-it('number of days should show error message if value is not a number', async () => {
+it('number of days ignores non-numeric inputs', async () => {
jest
.mocked(getNewCodePeriod)
.mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '60' });
@@ -222,21 +213,14 @@ it('number of days should show error message if value is not a number', async ()
await user.click(ui.ncdOptionDaysRadio.get());
expect(ui.ncdOptionDaysInput.get()).toBeInTheDocument();
- expect(ui.ncdOptionDaysInput.get()).toHaveValue('60');
+ expect(ui.ncdOptionDaysInput.get()).toHaveValue(60);
expect(ui.projectCreateButton.get()).toBeEnabled();
await user.click(ui.ncdOptionDaysInput.get());
await user.keyboard('abc');
- expect(ui.ncdOptionDaysInputError.get()).toBeInTheDocument();
- expect(ui.projectCreateButton.get()).toBeDisabled();
-
- await user.clear(ui.ncdOptionDaysInput.get());
- await user.click(ui.ncdOptionDaysInput.get());
- await user.keyboard('30');
-
- expect(ui.ncdOptionDaysInputError.query()).not.toBeInTheDocument();
- expect(ui.projectCreateButton.get()).toBeEnabled();
+ // it ignores the input and preserves its value
+ expect(ui.ncdOptionDaysInput.get()).toHaveValue(60);
});
it('the project onboarding page should be displayed when the project is created', async () => {
diff --git a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
new file mode 100644
index 00000000000..f0678eb6757
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 { ButtonPrimary, DeferredSpinner, Link, Title } from 'design-system';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Router } from '../../../../components/hoc/withRouter';
+import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector';
+import { useDocUrl } from '../../../../helpers/docs';
+import { addGlobalSuccessMessage } from '../../../../helpers/globalMessages';
+import { translate } from '../../../../helpers/l10n';
+import { getProjectUrl } from '../../../../helpers/urls';
+import { NewCodeDefinitiondWithCompliance } from '../../../../types/new-code-definition';
+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 [submitting, setSubmitting] = React.useState(false);
+ const [selectedDefinition, selectDefinition] = React.useState<NewCodeDefinitiondWithCompliance>();
+
+ const getDocUrl = useDocUrl();
+
+ const handleProjectCreation = React.useCallback(async () => {
+ if (createProjectFnRef && selectedDefinition) {
+ setSubmitting(true);
+ const { project } = await createProjectFnRef(
+ selectedDefinition.type,
+ selectedDefinition.value
+ );
+ setSubmitting(false);
+ router.push(getProjectUrl(project.key));
+
+ addGlobalSuccessMessage(translate('onboarding.create_project.success'));
+ }
+ }, [createProjectFnRef, router, selectedDefinition]);
+
+ return (
+ <div id="project-ncd-selection" className="sw-body-sm">
+ <Title className="sw-mt-8">
+ {translate('onboarding.create_project.new_code_definition.title')}
+ </Title>
+
+ <p className="sw-mb-2">
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_project.new_code_definition.description')}
+ id="onboarding.create_project.new_code_definition.description"
+ values={{
+ link: (
+ <Link to={getDocUrl('/project-administration/defining-new-code/')}>
+ {translate('onboarding.create_project.new_code_definition.description.link')}
+ </Link>
+ ),
+ }}
+ />
+ </p>
+
+ <NewCodeDefinitionSelector canAdmin={canAdmin} onNcdChanged={selectDefinition} />
+
+ <div className="sw-mt-10 sw-mb-8">
+ <ButtonPrimary
+ onClick={handleProjectCreation}
+ disabled={!selectedDefinition?.isCompliant || submitting}
+ type="submit"
+ >
+ {translate('onboarding.create_project.new_code_definition.create_project')}
+ <DeferredSpinner className="sw-ml-2" loading={submitting} />
+ </ButtonPrimary>
+ </div>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
index 4e9b28484af..59f65f0e004 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { SelectionCard } from 'design-system';
import * as React from 'react';
import { components, OptionProps } from 'react-select';
-import RadioCard from '../../../components/controls/RadioCard';
import Select from '../../../components/controls/Select';
import Tooltip from '../../../components/controls/Tooltip';
import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
@@ -94,8 +94,7 @@ export default function BaselineSettingReferenceBranch(props: BaselineSettingRef
};
return (
- <RadioCard
- noRadio
+ <SelectionCard
className={className}
disabled={disabled}
onClick={() => props.onSelect(NewCodeDefinitionType.ReferenceBranch)}
@@ -132,6 +131,6 @@ export default function BaselineSettingReferenceBranch(props: BaselineSettingRef
</>
)}
</>
- </RadioCard>
+ </SelectionCard>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
index e80de3b008c..5d14760a99b 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
@@ -179,7 +179,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
isBranchSupportEnabled
level="branch"
/>
- <div className="display-flex-row huge-spacer-bottom" role="radiogroup">
+ <div className="display-flex-column huge-spacer-bottom sw-gap-4" role="radiogroup">
<NewCodeDefinitionPreviousVersionOption
isDefault={false}
onSelect={this.handleSelectSetting}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index deb5da7ea24..20756d3f5eb 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -144,7 +144,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
isBranchSupportEnabled={branchesEnabled}
level="project"
/>
- <div className="display-flex-row big-spacer-bottom" role="radiogroup">
+ <div className="display-flex-column big-spacer-bottom sw-gap-4" role="radiogroup">
<NewCodeDefinitionPreviousVersionOption
disabled={!overrideGeneralSetting}
onSelect={props.onSelectSetting}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
index 6a425ddc54c..8a432064a92 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
@@ -312,7 +312,7 @@ function getPageObjects() {
name: /new_code_definition.previous_version.description/,
}),
numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
- numberDaysInput: byRole('textbox'),
+ numberDaysInput: byRole('spinbutton'),
referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
index e541c4e645f..caed26144de 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
@@ -42,7 +42,7 @@ const ui = {
prevVersionRadio: byRole('radio', { name: /new_code_definition.previous_version/ }),
daysNumberRadio: byRole('radio', { name: /new_code_definition.number_days/ }),
daysNumberErrorMessage: byText('new_code_definition.number_days.invalid', { exact: false }),
- daysInput: byRole('textbox'),
+ daysInput: byRole('spinbutton') /* spinbutton is the default role for a number input */,
saveButton: byRole('button', { name: 'save' }),
cancelButton: byRole('button', { name: 'cancel' }),
};
@@ -59,14 +59,16 @@ it('renders and behaves as expected', async () => {
await user.click(ui.daysNumberRadio.get());
expect(ui.daysNumberRadio.get()).toBeChecked();
- // Save should be disabled for zero or NaN
- expect(ui.daysInput.get()).toHaveValue('30');
+ // Save should be disabled for zero
+ expect(ui.daysInput.get()).toHaveValue(30);
await user.clear(ui.daysInput.get());
await user.type(ui.daysInput.get(), '0');
expect(await ui.saveButton.find()).toBeDisabled();
+
+ // Save should not appear at all for NaN
await user.clear(ui.daysInput.get());
await user.type(ui.daysInput.get(), 'asdas');
- expect(ui.saveButton.get()).toBeDisabled();
+ expect(ui.saveButton.query()).toBeDisabled();
// Save enabled for valid days number
await user.clear(ui.daysInput.get());
@@ -98,7 +100,7 @@ it('renders and behaves properly when the current value is not compliant', async
expect(await ui.newCodeTitle.find()).toBeInTheDocument();
expect(ui.daysNumberRadio.get()).toBeChecked();
- expect(ui.daysInput.get()).toHaveValue('91');
+ expect(ui.daysInput.get()).toHaveValue(91);
// Should warn about non compliant value
expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument();
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
index b8d3ad3898c..133e3e0b711 100644
--- a/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
+++ b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
@@ -17,12 +17,11 @@
* 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';
-import Link from '../common/Link';
-import { Alert } from '../ui/Alert';
interface Props {
globalNcd: NewCodeDefinition;
@@ -54,7 +53,7 @@ export default function GlobalNewCodeDefinitionDescription({
return (
<>
<div className="sw-flex sw-flex-col sw-gap-2 sw-max-w-[800px]">
- <span className="sw-font-bold flex-0">{setting}</span>
+ <strong className="sw-font-bold">{setting}</strong>
{isGlobalNcdCompliant && (
<>
<span>{description}</span>
@@ -63,32 +62,34 @@ export default function GlobalNewCodeDefinitionDescription({
)}
</div>
{!isGlobalNcdCompliant && (
- <Alert variant="warning" className="sw-mt-4 sw-max-w-[800px]">
- <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>
- </Alert>
+ <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>
)}
</>
);
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
index a3b52e9f642..126ae94e93c 100644
--- a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
+++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagErrorIcon, InputField, Note, SelectionCard } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../helpers/l10n';
import {
@@ -24,9 +25,6 @@ import {
NUMBER_OF_DAYS_MIN_VALUE,
} from '../../helpers/new-code-definition';
import { NewCodeDefinitionType } from '../../types/new-code-definition';
-import RadioCard from '../controls/RadioCard';
-import ValidationInput, { ValidationInputErrorPlacement } from '../controls/ValidationInput';
-import MandatoryFieldsExplanation from '../ui/MandatoryFieldsExplanation';
export interface Props {
className?: string;
@@ -43,8 +41,7 @@ export default function NewCodeDefinitionDaysOption(props: Props) {
const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props;
return (
- <RadioCard
- noRadio
+ <SelectionCard
className={className}
disabled={disabled}
onClick={() => onSelect(NewCodeDefinitionType.NumberOfDays)}
@@ -53,36 +50,36 @@ export default function NewCodeDefinitionDaysOption(props: Props) {
>
<>
<div>
- <p className="sw-mb-3">{translate('new_code_definition.number_days.description')}</p>
- <p className="sw-mb-4">{translate('new_code_definition.number_days.usecase')}</p>
+ <p className="sw-mb-2">{translate('new_code_definition.number_days.description')}</p>
+ <p>{translate('new_code_definition.number_days.usecase')}</p>
</div>
{selected && (
- <>
- <MandatoryFieldsExplanation />
-
- <ValidationInput
- labelHtmlFor="baseline_number_of_days"
- isInvalid={!isValid}
- isValid={isChanged && isValid}
- errorPlacement={ValidationInputErrorPlacement.Bottom}
- error={translateWithParameters(
+ <div className="sw-mt-4">
+ <label>
+ {translate('new_code_definition.number_days.specify_days')}
+ <div className="sw-my-2 sw-flex sw-items-center">
+ <InputField
+ id="baseline_number_of_days"
+ type="number"
+ required
+ isInvalid={!isValid}
+ isValid={isChanged && isValid}
+ onChange={(e) => onChangeDays(e.currentTarget.value)}
+ value={days}
+ />
+ {!isValid && <FlagErrorIcon className="sw-ml-2" />}
+ </div>
+ </label>
+ <Note>
+ {translateWithParameters(
'new_code_definition.number_days.invalid',
NUMBER_OF_DAYS_MIN_VALUE,
NUMBER_OF_DAYS_MAX_VALUE
)}
- label={translate('new_code_definition.number_days.specify_days')}
- required
- >
- <input
- id="baseline_number_of_days"
- onChange={(e) => onChangeDays(e.currentTarget.value)}
- type="text"
- value={days}
- />
- </ValidationInput>
- </>
+ </Note>
+ </div>
)}
</>
- </RadioCard>
+ </SelectionCard>
);
}
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx
index 60141f592e7..28aed689648 100644
--- a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx
+++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx
@@ -17,10 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { SelectionCard } from 'design-system';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { NewCodeDefinitionType } from '../../types/new-code-definition';
-import RadioCard from '../controls/RadioCard';
interface Props {
disabled?: boolean;
@@ -36,8 +36,7 @@ export default function NewCodeDefinitionPreviousVersionOption({
selected,
}: Props) {
return (
- <RadioCard
- noRadio
+ <SelectionCard
disabled={disabled}
onClick={() => onSelect(NewCodeDefinitionType.PreviousVersion)}
selected={selected}
@@ -47,9 +46,9 @@ export default function NewCodeDefinitionPreviousVersionOption({
}
>
<div>
- <p>{translate('new_code_definition.previous_version.description')}</p>
- <p className="sw-mt-3">{translate('new_code_definition.previous_version.usecase')}</p>
+ <p className="sw-mb-2">{translate('new_code_definition.previous_version.description')}</p>
+ <p>{translate('new_code_definition.previous_version.usecase')}</p>
</div>
- </RadioCard>
+ </SelectionCard>
);
}
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
index 2f4660713f6..47dd557065b 100644
--- a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
+++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
@@ -17,7 +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 { RadioButton } from 'design-system/lib';
+import styled from '@emotion/styled';
+import {
+ FlagMessage,
+ PageContentFontWrapper,
+ RadioButton,
+ SelectionCard,
+ themeColor,
+} from 'design-system';
import { noop } from 'lodash';
import * as React from 'react';
import { getNewCodePeriod } from '../../api/newCodePeriod';
@@ -31,9 +38,7 @@ import {
NewCodeDefinitionType,
NewCodeDefinitiondWithCompliance,
} from '../../types/new-code-definition';
-import RadioCard from '../controls/RadioCard';
import Tooltip from '../controls/Tooltip';
-import { Alert } from '../ui/Alert';
import GlobalNewCodeDefinitionDescription from './GlobalNewCodeDefinitionDescription';
import NewCodeDefinitionDaysOption from './NewCodeDefinitionDaysOption';
import NewCodeDefinitionPreviousVersionOption from './NewCodeDefinitionPreviousVersionOption';
@@ -99,15 +104,16 @@ export default function NewCodeDefinitionSelector(props: Props) {
}, [selectedNcdType, days, isCompliant, onNcdChanged]);
return (
- <>
+ <PageContentFontWrapper>
<p className="sw-mt-10">
- <strong>{translate('new_code_definition.question')}</strong>
+ <strong className="sw-body-md-highlight">
+ {translate('new_code_definition.question')}
+ </strong>
</p>
- <div className="big-spacer-top spacer-bottom" role="radiogroup">
+ <div className="sw-mt-7 sw-ml-1" role="radiogroup">
<RadioButton
aria-label={translate('new_code_definition.global_setting')}
checked={selectedNcdType === NewCodeDefinitionType.Inherited}
- className="big-spacer-bottom"
disabled={!isGlobalNcdCompliant}
onCheck={() => handleNcdChanged(NewCodeDefinitionType.Inherited)}
value="general"
@@ -119,11 +125,16 @@ export default function NewCodeDefinitionSelector(props: Props) {
: translate('new_code_definition.compliance.warning.title.global')
}
>
- <span>{translate('new_code_definition.global_setting')}</span>
+ <span className="sw-font-semibold">
+ {translate('new_code_definition.global_setting')}
+ </span>
</Tooltip>
</RadioButton>
- <div className="sw-ml-4">
+ <StyledGlobalSettingWrapper
+ className="sw-mt-4 sw-ml-6"
+ selected={selectedNcdType === NewCodeDefinitionType.Inherited}
+ >
{globalNcd && (
<GlobalNewCodeDefinitionDescription
globalNcd={globalNcd}
@@ -131,12 +142,12 @@ export default function NewCodeDefinitionSelector(props: Props) {
canAdmin={canAdmin}
/>
)}
- </div>
+ </StyledGlobalSettingWrapper>
<RadioButton
aria-label={translate('new_code_definition.specific_setting')}
checked={Boolean(selectedNcdType && selectedNcdType !== NewCodeDefinitionType.Inherited)}
- className="huge-spacer-top"
+ className="sw-mt-12 sw-font-semibold"
onCheck={() => handleNcdChanged(NewCodeDefinitionType.PreviousVersion)}
value="specific"
>
@@ -144,51 +155,52 @@ export default function NewCodeDefinitionSelector(props: Props) {
</RadioButton>
</div>
- <div className="big-spacer-left big-spacer-right project-baseline-setting">
- <div className="display-flex-row big-spacer-bottom" role="radiogroup">
- <NewCodeDefinitionPreviousVersionOption
- disabled={Boolean(
- !selectedNcdType || selectedNcdType === NewCodeDefinitionType.Inherited
- )}
- onSelect={handleNcdChanged}
- selected={selectedNcdType === NewCodeDefinitionType.PreviousVersion}
- />
-
- <NewCodeDefinitionDaysOption
- days={days}
- disabled={Boolean(
- !selectedNcdType || selectedNcdType === NewCodeDefinitionType.Inherited
- )}
- isChanged={isChanged}
- isValid={isCompliant}
- onChangeDays={setDays}
- onSelect={handleNcdChanged}
- selected={selectedNcdType === NewCodeDefinitionType.NumberOfDays}
- />
-
- <RadioCard
- noRadio
- disabled={Boolean(
- !selectedNcdType || selectedNcdType === NewCodeDefinitionType.Inherited
+ <div className="sw-flex sw-flex-col sw-my-4 sw-mr-4 sw-gap-4" role="radiogroup">
+ <NewCodeDefinitionPreviousVersionOption
+ disabled={Boolean(
+ !selectedNcdType || selectedNcdType === NewCodeDefinitionType.Inherited
+ )}
+ onSelect={handleNcdChanged}
+ selected={selectedNcdType === NewCodeDefinitionType.PreviousVersion}
+ />
+
+ <NewCodeDefinitionDaysOption
+ days={days}
+ disabled={Boolean(
+ !selectedNcdType || selectedNcdType === NewCodeDefinitionType.Inherited
+ )}
+ isChanged={isChanged}
+ isValid={isCompliant}
+ onChangeDays={setDays}
+ onSelect={handleNcdChanged}
+ selected={selectedNcdType === NewCodeDefinitionType.NumberOfDays}
+ />
+
+ <SelectionCard
+ disabled={Boolean(
+ !selectedNcdType || selectedNcdType === NewCodeDefinitionType.Inherited
+ )}
+ onClick={() => handleNcdChanged(NewCodeDefinitionType.ReferenceBranch)}
+ selected={selectedNcdType === NewCodeDefinitionType.ReferenceBranch}
+ title={translate('new_code_definition.reference_branch')}
+ >
+ <div>
+ <p className="sw-mb-2">
+ {translate('new_code_definition.reference_branch.description')}
+ </p>
+ <p>{translate('new_code_definition.reference_branch.usecase')}</p>
+ {selectedNcdType === NewCodeDefinitionType.ReferenceBranch && (
+ <FlagMessage className="sw-mt-4" variant="info">
+ {translate('new_code_definition.reference_branch.notice')}
+ </FlagMessage>
)}
- onClick={() => handleNcdChanged(NewCodeDefinitionType.ReferenceBranch)}
- selected={selectedNcdType === NewCodeDefinitionType.ReferenceBranch}
- title={translate('new_code_definition.reference_branch')}
- >
- <div>
- <p className="sw-mb-3">
- {translate('new_code_definition.reference_branch.description')}
- </p>
- <p className="sw-mb-4">{translate('new_code_definition.reference_branch.usecase')}</p>
- {selectedNcdType === NewCodeDefinitionType.ReferenceBranch && (
- <Alert variant="info">
- {translate('new_code_definition.reference_branch.notice')}
- </Alert>
- )}
- </div>
- </RadioCard>
- </div>
+ </div>
+ </SelectionCard>
</div>
- </>
+ </PageContentFontWrapper>
);
}
+
+const StyledGlobalSettingWrapper = styled.div<{ selected: boolean }>`
+ color: ${({ selected }) => (selected ? 'inherit' : themeColor('selectionCardDisabledText'))};
+`;