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/sonar-web/src/main/js/apps | |
parent | 08ef818d25713fcbd1e5c87f30521324b8f9e74c (diff) | |
download | sonarqube-3f570b118fa9a68469a34bb98f25367050f21025.tar.gz sonarqube-3f570b118fa9a68469a34bb98f25367050f21025.zip |
SONAR-20086 Migrate new code period setup page
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
8 files changed, 140 insertions, 127 deletions
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(); |