diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2023-08-07 17:34:23 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-08-14 20:02:57 +0000 |
commit | 3f570b118fa9a68469a34bb98f25367050f21025 (patch) | |
tree | e4cba8c1d56a44f4a08705bd37f77d36d40cd355 /server | |
parent | 08ef818d25713fcbd1e5c87f30521324b8f9e74c (diff) | |
download | sonarqube-3f570b118fa9a68469a34bb98f25367050f21025.tar.gz sonarqube-3f570b118fa9a68469a34bb98f25367050f21025.zip |
SONAR-20086 Migrate new code period setup page
Diffstat (limited to 'server')
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'))}; +`; |