@@ -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); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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') |
@@ -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); | |||
}); |
@@ -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); | |||
}); |
@@ -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`; |
@@ -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`; |
@@ -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> | |||
</> |
@@ -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' }); |
@@ -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} | |||
> |
@@ -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} |
@@ -27,6 +27,7 @@ it('should render', () => { | |||
<ValidationInput | |||
description="My description" | |||
error={undefined} | |||
help="Help message" | |||
id="field-id" | |||
isInvalid={false} | |||
isValid={false} |
@@ -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" |
@@ -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 { |
@@ -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 |