From ef563f2a2e98629a8be54164e43095d672762c3c Mon Sep 17 00:00:00 2001 From: Guillaume Peoc'h Date: Thu, 27 Oct 2022 16:57:58 +0200 Subject: [PATCH] SONAR-17527 Add main branch name during manual project creation --- .../sonar-web/src/main/js/api/components.ts | 1 + .../create/project/ManualProjectCreate.tsx | 96 ++++- .../__tests__/ManualProjectCreate-test.tsx | 25 +- .../projectsManagement/CreateProjectForm.tsx | 42 ++- .../__tests__/CreateProjectForm-test.tsx | 88 +++-- .../CreateProjectForm-test.tsx.snap | 343 ------------------ .../sonar-web/src/main/js/types/settings.ts | 3 +- .../resources/org/sonar/l10n/core.properties | 5 + 8 files changed, 204 insertions(+), 399 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index ea9c6f2621e..d05d132b1f7 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -86,6 +86,7 @@ export function deletePortfolio(portfolio: string): Promise { export function createProject(data: { name: string; project: string; + mainBranch: string; visibility?: Visibility; }): Promise<{ project: ProjectBase }> { return postJSON('/api/projects/create', data).catch(throwGlobalError); diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx index 696be7893d2..480c410f267 100644 --- a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx @@ -18,9 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; -import { debounce } from 'lodash'; +import { debounce, isEmpty } from 'lodash'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { createProject, doesComponentExists } from '../../../api/components'; +import { getValue } from '../../../api/settings'; +import DocLink from '../../../components/common/DocLink'; import ProjectKeyInput from '../../../components/common/ProjectKeyInput'; import { SubmitButton } from '../../../components/controls/buttons'; import ValidationInput from '../../../components/controls/ValidationInput'; @@ -30,6 +33,7 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx import { translate } from '../../../helpers/l10n'; import { PROJECT_KEY_INVALID_CHARACTERS, validateProjectKey } from '../../../helpers/projects'; import { ProjectKeyValidationResult } from '../../../types/component'; +import { GlobalSettingKeys } from '../../../types/settings'; import { PROJECT_NAME_MAX_LEN } from './constants'; import CreateProjectPageHeader from './CreateProjectPageHeader'; import './ManualProjectCreate.css'; @@ -47,6 +51,9 @@ interface State { projectKeyError?: string; projectKeyTouched: boolean; validatingProjectKey: boolean; + mainBranchName: string; + mainBranchNameError?: string; + mainBranchNameTouched: boolean; submitting: boolean; } @@ -63,6 +70,8 @@ export default class ManualProjectCreate extends React.PureComponent { + const mainBranchName = await getValue({ key: GlobalSettingKeys.MainBranchName }); + + if (this.mounted && mainBranchName.value !== undefined) { + this.setState({ mainBranchName: mainBranchName.value }); + } + }; + checkFreeKey = (key: string) => { this.setState({ validatingProjectKey: true }); @@ -98,23 +116,25 @@ export default class ManualProjectCreate extends React.PureComponent 0 && - projectName.length > 0 + !isEmpty(projectKey) && + !isEmpty(projectName) && + !isEmpty(mainBranchName) ); } handleFormSubmit = (event: React.FormEvent) => { event.preventDefault(); - const { state } = this; - if (this.canSubmit(state)) { + const { projectKey, projectName, mainBranchName } = this.state; + if (this.canSubmit(this.state)) { this.setState({ submitting: true }); createProject({ - project: state.projectKey, - name: (state.projectName || state.projectKey).trim() + project: projectKey, + name: (projectName || projectKey).trim(), + mainBranch: mainBranchName }).then( ({ project }) => this.props.onProjectCreate(project.key), () => { @@ -158,6 +178,14 @@ export default class ManualProjectCreate extends React.PureComponent { + this.setState({ + mainBranchName, + mainBranchNameError: this.validateMainBranchName(mainBranchName), + mainBranchNameTouched: fromUI + }); + }; + validateKey = (projectKey: string) => { const result = validateProjectKey(projectKey); return result === ProjectKeyValidationResult.Valid @@ -166,12 +194,19 @@ export default class ManualProjectCreate extends React.PureComponent { - if (projectName.length === 0) { + if (isEmpty(projectName)) { return translate('onboarding.create_project.display_name.error.empty'); } return undefined; }; + validateMainBranchName = (mainBranchName: string) => { + if (isEmpty(mainBranchName)) { + return translate('onboarding.create_project.main_branch_name.error.empty'); + } + return undefined; + }; + render() { const { projectKey, @@ -181,13 +216,18 @@ export default class ManualProjectCreate extends React.PureComponent @@ -230,6 +270,42 @@ export default class ManualProjectCreate extends React.PureComponent + + {translate('learn_more')} + + ) + }} + /> + } + error={mainBranchNameError} + id="main-branch-name" + isInvalid={mainBranchNameIsInvalid} + isValid={mainBranchNameIsValid} + label={translate('onboarding.create_project.main_branch_name')} + required={true}> + this.handleBranchNameChange(e.currentTarget.value, true)} + type="text" + value={mainBranchName} + /> + + {translate('set_up')} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx index 933d45fa607..de813558a76 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx @@ -31,6 +31,10 @@ jest.mock('../../../../api/components', () => ({ .mockImplementation(({ component }) => Promise.resolve(component === 'exists')) })); +jest.mock('../../../../api/settings', () => ({ + getValue: jest.fn().mockResolvedValue({ value: 'main' }) +})); + beforeEach(() => { jest.clearAllMocks(); }); @@ -70,12 +74,11 @@ it('should validate form input', async () => { ).toHaveValue('This-is-not-a-key-'); // Clear name - await user.click( - await screen.findByRole('textbox', { + await user.clear( + screen.getByRole('textbox', { name: 'onboarding.create_project.display_name field_required' }) ); - await user.keyboard('{Control>}a{/Control}{Backspace}'); expect( screen.getByRole('textbox', { name: 'onboarding.create_project.project_key field_required' }) ).toHaveValue(''); @@ -117,6 +120,16 @@ it('should validate form input', async () => { expect( await screen.findByText('onboarding.create_project.project_key.taken') ).toBeInTheDocument(); + + // Invalid main branch name + await user.clear( + screen.getByRole('textbox', { + name: 'onboarding.create_project.main_branch_name field_required' + }) + ); + expect( + await screen.findByText('onboarding.create_project.main_branch_name.error.empty') + ).toBeInTheDocument(); }); it('should submit form input', async () => { @@ -132,7 +145,11 @@ it('should submit form input', async () => { ); await user.keyboard('test'); await user.click(screen.getByRole('button', { name: 'set_up' })); - expect(createProject).toHaveBeenCalledWith({ name: 'test', project: 'test' }); + expect(createProject).toHaveBeenCalledWith({ + name: 'test', + project: 'test', + mainBranch: 'main' + }); expect(onProjectCreate).toHaveBeenCalled(); }); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx index c25cdf0455e..2471f9e93ea 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { createProject } from '../../api/components'; +import { getValue } from '../../api/settings'; import Link from '../../components/common/Link'; import VisibilitySelector from '../../components/common/VisibilitySelector'; import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons'; @@ -29,6 +30,7 @@ import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../helpers/l10n'; import { getProjectUrl } from '../../helpers/urls'; +import { GlobalSettingKeys } from '../../types/settings'; import { Visibility } from '../../types/types'; interface Props { @@ -45,6 +47,7 @@ interface State { visibility?: Visibility; // add index declaration to be able to do `this.setState({ [name]: value });` [x: string]: any; + mainBranchName: string; } export default class CreateProjectForm extends React.PureComponent { @@ -57,12 +60,14 @@ export default class CreateProjectForm extends React.PureComponent key: '', loading: false, name: '', - visibility: props.defaultProjectVisibility + visibility: props.defaultProjectVisibility, + mainBranchName: 'main' }; } componentDidMount() { this.mounted = true; + this.fetchMainBranchName(); } componentDidUpdate() { @@ -78,6 +83,14 @@ export default class CreateProjectForm extends React.PureComponent this.mounted = false; } + fetchMainBranchName = async () => { + const mainBranchName = await getValue({ key: GlobalSettingKeys.MainBranchName }); + + if (this.mounted && mainBranchName.value !== undefined) { + this.setState({ mainBranchName: mainBranchName.value }); + } + }; + handleInputChange = (event: React.SyntheticEvent) => { const { name, value } = event.currentTarget; this.setState({ [name]: value }); @@ -89,11 +102,13 @@ export default class CreateProjectForm extends React.PureComponent handleFormSubmit = (event: React.SyntheticEvent) => { event.preventDefault(); + const { name, key, mainBranchName, visibility } = this.state; const data = { - name: this.state.name, - project: this.state.key, - visibility: this.state.visibility + name, + project: key, + mainBranch: mainBranchName, + visibility }; this.setState({ loading: true }); @@ -159,7 +174,7 @@ export default class CreateProjectForm extends React.PureComponent
value={this.state.key} />
+
+ + +
({ - createProject: jest.fn(({ name }: { name: string }) => - Promise.resolve({ project: { key: name, name } }) - ) -})); -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { change, submit, waitAndUpdate } from '../../../helpers/testUtils'; +import { createProject } from '../../../api/components'; import CreateProjectForm from '../CreateProjectForm'; -const createProject = require('../../../api/components').createProject as jest.Mock; +jest.mock('../../../api/components', () => ({ + createProject: jest.fn().mockResolvedValue({}), + doesComponentExists: jest + .fn() + .mockImplementation(({ component }) => Promise.resolve(component === 'exists')) +})); + +jest.mock('../../../api/settings', () => ({ + getValue: jest.fn().mockResolvedValue({ value: 'main' }) +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('should render all inputs and create a project', async () => { + const user = userEvent.setup(); + renderCreateProjectForm(); -it('creates project', async () => { - const wrapper = shallow( - + await user.type( + screen.getByRole('textbox', { + name: 'onboarding.create_project.display_name field_required' + }), + 'ProjectName' ); - (wrapper.instance() as CreateProjectForm).mounted = true; - expect(wrapper).toMatchSnapshot(); - change(wrapper.find('input[name="name"]'), 'name', { - currentTarget: { name: 'name', value: 'name' } - }); - change(wrapper.find('input[name="key"]'), 'key', { - currentTarget: { name: 'key', value: 'key' } - }); - wrapper.find('VisibilitySelector').prop('onChange')('private'); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); + await user.type( + screen.getByRole('textbox', { + name: 'onboarding.create_project.project_key field_required' + }), + 'ProjectKey' + ); + + expect( + screen.getByRole('textbox', { + name: 'onboarding.create_project.main_branch_name field_required' + }) + ).toHaveValue('main'); - submit(wrapper.find('form')); + await user.type( + screen.getByRole('textbox', { + name: 'onboarding.create_project.main_branch_name field_required' + }), + '{Control>}a{/Control}{Backspace}ProjectMainBranch' + ); + + await user.click(screen.getByRole('button', { name: 'create' })); expect(createProject).toHaveBeenCalledWith({ - name: 'name', - project: 'key', - visibility: 'private' + name: 'ProjectName', + project: 'ProjectKey', + mainBranch: 'ProjectMainBranch' }); - expect(wrapper).toMatchSnapshot(); - - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); }); + +function renderCreateProjectForm(props: Partial = {}) { + render(); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap deleted file mode 100644 index 1a4fc2524bd..00000000000 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap +++ /dev/null @@ -1,343 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`creates project 1`] = ` - -
-
-

- qualifiers.create.TRK -

-
-
- -
- - -
-
- - -
-
- - -
-
-
- - create - - - cancel - -
-
-
-`; - -exports[`creates project 2`] = ` - -
-
-

- qualifiers.create.TRK -

-
-
- -
- - -
-
- - -
-
- - -
-
-
- - create - - - cancel - -
-
-
-`; - -exports[`creates project 3`] = ` - -
-
-

- qualifiers.create.TRK -

-
-
- -
- - -
-
- - -
-
- - -
-
-
- - - create - - - cancel - -
- -
-`; - -exports[`creates project 4`] = ` - -
-
-

- qualifiers.create.TRK -

-
-
- - - name - , - } - } - /> - -
- -
-
-`; diff --git a/server/sonar-web/src/main/js/types/settings.ts b/server/sonar-web/src/main/js/types/settings.ts index 32cfbc2004a..d1de09e3f7f 100644 --- a/server/sonar-web/src/main/js/types/settings.ts +++ b/server/sonar-web/src/main/js/types/settings.ts @@ -38,7 +38,8 @@ export enum GlobalSettingKeys { DeveloperAggregatedInfoDisabled = 'sonar.developerAggregatedInfo.disabled', UpdatecenterActivated = 'sonar.updatecenter.activate', DisplayAnnouncementMessage = 'sonar.announcement.displayMessage', - AnnouncementMessage = 'sonar.announcement.message' + AnnouncementMessage = 'sonar.announcement.message', + MainBranchName = 'sonar.projectCreation.mainBranchName' } export type SettingDefinitionAndValue = { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 763718660ab..2acd0079329 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3499,6 +3499,11 @@ onboarding.create_project.project_key.taken=This project key is already taken. onboarding.create_project.display_name=Project display name onboarding.create_project.display_name.error.empty=The display name is required. onboarding.create_project.display_name.description=Up to 255 characters. Some scanners might override the value you provide. + +onboarding.create_project.main_branch_name=Main branch name +onboarding.create_project.main_branch_name.error.empty=The main branch name is required. +onboarding.create_project.main_branch_name.description=The name of your project’s default branch { learn_more } + onboarding.create_project.pr_decoration.information=Manually created projects won’t benefit from the features associated with DevOps Platforms integration unless you configure them in the project settings. onboarding.create_project.repository_imported=Already set up onboarding.create_project.see_project=See the project -- 2.39.5