@@ -16,7 +16,7 @@ | |||
"d3-shape": "1.2.0", | |||
"d3-zoom": "1.7.1", | |||
"date-fns": "1.29.0", | |||
"formik": "0.11.11", | |||
"formik": "1.2.0", | |||
"history": "3.3.0", | |||
"intl-relativeformat": "2.1.0", | |||
"keymaster": "1.6.2", |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import { CurrentUser, isLoggedIn, Organization } from '../types'; | |||
import { CurrentUser, isLoggedIn } from '../types'; | |||
import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates'; | |||
import { EditionKey } from '../../apps/marketplace/utils'; | |||
import { getCurrentUser, getAppState, Store } from '../../store/rootReducer'; | |||
@@ -32,9 +32,6 @@ import { isSonarCloud } from '../../helpers/system'; | |||
import { skipOnboarding } from '../../api/users'; | |||
import { lazyLoad } from '../../components/lazyLoad'; | |||
const CreateOrganizationForm = lazyLoad(() => | |||
import('../../apps/account/organizations/CreateOrganizationForm') | |||
); | |||
const OnboardingModal = lazyLoad(() => import('../../apps/tutorials/onboarding/OnboardingModal')); | |||
const LicensePromptModal = lazyLoad( | |||
() => import('../../apps/marketplace/components/LicensePromptModal'), | |||
@@ -68,7 +65,6 @@ type Props = StateProps & DispatchProps & OwnProps; | |||
enum ModalKey { | |||
license, | |||
onboarding, | |||
organizationOnboarding, | |||
projectOnboarding, | |||
teamOnboarding | |||
} | |||
@@ -119,17 +115,13 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
}); | |||
}; | |||
closeOrganizationOnboarding = ({ key }: Pick<Organization, 'key'>) => { | |||
this.closeOnboarding(); | |||
this.context.router.push(`/organizations/${key}`); | |||
}; | |||
openOnboarding = () => { | |||
this.setState({ modal: ModalKey.onboarding }); | |||
}; | |||
openOrganizationOnboarding = () => { | |||
this.setState({ modal: ModalKey.organizationOnboarding }); | |||
this.closeOnboarding(); | |||
this.context.router.push('/create-organization'); | |||
}; | |||
openProjectOnboarding = () => { | |||
@@ -160,11 +152,11 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
this.setState({ automatic: true, modal: ModalKey.license }); | |||
return Promise.resolve(); | |||
} | |||
return Promise.reject('License exists'); | |||
return Promise.reject(); | |||
}); | |||
} | |||
} | |||
return Promise.reject('No license prompt'); | |||
return Promise.reject(); | |||
}; | |||
tryAutoOpenOnboarding = () => { | |||
@@ -201,12 +193,6 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
{modal === ModalKey.projectOnboarding && ( | |||
<ProjectOnboardingModal automatic={automatic} onFinish={this.closeOnboarding} /> | |||
)} | |||
{modal === ModalKey.organizationOnboarding && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeOnboarding} | |||
onCreate={this.closeOrganizationOnboarding} | |||
/> | |||
)} | |||
{modal === ModalKey.teamOnboarding && ( | |||
<TeamOnboardingModal onFinish={this.closeOnboarding} /> | |||
)} |
@@ -18,8 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import CreateOrganizationForm from '../../../../apps/account/organizations/CreateOrganizationForm'; | |||
import { Link } from 'react-router'; | |||
import PlusIcon from '../../../../components/icons-components/PlusIcon'; | |||
import Dropdown from '../../../../components/controls/Dropdown'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
@@ -28,40 +27,12 @@ interface Props { | |||
openProjectOnboarding: () => void; | |||
} | |||
interface State { | |||
createOrganization: boolean; | |||
} | |||
export default class GlobalNavPlus extends React.PureComponent<Props, State> { | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { createOrganization: false }; | |||
} | |||
export default class GlobalNavPlus extends React.PureComponent<Props> { | |||
handleNewProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.openProjectOnboarding(); | |||
}; | |||
openCreateOrganizationForm = () => this.setState({ createOrganization: true }); | |||
closeCreateOrganizationForm = () => this.setState({ createOrganization: false }); | |||
handleNewOrganizationClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.openCreateOrganizationForm(); | |||
}; | |||
handleCreateOrganization = ({ key }: { key: string }) => { | |||
this.closeCreateOrganizationForm(); | |||
this.context.router.push(`/organizations/${key}`); | |||
}; | |||
render() { | |||
return ( | |||
<Dropdown | |||
@@ -74,33 +45,19 @@ export default class GlobalNavPlus extends React.PureComponent<Props, State> { | |||
</li> | |||
<li className="divider" /> | |||
<li> | |||
<a className="js-new-organization" href="#" onClick={this.handleNewOrganizationClick}> | |||
<Link className="js-new-organization" to="/create-organization"> | |||
{translate('my_account.create_new_organization')} | |||
</a> | |||
</Link> | |||
</li> | |||
</ul> | |||
} | |||
tagName="li"> | |||
{({ onToggleClick, open }) => ( | |||
<> | |||
<a | |||
aria-expanded={open} | |||
aria-haspopup="true" | |||
className="navbar-plus" | |||
href="#" | |||
onClick={onToggleClick} | |||
title={translate('my_account.create_new_project_or_organization')}> | |||
<PlusIcon /> | |||
</a> | |||
{this.state.createOrganization && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeCreateOrganizationForm} | |||
onCreate={this.handleCreateOrganization} | |||
/> | |||
)} | |||
</> | |||
)} | |||
<a | |||
className="navbar-plus" | |||
href="#" | |||
title={translate('my_account.create_new_project_or_organization')}> | |||
<PlusIcon /> | |||
</a> | |||
</Dropdown> | |||
); | |||
} |
@@ -19,16 +19,25 @@ exports[`render 1`] = ` | |||
className="divider" | |||
/> | |||
<li> | |||
<a | |||
<Link | |||
className="js-new-organization" | |||
href="#" | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/create-organization" | |||
> | |||
my_account.create_new_organization | |||
</a> | |||
</Link> | |||
</li> | |||
</ul> | |||
} | |||
tagName="li" | |||
/> | |||
> | |||
<a | |||
className="navbar-plus" | |||
href="#" | |||
title="my_account.create_new_project_or_organization" | |||
> | |||
<PlusIcon /> | |||
</a> | |||
</Dropdown> | |||
`; |
@@ -246,21 +246,11 @@ | |||
min-height: var(--controlHeight); | |||
} | |||
.modal-validation-field input:not(.has-error), | |||
.modal-validation-field .Select:not(.has-error) { | |||
.modal-validation-field input:not(.is-invalid), | |||
.modal-validation-field .Select:not(.is-invalid) { | |||
margin-bottom: 18px; | |||
} | |||
.modal-validation-field .has-error, | |||
.modal-validation-field .has-error > .Select-control { | |||
border-color: var(--red); | |||
} | |||
.modal-validation-field .is-valid, | |||
.modal-validation-field .is-valid > .Select-control { | |||
border-color: var(--green); | |||
} | |||
.modal-field-description { | |||
padding-bottom: 4px; | |||
line-height: 1.4; |
@@ -69,14 +69,27 @@ select:invalid { | |||
outline: none; | |||
} | |||
input[type='text'].invalid, | |||
input[type='password'].invalid, | |||
input[type='email'].invalid, | |||
input[type='search'].invalid, | |||
input[type='date'].invalid, | |||
input[type='number'].invalid, | |||
textarea.invalid, | |||
select.invalid { | |||
input[type='text'].is-valid, | |||
input[type='password'].is-valid, | |||
input[type='email'].is-valid, | |||
input[type='search'].is-valid, | |||
input[type='date'].is-valid, | |||
input[type='number'].is-valid, | |||
textarea.is-valid, | |||
select.is-valid, | |||
.is-valid > .Select-control { | |||
border-color: var(--green); | |||
} | |||
input[type='text'].is-invalid, | |||
input[type='password'].is-invalid, | |||
input[type='email'].is-invalid, | |||
input[type='search'].is-invalid, | |||
input[type='date'].is-invalid, | |||
input[type='number'].is-invalid, | |||
textarea.is-invalid, | |||
select.is-invalid, | |||
.is-invalid > .Select-control { | |||
border-color: var(--red); | |||
} | |||
@@ -67,6 +67,7 @@ import webhooksRoutes from '../../apps/webhooks/routes'; | |||
import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes'; | |||
import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes'; | |||
import { lazyLoad } from '../../components/lazyLoad'; | |||
import { isSonarCloud } from '../../helpers/system'; | |||
function handleUpdate() { | |||
const { action } = this.state.location; | |||
@@ -171,6 +172,14 @@ const startReactApp = (lang, currentUser, appState) => { | |||
/> | |||
<Route path="issues" component={IssuesPageSelector} /> | |||
<Route path="onboarding" childRoutes={onboardingRoutes} /> | |||
{isSonarCloud() && ( | |||
<Route | |||
path="create-organization" | |||
component={lazyLoad(() => | |||
import('../../apps/create/organization/CreateOrganization') | |||
)} | |||
/> | |||
)} | |||
<Route path="organizations" childRoutes={organizationsRoutes} /> | |||
<Route path="projects" childRoutes={projectsRoutes} /> | |||
<Route path="quality_gates" childRoutes={qualityGatesRoutes} /> |
@@ -1,252 +0,0 @@ | |||
/* | |||
* 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 { debounce } from 'lodash'; | |||
import { connect } from 'react-redux'; | |||
import * as PropTypes from 'prop-types'; | |||
import { createOrganization } from '../../organizations/actions'; | |||
import { Organization, OrganizationBase } from '../../../app/types'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import DocTooltip from '../../../components/docs/DocTooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; | |||
interface DispatchProps { | |||
createOrganization: (fields: OrganizationBase) => Promise<Organization>; | |||
} | |||
interface Props extends DispatchProps { | |||
onClose: () => void; | |||
onCreate: (organization: { key: string }) => void; | |||
} | |||
interface State { | |||
avatar: string; | |||
avatarImage: string; | |||
description: string; | |||
key: string; | |||
loading: boolean; | |||
name: string; | |||
url: string; | |||
} | |||
class CreateOrganizationForm extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
avatar: '', | |||
avatarImage: '', | |||
description: '', | |||
key: '', | |||
loading: false, | |||
name: '', | |||
url: '' | |||
}; | |||
this.changeAvatarImage = debounce(this.changeAvatarImage, 500); | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
stopProcessing = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
handleAvatarInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
const { value } = event.currentTarget; | |||
this.setState({ avatar: value }); | |||
this.changeAvatarImage(value); | |||
}; | |||
changeAvatarImage = (value: string) => { | |||
this.setState({ avatarImage: value }); | |||
}; | |||
handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ name: event.currentTarget.value }); | |||
handleKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ key: event.currentTarget.value }); | |||
handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) => | |||
this.setState({ description: event.currentTarget.value }); | |||
handleUrlChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ url: event.currentTarget.value }); | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const organization = { name: this.state.name }; | |||
if (this.state.avatar) { | |||
Object.assign(organization, { avatar: this.state.avatar }); | |||
} | |||
if (this.state.description) { | |||
Object.assign(organization, { description: this.state.description }); | |||
} | |||
if (this.state.key) { | |||
Object.assign(organization, { key: this.state.key }); | |||
} | |||
if (this.state.url) { | |||
Object.assign(organization, { url: this.state.url }); | |||
} | |||
this.setState({ loading: true }); | |||
this.props.createOrganization(organization).then(this.props.onCreate, this.stopProcessing); | |||
}; | |||
render() { | |||
return ( | |||
<Modal contentLabel="modal form" onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2> | |||
{translate('my_account.create_organization')} | |||
<DocTooltip | |||
className="spacer-left" | |||
doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/organization.md')} | |||
/> | |||
</h2> | |||
</header> | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body"> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-name"> | |||
{translate('organization.name')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
disabled={this.state.loading} | |||
id="organization-name" | |||
maxLength={64} | |||
minLength={2} | |||
name="name" | |||
onChange={this.handleNameChange} | |||
required={true} | |||
type="text" | |||
value={this.state.name} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.name.description')} | |||
</div> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-key">{translate('organization.key')}</label> | |||
<input | |||
disabled={this.state.loading} | |||
id="organization-key" | |||
maxLength={64} | |||
minLength={2} | |||
name="key" | |||
onChange={this.handleKeyChange} | |||
type="text" | |||
value={this.state.key} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.key.description')} | |||
</div> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-avatar">{translate('organization.avatar')}</label> | |||
<input | |||
disabled={this.state.loading} | |||
id="organization-avatar" | |||
maxLength={256} | |||
name="avatar" | |||
onChange={this.handleAvatarInputChange} | |||
type="text" | |||
value={this.state.avatar} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.avatar.description')} | |||
</div> | |||
{!!this.state.avatarImage && ( | |||
<div className="spacer-top spacer-bottom"> | |||
<div className="little-spacer-bottom"> | |||
{translate('organization.avatar.preview')} | |||
{':'} | |||
</div> | |||
<img alt="" height={30} src={this.state.avatarImage} /> | |||
</div> | |||
)} | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-description">{translate('description')}</label> | |||
<textarea | |||
disabled={this.state.loading} | |||
id="organization-description" | |||
maxLength={256} | |||
name="description" | |||
onChange={this.handleDescriptionChange} | |||
rows={3} | |||
value={this.state.description} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.description.description')} | |||
</div> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-url">{translate('organization.url')}</label> | |||
<input | |||
disabled={this.state.loading} | |||
id="organization-url" | |||
maxLength={256} | |||
name="url" | |||
onChange={this.handleUrlChange} | |||
type="text" | |||
value={this.state.url} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.url.description')} | |||
</div> | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
<div> | |||
{this.state.loading && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={this.state.loading}>{translate('create')}</SubmitButton> | |||
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</div> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} | |||
} | |||
const mapDispatchToProps: DispatchProps = { createOrganization: createOrganization as any }; | |||
export default connect( | |||
null, | |||
mapDispatchToProps | |||
)(CreateOrganizationForm); |
@@ -20,8 +20,8 @@ | |||
import * as React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import { Link } from 'react-router'; | |||
import OrganizationsList from './OrganizationsList'; | |||
import CreateOrganizationForm from './CreateOrganizationForm'; | |||
import { fetchIfAnyoneCanCreateOrganizations } from './actions'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { | |||
@@ -31,7 +31,6 @@ import { | |||
Store | |||
} from '../../../store/rootReducer'; | |||
import { Organization } from '../../../app/types'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
interface StateProps { | |||
anyoneCanCreate?: { value: string }; | |||
@@ -46,13 +45,12 @@ interface DispatchProps { | |||
interface Props extends StateProps, DispatchProps {} | |||
interface State { | |||
createOrganization: boolean; | |||
loading: boolean; | |||
} | |||
class UserOrganizations extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { createOrganization: false, loading: true }; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -69,14 +67,6 @@ class UserOrganizations extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
openCreateOrganizationForm = () => { | |||
this.setState({ createOrganization: true }); | |||
}; | |||
closeCreateOrganizationForm = () => { | |||
this.setState({ createOrganization: false }); | |||
}; | |||
render() { | |||
const anyoneCanCreate = | |||
this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true'; | |||
@@ -91,7 +81,9 @@ class UserOrganizations extends React.PureComponent<Props, State> { | |||
{canCreateOrganizations && ( | |||
<div className="clearfix"> | |||
<div className="boxed-group-actions"> | |||
<Button onClick={this.openCreateOrganizationForm}>{translate('create')}</Button> | |||
<Link className="button" to="/create-organization"> | |||
{translate('create')} | |||
</Link> | |||
</div> | |||
</div> | |||
)} | |||
@@ -103,13 +95,6 @@ class UserOrganizations extends React.PureComponent<Props, State> { | |||
)} | |||
</div> | |||
</div> | |||
{this.state.createOrganization && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeCreateOrganizationForm} | |||
onCreate={this.closeCreateOrganizationForm} | |||
/> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,107 @@ | |||
/* | |||
* 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 { Helmet } from 'react-helmet'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link, withRouter, WithRouterProps } from 'react-router'; | |||
import { connect } from 'react-redux'; | |||
import OrganizationDetailsStep from './OrganizationDetailsStep'; | |||
import { whenLoggedIn } from './whenLoggedIn'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { OrganizationBase, Organization } from '../../../app/types'; | |||
import { createOrganization } from '../../organizations/actions'; | |||
import { getOrganizationUrl } from '../../../helpers/urls'; | |||
import '../../../app/styles/sonarcloud.css'; | |||
import '../../tutorials/styles.css'; // TODO remove me | |||
interface Props { | |||
createOrganization: (organization: OrganizationBase) => Promise<Organization>; | |||
} | |||
export class CreateOrganization extends React.PureComponent<Props & WithRouterProps> { | |||
mounted = false; | |||
componentDidMount() { | |||
this.mounted = true; | |||
document.body.classList.add('white-page'); | |||
document.documentElement.classList.add('white-page'); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
document.body.classList.remove('white-page'); | |||
} | |||
handleOrganizationCreate = (organization: Required<OrganizationBase>) => { | |||
return this.props | |||
.createOrganization({ | |||
avatar: organization.avatar, | |||
description: organization.description, | |||
key: organization.key, | |||
name: organization.name || organization.key, | |||
url: organization.url | |||
}) | |||
.then(organization => { | |||
this.props.router.push(getOrganizationUrl(organization.key)); | |||
}); | |||
}; | |||
render() { | |||
const header = translate('onboarding.create_organization.page.header'); | |||
return ( | |||
<> | |||
<Helmet title={header} titleTemplate="%s" /> | |||
<div className="sonarcloud page page-limited"> | |||
<header className="page-header"> | |||
<h1 className="page-title big-spacer-bottom">{header}</h1> | |||
<div className="page-actions"> | |||
<Link to="/">{translate('cancel')}</Link> | |||
</div> | |||
<p className="page-description"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.create_organization.page.description')} | |||
id="onboarding.create_organization.page.description" | |||
values={{ | |||
break: <br />, | |||
price: '€10', // TODO | |||
more: ( | |||
<Link to="/documentation/sonarcloud-pricing">{translate('learn_more')}</Link> | |||
) | |||
}} | |||
/> | |||
</p> | |||
</header> | |||
<OrganizationDetailsStep onContinue={this.handleOrganizationCreate} /> | |||
</div> | |||
</> | |||
); | |||
} | |||
} | |||
const mapDispatchToProps = { createOrganization: createOrganization as any }; | |||
export default whenLoggedIn( | |||
connect( | |||
null, | |||
mapDispatchToProps | |||
)(withRouter(CreateOrganization)) | |||
); |
@@ -0,0 +1,75 @@ | |||
/* | |||
* 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 AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'; | |||
import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; | |||
interface Props { | |||
description?: string; | |||
dirty: boolean; | |||
children: (inputProps: React.InputHTMLAttributes<Element>) => React.ReactElement<any>; | |||
error: string | undefined; | |||
id: string; | |||
isSubmitting: boolean; | |||
label: React.ReactNode; | |||
name: string; | |||
onBlur: React.FocusEventHandler; | |||
onChange: React.ChangeEventHandler; | |||
required?: boolean; | |||
touched?: boolean; | |||
value: string; | |||
} | |||
export default function OrganizationDetailsInput(props: Props) { | |||
const hasError = props.dirty && props.touched && props.error !== undefined; | |||
const isValid = props.dirty && props.touched && props.error === undefined; | |||
return ( | |||
<div> | |||
<label htmlFor={props.id}> | |||
{props.label} | |||
{props.required && <em className="mandatory">*</em>} | |||
</label> | |||
<div className="little-spacer-top spacer-bottom"> | |||
{props.children({ | |||
className: classNames('input-super-large', 'text-middle', { | |||
'is-invalid': hasError, | |||
'is-valid': isValid | |||
}), | |||
disabled: props.isSubmitting, | |||
id: props.id, | |||
name: props.name, | |||
onBlur: props.onBlur, | |||
onChange: props.onChange, | |||
type: 'text', | |||
value: props.value | |||
})} | |||
{hasError && ( | |||
<> | |||
<AlertErrorIcon className="spacer-left text-middle" /> | |||
<span className="little-spacer-left text-danger text-middle">{props.error}</span> | |||
</> | |||
)} | |||
{isValid && <AlertSuccessIcon className="spacer-left text-middle" />} | |||
</div> | |||
{props.description && <div className="note abs-width-400">{props.description}</div>} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,216 @@ | |||
/* | |||
* 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 OrganizationDetailsInput from './OrganizationDetailsInput'; | |||
import Step from '../../tutorials/components/Step'; | |||
import ValidationForm, { ChildrenProps } from '../../../components/controls/ValidationForm'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons'; | |||
import DropdownIcon from '../../../components/icons-components/DropdownIcon'; | |||
import { isUrl } from '../../../helpers/urls'; | |||
import { OrganizationBase } from '../../../app/types'; | |||
import { getOrganization } from '../../../api/organizations'; | |||
type Values = Required<OrganizationBase>; | |||
const initialValues: Values = { | |||
avatar: '', | |||
description: '', | |||
name: '', | |||
key: '', | |||
url: '' | |||
}; | |||
interface Props { | |||
onContinue: (organization: Required<OrganizationBase>) => Promise<void>; | |||
} | |||
interface State { | |||
additional: boolean; | |||
} | |||
export default class OrganizationDetailsStep extends React.PureComponent<Props, State> { | |||
state: State = { additional: false }; | |||
handleAdditionalClick = () => { | |||
this.setState(state => ({ additional: !state.additional })); | |||
}; | |||
checkFreeKey = (key: string) => { | |||
return getOrganization(key).then(organization => organization === undefined, () => true); | |||
}; | |||
handleValidate = ({ avatar, name, key, url }: Values) => { | |||
const errors: { [P in keyof Values]?: string } = {}; | |||
if (avatar.length > 0 && !isUrl(avatar)) { | |||
errors.avatar = translate('onboarding.create_organization.avatar.error'); | |||
} | |||
if (name.length > 0 && (name.length < 2 || name.length > 64)) { | |||
errors.name = translate('onboarding.create_organization.display_name.error'); | |||
} | |||
if (key.length < 2 || key.length > 32 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(key)) { | |||
errors.key = translate('onboarding.create_organization.organization_name.error'); | |||
} | |||
if (url.length > 0 && !isUrl(url)) { | |||
errors.url = translate('onboarding.create_organization.url.error'); | |||
} | |||
// don't try to check if the organization key is already taken if the key is invalid | |||
if (errors.key) { | |||
return Promise.reject(errors); | |||
} | |||
return this.checkFreeKey(key).then(free => { | |||
if (!free) { | |||
errors.key = translate('onboarding.create_organization.organization_name.taken'); | |||
} | |||
return Object.keys(errors).length ? Promise.reject(errors) : Promise.resolve(errors); | |||
}); | |||
}; | |||
renderInnerForm = (props: ChildrenProps<Values>) => { | |||
const { | |||
dirty, | |||
errors, | |||
handleBlur, | |||
handleChange, | |||
isSubmitting, | |||
isValid, | |||
touched, | |||
values | |||
} = props; | |||
const commonProps = { dirty, isSubmitting, onBlur: handleBlur, onChange: handleChange }; | |||
return ( | |||
<> | |||
<OrganizationDetailsInput | |||
{...commonProps} | |||
description={translate('onboarding.create_organization.organization_name.description')} | |||
error={errors.key} | |||
id="organization-key" | |||
label={translate('onboarding.create_organization.organization_name')} | |||
name="key" | |||
required={true} | |||
touched={touched.key} | |||
value={values.key}> | |||
{props => <input autoFocus={true} {...props} />} | |||
</OrganizationDetailsInput> | |||
<div className="big-spacer-top"> | |||
<ResetButtonLink onClick={this.handleAdditionalClick}> | |||
{translate( | |||
this.state.additional | |||
? 'onboarding.create_organization.hide_additional_info' | |||
: 'onboarding.create_organization.add_additional_info' | |||
)} | |||
<DropdownIcon className="little-spacer-left" turned={this.state.additional} /> | |||
</ResetButtonLink> | |||
</div> | |||
<div className="js-additional-info" hidden={!this.state.additional}> | |||
<div className="big-spacer-top"> | |||
<OrganizationDetailsInput | |||
{...commonProps} | |||
description={translate('onboarding.create_organization.display_name.description')} | |||
error={errors.name} | |||
id="organization-display-name" | |||
label={translate('onboarding.create_organization.display_name')} | |||
name="name" | |||
touched={touched.name && values.name !== ''} | |||
value={values.name}> | |||
{props => <input {...props} />} | |||
</OrganizationDetailsInput> | |||
</div> | |||
<div className="big-spacer-top"> | |||
<OrganizationDetailsInput | |||
{...commonProps} | |||
description={translate('onboarding.create_organization.avatar.description')} | |||
error={errors.avatar} | |||
id="organization-avatar" | |||
label={translate('onboarding.create_organization.avatar')} | |||
name="avatar" | |||
touched={touched.avatar && values.avatar !== ''} | |||
value={values.avatar}> | |||
{props => <input {...props} />} | |||
</OrganizationDetailsInput> | |||
</div> | |||
<div className="big-spacer-top"> | |||
<OrganizationDetailsInput | |||
{...commonProps} | |||
error={errors.description} | |||
id="organization-description" | |||
label={translate('description')} | |||
name="description" | |||
touched={touched.description && values.description !== ''} | |||
value={values.description}> | |||
{props => <textarea {...props} rows={3} />} | |||
</OrganizationDetailsInput> | |||
</div> | |||
<div className="big-spacer-top"> | |||
<OrganizationDetailsInput | |||
{...commonProps} | |||
error={errors.url} | |||
id="organization-url" | |||
label={translate('onboarding.create_organization.url')} | |||
name="url" | |||
touched={touched.url && values.url !== ''} | |||
value={values.url}> | |||
{props => <input {...props} />} | |||
</OrganizationDetailsInput> | |||
</div> | |||
</div> | |||
<div className="big-spacer-top"> | |||
<SubmitButton disabled={isSubmitting || !isValid || !dirty}> | |||
{/* // TODO change me */} | |||
{translate('onboarding.create_organization.page.header')} | |||
</SubmitButton> | |||
</div> | |||
</> | |||
); | |||
}; | |||
renderForm = () => { | |||
return ( | |||
<div className="boxed-group-inner"> | |||
<ValidationForm<Values> | |||
initialValues={initialValues} | |||
onSubmit={this.props.onContinue} | |||
validate={this.handleValidate}> | |||
{this.renderInnerForm} | |||
</ValidationForm> | |||
</div> | |||
); | |||
}; | |||
render() { | |||
return ( | |||
<Step | |||
finished={false} | |||
onOpen={() => {}} | |||
open={true} | |||
renderForm={this.renderForm} | |||
renderResult={() => <div />} | |||
stepNumber={1} | |||
stepTitle={translate('onboarding.create_organization.enter_org_details')} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
* 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 { CreateOrganization } from '../CreateOrganization'; | |||
import { mockRouter } from '../../../../helpers/testUtils'; | |||
it('should render and create organization', async () => { | |||
const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' }); | |||
const router = mockRouter(); | |||
const wrapper = shallow( | |||
// @ts-ignore avoid passing everything from WithRouterProps | |||
<CreateOrganization createOrganization={createOrganization} router={router} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
const organization = { | |||
avatar: 'http://example.com/avatar', | |||
description: 'description-foo', | |||
key: 'key-foo', | |||
name: 'name-foo', | |||
url: 'http://example.com/foo' | |||
}; | |||
wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); | |||
await new Promise(setImmediate); | |||
expect(createOrganization).toBeCalledWith(organization); | |||
expect(router.push).toBeCalledWith('/organizations/foo'); | |||
}); |
@@ -0,0 +1,54 @@ | |||
/* | |||
* 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 OrganizationDetailsInput from '../OrganizationDetailsInput'; | |||
it('should render', () => { | |||
const render = jest.fn().mockReturnValue(<div />); | |||
expect( | |||
shallow( | |||
<OrganizationDetailsInput | |||
dirty={true} | |||
error="This field is bad!" | |||
id="field" | |||
isSubmitting={true} | |||
label="Label" | |||
name="field" | |||
onBlur={jest.fn()} | |||
onChange={jest.fn()} | |||
required={true} | |||
touched={true} | |||
value="foo"> | |||
{render} | |||
</OrganizationDetailsInput> | |||
) | |||
).toMatchSnapshot(); | |||
expect(render).toBeCalledWith( | |||
expect.objectContaining({ | |||
className: 'input-super-large text-middle is-invalid', | |||
disabled: true, | |||
id: 'field', | |||
name: 'field', | |||
type: 'text', | |||
value: 'foo' | |||
}) | |||
); | |||
}); |
@@ -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 { shallow, ShallowWrapper } from 'enzyme'; | |||
import OrganizationDetailsStep from '../OrganizationDetailsStep'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import { getOrganization } from '../../../../api/organizations'; | |||
jest.mock('../../../../api/organizations', () => ({ | |||
getOrganization: jest.fn() | |||
})); | |||
beforeEach(() => { | |||
(getOrganization as jest.Mock).mockResolvedValue(undefined); | |||
}); | |||
it('should render', () => { | |||
const wrapper = shallow(<OrganizationDetailsStep onContinue={jest.fn()} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.dive()).toMatchSnapshot(); | |||
expect(getForm(wrapper)).toMatchSnapshot(); | |||
expect( | |||
getForm(wrapper) | |||
.find('.js-additional-info') | |||
.prop('hidden') | |||
).toBe(true); | |||
click(getForm(wrapper).find('ResetButtonLink')); | |||
wrapper.update(); | |||
expect( | |||
getForm(wrapper) | |||
.find('.js-additional-info') | |||
.prop('hidden') | |||
).toBe(false); | |||
}); | |||
it('should validate', () => { | |||
const wrapper = shallow(<OrganizationDetailsStep onContinue={jest.fn()} />); | |||
const instance = wrapper.instance() as OrganizationDetailsStep; | |||
expect( | |||
instance.handleValidate({ avatar: '', description: '', name: '', key: 'foo', url: '' }) | |||
).resolves.toEqual({}); | |||
expect( | |||
instance.handleValidate({ avatar: '', description: '', name: '', key: '', url: '' }) | |||
).rejects.toEqual({ key: 'onboarding.create_organization.organization_name.error' }); | |||
expect( | |||
instance.handleValidate({ avatar: 'bla', description: '', name: '', key: 'foo', url: '' }) | |||
).rejects.toEqual({ avatar: 'onboarding.create_organization.avatar.error' }); | |||
expect( | |||
instance.handleValidate({ avatar: '', description: '', name: 'x', key: 'foo', url: '' }) | |||
).rejects.toEqual({ name: 'onboarding.create_organization.display_name.error' }); | |||
expect( | |||
instance.handleValidate({ | |||
avatar: '', | |||
description: '', | |||
name: 'x'.repeat(65), | |||
key: 'foo', | |||
url: '' | |||
}) | |||
).rejects.toEqual({ name: 'onboarding.create_organization.display_name.error' }); | |||
expect( | |||
instance.handleValidate({ avatar: '', description: '', name: '', key: 'foo', url: 'bla' }) | |||
).rejects.toEqual({ url: 'onboarding.create_organization.url.error' }); | |||
(getOrganization as jest.Mock).mockResolvedValue({}); | |||
expect( | |||
instance.handleValidate({ avatar: '', description: '', name: '', key: 'foo', url: '' }) | |||
).rejects.toEqual({ key: 'onboarding.create_organization.organization_name.taken' }); | |||
}); | |||
function getForm(wrapper: ShallowWrapper) { | |||
return wrapper | |||
.dive() | |||
.find('ValidationForm') | |||
.dive() | |||
.dive() | |||
.children(); | |||
} |
@@ -0,0 +1,60 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render and create organization 1`] = ` | |||
<React.Fragment> | |||
<HelmetWrapper | |||
defer={true} | |||
encodeSpecialCharacters={true} | |||
title="onboarding.create_organization.page.header" | |||
titleTemplate="%s" | |||
/> | |||
<div | |||
className="sonarcloud page page-limited" | |||
> | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title big-spacer-bottom" | |||
> | |||
onboarding.create_organization.page.header | |||
</h1> | |||
<div | |||
className="page-actions" | |||
> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/" | |||
> | |||
cancel | |||
</Link> | |||
</div> | |||
<p | |||
className="page-description" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.create_organization.page.description" | |||
id="onboarding.create_organization.page.description" | |||
values={ | |||
Object { | |||
"break": <br />, | |||
"more": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/sonarcloud-pricing" | |||
> | |||
learn_more | |||
</Link>, | |||
"price": "€10", | |||
} | |||
} | |||
/> | |||
</p> | |||
</header> | |||
<OrganizationDetailsStep | |||
onContinue={[Function]} | |||
/> | |||
</div> | |||
</React.Fragment> | |||
`; |
@@ -0,0 +1,31 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<div> | |||
<label | |||
htmlFor="field" | |||
> | |||
Label | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<div | |||
className="little-spacer-top spacer-bottom" | |||
> | |||
<div /> | |||
<React.Fragment> | |||
<AlertErrorIcon | |||
className="spacer-left text-middle" | |||
/> | |||
<span | |||
className="little-spacer-left text-danger text-middle" | |||
> | |||
This field is bad! | |||
</span> | |||
</React.Fragment> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,155 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<Step | |||
finished={false} | |||
onOpen={[Function]} | |||
open={true} | |||
renderForm={[Function]} | |||
renderResult={[Function]} | |||
stepNumber={1} | |||
stepTitle="onboarding.create_organization.enter_org_details" | |||
/> | |||
`; | |||
exports[`should render 2`] = ` | |||
<div | |||
className="boxed-group onboarding-step is-open" | |||
> | |||
<div | |||
className="onboarding-step-number" | |||
> | |||
1 | |||
</div> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.create_organization.enter_org_details | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<ValidationForm | |||
initialValues={ | |||
Object { | |||
"avatar": "", | |||
"description": "", | |||
"key": "", | |||
"name": "", | |||
"url": "", | |||
} | |||
} | |||
onSubmit={[MockFunction]} | |||
validate={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render 3`] = ` | |||
<form | |||
onSubmit={[Function]} | |||
> | |||
<React.Fragment> | |||
<OrganizationDetailsInput | |||
description="onboarding.create_organization.organization_name.description" | |||
dirty={false} | |||
id="organization-key" | |||
isSubmitting={false} | |||
label="onboarding.create_organization.organization_name" | |||
name="key" | |||
onBlur={[Function]} | |||
onChange={[Function]} | |||
required={true} | |||
value="" | |||
/> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<ResetButtonLink | |||
onClick={[Function]} | |||
> | |||
onboarding.create_organization.add_additional_info | |||
<DropdownIcon | |||
className="little-spacer-left" | |||
turned={false} | |||
/> | |||
</ResetButtonLink> | |||
</div> | |||
<div | |||
className="js-additional-info" | |||
hidden={true} | |||
> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<OrganizationDetailsInput | |||
description="onboarding.create_organization.display_name.description" | |||
dirty={false} | |||
id="organization-display-name" | |||
isSubmitting={false} | |||
label="onboarding.create_organization.display_name" | |||
name="name" | |||
onBlur={[Function]} | |||
onChange={[Function]} | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<OrganizationDetailsInput | |||
description="onboarding.create_organization.avatar.description" | |||
dirty={false} | |||
id="organization-avatar" | |||
isSubmitting={false} | |||
label="onboarding.create_organization.avatar" | |||
name="avatar" | |||
onBlur={[Function]} | |||
onChange={[Function]} | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<OrganizationDetailsInput | |||
dirty={false} | |||
id="organization-description" | |||
isSubmitting={false} | |||
label="description" | |||
name="description" | |||
onBlur={[Function]} | |||
onChange={[Function]} | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<OrganizationDetailsInput | |||
dirty={false} | |||
id="organization-url" | |||
isSubmitting={false} | |||
label="onboarding.create_organization.url" | |||
name="url" | |||
onBlur={[Function]} | |||
onChange={[Function]} | |||
value="" | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
onboarding.create_organization.page.header | |||
</SubmitButton> | |||
</div> | |||
</React.Fragment> | |||
</form> | |||
`; |
@@ -0,0 +1,53 @@ | |||
/* | |||
* 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, ShallowWrapper } from 'enzyme'; | |||
import { createStore } from 'redux'; | |||
import { whenLoggedIn } from '../whenLoggedIn'; | |||
import { mockRouter } from '../../../../helpers/testUtils'; | |||
class X extends React.Component { | |||
render() { | |||
return <div />; | |||
} | |||
} | |||
const UnderTest = whenLoggedIn(X); | |||
it('should render for logged in user', () => { | |||
const store = createStore(state => state, { users: { currentUser: { isLoggedIn: true } } }); | |||
const wrapper = shallow(<UnderTest />, { context: { store } }); | |||
expect(getRenderedType(wrapper)).toBe(X); | |||
}); | |||
it('should not render for anonymous user', () => { | |||
const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } }); | |||
const router = mockRouter({ replace: jest.fn() }); | |||
const wrapper = shallow(<UnderTest />, { context: { store, router } }); | |||
expect(getRenderedType(wrapper)).toBe(null); | |||
expect(router.replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' })); | |||
}); | |||
function getRenderedType(wrapper: ShallowWrapper) { | |||
return wrapper | |||
.dive() | |||
.dive() | |||
.type(); | |||
} |
@@ -0,0 +1,54 @@ | |||
/* | |||
* 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 { connect } from 'react-redux'; | |||
import { withRouter, WithRouterProps } from 'react-router'; | |||
import { CurrentUser, isLoggedIn } from '../../../app/types'; | |||
import { Store, getCurrentUser } from '../../../store/rootReducer'; | |||
export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) { | |||
class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> { | |||
static displayName = `whenLoggedIn(${WrappedComponent.displayName})`; | |||
componentDidMount() { | |||
if (!isLoggedIn(this.props.currentUser)) { | |||
const returnTo = window.location.pathname + window.location.search + window.location.hash; | |||
this.props.router.replace({ | |||
pathname: '/sessions/new', | |||
query: { return_to: returnTo } // eslint-disable-line camelcase | |||
}); | |||
} | |||
} | |||
render() { | |||
if (isLoggedIn(this.props.currentUser)) { | |||
return <WrappedComponent {...this.props} />; | |||
} else { | |||
return null; | |||
} | |||
} | |||
} | |||
function mapStateToProps(state: Store) { | |||
return { currentUser: getCurrentUser(state) }; | |||
} | |||
return connect(mapStateToProps)(withRouter(Wrapper)); | |||
} |
@@ -20,9 +20,9 @@ | |||
import * as React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import { connect } from 'react-redux'; | |||
import CreateOrganizationForm from '../../account/organizations/CreateOrganizationForm'; | |||
import { Link } from 'react-router'; | |||
import Select from '../../../components/controls/Select'; | |||
import { Button, SubmitButton } from '../../../components/ui/buttons'; | |||
import { SubmitButton } from '../../../components/ui/buttons'; | |||
import { LoggedInUser, Organization } from '../../../app/types'; | |||
import { fetchMyOrganizations } from '../../account/organizations/actions'; | |||
import { getMyOrganizations, Store } from '../../../store/rootReducer'; | |||
@@ -46,7 +46,6 @@ interface OwnProps { | |||
type Props = OwnProps & StateProps & DispatchProps; | |||
interface State { | |||
createOrganizationModal: boolean; | |||
projectName: string; | |||
projectKey: string; | |||
selectedOrganization: string; | |||
@@ -59,7 +58,6 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
createOrganizationModal: false, | |||
projectName: '', | |||
projectKey: '', | |||
selectedOrganization: | |||
@@ -76,10 +74,6 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> { | |||
this.mounted = false; | |||
} | |||
closeCreateOrganization = () => { | |||
this.setState({ createOrganizationModal: false }); | |||
}; | |||
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
@@ -118,22 +112,6 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> { | |||
return Boolean(projectKey && projectName && selectedOrganization); | |||
}; | |||
onCreateOrganization = (organization: { key: string }) => { | |||
this.props.fetchMyOrganizations().then( | |||
() => { | |||
this.handleOrganizationSelect({ value: organization.key }); | |||
this.closeCreateOrganization(); | |||
}, | |||
() => { | |||
this.closeCreateOrganization(); | |||
} | |||
); | |||
}; | |||
showCreateOrganization = () => { | |||
this.setState({ createOrganizationModal: true }); | |||
}; | |||
render() { | |||
const { submitting } = this.state; | |||
return ( | |||
@@ -159,11 +137,9 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> { | |||
required={true} | |||
value={this.state.selectedOrganization} | |||
/> | |||
<Button | |||
className="button-link big-spacer-left js-new-org" | |||
onClick={this.showCreateOrganization}> | |||
<Link className="big-spacer-left js-new-org" to="/create-organization"> | |||
{translate('onboarding.create_project.create_new_org')} | |||
</Button> | |||
</Link> | |||
</div> | |||
<div className="form-field"> | |||
<label htmlFor="project-name"> | |||
@@ -202,12 +178,6 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> { | |||
</SubmitButton> | |||
<DeferredSpinner className="spacer-left" loading={submitting} /> | |||
</form> | |||
{this.state.createOrganizationModal && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeCreateOrganization} | |||
onCreate={this.onCreateOrganization} | |||
/> | |||
)} | |||
</> | |||
); | |||
} |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { ManualProjectCreate } from '../ManualProjectCreate'; | |||
import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { createProject } from '../../../../api/components'; | |||
jest.mock('../../../../api/components', () => ({ | |||
@@ -35,20 +35,6 @@ it('should render correctly', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
}); | |||
it('should allow to create a new org', async () => { | |||
const fetchMyOrganizations = jest.fn().mockResolvedValueOnce([]); | |||
const wrapper = getWrapper({ fetchMyOrganizations }); | |||
click(wrapper.find('.js-new-org')); | |||
const createForm = wrapper.find('Connect(CreateOrganizationForm)'); | |||
expect(createForm.exists()).toBeTruthy(); | |||
createForm.prop<Function>('onCreate')({ key: 'baz' }); | |||
expect(fetchMyOrganizations).toHaveBeenCalled(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('selectedOrganization')).toBe('baz'); | |||
}); | |||
it('should correctly create a project', async () => { | |||
const onProjectCreate = jest.fn(); | |||
const wrapper = getWrapper({ onProjectCreate }); |
@@ -55,12 +55,14 @@ exports[`should render correctly 1`] = ` | |||
required={true} | |||
value="" | |||
/> | |||
<Button | |||
className="button-link big-spacer-left js-new-org" | |||
onClick={[Function]} | |||
<Link | |||
className="big-spacer-left js-new-org" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/create-organization" | |||
> | |||
onboarding.create_project.create_new_org | |||
</Button> | |||
</Link> | |||
</div> | |||
<div | |||
className="form-field" |
@@ -23,7 +23,6 @@ import { connect } from 'react-redux'; | |||
import OnboardingModal from './OnboardingModal'; | |||
import { skipOnboarding } from '../../../api/users'; | |||
import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; | |||
import CreateOrganizationForm from '../../account/organizations/CreateOrganizationForm'; | |||
import TeamOnboardingModal from '../teamOnboarding/TeamOnboardingModal'; | |||
import { Organization } from '../../../app/types'; | |||
@@ -33,7 +32,6 @@ interface DispatchProps { | |||
enum ModalKey { | |||
onboarding, | |||
organizationOnboarding, | |||
teamOnboarding | |||
} | |||
@@ -61,7 +59,7 @@ export class OnboardingPage extends React.PureComponent<DispatchProps, State> { | |||
}; | |||
openOrganizationOnboarding = () => { | |||
this.setState({ modal: ModalKey.organizationOnboarding }); | |||
this.context.router.push('/create-organizations'); | |||
}; | |||
openTeamOnboarding = () => { | |||
@@ -80,12 +78,6 @@ export class OnboardingPage extends React.PureComponent<DispatchProps, State> { | |||
onOpenTeamOnboarding={this.openTeamOnboarding} | |||
/> | |||
)} | |||
{modal === ModalKey.organizationOnboarding && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeOnboarding} | |||
onCreate={this.closeOrganizationOnboarding} | |||
/> | |||
)} | |||
{modal === ModalKey.teamOnboarding && ( | |||
<TeamOnboardingModal onFinish={this.closeOnboarding} /> | |||
)} |
@@ -101,7 +101,7 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { | |||
name="name" | |||
onBlur={handleBlur} | |||
onChange={handleChange} | |||
touched={touched.name !== ''} | |||
touched={touched.name} | |||
type="text" | |||
value={values.name} | |||
/> | |||
@@ -120,7 +120,7 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { | |||
name="url" | |||
onBlur={handleBlur} | |||
onChange={handleChange} | |||
touched={touched.url !== ''} | |||
touched={touched.url} | |||
type="text" | |||
value={values.url} | |||
/> |
@@ -34,7 +34,7 @@ interface Props { | |||
onBlur: (event: React.FocusEvent<any>) => void; | |||
onChange: (event: React.ChangeEvent<any>) => void; | |||
placeholder?: string; | |||
touched: boolean; | |||
touched: boolean | undefined; | |||
type?: string; | |||
value: string; | |||
} |
@@ -28,7 +28,7 @@ interface Props { | |||
dirty: boolean; | |||
error: string | undefined; | |||
label?: React.ReactNode; | |||
touched: boolean; | |||
touched: boolean | undefined; | |||
} | |||
export default function ModalValidationField(props: Props) { | |||
@@ -39,7 +39,7 @@ export default function ModalValidationField(props: Props) { | |||
return ( | |||
<div className="modal-validation-field"> | |||
{props.label} | |||
{props.children({ className: classNames({ 'has-error': showError, 'is-valid': isValid }) })} | |||
{props.children({ className: classNames({ 'is-invalid': showError, 'is-valid': isValid }) })} | |||
{showError && <AlertErrorIcon className="little-spacer-top" />} | |||
{isValid && <AlertSuccessIcon className="little-spacer-top" />} | |||
{showError && <p className="text-danger">{error}</p>} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* 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 { FormikActions, FormikProps, Formik } from 'formik'; | |||
import { Omit } from '../../app/types'; | |||
export type ChildrenProps<V> = Omit<FormikProps<V>, 'handleSubmit'>; | |||
interface Props<V> { | |||
children: (props: ChildrenProps<V>) => React.ReactNode; | |||
initialValues: V; | |||
isInitialValid?: boolean; | |||
onSubmit: (data: V) => Promise<void>; | |||
validate: (data: V) => { [P in keyof V]?: string } | Promise<{ [P in keyof V]?: string }>; | |||
} | |||
export default class ValidationForm<V> extends React.Component<Props<V>> { | |||
handleSubmit = (data: V, { setSubmitting }: FormikActions<V>) => { | |||
const result = this.props.onSubmit(data); | |||
if (result) { | |||
result.then( | |||
() => { | |||
setSubmitting(false); | |||
}, | |||
() => { | |||
setSubmitting(false); | |||
} | |||
); | |||
} else { | |||
setSubmitting(false); | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<Formik<V> | |||
initialValues={this.props.initialValues} | |||
isInitialValid={this.props.isInitialValid} | |||
onSubmit={this.handleSubmit} | |||
validate={this.props.validate}> | |||
{({ handleSubmit, ...props }) => ( | |||
<form onSubmit={handleSubmit}>{this.props.children(props)}</form> | |||
)} | |||
</Formik> | |||
); | |||
} | |||
} |
@@ -18,81 +18,58 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { withFormik, Form, FormikActions, FormikProps } from 'formik'; | |||
import Modal from './Modal'; | |||
import { ResetButtonLink, SubmitButton } from '../ui/buttons'; | |||
import ValidationForm, { ChildrenProps } from './ValidationForm'; | |||
import DeferredSpinner from '../common/DeferredSpinner'; | |||
import { SubmitButton, ResetButtonLink } from '../ui/buttons'; | |||
import { translate } from '../../helpers/l10n'; | |||
interface InnerFormProps<Values> { | |||
children: (props: FormikProps<Values>) => React.ReactNode; | |||
interface Props<V> { | |||
children: (props: ChildrenProps<V>) => React.ReactNode; | |||
confirmButtonText: string; | |||
header: string; | |||
initialValues: Values; | |||
} | |||
interface Props<Values> extends InnerFormProps<Values> { | |||
initialValues: V; | |||
isInitialValid?: boolean; | |||
onClose: () => void; | |||
validate: (data: Values) => void | object | Promise<object>; | |||
onSubmit: (data: Values) => void | Promise<void>; | |||
onSubmit: (data: V) => Promise<void>; | |||
validate: (data: V) => { [P in keyof V]?: string }; | |||
} | |||
export default class ValidationModal<Values> extends React.PureComponent<Props<Values>> { | |||
handleSubmit = (data: Values, { setSubmitting }: FormikActions<Values>) => { | |||
const result = this.props.onSubmit(data); | |||
if (result) { | |||
result.then( | |||
() => { | |||
setSubmitting(false); | |||
this.props.onClose(); | |||
}, | |||
() => { | |||
setSubmitting(false); | |||
} | |||
); | |||
} else { | |||
setSubmitting(false); | |||
export default class ValidationModal<V> extends React.PureComponent<Props<V>> { | |||
handleSubmit = (data: V) => { | |||
return this.props.onSubmit(data).then(() => { | |||
this.props.onClose(); | |||
} | |||
}); | |||
}; | |||
render() { | |||
const { header } = this.props; | |||
const InnerForm = withFormik<InnerFormProps<Values>, Values>({ | |||
handleSubmit: this.handleSubmit, | |||
isInitialValid: this.props.isInitialValid, | |||
mapPropsToValues: props => props.initialValues, | |||
validate: this.props.validate | |||
})(props => ( | |||
<Form> | |||
<div className="modal-head"> | |||
<h2>{props.header}</h2> | |||
</div> | |||
<div className="modal-body">{props.children(props)}</div> | |||
return ( | |||
<Modal contentLabel={this.props.header} onRequestClose={this.props.onClose}> | |||
<ValidationForm | |||
initialValues={this.props.initialValues} | |||
isInitialValid={this.props.isInitialValid} | |||
onSubmit={this.handleSubmit} | |||
validate={this.props.validate}> | |||
{props => ( | |||
<> | |||
<header className="modal-head"> | |||
<h2>{this.props.header}</h2> | |||
</header> | |||
<footer className="modal-foot"> | |||
<DeferredSpinner className="spacer-right" loading={props.isSubmitting} /> | |||
<SubmitButton disabled={props.isSubmitting || !props.isValid || !props.dirty}> | |||
{props.confirmButtonText} | |||
</SubmitButton> | |||
<ResetButtonLink disabled={props.isSubmitting} onClick={this.props.onClose}> | |||
{translate('cancel')} | |||
</ResetButtonLink> | |||
</footer> | |||
</Form> | |||
)); | |||
<div className="modal-body">{this.props.children(props)}</div> | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={this.props.onClose}> | |||
<InnerForm | |||
confirmButtonText={this.props.confirmButtonText} | |||
header={header} | |||
initialValues={this.props.initialValues}> | |||
{this.props.children} | |||
</InnerForm> | |||
<footer className="modal-foot"> | |||
<DeferredSpinner className="spacer-right" loading={props.isSubmitting} /> | |||
<SubmitButton disabled={props.isSubmitting || !props.isValid || !props.dirty}> | |||
{this.props.confirmButtonText} | |||
</SubmitButton> | |||
<ResetButtonLink disabled={props.isSubmitting} onClick={this.props.onClose}> | |||
{translate('cancel')} | |||
</ResetButtonLink> | |||
</footer> | |||
</> | |||
)} | |||
</ValidationForm> | |||
</Modal> | |||
); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 ValidationForm from '../ValidationForm'; | |||
it('should render and submit', async () => { | |||
const render = jest.fn(); | |||
const onSubmit = jest.fn(); | |||
const setSubmitting = jest.fn(); | |||
const wrapper = shallow( | |||
<ValidationForm initialValues={{ foo: 'bar' }} onSubmit={onSubmit} validate={jest.fn()}> | |||
{render} | |||
</ValidationForm> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.dive(); | |||
expect(render).toBeCalledWith( | |||
expect.objectContaining({ dirty: false, errors: {}, values: { foo: 'bar' } }) | |||
); | |||
wrapper.prop<Function>('onSubmit')({ foo: 'bar' }, { setSubmitting }); | |||
expect(setSubmitting).toBeCalledWith(false); | |||
onSubmit.mockResolvedValue(undefined).mockClear(); | |||
setSubmitting.mockClear(); | |||
wrapper.prop<Function>('onSubmit')({ foo: 'bar' }, { setSubmitting }); | |||
await new Promise(setImmediate); | |||
expect(setSubmitting).toBeCalledWith(false); | |||
}); |
@@ -19,49 +19,28 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { FormikProps } from 'formik'; | |||
import ValidationModal from '../ValidationModal'; | |||
it('should render correctly', () => { | |||
const { wrapper, inner } = getWrapper(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(inner).toMatchSnapshot(); | |||
}); | |||
interface Values { | |||
field: string; | |||
} | |||
function getWrapper(props = {}) { | |||
const wrapper = shallow( | |||
<ValidationModal | |||
<ValidationModal<{ field: string }> | |||
confirmButtonText="confirm" | |||
header="title" | |||
initialValues={{ field: 'foo' }} | |||
isInitialValid={true} | |||
onClose={jest.fn()} | |||
onSubmit={jest.fn(() => Promise.resolve())} | |||
validate={(values: Values) => ({ field: values.field.length < 2 && 'Too small' })} | |||
{...props}> | |||
{(props: FormikProps<Values>) => ( | |||
<form onSubmit={props.handleSubmit}> | |||
<input | |||
name="field" | |||
onBlur={props.handleBlur} | |||
onChange={props.handleChange} | |||
type="text" | |||
value={props.values.field} | |||
/> | |||
</form> | |||
onSubmit={jest.fn()} | |||
validate={jest.fn()}> | |||
{props => ( | |||
<input | |||
name="field" | |||
onBlur={props.handleBlur} | |||
onChange={props.handleChange} | |||
type="text" | |||
value={props.values.field} | |||
/> | |||
)} | |||
</ValidationModal> | |||
); | |||
return { | |||
wrapper, | |||
inner: wrapper | |||
.childAt(0) | |||
.dive() | |||
.dive() | |||
.dive() | |||
}; | |||
} | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -25,7 +25,7 @@ exports[`should display the field with an error 1`] = ` | |||
Foo | |||
</label> | |||
<input | |||
className="has-error" | |||
className="is-invalid" | |||
type="text" | |||
/> | |||
<AlertErrorIcon |
@@ -0,0 +1,17 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render and submit 1`] = ` | |||
<Formik | |||
enableReinitialize={false} | |||
initialValues={ | |||
Object { | |||
"foo": "bar", | |||
} | |||
} | |||
isInitialValid={false} | |||
onSubmit={[Function]} | |||
validate={[MockFunction]} | |||
validateOnBlur={true} | |||
validateOnChange={true} | |||
/> | |||
`; |
@@ -5,61 +5,15 @@ exports[`should render correctly 1`] = ` | |||
contentLabel="title" | |||
onRequestClose={[MockFunction]} | |||
> | |||
<C | |||
confirmButtonText="confirm" | |||
header="title" | |||
<ValidationForm | |||
initialValues={ | |||
Object { | |||
"field": "foo", | |||
} | |||
} | |||
isInitialValid={true} | |||
onSubmit={[Function]} | |||
validate={[MockFunction]} | |||
/> | |||
</Modal> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Form> | |||
<div | |||
className="modal-head" | |||
> | |||
<h2> | |||
title | |||
</h2> | |||
</div> | |||
<div | |||
className="modal-body" | |||
> | |||
<form | |||
onSubmit={[Function]} | |||
> | |||
<input | |||
name="field" | |||
onBlur={[Function]} | |||
onChange={[Function]} | |||
type="text" | |||
value="foo" | |||
/> | |||
</form> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
confirm | |||
</SubmitButton> | |||
<ResetButtonLink | |||
disabled={false} | |||
onClick={[MockFunction]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</footer> | |||
</Form> | |||
`; |
@@ -114,3 +114,18 @@ export async function waitAndUpdate(wrapper: ShallowWrapper<any, any> | ReactWra | |||
await new Promise(setImmediate); | |||
wrapper.update(); | |||
} | |||
export function mockRouter(overrides: { push?: Function; replace?: Function } = {}) { | |||
return { | |||
createHref: jest.fn(), | |||
createPath: jest.fn(), | |||
go: jest.fn(), | |||
goBack: jest.fn(), | |||
goForward: jest.fn(), | |||
isActive: jest.fn(), | |||
push: jest.fn(), | |||
replace: jest.fn(), | |||
setRouteLeaveHook: jest.fn(), | |||
...overrides | |||
}; | |||
} |
@@ -2172,6 +2172,13 @@ create-react-class@15.6.3, create-react-class@^15.5.1: | |||
loose-envify "^1.3.1" | |||
object-assign "^4.1.1" | |||
create-react-context@^0.2.2: | |||
version "0.2.3" | |||
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3" | |||
dependencies: | |||
fbjs "^0.8.0" | |||
gud "^1.0.0" | |||
cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0: | |||
version "5.1.0" | |||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" | |||
@@ -2482,6 +2489,10 @@ deep-is@~0.1.3: | |||
version "0.1.3" | |||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" | |||
deepmerge@^2.1.1: | |||
version "2.1.1" | |||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.1.tgz#e862b4e45ea0555072bf51e7fd0d9845170ae768" | |||
default-require-extensions@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" | |||
@@ -3288,6 +3299,18 @@ fb-watchman@^2.0.0: | |||
dependencies: | |||
bser "^2.0.0" | |||
fbjs@^0.8.0: | |||
version "0.8.17" | |||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" | |||
dependencies: | |||
core-js "^1.0.0" | |||
isomorphic-fetch "^2.1.1" | |||
loose-envify "^1.0.0" | |||
object-assign "^4.1.0" | |||
promise "^7.1.1" | |||
setimmediate "^1.0.5" | |||
ua-parser-js "^0.7.18" | |||
fbjs@^0.8.16, fbjs@^0.8.9: | |||
version "0.8.16" | |||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" | |||
@@ -3453,14 +3476,18 @@ form-data@~2.3.1: | |||
combined-stream "^1.0.5" | |||
mime-types "^2.1.12" | |||
formik@0.11.11: | |||
version "0.11.11" | |||
resolved "https://registry.yarnpkg.com/formik/-/formik-0.11.11.tgz#4b02838133c0196b1ef443aa973766cd097ec4a5" | |||
formik@1.2.0: | |||
version "1.2.0" | |||
resolved "https://registry.yarnpkg.com/formik/-/formik-1.2.0.tgz#a0daf8512ce2ec18d88ff59a5bb172b0167e85d1" | |||
dependencies: | |||
create-react-context "^0.2.2" | |||
deepmerge "^2.1.1" | |||
hoist-non-react-statics "^2.5.5" | |||
lodash.clonedeep "^4.5.0" | |||
lodash.isequal "4.5.0" | |||
lodash.topath "4.5.2" | |||
prop-types "^15.5.10" | |||
prop-types "^15.6.1" | |||
react-fast-compare "^1.0.0" | |||
tslib "^1.9.3" | |||
warning "^3.0.0" | |||
forwarded@~0.1.2: | |||
@@ -3699,6 +3726,10 @@ growly@^1.3.0: | |||
version "1.3.0" | |||
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" | |||
gud@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" | |||
gzip-size@3.0.0: | |||
version "3.0.0" | |||
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520" | |||
@@ -3896,6 +3927,10 @@ hoist-non-react-statics@^2.5.0: | |||
version "2.5.0" | |||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40" | |||
hoist-non-react-statics@^2.5.5: | |||
version "2.5.5" | |||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" | |||
home-or-tmp@^2.0.0: | |||
version "2.0.0" | |||
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" | |||
@@ -5232,10 +5267,6 @@ lodash.flattendeep@^4.4.0: | |||
version "4.4.0" | |||
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" | |||
lodash.isequal@4.5.0: | |||
version "4.5.0" | |||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" | |||
lodash.memoize@^4.1.2: | |||
version "4.1.2" | |||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" | |||
@@ -6862,6 +6893,10 @@ react-error-overlay@^4.0.0: | |||
version "4.0.0" | |||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" | |||
react-fast-compare@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-1.0.0.tgz#813a039155e49b43ceffe99528fe5e9d97a6c938" | |||
react-ga@2.5.3: | |||
version "2.5.3" | |||
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.5.3.tgz#0f447c73664c069a5fc341f6f431262e3d4c23c4" | |||
@@ -8258,7 +8293,7 @@ ts-loader@4.3.0: | |||
micromatch "^3.1.4" | |||
semver "^5.0.1" | |||
tslib@^1.9.0: | |||
tslib@^1.9.0, tslib@^1.9.3: | |||
version "1.9.3" | |||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" | |||
@@ -8304,6 +8339,10 @@ typescript@3.0.1: | |||
version "3.0.1" | |||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb" | |||
ua-parser-js@^0.7.18: | |||
version "0.7.18" | |||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" | |||
ua-parser-js@^0.7.9: | |||
version "0.7.17" | |||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" |
@@ -2699,6 +2699,25 @@ onboarding.create_project.project_key=Project key | |||
onboarding.create_project.project_name=Project name | |||
onboarding.create_project.select_repositories=Select repositories | |||
onboarding.create_organization.page.header=Create Organization | |||
onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.{break}To analyze a private project you must subscribe your organization to a paid plan. From {price} a month. {more} | |||
onboarding.create_organization.organization_name=Organization Name | |||
onboarding.create_organization.organization_name.description=2 to 32 characters. All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading). The display name can be specified in the additional info. | |||
onboarding.create_organization.organization_name.error=The provided value doesn't match the expected format. | |||
onboarding.create_organization.organization_name.taken=This name is already taken. | |||
onboarding.create_organization.add_additional_info=Add additional info | |||
onboarding.create_organization.hide_additional_info=Hide additional info | |||
onboarding.create_organization.display_name=Display Name | |||
onboarding.create_organization.display_name.description=2 to 64 characters | |||
onboarding.create_organization.display_name.error=The provided value doesn't match the expected format. | |||
onboarding.create_organization.avatar=Avatar | |||
onboarding.create_organization.avatar.description=Url of a small image that represents the organization (preferably 30px height). | |||
onboarding.create_organization.avatar.error=The value must be a valid url. | |||
onboarding.create_organization.url=URL | |||
onboarding.create_organization.url.error=The value must be a valid url. | |||
onboarding.create_organization.description=Description | |||
onboarding.create_organization.enter_org_details=Enter your organization details | |||
onboarding.team.header=Join a team | |||
onboarding.team.first_step=Well congrats, the first step is done! | |||
onboarding.team.how_to_join=To join a team, the only thing you need to do is to be a user registered on Sonarcloud. The administrator of the Sonarcloud organization you wish to join has to add you to his organization's members {link}. Ask him to do so! |