]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11321 Improve project page manual fields validate
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 9 Nov 2018 13:44:07 +0000 (14:44 +0100)
committerSonarTech <sonartech@sonarsource.com>
Fri, 16 Nov 2018 19:21:06 +0000 (20:21 +0100)
16 files changed:
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/ProjectNameInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx
server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectNameInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectNameInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
server/sonar-web/src/main/js/helpers/testUtils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index df793ee8a874b0d8fb1fbd79503c887fb738e3f8..ae01e608b7b036200e148ef14aa630144d8f40d4 100644 (file)
@@ -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 (file)
index 0000000..f6a5a4a
--- /dev/null
@@ -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 (file)
index 0000000..79eb39c
--- /dev/null
@@ -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>
+    );
+  }
+}
index ecbfdb1f19015278f9962631a6103183b92602cb..ff99e0dd15da82d1c95c8fa111e4f71d26e13675 100644 (file)
@@ -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 (file)
index 0000000..29227f3
--- /dev/null
@@ -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 (file)
index 0000000..9082a0b
--- /dev/null
@@ -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 (file)
index 0000000..1842397
--- /dev/null
@@ -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 (file)
index 0000000..41b7f89
--- /dev/null
@@ -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`;
index 41d05c735ed5d7b10c243d9811a01d55ac4588e7..2fc7cb8d183e0ad395667bf180f39f222da82833 100644 (file)
@@ -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>
       </>
index aad0cab3ad2f11f2d3f275341dca2997302764ff..dedf50e95e16c039e986508b5707e674cde76fdc 100644 (file)
@@ -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' });
index 66d10b4b7be2ac6330ea2d2c0b571700806145e9..131324325a904ef5604ca43d726d28ffeca63194 100644 (file)
@@ -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}
     >
index f95fb297879a9db9eef7d62b5b6f7e98ac96d90b..a0be10383b3813a2ae1dcf886bcd5ba949f09ac2 100644 (file)
  * 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}
index 37b3d8e6a413dfdff4005357f7412bc86c090b3d..c5245f0d81be8572d5413fbcf4b0bff8e593c34b 100644 (file)
@@ -27,6 +27,7 @@ it('should render', () => {
       <ValidationInput
         description="My description"
         error={undefined}
+        help="Help message"
         id="field-id"
         isInvalid={false}
         isValid={false}
index f49d27a83becddf2f047256fbd2e8c6912d1085b..e417b038bc61e942ba23b6fd55eb41432ee40323 100644 (file)
@@ -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"
index 17ae73d203f76399f688b70dac0ef672b16dceaf..ed1f5f42a4b48b96ccc59c89b0481955a0c7e8f1 100644 (file)
@@ -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 {
index 59eba87effea9165159cdab0cf8b8d3cfc114f7c..93e061c69ac67f38b01a12b21d2f8bf7211bbc32 100644 (file)
@@ -2725,7 +2725,13 @@ onboarding.create_project.install_app_description.bitbucket=We need you to insta
 onboarding.create_project.install_app_description.github=We need you to install the SonarCloud GitHub application on one of your organization in order to select which repositories you want to analyze.
 onboarding.create_project.organization=Organization
 onboarding.create_project.project_key=Project key
-onboarding.create_project.project_name=Project name
+onboarding.create_project.project_key.description=Up to 400 characters. All letters, digits, dash, underscore, point or colon.
+onboarding.create_project.project_key.error=The provided value doesn't match the expected format.
+onboarding.create_project.project_key.help=Your project key is a unique identifier for your project.
+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.description=Up to 500 characters
 onboarding.create_project.repository_imported=Already imported: {link}
 onboarding.create_project.see_project=See the project
 onboarding.create_project.select_repositories=Select repositories