aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2019-08-27 12:11:41 +0200
committerSonarTech <sonartech@sonarsource.com>2019-09-10 20:21:02 +0200
commit2fba950fd31175b42a3b097ff58ddda9c76fddfa (patch)
tree8df1398f436efba2af3352efab33cb81a255237b
parentf1ba685b86d6435ae21960ee6962b83c447dd3da (diff)
downloadsonarqube-2fba950fd31175b42a3b097ff58ddda9c76fddfa.tar.gz
sonarqube-2fba950fd31175b42a3b097ff58ddda9c76fddfa.zip
SONAR-12360 Improve project creation validation
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx129
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx191
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx91
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap82
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
8 files changed, 269 insertions, 339 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx
deleted file mode 100644
index 0fd857b8b2d..00000000000
--- a/server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { debounce } from 'lodash';
-import * as React from 'react';
-import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { doesComponentExists } from '../../../api/components';
-
-interface Props {
- className?: string;
- value: string;
- onChange: (value: string | undefined) => void;
-}
-
-interface State {
- error?: string;
- touched: boolean;
- validating: boolean;
-}
-
-export default class ProjectKeyInput extends React.PureComponent<Props, State> {
- mounted = false;
- constructor(props: Props) {
- super(props);
- this.state = { error: undefined, touched: false, validating: false };
- this.checkFreeKey = debounce(this.checkFreeKey, 250);
- }
-
- componentDidMount() {
- this.mounted = true;
- if (this.props.value) {
- this.validateKey(this.props.value);
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- checkFreeKey = (key: string) => {
- this.setState({ validating: true });
- return doesComponentExists({ component: key })
- .then(alreadyExist => {
- if (this.mounted && key === this.props.value) {
- if (!alreadyExist) {
- this.setState({ error: undefined, validating: false });
- } else {
- this.setState({
- error: translate('onboarding.create_project.project_key.taken'),
- touched: true,
- validating: false
- });
- }
- }
- })
- .catch(() => {
- if (this.mounted && key === this.props.value) {
- this.setState({ error: undefined, validating: false });
- }
- });
- };
-
- handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- const { value } = event.currentTarget;
- this.setState({ touched: true });
- this.validateKey(value);
- this.props.onChange(value);
- };
-
- validateKey(key: string) {
- if (key.length > 400 || !/^[\w-.:]*[a-zA-Z]+[\w-.:]*$/.test(key)) {
- this.setState({
- error: translate('onboarding.create_project.project_key.error'),
- touched: true
- });
- } else {
- this.checkFreeKey(key);
- }
- }
-
- render() {
- const isInvalid = this.state.touched && this.state.error !== undefined;
- const isValid = this.state.touched && !this.state.validating && this.state.error === undefined;
- return (
- <ValidationInput
- className={this.props.className}
- description={translate('onboarding.create_project.project_key.description')}
- error={this.state.error}
- help={translate('onboarding.create_project.project_key.help')}
- id="project-key"
- isInvalid={isInvalid}
- isValid={isValid}
- label={translate('onboarding.create_project.project_key')}
- required={true}>
- <input
- autoFocus={true}
- className={classNames('input-super-large', {
- 'is-invalid': isInvalid,
- 'is-valid': isValid
- })}
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={this.handleChange}
- type="text"
- value={this.props.value}
- />
- </ValidationInput>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx
deleted file mode 100644
index 4f0214d7e9a..00000000000
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import ProjectKeyInput from '../ProjectKeyInput';
-
-jest.useFakeTimers();
-
-jest.mock('../../../../api/components', () => ({
- doesComponentExists: jest
- .fn()
- .mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
-}));
-
-it('should render correctly', async () => {
- const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="key" />);
- expect(wrapper).toMatchSnapshot();
- wrapper.setState({ touched: true });
- await waitAndUpdate(wrapper);
- expect(wrapper.find('ValidationInput').prop('isValid')).toBe(true);
-});
-
-it('should not display any status when the key is not defined', async () => {
- const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="" />);
- await waitAndUpdate(wrapper);
- expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(false);
- expect(wrapper.find('ValidationInput').prop('isValid')).toBe(false);
-});
-
-it('should have an error when the key is invalid', async () => {
- const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="KEy-with#speci@l_char" />);
- await waitAndUpdate(wrapper);
- expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
-});
-
-it('should have an error when the key already exists', async () => {
- const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="exists" />);
- await waitAndUpdate(wrapper);
-
- jest.runAllTimers();
- await new Promise(setImmediate);
- expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
-});
-
-it('should handle Change', async () => {
- const onChange = jest.fn();
- const wrapper = shallow(<ProjectKeyInput onChange={onChange} value="" />);
- await waitAndUpdate(wrapper);
-
- wrapper.find('input').simulate('change', { currentTarget: { value: 'key' } });
-
- expect(wrapper.state('touched')).toBe(true);
- expect(onChange).toBeCalledWith('key');
-});
-
-it('should ignore promise return if value has been changed in the meantime', async () => {
- const onChange = (value: string) => wrapper.setProps({ value });
- const wrapper = shallow(<ProjectKeyInput onChange={onChange} value="" />);
- await waitAndUpdate(wrapper);
-
- wrapper.find('input').simulate('change', { currentTarget: { value: 'exists' } });
- wrapper.find('input').simulate('change', { currentTarget: { value: 'exists%' } });
-
- jest.runAllTimers();
- await new Promise(setImmediate);
-
- expect(wrapper.state('touched')).toBe(true);
- expect(wrapper.state('error')).toBe('onboarding.create_project.project_key.error');
-});
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap
deleted file mode 100644
index 58f34b9f7d4..00000000000
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ValidationInput
- description="onboarding.create_project.project_key.description"
- 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="key"
- />
-</ValidationInput>
-`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css
index 54c67f4766d..5897ef1a9fa 100644
--- a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css
+++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.manual-project-create {
- max-width: 650px;
+ max-width: 700px;
}
.manual-project-create .visibility-select-option {
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 40513c90d50..485da00e2f8 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,15 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as classNames from 'classnames';
+import { debounce } from 'lodash';
import * as React from 'react';
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import { createProject } from '../../../api/components';
+import { createProject, doesComponentExists } from '../../../api/components';
import VisibilitySelector from '../../../components/common/VisibilitySelector';
import { isSonarCloud } from '../../../helpers/system';
-import ProjectKeyInput from '../components/ProjectKeyInput';
import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox';
import './ManualProjectCreate.css';
import OrganizationInput from './OrganizationInput';
@@ -42,10 +42,14 @@ interface Props {
interface State {
projectName: string;
projectNameChanged: boolean;
+ projectNameError?: string;
projectKey: string;
+ projectKeyError?: string;
selectedOrganization?: T.Organization;
selectedVisibility?: T.Visibility;
submitting: boolean;
+ touched: boolean;
+ validating: boolean;
}
type ValidState = State & Required<Pick<State, 'projectKey' | 'projectName'>>;
@@ -60,8 +64,11 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
projectName: '',
projectNameChanged: false,
selectedOrganization: this.getInitialSelectedOrganization(props),
- submitting: false
+ submitting: false,
+ touched: false,
+ validating: false
};
+ this.checkFreeKey = debounce(this.checkFreeKey, 250);
}
componentDidMount() {
@@ -72,13 +79,46 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
this.mounted = false;
}
+ checkFreeKey = (key: string) => {
+ return doesComponentExists({ component: key })
+ .then(alreadyExist => {
+ if (this.mounted && key === this.state.projectKey) {
+ if (!alreadyExist) {
+ this.setState({ projectKeyError: undefined, validating: false });
+ } else {
+ this.setState({
+ projectKeyError: translate('onboarding.create_project.project_key.taken'),
+ touched: true,
+ validating: false
+ });
+ }
+ }
+ })
+ .catch(() => {
+ if (this.mounted && key === this.state.projectKey) {
+ this.setState({ projectKeyError: undefined, validating: false });
+ }
+ });
+ };
+
canChoosePrivate = (selectedOrganization: T.Organization | undefined) => {
return Boolean(selectedOrganization && selectedOrganization.subscription === 'PAID');
};
canSubmit(state: State): state is ValidState {
+ const {
+ projectKey,
+ projectKeyError,
+ projectName,
+ projectNameError,
+ selectedOrganization
+ } = state;
return Boolean(
- state.projectKey && state.projectName && (!isSonarCloud() || state.selectedOrganization)
+ projectKeyError === undefined &&
+ projectNameError === undefined &&
+ projectKey.length > 0 &&
+ projectName.length > 0 &&
+ (!isSonarCloud() || selectedOrganization)
);
}
@@ -151,24 +191,67 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
);
};
- handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- const projectName = event.currentTarget.value;
- this.setState({ projectName, projectNameChanged: true });
+ handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const projectKey = event.currentTarget.value || '';
+ const projectKeyError = this.validateKey(projectKey);
+
+ this.setState(prevState => {
+ const projectName = prevState.projectNameChanged ? prevState.projectName : projectKey;
+ return {
+ projectKey,
+ projectKeyError,
+ projectName,
+ projectNameError: this.validateName(projectName),
+ touched: true,
+ validating: projectKeyError === undefined
+ };
+ });
+
+ if (projectKeyError === undefined) {
+ this.checkFreeKey(projectKey);
+ }
};
- handleProjectKeyChange = (projectKey: string) => {
- this.setState(state => ({
- projectKey,
- projectName: state.projectNameChanged ? state.projectName : projectKey || ''
- }));
+ handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const projectName = event.currentTarget.value;
+ this.setState({
+ projectName,
+ projectNameChanged: true,
+ projectNameError: this.validateName(projectName)
+ });
};
handleVisibilityChange = (selectedVisibility: T.Visibility) => {
this.setState({ selectedVisibility });
};
+ validateKey = (projectKey: string) => {
+ return projectKey.length > 400 || !/^[\w-.:]*[a-zA-Z]+[\w-.:]*$/.test(projectKey)
+ ? translate('onboarding.create_project.project_key.error')
+ : undefined;
+ };
+
+ validateName = (projectName: string) => {
+ return projectName.length === 0 || projectName.length > 255
+ ? translate('onboarding.create_project.display_name.error')
+ : undefined;
+ };
+
render() {
- const { selectedOrganization, submitting } = this.state;
+ const {
+ projectKey,
+ projectKeyError,
+ projectName,
+ projectNameError,
+ selectedOrganization,
+ submitting,
+ touched,
+ validating
+ } = this.state;
+ const projectKeyIsInvalid = touched && projectKeyError !== undefined;
+ const projectKeyIsValid = touched && !validating && projectKeyError === undefined;
+ const projectNameIsInvalid = touched && projectNameError !== undefined;
+ const projectNameIsValid = touched && projectNameError === undefined;
const canChoosePrivate = this.canChoosePrivate(selectedOrganization);
return (
@@ -182,37 +265,56 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
organizations={this.props.userOrganizations}
/>
)}
- <ProjectKeyInput
+
+ <ValidationInput
className="form-field"
- onChange={this.handleProjectKeyChange}
- value={this.state.projectKey}
- />
- <div className="form-field">
- <label htmlFor="project-name">
- <span className="text-middle">
- <strong>{translate('onboarding.create_project.display_name')}</strong>
- <em className="mandatory">*</em>
- </span>
- <HelpTooltip
- className="spacer-left"
- overlay={translate('onboarding.create_project.display_name.help')}
- />
- </label>
- <div className="little-spacer-top spacer-bottom">
- <input
- className="input-super-large"
- id="project-name"
- maxLength={255}
- minLength={1}
- onChange={this.handleProjectNameChange}
- type="text"
- value={this.state.projectName}
- />
- </div>
- <div className="note abs-width-400">
- {translate('onboarding.create_project.display_name.description')}
- </div>
- </div>
+ description={translate('onboarding.create_project.project_key.description')}
+ 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>
+
+ <ValidationInput
+ className="form-field"
+ description={translate('onboarding.create_project.display_name.description')}
+ error={projectNameError}
+ help={translate('onboarding.create_project.display_name.help')}
+ id="project-name"
+ isInvalid={projectNameIsInvalid}
+ isValid={projectNameIsValid}
+ label={translate('onboarding.create_project.display_name')}
+ required={true}>
+ <input
+ className={classNames('input-super-large', {
+ 'is-invalid': projectNameIsInvalid,
+ 'is-valid': projectNameIsValid
+ })}
+ id="project-name"
+ maxLength={255}
+ minLength={1}
+ onChange={this.handleProjectNameChange}
+ type="text"
+ value={projectName}
+ />
+ </ValidationInput>
+
{isSonarCloud() && selectedOrganization && (
<div
className={classNames('visibility-select-wrapper', {
@@ -226,6 +328,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
/>
</div>
)}
+
<SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
{translate('set_up')}
</SubmitButton>
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 d0815b405c7..93ec3cfb70f 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
@@ -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.
*/
+/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
import { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
@@ -24,7 +25,10 @@ import { createProject } from '../../../../api/components';
import ManualProjectCreate from '../ManualProjectCreate';
jest.mock('../../../../api/components', () => ({
- createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
+ createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } }),
+ doesComponentExists: jest
+ .fn()
+ .mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
}));
jest.mock('../../../../helpers/system', () => ({
@@ -32,19 +36,19 @@ jest.mock('../../../../helpers/system', () => ({
}));
beforeEach(() => {
- (createProject as jest.Mock<any>).mockClear();
+ jest.clearAllMocks();
});
it('should render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
it('should correctly create a public project', async () => {
const onProjectCreate = jest.fn();
- const wrapper = getWrapper({ onProjectCreate });
+ const wrapper = shallowRender({ onProjectCreate });
wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'foo' });
- change(wrapper.find('ProjectKeyInput'), 'bar');
+ change(wrapper.find('input#project-key'), 'bar');
change(wrapper.find('input#project-name'), 'Bar');
expect(wrapper.find('SubmitButton').prop('disabled')).toBe(false);
@@ -62,10 +66,10 @@ it('should correctly create a public project', async () => {
it('should correctly create a private project', async () => {
const onProjectCreate = jest.fn();
- const wrapper = getWrapper({ onProjectCreate });
+ const wrapper = shallowRender({ onProjectCreate });
wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'bar' });
- change(wrapper.find('ProjectKeyInput'), 'bar');
+ change(wrapper.find('input#project-key'), 'bar');
change(wrapper.find('input#project-name'), 'Bar');
submit(wrapper.find('form'));
@@ -80,8 +84,77 @@ it('should correctly create a private project', async () => {
expect(onProjectCreate).toBeCalledWith(['bar']);
});
-function getWrapper(props = {}) {
- return shallow(
+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);
+});
+
+it('should have an error when the key is invalid', () => {
+ const wrapper = shallowRender();
+ change(wrapper.find('input#project-key'), 'KEy-with#speci@l_char');
+ expect(
+ wrapper
+ .find('ValidationInput')
+ .first()
+ .prop('isInvalid')
+ ).toBe(true);
+});
+
+it('should have an error when the key already exists', async () => {
+ const wrapper = shallowRender();
+ change(wrapper.find('input#project-key'), 'exists');
+
+ await waitAndUpdate(wrapper);
+ expect(
+ wrapper
+ .find('ValidationInput')
+ .first()
+ .prop('isInvalid')
+ ).toBe(true);
+});
+
+it('should ignore promise return if value has been changed in the meantime', async () => {
+ const wrapper = shallowRender();
+
+ change(wrapper.find('input#project-key'), 'exists');
+ change(wrapper.find('input#project-key'), 'exists%');
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state('touched')).toBe(true);
+ expect(wrapper.state('projectKeyError')).toBe('onboarding.create_project.project_key.error');
+});
+
+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');
+});
+
+it('should have an error when the name is empty', () => {
+ 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');
+});
+
+function shallowRender(props: Partial<ManualProjectCreate['props']> = {}) {
+ return shallow<ManualProjectCreate>(
<ManualProjectCreate
currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
fetchMyOrganizations={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
index 3f8d2aaee08..a68eafd3480 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
@@ -28,53 +28,47 @@ exports[`should render correctly 1`] = `
]
}
/>
- <ProjectKeyInput
+ <ValidationInput
className="form-field"
- onChange={[Function]}
- value=""
- />
- <div
+ description="onboarding.create_project.project_key.description"
+ 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>
+ <ValidationInput
className="form-field"
+ description="onboarding.create_project.display_name.description"
+ help="onboarding.create_project.display_name.help"
+ id="project-name"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_project.display_name"
+ required={true}
>
- <label
- htmlFor="project-name"
- >
- <span
- className="text-middle"
- >
- <strong>
- onboarding.create_project.display_name
- </strong>
- <em
- className="mandatory"
- >
- *
- </em>
- </span>
- <HelpTooltip
- className="spacer-left"
- overlay="onboarding.create_project.display_name.help"
- />
- </label>
- <div
- className="little-spacer-top spacer-bottom"
- >
- <input
- className="input-super-large"
- id="project-name"
- maxLength={255}
- minLength={1}
- onChange={[Function]}
- type="text"
- value=""
- />
- </div>
- <div
- className="note abs-width-400"
- >
- onboarding.create_project.display_name.description
- </div>
- </div>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={255}
+ minLength={1}
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
+ </ValidationInput>
<SubmitButton
disabled={true}
>
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 d7c8865ddcc..9bb347d45f2 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2857,7 +2857,7 @@ onboarding.create_project.project_key.error=The provided value doesn't match the
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 provided value doesn't match the expected format.
+onboarding.create_project.display_name.error=The display name is required.
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 imported: {link}