import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { createProject, doesComponentExists } from '../../../api/components';
+import ProjectKeyInput from '../../../components/common/ProjectKeyInput';
+import { validateProjectKey } from '../../../helpers/projects';
+import { ProjectKeyValidationResult } from '../../../types/component';
+import { PROJECT_NAME_MAX_LEN } from './constants';
import CreateProjectPageHeader from './CreateProjectPageHeader';
import './ManualProjectCreate.css';
};
validateKey = (projectKey: string) => {
- return projectKey.length > 400 || !/^[\w-.:]*[a-zA-Z]+[\w-.:]*$/.test(projectKey)
- ? translate('onboarding.create_project.project_key.error')
- : undefined;
+ const result = validateProjectKey(projectKey);
+ return result === ProjectKeyValidationResult.Valid
+ ? undefined
+ : translate('onboarding.create_project.project_key.error', result);
};
validateName = (projectName: string) => {
- return projectName.length === 0 || projectName.length > 255
- ? translate('onboarding.create_project.display_name.error')
- : undefined;
+ if (projectName.length === 0) {
+ return translate('onboarding.create_project.display_name.error.empty');
+ } else if (projectName.length > PROJECT_NAME_MAX_LEN) {
+ return translate('onboarding.create_project.display_name.error.too_long');
+ }
+ return undefined;
};
render() {
validating
} = this.state;
const { branchesEnabled } = this.props;
- const projectKeyIsInvalid = touched && projectKeyError !== undefined;
- const projectKeyIsValid = touched && !validating && projectKeyError === undefined;
const projectNameIsInvalid = touched && projectNameError !== undefined;
const projectNameIsValid = touched && projectNameError === undefined;
<div className="create-project-manual">
<div className="flex-1 huge-spacer-right">
<form className="manual-project-create" onSubmit={this.handleFormSubmit}>
- <ValidationInput
- className="form-field"
- description={translate('onboarding.create_project.project_key.description')}
+ <ProjectKeyInput
error={projectKeyError}
help={translate('onboarding.create_project.project_key.help')}
- id="project-key"
- isInvalid={projectKeyIsInvalid}
- isValid={projectKeyIsValid}
label={translate('onboarding.create_project.project_key')}
- required={true}>
- <input
- autoFocus={true}
- className={classNames('input-super-large', {
- 'is-invalid': projectKeyIsInvalid,
- 'is-valid': projectKeyIsValid
- })}
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={this.handleProjectKeyChange}
- type="text"
- value={projectKey}
- />
- </ValidationInput>
+ onProjectKeyChange={this.handleProjectKeyChange}
+ projectKey={projectKey}
+ touched={touched}
+ validating={validating}
+ />
<ValidationInput
className="form-field"
'is-valid': projectNameIsValid
})}
id="project-name"
- maxLength={255}
+ maxLength={PROJECT_NAME_MAX_LEN}
minLength={1}
onChange={this.handleProjectNameChange}
type="text"
/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
import { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { createProject } from '../../../../api/components';
+import { createProject, doesComponentExists } from '../../../../api/components';
+import ProjectKeyInput from '../../../../components/common/ProjectKeyInput';
+import { validateProjectKey } from '../../../../helpers/projects';
+import { mockEvent } from '../../../../helpers/testMocks';
+import { ProjectKeyValidationResult } from '../../../../types/component';
+import { PROJECT_NAME_MAX_LEN } from '../constants';
import ManualProjectCreate from '../ManualProjectCreate';
jest.mock('../../../../api/components', () => ({
.mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
}));
-jest.mock('../../../../helpers/system', () => ({
- isSonarCloud: jest.fn().mockReturnValue(true)
-}));
+jest.mock('../../../../helpers/projects', () => {
+ const { ProjectKeyValidationResult } = jest.requireActual('../../../../types/component');
+ return { validateProjectKey: jest.fn(() => ProjectKeyValidationResult.Valid) };
+});
beforeEach(() => {
jest.clearAllMocks();
const onProjectCreate = jest.fn();
const wrapper = shallowRender({ onProjectCreate });
- change(wrapper.find('input#project-key'), 'bar');
+ wrapper
+ .find(ProjectKeyInput)
+ .props()
+ .onProjectKeyChange(mockEvent({ currentTarget: { value: 'bar' } }));
change(wrapper.find('input#project-name'), 'Bar');
- expect(wrapper.find('SubmitButton').prop('disabled')).toBe(false);
+ expect(wrapper.find(SubmitButton).props().disabled).toBe(false);
+ expect(validateProjectKey).toBeCalledWith('bar');
+ expect(doesComponentExists).toBeCalledWith({ component: 'bar' });
submit(wrapper.find('form'));
expect(createProject).toBeCalledWith({
expect(onProjectCreate).toBeCalledWith(['bar']);
});
-it('should not display any status when the key is not defined', () => {
- const wrapper = shallowRender();
- const projectKeyInput = wrapper.find('ValidationInput').first();
- expect(projectKeyInput.prop('isInvalid')).toBe(false);
- expect(projectKeyInput.prop('isValid')).toBe(false);
-});
-
it('should not display any status when the name is not defined', () => {
const wrapper = shallowRender();
- const projectKeyInput = wrapper.find('ValidationInput').last();
- expect(projectKeyInput.prop('isInvalid')).toBe(false);
- expect(projectKeyInput.prop('isValid')).toBe(false);
+ const projectNameInput = wrapper.find(ValidationInput);
+ expect(projectNameInput.props().isInvalid).toBe(false);
+ expect(projectNameInput.props().isValid).toBe(false);
});
it('should have an error when the key is invalid', () => {
+ (validateProjectKey as jest.Mock).mockReturnValueOnce(ProjectKeyValidationResult.TooLong);
const wrapper = shallowRender();
- change(wrapper.find('input#project-key'), 'KEy-with#speci@l_char');
- expect(
- wrapper
- .find('ValidationInput')
- .first()
- .prop('isInvalid')
- ).toBe(true);
+ const instance = wrapper.instance();
+ instance.handleProjectKeyChange(mockEvent());
+ expect(wrapper.find(ProjectKeyInput).props().error).toBe(
+ `onboarding.create_project.project_key.error.${ProjectKeyValidationResult.TooLong}`
+ );
});
it('should have an error when the key already exists', async () => {
const wrapper = shallowRender();
- change(wrapper.find('input#project-key'), 'exists');
-
+ wrapper.instance().handleProjectKeyChange(mockEvent({ currentTarget: { value: 'exists' } }));
await waitAndUpdate(wrapper);
- expect(
- wrapper
- .find('ValidationInput')
- .first()
- .prop('isInvalid')
- ).toBe(true);
+ expect(wrapper.state().projectKeyError).toBe('onboarding.create_project.project_key.taken');
});
it('should ignore promise return if value has been changed in the meantime', async () => {
+ (validateProjectKey as jest.Mock)
+ .mockReturnValueOnce(ProjectKeyValidationResult.Valid)
+ .mockReturnValueOnce(ProjectKeyValidationResult.InvalidChar);
const wrapper = shallowRender();
+ const instance = wrapper.instance();
- change(wrapper.find('input#project-key'), 'exists');
- change(wrapper.find('input#project-key'), 'exists%');
+ instance.handleProjectKeyChange(mockEvent({ currentTarget: { value: 'exists' } }));
+ instance.handleProjectKeyChange(mockEvent({ currentTarget: { value: 'exists%' } }));
await waitAndUpdate(wrapper);
- expect(wrapper.state('touched')).toBe(true);
- expect(wrapper.state('projectKeyError')).toBe('onboarding.create_project.project_key.error');
+ expect(wrapper.state().touched).toBe(true);
+ expect(wrapper.state().projectKeyError).toBe(
+ `onboarding.create_project.project_key.error.${ProjectKeyValidationResult.InvalidChar}`
+ );
});
it('should autofill the name based on the key', () => {
const wrapper = shallowRender();
- change(wrapper.find('input#project-key'), 'bar');
- expect(wrapper.find('input#project-name').prop('value')).toBe('bar');
+ wrapper.instance().handleProjectKeyChange(mockEvent({ currentTarget: { value: 'bar' } }));
+ expect(wrapper.find('input#project-name').props().value).toBe('bar');
});
-it('should have an error when the name is empty', () => {
+it('should have an error when the name is incorrect', () => {
const wrapper = shallowRender();
- change(wrapper.find('input#project-key'), 'bar');
- change(wrapper.find('input#project-name'), '');
- expect(
- wrapper
- .find('ValidationInput')
- .last()
- .prop('isInvalid')
- ).toBe(true);
- expect(wrapper.state('projectNameError')).toBe('onboarding.create_project.display_name.error');
+ wrapper.setState({ touched: true });
+ const instance = wrapper.instance();
+
+ instance.handleProjectNameChange(mockEvent({ currentTarget: { value: '' } }));
+ expect(wrapper.find(ValidationInput).props().isInvalid).toBe(true);
+ expect(wrapper.state().projectNameError).toBe(
+ 'onboarding.create_project.display_name.error.empty'
+ );
+
+ instance.handleProjectNameChange(
+ mockEvent({ currentTarget: { value: new Array(PROJECT_NAME_MAX_LEN + 1).fill('a').join('') } })
+ );
+ expect(wrapper.find(ValidationInput).props().isInvalid).toBe(true);
+ expect(wrapper.state().projectNameError).toBe(
+ 'onboarding.create_project.display_name.error.too_long'
+ );
});
function shallowRender(props: Partial<ManualProjectCreate['props']> = {}) {
className="manual-project-create"
onSubmit={[Function]}
>
- <ValidationInput
- className="form-field"
- description="onboarding.create_project.project_key.description"
+ <ProjectKeyInput
help="onboarding.create_project.project_key.help"
- id="project-key"
- isInvalid={false}
- isValid={false}
label="onboarding.create_project.project_key"
- required={true}
- >
- <input
- autoFocus={true}
- className="input-super-large"
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={[Function]}
- type="text"
- value=""
- />
- </ValidationInput>
+ onProjectKeyChange={[Function]}
+ projectKey=""
+ touched={false}
+ validating={false}
+ />
<ValidationInput
className="form-field"
description="onboarding.create_project.display_name.description"
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+
+export const PROJECT_NAME_MAX_LEN = 255;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 * as classNames from 'classnames';
+import * as React from 'react';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { PROJECT_KEY_MAX_LEN } from '../../helpers/constants';
+
+export interface ProjectKeyInputProps {
+ error?: string;
+ help?: string;
+ label?: string;
+ onProjectKeyChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+ placeholder?: string;
+ projectKey?: string;
+ touched: boolean;
+ validating?: boolean;
+}
+
+export default function ProjectKeyInput(props: ProjectKeyInputProps) {
+ const { error, help, label, placeholder, projectKey, touched, validating } = props;
+
+ const isInvalid = touched && error !== undefined;
+ const isValid = touched && !validating && error === undefined;
+
+ return (
+ <ValidationInput
+ className="form-field"
+ description={translate('onboarding.create_project.project_key.description')}
+ error={error}
+ help={help}
+ id="project-key"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={label}
+ required={label !== undefined}>
+ <input
+ autoFocus={true}
+ className={classNames('input-super-large', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="project-key"
+ maxLength={PROJECT_KEY_MAX_LEN}
+ minLength={1}
+ onChange={props.onProjectKeyChange}
+ placeholder={placeholder}
+ type="text"
+ value={projectKey}
+ />
+ </ValidationInput>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
+import ProjectKeyInput, { ProjectKeyInputProps } from '../ProjectKeyInput';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ projectKey: 'foo' })).toMatchSnapshot('with value');
+ expect(
+ shallowRender({ help: 'foo.help', label: 'foo.label', placeholder: 'foo.placeholder' })
+ ).toMatchSnapshot('with label, help, and placeholder');
+ expect(shallowRender({ touched: true })).toMatchSnapshot('valid');
+ expect(shallowRender({ touched: true, error: 'bar.baz' })).toMatchSnapshot('invalid');
+ expect(shallowRender({ touched: true, validating: true })).toMatchSnapshot('validating');
+});
+
+it('should not display any status when the key is not defined', () => {
+ const wrapper = shallowRender();
+ const input = wrapper.find(ValidationInput);
+ expect(input.props().isInvalid).toBe(false);
+ expect(input.props().isValid).toBe(false);
+});
+
+function shallowRender(props: Partial<ProjectKeyInputProps> = {}) {
+ return shallow<ProjectKeyInputProps>(
+ <ProjectKeyInput onProjectKeyChange={jest.fn()} touched={false} {...props} />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<ValidationInput
+ className="form-field"
+ description="onboarding.create_project.project_key.description"
+ id="project-key"
+ isInvalid={false}
+ isValid={false}
+ required={false}
+>
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[MockFunction]}
+ type="text"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly: invalid 1`] = `
+<ValidationInput
+ className="form-field"
+ description="onboarding.create_project.project_key.description"
+ error="bar.baz"
+ id="project-key"
+ isInvalid={true}
+ isValid={false}
+ required={false}
+>
+ <input
+ autoFocus={true}
+ className="input-super-large is-invalid"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[MockFunction]}
+ type="text"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly: valid 1`] = `
+<ValidationInput
+ className="form-field"
+ description="onboarding.create_project.project_key.description"
+ id="project-key"
+ isInvalid={false}
+ isValid={true}
+ required={false}
+>
+ <input
+ autoFocus={true}
+ className="input-super-large is-valid"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[MockFunction]}
+ type="text"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly: validating 1`] = `
+<ValidationInput
+ className="form-field"
+ description="onboarding.create_project.project_key.description"
+ id="project-key"
+ isInvalid={false}
+ isValid={false}
+ required={false}
+>
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[MockFunction]}
+ type="text"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly: with label, help, and placeholder 1`] = `
+<ValidationInput
+ className="form-field"
+ description="onboarding.create_project.project_key.description"
+ help="foo.help"
+ id="project-key"
+ isInvalid={false}
+ isValid={false}
+ label="foo.label"
+ required={true}
+>
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[MockFunction]}
+ placeholder="foo.placeholder"
+ type="text"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly: with value 1`] = `
+<ValidationInput
+ className="form-field"
+ description="onboarding.create_project.project_key.description"
+ id="project-key"
+ isInvalid={false}
+ isValid={false}
+ required={false}
+>
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[MockFunction]}
+ type="text"
+ value="foo"
+ />
+</ValidationInput>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { ProjectKeyValidationResult } from '../../types/component';
+import { PROJECT_KEY_MAX_LEN } from '../constants';
+import { validateProjectKey } from '../projects';
+
+describe('validateProjectKey', () => {
+ it('should correctly flag an invalid key', () => {
+ // Cannot have special characters except whitelist.
+ expect(validateProjectKey('foo/bar')).toBe(ProjectKeyValidationResult.InvalidChar);
+ // Cannot contain only numbers.
+ expect(validateProjectKey('123')).toBe(ProjectKeyValidationResult.OnlyDigits);
+ // Cannot be more than 400 chars long.
+ expect(validateProjectKey(new Array(PROJECT_KEY_MAX_LEN + 1).fill('a').join(''))).toBe(
+ ProjectKeyValidationResult.TooLong
+ );
+ // Cannot be empty.
+ expect(validateProjectKey('')).toBe(ProjectKeyValidationResult.Empty);
+ });
+
+ it('should not flag a valid key', () => {
+ expect(validateProjectKey('foo:bar_baz-12.is')).toBe(ProjectKeyValidationResult.Valid);
+ expect(validateProjectKey('12:34')).toBe(ProjectKeyValidationResult.Valid);
+ });
+});
colors.orange,
colors.red
];
+
+export const PROJECT_KEY_MAX_LEN = 400;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { ProjectKeyValidationResult } from '../types/component';
+import { PROJECT_KEY_MAX_LEN } from './constants';
+
+export function validateProjectKey(projectKey: string): ProjectKeyValidationResult {
+ // This is the regex used on the backend:
+ // [\p{Alnum}\-_.:]*[\p{Alpha}\-_.:]+[\p{Alnum}\-_.:]*
+ // See sonar-core/src/main/java/org/sonar/core/component/ComponentKeys.java
+ const regex = /^[\w\-.:]*[a-z\-_.:]+[\w\-.:]*$/i;
+ if (projectKey.length === 0) {
+ return ProjectKeyValidationResult.Empty;
+ } else if (projectKey.length > PROJECT_KEY_MAX_LEN) {
+ return ProjectKeyValidationResult.TooLong;
+ } else if (regex.test(projectKey)) {
+ return ProjectKeyValidationResult.Valid;
+ } else {
+ return /^[0-9]+$/.test(projectKey)
+ ? ProjectKeyValidationResult.OnlyDigits
+ : ProjectKeyValidationResult.InvalidChar;
+ }
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+export enum Visibility {
+ Public = 'public',
+ Private = 'private'
+}
export enum ComponentQualifier {
Application = 'APP',
TestFile = 'UTS'
}
+export enum ProjectKeyValidationResult {
+ Valid = 'valid',
+ Empty = 'empty',
+ TooLong = 'too_long',
+ InvalidChar = 'invalid_char',
+ OnlyDigits = 'only_digits'
+}
+
export function isPortfolioLike(componentQualifier?: string | ComponentQualifier) {
return Boolean(
componentQualifier &&
].includes(componentQualifier)
);
}
-
-export enum Visibility {
- Public = 'public',
- Private = 'private'
-}
onboarding.create_project.setup_manually=Create manually
onboarding.create_project.project_key=Project key
-onboarding.create_project.project_key.description=Up to 400 characters. All letters, digits, dash, underscore, period or colon.
-onboarding.create_project.project_key.error=The provided value doesn't match the expected format.
+onboarding.create_project.project_key.description=Up to 400 characters. Allowed characters are alphanumeric, '-' (dash), '_' (underscore), '.' (period) and ':' (colon), with at least one non-digit.
+onboarding.create_project.project_key.error.empty=You must provide at least one character.
+onboarding.create_project.project_key.error.too_long=The provided key is too long.
+onboarding.create_project.project_key.error.invalid_char=The provided key contains invalid characters.
+onboarding.create_project.project_key.error.only_digits=The provided key contains only digits.
onboarding.create_project.project_key.help=Your project key is a unique identifier for your project. If you are using Maven, make sure the key matches the "groupId:artifactId" format.
onboarding.create_project.project_key.taken=This project key is already taken.
onboarding.create_project.display_name=Display name
-onboarding.create_project.display_name.error=The display name is required.
+onboarding.create_project.display_name.error.empty=The display name is required.
+onboarding.create_project.display_name.error.too_long=The display name is too long.
onboarding.create_project.display_name.description=Up to 255 characters
onboarding.create_project.display_name.help=Some scanners might override the value you provide.
onboarding.create_project.repository_imported=Already set up