Pārlūkot izejas kodu

SONARCLOUD-120 Add new "Create Organization" page (#691)

tags/7.5
Stas Vilchik pirms 5 gadiem
vecāks
revīzija
c003387eb6
37 mainītis faili ar 1301 papildinājumiem un 591 dzēšanām
  1. 1
    1
      server/sonar-web/package.json
  2. 5
    19
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  3. 10
    53
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  4. 14
    5
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
  5. 2
    12
      server/sonar-web/src/main/js/app/styles/components/modals.css
  6. 21
    8
      server/sonar-web/src/main/js/app/styles/init/forms.css
  7. 9
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  8. 0
    252
      server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx
  9. 5
    20
      server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx
  10. 107
    0
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  11. 75
    0
      server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
  12. 216
    0
      server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
  13. 45
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
  14. 54
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx
  15. 101
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
  16. 60
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
  17. 31
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
  18. 155
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
  19. 53
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx
  20. 54
    0
      server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
  21. 4
    34
      server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx
  22. 1
    15
      server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx
  23. 6
    4
      server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
  24. 1
    9
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
  25. 2
    2
      server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
  26. 1
    1
      server/sonar-web/src/main/js/components/controls/InputValidationField.tsx
  27. 2
    2
      server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx
  28. 65
    0
      server/sonar-web/src/main/js/components/controls/ValidationForm.tsx
  29. 36
    59
      server/sonar-web/src/main/js/components/controls/ValidationModal.tsx
  30. 47
    0
      server/sonar-web/src/main/js/components/controls/__tests__/ValidationForm-test.tsx
  31. 13
    34
      server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx
  32. 1
    1
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap
  33. 17
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationForm-test.tsx.snap
  34. 4
    50
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap
  35. 15
    0
      server/sonar-web/src/main/js/helpers/testUtils.ts
  36. 49
    10
      server/sonar-web/yarn.lock
  37. 19
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/package.json Parādīt failu

@@ -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",

+ 5
- 19
server/sonar-web/src/main/js/app/components/StartupModal.tsx Parādīt failu

@@ -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} />
)}

+ 10
- 53
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx Parādīt failu

@@ -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>
);
}

+ 14
- 5
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap Parādīt failu

@@ -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>
`;

+ 2
- 12
server/sonar-web/src/main/js/app/styles/components/modals.css Parādīt failu

@@ -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;

+ 21
- 8
server/sonar-web/src/main/js/app/styles/init/forms.css Parādīt failu

@@ -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);
}


+ 9
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js Parādīt failu

@@ -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} />

+ 0
- 252
server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx Parādīt failu

@@ -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);

+ 5
- 20
server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx Parādīt failu

@@ -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>
);
}

+ 107
- 0
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx Parādīt failu

@@ -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))
);

+ 75
- 0
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx Parādīt failu

@@ -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>
);
}

+ 216
- 0
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx Parādīt failu

@@ -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')}
/>
);
}
}

+ 45
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx Parādīt failu

@@ -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');
});

+ 54
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx Parādīt failu

@@ -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'
})
);
});

+ 101
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx Parādīt failu

@@ -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();
}

+ 60
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap Parādīt failu

@@ -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>
`;

+ 31
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap Parādīt failu

@@ -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>
`;

+ 155
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap Parādīt failu

@@ -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>
`;

+ 53
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx Parādīt failu

@@ -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();
}

+ 54
- 0
server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx Parādīt failu

@@ -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));
}

+ 4
- 34
server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx Parādīt failu

@@ -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}
/>
)}
</>
);
}

+ 1
- 15
server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx Parādīt failu

@@ -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 });

+ 6
- 4
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap Parādīt failu

@@ -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"

+ 1
- 9
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx Parādīt failu

@@ -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} />
)}

+ 2
- 2
server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx Parādīt failu

@@ -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}
/>

+ 1
- 1
server/sonar-web/src/main/js/components/controls/InputValidationField.tsx Parādīt failu

@@ -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;
}

+ 2
- 2
server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx Parādīt failu

@@ -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>}

+ 65
- 0
server/sonar-web/src/main/js/components/controls/ValidationForm.tsx Parādīt failu

@@ -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>
);
}
}

+ 36
- 59
server/sonar-web/src/main/js/components/controls/ValidationModal.tsx Parādīt failu

@@ -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>
);
}

+ 47
- 0
server/sonar-web/src/main/js/components/controls/__tests__/ValidationForm-test.tsx Parādīt failu

@@ -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);
});

+ 13
- 34
server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx Parādīt failu

@@ -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();
});

+ 1
- 1
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap Parādīt failu

@@ -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

+ 17
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationForm-test.tsx.snap Parādīt failu

@@ -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}
/>
`;

+ 4
- 50
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap Parādīt failu

@@ -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>
`;

+ 15
- 0
server/sonar-web/src/main/js/helpers/testUtils.ts Parādīt failu

@@ -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
};
}

+ 49
- 10
server/sonar-web/yarn.lock Parādīt failu

@@ -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"

+ 19
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Parādīt failu

@@ -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!

Notiek ielāde…
Atcelt
Saglabāt