export function createProject(data: {
name: string;
project: string;
+ mainBranch: string;
visibility?: Visibility;
}): Promise<{ project: ProjectBase }> {
return postJSON('/api/projects/create', data).catch(throwGlobalError);
* 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';
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';
projectKeyError?: string;
projectKeyTouched: boolean;
validatingProjectKey: boolean;
+ mainBranchName: string;
+ mainBranchNameError?: string;
+ mainBranchNameTouched: boolean;
submitting: boolean;
}
submitting: false,
projectKeyTouched: false,
projectNameTouched: false,
+ mainBranchName: 'main',
+ mainBranchNameTouched: false,
validatingProjectKey: false
};
this.checkFreeKey = debounce(this.checkFreeKey, 250);
componentDidMount() {
this.mounted = true;
+ this.fetchMainBranchName();
}
componentWillUnmount() {
this.mounted = false;
}
+ fetchMainBranchName = async () => {
+ 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 });
};
canSubmit(state: State): state is ValidState {
- const { projectKey, projectKeyError, projectName, projectNameError } = state;
+ const { projectKey, projectKeyError, projectName, projectNameError, mainBranchName } = state;
return Boolean(
projectKeyError === undefined &&
projectNameError === undefined &&
- projectKey.length > 0 &&
- projectName.length > 0
+ !isEmpty(projectKey) &&
+ !isEmpty(projectName) &&
+ !isEmpty(mainBranchName)
);
}
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
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),
() => {
);
};
+ handleBranchNameChange = (mainBranchName: string, fromUI = false) => {
+ this.setState({
+ mainBranchName,
+ mainBranchNameError: this.validateMainBranchName(mainBranchName),
+ mainBranchNameTouched: fromUI
+ });
+ };
+
validateKey = (projectKey: string) => {
const result = validateProjectKey(projectKey);
return result === ProjectKeyValidationResult.Valid
};
validateName = (projectName: string) => {
- 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,
projectNameError,
projectNameTouched,
validatingProjectKey,
+ mainBranchName,
+ mainBranchNameError,
+ mainBranchNameTouched,
submitting
} = this.state;
const { branchesEnabled } = this.props;
- const touched = !!(projectKeyTouched || projectNameTouched);
+ const touched = Boolean(projectKeyTouched || projectNameTouched);
const projectNameIsInvalid = projectNameTouched && projectNameError !== undefined;
const projectNameIsValid = projectNameTouched && projectNameError === undefined;
+ const mainBranchNameIsValid = mainBranchNameTouched && mainBranchNameError === undefined;
+ const mainBranchNameIsInvalid = mainBranchNameTouched && mainBranchNameError !== undefined;
return (
<>
validating={validatingProjectKey}
/>
+ <ValidationInput
+ className="form-field"
+ description={
+ <FormattedMessage
+ id="onboarding.create_project.main_branch_name.description"
+ defaultMessage={translate(
+ 'onboarding.create_project.main_branch_name.description'
+ )}
+ values={{
+ learn_more: (
+ <DocLink to="/project-administration/project-existence">
+ {translate('learn_more')}
+ </DocLink>
+ )
+ }}
+ />
+ }
+ error={mainBranchNameError}
+ id="main-branch-name"
+ isInvalid={mainBranchNameIsInvalid}
+ isValid={mainBranchNameIsValid}
+ label={translate('onboarding.create_project.main_branch_name')}
+ required={true}>
+ <input
+ id="main-branch-name"
+ className={classNames('input-super-large', {
+ 'is-invalid': mainBranchNameIsInvalid,
+ 'is-valid': mainBranchNameIsValid
+ })}
+ minLength={1}
+ onChange={e => this.handleBranchNameChange(e.currentTarget.value, true)}
+ type="text"
+ value={mainBranchName}
+ />
+ </ValidationInput>
+
<SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
{translate('set_up')}
</SubmitButton>
.mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
}));
+jest.mock('../../../../api/settings', () => ({
+ getValue: jest.fn().mockResolvedValue({ value: 'main' })
+}));
+
beforeEach(() => {
jest.clearAllMocks();
});
).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('');
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 () => {
);
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();
});
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';
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 {
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<Props, State> {
key: '',
loading: false,
name: '',
- visibility: props.defaultProjectVisibility
+ visibility: props.defaultProjectVisibility,
+ mainBranchName: 'main'
};
}
componentDidMount() {
this.mounted = true;
+ this.fetchMainBranchName();
}
componentDidUpdate() {
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<HTMLInputElement>) => {
const { name, value } = event.currentTarget;
this.setState({ [name]: value });
handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
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 });
<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="create-project-name">
- {translate('name')}
+ {translate('onboarding.create_project.display_name')}
<MandatoryFieldMarker />
</label>
<input
</div>
<div className="modal-field">
<label htmlFor="create-project-key">
- {translate('key')}
+ {translate('onboarding.create_project.project_key')}
<MandatoryFieldMarker />
</label>
<input
value={this.state.key}
/>
</div>
+ <div className="modal-field">
+ <label htmlFor="create-project-main-branch-name">
+ {translate('onboarding.create_project.main_branch_name')}
+ <MandatoryFieldMarker />
+ </label>
+ <input
+ id="create-project-main-branch-name"
+ maxLength={400}
+ name="mainBranchName"
+ onChange={this.handleInputChange}
+ required={true}
+ type="text"
+ value={this.state.mainBranchName}
+ />
+ </div>
<div className="modal-field">
<label>{translate('visibility')}</label>
<VisibilitySelector
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-/* eslint-disable import/first */
-jest.mock('../../../api/components', () => ({
- 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<any>;
+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(
- <CreateProjectForm
- defaultProjectVisibility="public"
- onClose={jest.fn()}
- onProjectCreated={jest.fn()}
- />
+ 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<Function>('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<CreateProjectForm['props']> = {}) {
+ render(<CreateProjectForm onClose={jest.fn()} onProjectCreated={jest.fn()} {...props} />);
+}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`creates project 1`] = `
-<Modal
- contentLabel="modal form"
- onRequestClose={[MockFunction]}
->
- <form
- id="create-project-form"
- onSubmit={[Function]}
- >
- <header
- className="modal-head"
- >
- <h2>
- qualifiers.create.TRK
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <MandatoryFieldsExplanation
- className="modal-field"
- />
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-project-name"
- >
- name
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus={true}
- id="create-project-name"
- maxLength={2000}
- name="name"
- onChange={[Function]}
- required={true}
- type="text"
- value=""
- />
- </div>
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-project-key"
- >
- key
- <MandatoryFieldMarker />
- </label>
- <input
- id="create-project-key"
- maxLength={400}
- name="key"
- onChange={[Function]}
- required={true}
- type="text"
- value=""
- />
- </div>
- <div
- className="modal-field"
- >
- <label>
- visibility
- </label>
- <VisibilitySelector
- canTurnToPrivate={true}
- className="little-spacer-top"
- onChange={[Function]}
- visibility="public"
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <SubmitButton
- disabled={false}
- id="create-project-submit"
- >
- create
- </SubmitButton>
- <ResetButtonLink
- id="create-project-cancel"
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`creates project 2`] = `
-<Modal
- contentLabel="modal form"
- onRequestClose={[MockFunction]}
->
- <form
- id="create-project-form"
- onSubmit={[Function]}
- >
- <header
- className="modal-head"
- >
- <h2>
- qualifiers.create.TRK
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <MandatoryFieldsExplanation
- className="modal-field"
- />
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-project-name"
- >
- name
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus={true}
- id="create-project-name"
- maxLength={2000}
- name="name"
- onChange={[Function]}
- required={true}
- type="text"
- value="name"
- />
- </div>
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-project-key"
- >
- key
- <MandatoryFieldMarker />
- </label>
- <input
- id="create-project-key"
- maxLength={400}
- name="key"
- onChange={[Function]}
- required={true}
- type="text"
- value="key"
- />
- </div>
- <div
- className="modal-field"
- >
- <label>
- visibility
- </label>
- <VisibilitySelector
- canTurnToPrivate={true}
- className="little-spacer-top"
- onChange={[Function]}
- visibility="private"
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <SubmitButton
- disabled={false}
- id="create-project-submit"
- >
- create
- </SubmitButton>
- <ResetButtonLink
- id="create-project-cancel"
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`creates project 3`] = `
-<Modal
- contentLabel="modal form"
- onRequestClose={[MockFunction]}
->
- <form
- id="create-project-form"
- onSubmit={[Function]}
- >
- <header
- className="modal-head"
- >
- <h2>
- qualifiers.create.TRK
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <MandatoryFieldsExplanation
- className="modal-field"
- />
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-project-name"
- >
- name
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus={true}
- id="create-project-name"
- maxLength={2000}
- name="name"
- onChange={[Function]}
- required={true}
- type="text"
- value="name"
- />
- </div>
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-project-key"
- >
- key
- <MandatoryFieldMarker />
- </label>
- <input
- id="create-project-key"
- maxLength={400}
- name="key"
- onChange={[Function]}
- required={true}
- type="text"
- value="key"
- />
- </div>
- <div
- className="modal-field"
- >
- <label>
- visibility
- </label>
- <VisibilitySelector
- canTurnToPrivate={true}
- className="little-spacer-top"
- onChange={[Function]}
- visibility="private"
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <i
- className="spinner spacer-right"
- />
- <SubmitButton
- disabled={true}
- id="create-project-submit"
- >
- create
- </SubmitButton>
- <ResetButtonLink
- id="create-project-cancel"
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`creates project 4`] = `
-<Modal
- contentLabel="modal form"
- onRequestClose={[MockFunction]}
->
- <div>
- <header
- className="modal-head"
- >
- <h2>
- qualifiers.create.TRK
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <Alert
- variant="success"
- >
- <FormattedMessage
- defaultMessage="projects_management.project_has_been_successfully_created"
- id="projects_management.project_has_been_successfully_created"
- values={
- Object {
- "project": <ForwardRef(Link)
- to={
- Object {
- "pathname": "/dashboard",
- "search": "?id=name",
- }
- }
- >
- name
- </ForwardRef(Link)>,
- }
- }
- />
- </Alert>
- </div>
- <footer
- className="modal-foot"
- >
- <ResetButtonLink
- id="create-project-close"
- innerRef={[Function]}
- onClick={[MockFunction]}
- >
- close
- </ResetButtonLink>
- </footer>
- </div>
-</Modal>
-`;
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 = {
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