"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",
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';
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'),
enum ModalKey {
license,
onboarding,
- organizationOnboarding,
projectOnboarding,
teamOnboarding
}
});
};
- 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 = () => {
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 = () => {
{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} />
)}
* 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';
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
</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>
);
}
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>
`;
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;
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);
}
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;
/>
<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} />
+++ /dev/null
-/*
- * 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);
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 {
Store
} from '../../../store/rootReducer';
import { Organization } from '../../../app/types';
-import { Button } from '../../../components/ui/buttons';
interface StateProps {
anyoneCanCreate?: { value: string };
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;
}
};
- openCreateOrganizationForm = () => {
- this.setState({ createOrganization: true });
- };
-
- closeCreateOrganizationForm = () => {
- this.setState({ createOrganization: false });
- };
-
render() {
const anyoneCanCreate =
this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true';
{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>
)}
)}
</div>
</div>
-
- {this.state.createOrganization && (
- <CreateOrganizationForm
- onClose={this.closeCreateOrganizationForm}
- onCreate={this.closeCreateOrganizationForm}
- />
- )}
</div>
);
}
--- /dev/null
+/*
+ * 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))
+);
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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')}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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');
+});
--- /dev/null
+/*
+ * 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'
+ })
+ );
+});
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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));
+}
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';
type Props = OwnProps & StateProps & DispatchProps;
interface State {
- createOrganizationModal: boolean;
projectName: string;
projectKey: string;
selectedOrganization: string;
constructor(props: Props) {
super(props);
this.state = {
- createOrganizationModal: false,
projectName: '',
projectKey: '',
selectedOrganization:
this.mounted = false;
}
- closeCreateOrganization = () => {
- this.setState({ createOrganizationModal: false });
- };
-
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
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 (
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">
</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>
- {this.state.createOrganizationModal && (
- <CreateOrganizationForm
- onClose={this.closeCreateOrganization}
- onCreate={this.onCreateOrganization}
- />
- )}
</>
);
}
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', () => ({
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 });
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"
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';
enum ModalKey {
onboarding,
- organizationOnboarding,
teamOnboarding
}
};
openOrganizationOnboarding = () => {
- this.setState({ modal: ModalKey.organizationOnboarding });
+ this.context.router.push('/create-organizations');
};
openTeamOnboarding = () => {
onOpenTeamOnboarding={this.openTeamOnboarding}
/>
)}
- {modal === ModalKey.organizationOnboarding && (
- <CreateOrganizationForm
- onClose={this.closeOnboarding}
- onCreate={this.closeOrganizationOnboarding}
- />
- )}
{modal === ModalKey.teamOnboarding && (
<TeamOnboardingModal onFinish={this.closeOnboarding} />
)}
name="name"
onBlur={handleBlur}
onChange={handleChange}
- touched={touched.name !== ''}
+ touched={touched.name}
type="text"
value={values.name}
/>
name="url"
onBlur={handleBlur}
onChange={handleChange}
- touched={touched.url !== ''}
+ touched={touched.url}
type="text"
value={values.url}
/>
onBlur: (event: React.FocusEvent<any>) => void;
onChange: (event: React.ChangeEvent<any>) => void;
placeholder?: string;
- touched: boolean;
+ touched: boolean | undefined;
type?: string;
value: string;
}
dirty: boolean;
error: string | undefined;
label?: React.ReactNode;
- touched: boolean;
+ touched: boolean | undefined;
}
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>}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
* 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>
);
}
--- /dev/null
+/*
+ * 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);
+});
*/
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();
+});
Foo
</label>
<input
- className="has-error"
+ className="is-invalid"
type="text"
/>
<AlertErrorIcon
--- /dev/null
+// 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}
+/>
+`;
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>
-`;
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
+ };
+}
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"
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"
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"
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:
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"
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"
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"
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"
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"
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"
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!