aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-11-09 14:44:07 +0100
committerSonarTech <sonartech@sonarsource.com>2018-11-16 20:21:06 +0100
commite86e8c1fe6f96ea84d2f38ee01b3610d21eebe94 (patch)
treec3cc3465112ccff2055867a4b143664ed9108f1f /server/sonar-web
parent4e72416a414f4651cf9e0347b161c9be74b9782a (diff)
downloadsonarqube-e86e8c1fe6f96ea84d2f38ee01b3610d21eebe94.tar.gz
sonarqube-e86e8c1fe6f96ea84d2f38ee01b3610d21eebe94.zip
SONAR-11321 Improve project page manual fields validate
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/components.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx145
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/ProjectNameInput.tsx101
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectNameInput-test.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap28
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectNameInput-test.tsx.snap27
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx86
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap68
-rw-r--r--server/sonar-web/src/main/js/components/controls/ValidationInput.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap50
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.ts18
15 files changed, 509 insertions, 145 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index df793ee8a87..ae01e608b7b 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -164,6 +164,15 @@ export function getTree(data: {
return getJSON('/api/components/tree', data).catch(throwGlobalError);
}
+export function doesComponentExists(
+ data: { component: string } & BranchParameters
+): Promise<boolean> {
+ return getJSON('/api/components/show', data).then(
+ ({ component }) => component !== undefined,
+ () => false
+ );
+}
+
export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> {
return getJSON('/api/components/show', data).catch(throwGlobalError);
}
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
new file mode 100644
index 00000000000..f6a5a4adb02
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx
@@ -0,0 +1,145 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import * as classNames from 'classnames';
+import { debounce } from 'lodash';
+import ValidationInput from '../../../components/controls/ValidationInput';
+import { doesComponentExists } from '../../../api/components';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ className?: string;
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ validating: boolean;
+ value: string;
+}
+
+export default class ProjectKeyInput extends React.PureComponent<Props, State> {
+ mounted = false;
+ constructor(props: Props) {
+ super(props);
+ this.state = { error: undefined, editing: false, touched: false, validating: false, value: '' };
+ this.checkFreeKey = debounce(this.checkFreeKey, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.props.initialValue !== undefined) {
+ this.setState({ value: this.props.initialValue });
+ this.validateKey(this.props.initialValue);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkFreeKey = (key: string) => {
+ this.setState({ validating: true });
+ return doesComponentExists({ component: key })
+ .then(alreadyExist => {
+ if (this.mounted) {
+ if (!alreadyExist) {
+ this.setState({ error: undefined, validating: false });
+ this.props.onChange(key);
+ } else {
+ this.setState({
+ error: translate('onboarding.create_project.project_key.taken'),
+ touched: true,
+ validating: false
+ });
+ this.props.onChange(undefined);
+ }
+ }
+ })
+ .catch(() => {
+ if (this.mounted) {
+ this.setState({ error: undefined, validating: false });
+ this.props.onChange(key);
+ }
+ });
+ };
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ this.setState({ touched: true, value });
+ this.validateKey(value);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ 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
+ });
+ this.props.onChange(undefined);
+ } else {
+ this.checkFreeKey(key);
+ }
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && 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}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/components/ProjectNameInput.tsx b/server/sonar-web/src/main/js/apps/create/components/ProjectNameInput.tsx
new file mode 100644
index 00000000000..79eb39ca233
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/ProjectNameInput.tsx
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import * as classNames from 'classnames';
+import ValidationInput from '../../../components/controls/ValidationInput';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ className?: string;
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class ProjectNameInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const error = this.validateName(this.props.initialValue);
+ this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ const error = this.validateName(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateName(name: string) {
+ if (name.length > 255) {
+ return translate('onboarding.create_project.display_name.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ className={this.props.className}
+ description={translate('onboarding.create_project.display_name.description')}
+ error={this.state.error}
+ id="project-name"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_project.display_name')}
+ required={true}>
+ <input
+ className={classNames('input-super-large', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="project-name"
+ maxLength={500}
+ minLength={1}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ required={true}
+ type="text"
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx
index ecbfdb1f190..ff99e0dd15d 100644
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx
@@ -28,7 +28,7 @@ it('should render correctly', () => {
expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
});
-it('should have an error when description is too long', () => {
+it('should have an error when name is too long', () => {
expect(
shallow(<OrganizationNameInput initialValue={'x'.repeat(256)} onChange={jest.fn()} />)
.find('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
new file mode 100644
index 00000000000..29227f371d7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import { shallow } from 'enzyme';
+import ProjectKeyInput from '../ProjectKeyInput';
+import { doesComponentExists } from '../../../../api/components';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/components', () => ({
+ doesComponentExists: jest.fn().mockResolvedValue(false)
+}));
+
+beforeEach(() => {
+ (doesComponentExists as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ const wrapper = shallow(<ProjectKeyInput initialValue="key" onChange={jest.fn()} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should not display any status when the key is not defined', async () => {
+ const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} />);
+ 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 initialValue="KEy-with#speci@l_char" onChange={jest.fn()} />
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
+
+it('should have an error when the key already exists', async () => {
+ (doesComponentExists as jest.Mock<any>).mockResolvedValue(true);
+ const wrapper = shallow(<ProjectKeyInput initialValue="" onChange={jest.fn()} />);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectNameInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectNameInput-test.tsx
new file mode 100644
index 00000000000..9082a0b1671
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectNameInput-test.tsx
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 React from 'react';
+import { shallow } from 'enzyme';
+import ProjectNameInput from '../ProjectNameInput';
+
+it('should render correctly', () => {
+ const wrapper = shallow(<ProjectNameInput initialValue="Project Name" onChange={jest.fn()} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when name is too long', () => {
+ expect(
+ shallow(<ProjectNameInput initialValue={'x'.repeat(501)} onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
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
new file mode 100644
index 00000000000..18423976d60
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap
@@ -0,0 +1,28 @@
+// 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}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="key"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectNameInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectNameInput-test.tsx.snap
new file mode 100644
index 00000000000..41b7f890f51
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectNameInput-test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ description="onboarding.create_project.display_name.description"
+ id="project-name"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_project.display_name"
+ required={true}
+>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={500}
+ minLength={1}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ required={true}
+ type="text"
+ value="Project Name"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
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 41d05c735ed..2fc7cb8d183 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
@@ -21,9 +21,11 @@ import * as React from 'react';
import OrganizationInput from './OrganizationInput';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { SubmitButton } from '../../../components/ui/buttons';
+import { createProject } from '../../../api/components';
import { LoggedInUser, Organization } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
-import { createProject } from '../../../api/components';
+import ProjectKeyInput from '../components/ProjectKeyInput';
+import ProjectNameInput from '../components/ProjectNameInput';
interface Props {
currentUser: LoggedInUser;
@@ -33,20 +35,20 @@ interface Props {
}
interface State {
- projectName: string;
- projectKey: string;
+ projectName?: string;
+ projectKey?: string;
selectedOrganization: string;
submitting: boolean;
}
+type ValidState = State & Required<Pick<State, 'projectName' | 'projectKey'>>;
+
export default class ManualProjectCreate extends React.PureComponent<Props, State> {
mounted = false;
constructor(props: Props) {
super(props);
this.state = {
- projectName: '',
- projectKey: '',
selectedOrganization: this.getInitialSelectedOrganization(props),
submitting: false
};
@@ -60,6 +62,10 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
this.mounted = false;
}
+ canSubmit(state: State): state is ValidState {
+ return Boolean(state.projectKey && state.projectName && state.selectedOrganization);
+ }
+
getInitialSelectedOrganization(props: Props) {
if (props.organization) {
return props.organization;
@@ -72,14 +78,13 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
-
- if (this.isValid()) {
- const { projectKey, projectName, selectedOrganization } = this.state;
+ const { state } = this;
+ if (this.canSubmit(state)) {
this.setState({ submitting: true });
createProject({
- project: projectKey,
- name: projectName,
- organization: selectedOrganization
+ project: state.projectKey,
+ name: state.projectName,
+ organization: state.selectedOrganization
}).then(
({ project }) => this.props.onProjectCreate([project.key]),
() => {
@@ -95,17 +100,12 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
this.setState({ selectedOrganization: key });
};
- handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.setState({ projectName: event.currentTarget.value });
- };
-
- handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.setState({ projectKey: event.currentTarget.value });
+ handleProjectNameChange = (projectName?: string) => {
+ this.setState({ projectName });
};
- isValid = () => {
- const { projectKey, projectName, selectedOrganization } = this.state;
- return Boolean(projectKey && projectName && selectedOrganization);
+ handleProjectKeyChange = (projectKey?: string) => {
+ this.setState({ projectKey });
};
render() {
@@ -118,39 +118,19 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
organization={this.state.selectedOrganization}
organizations={this.props.userOrganizations}
/>
- <div className="form-field">
- <label htmlFor="project-name">
- {translate('onboarding.create_project.project_name')}
- <em className="mandatory">*</em>
- </label>
- <input
- className="input-super-large"
- id="project-name"
- maxLength={400}
- minLength={1}
- onChange={this.handleProjectNameChange}
- required={true}
- type="text"
- value={this.state.projectName}
- />
- </div>
- <div className="form-field">
- <label htmlFor="project-key">
- {translate('onboarding.create_project.project_key')}
- <em className="mandatory">*</em>
- </label>
- <input
- className="input-super-large"
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={this.handleProjectKeyChange}
- required={true}
- type="text"
- value={this.state.projectKey}
- />
- </div>
- <SubmitButton disabled={!this.isValid() || submitting}>{translate('setup')}</SubmitButton>
+ <ProjectKeyInput
+ className="form-field"
+ initialValue={this.state.projectKey}
+ onChange={this.handleProjectKeyChange}
+ />
+ <ProjectNameInput
+ className="form-field"
+ initialValue={this.state.projectName}
+ onChange={this.handleProjectNameChange}
+ />
+ <SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
+ {translate('setup')}
+ </SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>
</>
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 aad0cab3ad2..dedf50e95e1 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
@@ -39,11 +39,12 @@ it('should correctly create a project', async () => {
const onProjectCreate = jest.fn();
const wrapper = getWrapper({ onProjectCreate });
wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'foo' });
- change(wrapper.find('#project-name'), 'Bar');
- expect(wrapper.find('SubmitButton')).toMatchSnapshot();
- change(wrapper.find('#project-key'), 'bar');
- expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+ change(wrapper.find('ProjectKeyInput'), 'bar');
+ expect(wrapper.find('SubmitButton').prop('disabled')).toBe(true);
+
+ change(wrapper.find('ProjectNameInput'), 'Bar');
+ expect(wrapper.find('SubmitButton').prop('disabled')).toBe(false);
submit(wrapper.find('form'));
expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
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 66d10b4b7be..131324325a9 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
@@ -1,21 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should correctly create a project 1`] = `
-<SubmitButton
- disabled={true}
->
- setup
-</SubmitButton>
-`;
-
-exports[`should correctly create a project 2`] = `
-<SubmitButton
- disabled={false}
->
- setup
-</SubmitButton>
-`;
-
exports[`should render correctly 1`] = `
<Fragment>
<form
@@ -37,54 +21,14 @@ exports[`should render correctly 1`] = `
]
}
/>
- <div
+ <ProjectKeyInput
className="form-field"
- >
- <label
- htmlFor="project-name"
- >
- onboarding.create_project.project_name
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- className="input-super-large"
- id="project-name"
- maxLength={400}
- minLength={1}
- onChange={[Function]}
- required={true}
- type="text"
- value=""
- />
- </div>
- <div
+ onChange={[Function]}
+ />
+ <ProjectNameInput
className="form-field"
- >
- <label
- htmlFor="project-key"
- >
- onboarding.create_project.project_key
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- className="input-super-large"
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={[Function]}
- required={true}
- type="text"
- value=""
- />
- </div>
+ onChange={[Function]}
+ />
<SubmitButton
disabled={true}
>
diff --git a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
index f95fb297879..a0be10383b3 100644
--- a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
@@ -18,13 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import HelpTooltip from './HelpTooltip';
import AlertErrorIcon from '../icons-components/AlertErrorIcon';
import AlertSuccessIcon from '../icons-components/AlertSuccessIcon';
interface Props {
description?: string;
children: React.ReactNode;
+ className?: string;
error: string | undefined;
+ help?: string;
id: string;
isInvalid: boolean;
isValid: boolean;
@@ -35,10 +38,13 @@ interface Props {
export default function ValidationInput(props: Props) {
const hasError = props.isInvalid && props.error !== undefined;
return (
- <div>
+ <div className={props.className}>
<label htmlFor={props.id}>
- <strong>{props.label}</strong>
- {props.required && <em className="mandatory">*</em>}
+ <span className="text-middle">
+ <strong>{props.label}</strong>
+ {props.required && <em className="mandatory">*</em>}
+ </span>
+ {props.help && <HelpTooltip className="spacer-left" overlay={props.help} />}
</label>
<div className="little-spacer-top spacer-bottom">
{props.children}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx
index 37b3d8e6a41..c5245f0d81b 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx
@@ -27,6 +27,7 @@ it('should render', () => {
<ValidationInput
description="My description"
error={undefined}
+ help="Help message"
id="field-id"
isInvalid={false}
isValid={false}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
index f49d27a83be..e417b038bc6 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
@@ -5,14 +5,22 @@ exports[`should render 1`] = `
<label
htmlFor="field-id"
>
- <strong>
- Field label
- </strong>
- <em
- className="mandatory"
+ <span
+ className="text-middle"
>
- *
- </em>
+ <strong>
+ Field label
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </span>
+ <HelpTooltip
+ className="spacer-left"
+ overlay="Help message"
+ />
</label>
<div
className="little-spacer-top spacer-bottom"
@@ -32,14 +40,18 @@ exports[`should render when valid 1`] = `
<label
htmlFor="field-id"
>
- <strong>
- Field label
- </strong>
- <em
- className="mandatory"
+ <span
+ className="text-middle"
>
- *
- </em>
+ <strong>
+ Field label
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </span>
</label>
<div
className="little-spacer-top spacer-bottom"
@@ -62,9 +74,13 @@ exports[`should render with error 1`] = `
<label
htmlFor="field-id"
>
- <strong>
- Field label
- </strong>
+ <span
+ className="text-middle"
+ >
+ <strong>
+ Field label
+ </strong>
+ </span>
</label>
<div
className="little-spacer-top spacer-bottom"
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts
index 17ae73d203f..ed1f5f42a4b 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.ts
+++ b/server/sonar-web/src/main/js/helpers/testUtils.ts
@@ -52,11 +52,19 @@ export function submit(element: ShallowWrapper | ReactWrapper): void {
}
export function change(element: ShallowWrapper | ReactWrapper, value: string, event = {}): void {
- element.simulate('change', {
- target: { value },
- currentTarget: { value },
- ...event
- });
+ // `type()` returns a component constructor for a composite element and string for DOM nodes
+ if (typeof element.type() === 'function') {
+ element.prop<Function>('onChange')(value);
+ // TODO find out if `root` is a public api
+ // https://github.com/airbnb/enzyme/blob/master/packages/enzyme/src/ReactWrapper.js#L109
+ (element as any).root().update();
+ } else {
+ element.simulate('change', {
+ target: { value },
+ currentTarget: { value },
+ ...event
+ });
+ }
}
export function keydown(keyCode: number): void {