diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-11-09 14:44:07 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-16 20:21:06 +0100 |
commit | e86e8c1fe6f96ea84d2f38ee01b3610d21eebe94 (patch) | |
tree | c3cc3465112ccff2055867a4b143664ed9108f1f /server/sonar-web | |
parent | 4e72416a414f4651cf9e0347b161c9be74b9782a (diff) | |
download | sonarqube-e86e8c1fe6f96ea84d2f38ee01b3610d21eebe94.tar.gz sonarqube-e86e8c1fe6f96ea84d2f38ee01b3610d21eebe94.zip |
SONAR-11321 Improve project page manual fields validate
Diffstat (limited to 'server/sonar-web')
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 { |