From 1ea65862086353a3bf23d52e9dc7c87effa8a005 Mon Sep 17 00:00:00 2001
From: =?utf8?q?Gr=C3=A9goire=20Aubert?=
Date: Thu, 22 Nov 2018 14:30:49 +0100
Subject: [PATCH] SONARCLOUD-175 Support step to upgrade organization when
importing from ALM
---
.../organization/AutoOrganizationCreate.tsx | 225 +++++++++---------
.../AutoPersonalOrganizationBind.tsx | 74 ++++--
.../organization/CreateOrganization.tsx | 212 +++++++++++------
.../organization/ManualOrganizationCreate.tsx | 105 ++------
.../organization/OrganizationDetailsForm.tsx | 6 +-
.../organization/OrganizationDetailsStep.tsx | 7 +-
.../js/apps/create/organization/PlanStep.tsx | 34 ++-
.../organization/RemoteOrganizationChoose.tsx | 188 ++++++++-------
.../__tests__/AutoOrganizationCreate-test.tsx | 52 ++--
.../AutoPersonalOrganizationBind-test.tsx | 40 ++--
.../__tests__/CreateOrganization-test.tsx | 91 ++++---
.../ManualOrganizationCreate-test.tsx | 36 +--
.../organization/__tests__/PlanStep-test.tsx | 43 ++--
.../AutoOrganizationCreate-test.tsx.snap | 108 ++++-----
...AutoPersonalOrganizationBind-test.tsx.snap | 37 ++-
.../CreateOrganization-test.tsx.snap | 217 ++++++++++++++---
.../ManualOrganizationCreate-test.tsx.snap | 56 +++--
.../__snapshots__/PlanStep-test.tsx.snap | 29 ++-
.../RemoteOrganizationChoose-test.tsx.snap | 194 ++++++++-------
.../main/js/apps/create/organization/utils.ts | 5 +
.../resources/org/sonar/l10n/core.properties | 1 +
21 files changed, 1016 insertions(+), 744 deletions(-)
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
index 06fa37b4e9d..85627bed7ca 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
@@ -18,37 +18,42 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import AutoOrganizationBind from './AutoOrganizationBind';
-import RemoteOrganizationChoose from './RemoteOrganizationChoose';
import OrganizationDetailsForm from './OrganizationDetailsForm';
-import { Query } from './utils';
-import RadioToggle from '../../../components/controls/RadioToggle';
+import OrganizationDetailsStep from './OrganizationDetailsStep';
+import PlanStep from './PlanStep';
+import { Step } from './utils';
import { DeleteButton } from '../../../components/ui/buttons';
+import RadioToggle from '../../../components/controls/RadioToggle';
import { bindAlmOrganization } from '../../../api/alm-integration';
import { sanitizeAlmId } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
-export enum Filters {
+enum Filters {
Bind = 'bind',
Create = 'create'
}
interface Props {
almApplication: T.AlmApplication;
- almInstallId?: string;
- almOrganization?: T.AlmOrganization;
- almUnboundApplications: T.AlmUnboundApplication[];
- boundOrganization?: T.OrganizationBase;
+ almInstallId: string;
+ almOrganization: T.AlmOrganization;
className?: string;
createOrganization: (
- organization: T.OrganizationBase & { installationId?: string }
- ) => Promise;
+ organization: T.Organization & { installationId?: string }
+ ) => Promise;
+ handleCancelImport: () => void;
+ handleOrgDetailsFinish: (organization: T.Organization) => Promise;
+ handleOrgDetailsStepOpen: () => void;
+ onDone: () => void;
onOrgCreated: (organization: string, justCreated?: boolean) => void;
+ onUpgradeFail: () => void;
+ organization?: T.Organization;
+ step: Step;
+ subscriptionPlans?: T.SubscriptionPlan[];
unboundOrganizations: T.Organization[];
- updateUrlQuery: (query: Partial) => void;
}
interface State {
@@ -64,121 +69,117 @@ export default class AutoOrganizationCreate extends React.PureComponent {
- if (this.props.almInstallId) {
- return bindAlmOrganization({
- organization,
- installationId: this.props.almInstallId
- }).then(() => this.props.onOrgCreated(organization, false));
- }
- return Promise.reject();
+ return bindAlmOrganization({
+ organization,
+ installationId: this.props.almInstallId
+ }).then(() => this.props.onOrgCreated(organization, false));
};
- handleCancelImport = () => {
- this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined });
- };
-
- handleCreateOrganization = (organization: Required) => {
- return this.props
- .createOrganization({
- avatar: organization.avatar,
- description: organization.description,
- installationId: this.props.almInstallId,
- key: organization.key,
- name: organization.name || organization.key,
- url: organization.url
- })
- .then(({ key }) => this.props.onOrgCreated(key));
+ handleCreateOrganization = () => {
+ const { organization } = this.props;
+ if (!organization) {
+ return Promise.reject();
+ }
+ return this.props.createOrganization({
+ ...organization,
+ installationId: this.props.almInstallId
+ });
};
handleOptionChange = (filter: Filters) => {
this.setState({ filter });
};
- renderContent = (almOrganization: T.AlmOrganization) => {
- const { almApplication, unboundOrganizations } = this.props;
-
+ render() {
+ const {
+ almApplication,
+ almOrganization,
+ className,
+ organization,
+ step,
+ subscriptionPlans,
+ unboundOrganizations
+ } = this.props;
const { filter } = this.state;
const hasUnboundOrgs = unboundOrganizations.length > 0;
return (
-
-
-
-
- ),
- name: {almOrganization.name}
- }}
- />
-
-
+
+
+
+
+
+ ),
+ name: {almOrganization.name}
+ }}
+ />
+
+
- {hasUnboundOrgs && (
-
+ )}
+
+
+ {filter === Filters.Create && (
+
)}
-
-
- {filter === Filters.Create && (
-
- )}
- {filter === Filters.Bind && (
-
- )}
-
- );
- };
-
- render() {
- const { almInstallId, almOrganization, boundOrganization, className } = this.props;
-
- return (
-
-
-
{translate('onboarding.import_organization.import_org_details')}
-
+ {filter === Filters.Bind && (
+
+ )}
+
- {almInstallId && almOrganization && !boundOrganization ? (
- this.renderContent(almOrganization)
- ) : (
-
- )}
+ {subscriptionPlans !== undefined &&
+ filter !== Filters.Bind && (
+
+ )}
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx
index 875e0b8555b..fb903669336 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx
@@ -20,7 +20,9 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import OrganizationDetailsForm from './OrganizationDetailsForm';
-import { Query } from './utils';
+import OrganizationDetailsStep from './OrganizationDetailsStep';
+import PlanStep from './PlanStep';
+import { Step } from './utils';
import { DeleteButton } from '../../../components/ui/buttons';
import { getBaseUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
@@ -31,37 +33,48 @@ interface Props {
almApplication: T.AlmApplication;
almInstallId?: string;
almOrganization: T.AlmOrganization;
+ handleCancelImport: () => void;
+ handleOrgDetailsFinish: (organization: T.Organization) => Promise
;
+ handleOrgDetailsStepOpen: () => void;
importPersonalOrg: T.Organization;
- onOrgCreated: (organization: string) => void;
+ onDone: () => void;
+ organization?: T.Organization;
+ step: Step;
+ subscriptionPlans?: T.SubscriptionPlan[];
updateOrganization: (
- organization: T.OrganizationBase & { installationId?: string }
- ) => Promise;
- updateUrlQuery: (query: Partial) => void;
+ organization: T.Organization & { installationId?: string }
+ ) => Promise;
}
export default class AutoPersonalOrganizationBind extends React.PureComponent {
- handleCancelImport = () => {
- this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined });
+ handleCreateOrganization = () => {
+ const { organization } = this.props;
+ if (!organization) {
+ return Promise.reject();
+ }
+ return this.props.updateOrganization({
+ ...organization,
+ installationId: this.props.almInstallId
+ });
};
- handleCreateOrganization = (organization: Required) => {
- return this.props
- .updateOrganization({
- avatar: organization.avatar,
- description: organization.description,
- installationId: this.props.almInstallId,
- key: this.props.importPersonalOrg.key,
- name: organization.name || organization.key,
- url: organization.url
- })
- .then(({ key }) => this.props.onOrgCreated(key));
+ handleOrgDetailsFinish = (organization: T.Organization) => {
+ return this.props.handleOrgDetailsFinish({
+ ...organization,
+ key: this.props.importPersonalOrg.key
+ });
};
render() {
- const { almApplication, importPersonalOrg } = this.props;
+ const { almApplication, importPersonalOrg, organization, step, subscriptionPlans } = this.props;
return (
-
-
+ <>
+
{importPersonalOrg.name}
}}
/>
-
+
-
-
+
+ {subscriptionPlans !== undefined && (
+
+ )}
+ >
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
index 1de0e054c9b..df312aa7804 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
@@ -32,12 +32,14 @@ import {
parseQuery,
serializeQuery,
Query,
- ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP
+ ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP,
+ Step
} from './utils';
import AlmApplicationInstalling from './AlmApplicationInstalling';
import AutoOrganizationCreate from './AutoOrganizationCreate';
import AutoPersonalOrganizationBind from './AutoPersonalOrganizationBind';
import ManualOrganizationCreate from './ManualOrganizationCreate';
+import RemoteOrganizationChoose from './RemoteOrganizationChoose';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tabs from '../../../components/controls/Tabs';
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
@@ -63,13 +65,13 @@ import '../../tutorials/styles.css'; // TODO remove me
interface Props {
createOrganization: (
- organization: T.OrganizationBase & { installationId?: string }
- ) => Promise;
+ organization: T.Organization & { installationId?: string }
+ ) => Promise;
currentUser: T.LoggedInUser;
deleteOrganization: (key: string) => Promise;
updateOrganization: (
- organization: T.OrganizationBase & { installationId?: string }
- ) => Promise;
+ organization: T.Organization & { installationId?: string }
+ ) => Promise;
userOrganizations: T.Organization[];
skipOnboarding: () => void;
}
@@ -82,6 +84,7 @@ interface State {
boundOrganization?: T.OrganizationBase;
loading: boolean;
organization?: T.Organization;
+ step: Step;
subscriptionPlans?: T.SubscriptionPlan[];
}
@@ -96,7 +99,12 @@ interface LocationState {
export class CreateOrganization extends React.PureComponent {
mounted = false;
- state: State = { almOrgLoading: false, almUnboundApplications: [], loading: true };
+ state: State = {
+ almOrgLoading: false,
+ almUnboundApplications: [],
+ loading: true,
+ step: Step.OrganizationDetails
+ };
componentDidMount() {
this.mounted = true;
@@ -139,6 +147,12 @@ export class CreateOrganization extends React.PureComponent {
+ if (this.state.organization) {
+ this.props.deleteOrganization(this.state.organization.key);
+ }
+ };
+
fetchAlmApplication = () => {
return getAlmAppInfo().then(({ application }) => {
if (this.mounted) {
@@ -147,35 +161,6 @@ export class CreateOrganization extends React.PureComponent {
- return listUnboundApplications().then(almUnboundApplications => {
- if (this.mounted) {
- this.setState({ almUnboundApplications });
- }
- });
- };
-
- hasAutoImport(state: State, paid?: boolean): state is StateWithAutoImport {
- return Boolean(state.almApplication && !paid);
- }
-
- setValidOrgKey = (almOrganization: T.AlmOrganization) => {
- const key = slugify(almOrganization.key);
- const keys = [key, ...times(9, i => `${key}-${i + 1}`)];
- return api
- .getOrganizations({ organizations: keys.join(',') })
- .then(
- ({ organizations }) => {
- const availableKey = keys.find(key => !organizations.find(o => o.key === key));
- return availableKey || `${key}-${Math.ceil(Math.random() * 1000) + 10}`;
- },
- () => key
- )
- .then(key => {
- return { almOrganization: { ...almOrganization, key } };
- });
- };
-
fetchAlmOrganization = (installationId: string) => {
this.setState({ almOrgLoading: true });
return getAlmOrganization({ installationId })
@@ -209,6 +194,14 @@ export class CreateOrganization extends React.PureComponent {
+ return listUnboundApplications().then(almUnboundApplications => {
+ if (this.mounted) {
+ this.setState({ almUnboundApplications });
+ }
+ });
+ };
+
fetchSubscriptionPlans = () => {
return getSubscriptionPlans().then(subscriptionPlans => {
if (this.mounted) {
@@ -217,6 +210,10 @@ export class CreateOrganization extends React.PureComponent {
+ this.updateUrlQuery({ almInstallId: undefined, almKey: undefined });
+ };
+
handleOrgCreated = (organization: string, justCreated = true) => {
this.props.skipOnboarding();
if (this.isStoredTimestampValid(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP)) {
@@ -232,6 +229,25 @@ export class CreateOrganization extends React.PureComponent {
+ this.setState({ organization, step: Step.Plan });
+ return Promise.resolve();
+ };
+
+ handleOrgDetailsStepOpen = () => {
+ this.setState({ step: Step.OrganizationDetails });
+ };
+
+ handlePlanDone = () => {
+ if (this.state.organization) {
+ this.handleOrgCreated(this.state.organization.key);
+ }
+ };
+
+ hasAutoImport(state: State): state is StateWithAutoImport {
+ return Boolean(state.almApplication);
+ }
+
isStoredTimestampValid = (timestampKey: string) => {
const storedTimestamp = get(timestampKey);
remove(timestampKey);
@@ -242,6 +258,23 @@ export class CreateOrganization extends React.PureComponent {
+ const key = slugify(almOrganization.key);
+ const keys = [key, ...times(9, i => `${key}-${i + 1}`)];
+ return api
+ .getOrganizations({ organizations: keys.join(',') })
+ .then(
+ ({ organizations }) => {
+ const availableKey = keys.find(key => !organizations.find(o => o.key === key));
+ return availableKey || `${key}-${Math.ceil(Math.random() * 1000) + 10}`;
+ },
+ () => key
+ )
+ .then(key => {
+ return { almOrganization: { ...almOrganization, key } };
+ });
+ };
+
stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
@@ -267,66 +300,97 @@ export class CreateOrganization extends React.PureComponent {
const { currentUser, location } = this.props;
const { state } = this;
- const { almOrganization } = state;
+ const { organization, step, subscriptionPlans } = state;
const { paid, tab = 'auto' } = (location.state || {}) as LocationState;
- if (importPersonalOrg && almOrganization && state.almApplication) {
+ const commonProps = {
+ handleOrgDetailsFinish: this.handleOrgDetailsFinish,
+ handleOrgDetailsStepOpen: this.handleOrgDetailsStepOpen,
+ onDone: this.handlePlanDone,
+ organization,
+ step,
+ subscriptionPlans
+ };
+
+ if (!this.hasAutoImport(state)) {
+ return (
+
+ );
+ }
+
+ const { almApplication, almOrganization, boundOrganization } = state;
+
+ if (importPersonalOrg && almOrganization && almApplication) {
return (
);
}
return (
<>
- {this.hasAutoImport(state, paid) && (
-
- onChange={this.onTabChange}
- selected={tab || 'auto'}
- tabs={[
- {
- key: 'auto',
- node: translate('onboarding.import_organization', state.almApplication.key)
- },
- {
- key: 'manual',
- node: translate('onboarding.create_organization.create_manually')
- }
- ]}
- />
- )}
+
+ onChange={this.onTabChange}
+ selected={tab || 'auto'}
+ tabs={[
+ {
+ key: 'auto',
+ node: translate('onboarding.import_organization', almApplication.key)
+ },
+ {
+ key: 'manual',
+ node: translate('onboarding.create_organization.create_manually')
+ }
+ ]}
+ />
- {this.hasAutoImport(state, paid) && (
+ {almInstallId && almOrganization && !boundOrganization ? (
!alm && key !== currentUser.personalOrganization && actions.admin
)}
- updateUrlQuery={this.updateUrlQuery}
+ />
+ ) : (
+
)}
>
@@ -387,18 +451,18 @@ export class CreateOrganization extends React.PureComponent {
- return api.createOrganization(organization).then((organization: T.Organization) => {
- dispatch(actions.createOrganization(organization));
- return organization;
- });
+ return api
+ .createOrganization({ ...organization, name: organization.name || organization.key })
+ .then((organization: T.Organization) => {
+ dispatch(actions.createOrganization(organization));
+ return organization.key;
+ });
};
}
-function updateOrganization(
- organization: T.OrganizationBase & { key: string; installationId?: string }
-) {
+function updateOrganization(organization: T.Organization & { installationId?: string }) {
return (dispatch: Dispatch) => {
const { key, installationId, ...changes } = organization;
const promises = [api.updateOrganization(key, changes)];
@@ -407,7 +471,7 @@ function updateOrganization(
}
return Promise.all(promises).then(() => {
dispatch(actions.updateOrganization(key, changes));
- return organization;
+ return organization.key;
});
};
}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx
index ea69f4ad3a5..45afee9f63c 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx
@@ -21,113 +21,54 @@ import * as React from 'react';
import OrganizationDetailsForm from './OrganizationDetailsForm';
import OrganizationDetailsStep from './OrganizationDetailsStep';
import PlanStep from './PlanStep';
-import { formatPrice } from './utils';
+import { Step } from './utils';
import { translate } from '../../../helpers/l10n';
interface Props {
- createOrganization: (organization: T.OrganizationBase) => Promise;
+ createOrganization: (organization: T.Organization) => Promise;
className?: string;
- deleteOrganization: (key: string) => Promise;
- onOrgCreated: (organization: string) => void;
+ onUpgradeFail: () => void;
+ handleOrgDetailsFinish: (organization: T.Organization) => Promise;
+ handleOrgDetailsStepOpen: () => void;
+ onDone: () => void;
onlyPaid?: boolean;
- subscriptionPlans?: T.SubscriptionPlan[];
-}
-
-enum Step {
- OrganizationDetails,
- Plan
-}
-
-interface State {
organization?: T.Organization;
step: Step;
+ subscriptionPlans?: T.SubscriptionPlan[];
}
-export default class ManualOrganizationCreate extends React.PureComponent {
- mounted = false;
- state: State = { step: Step.OrganizationDetails };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleOrganizationDetailsStepOpen = () => {
- this.setState({ step: Step.OrganizationDetails });
- };
-
- handleOrganizationDetailsFinish = (organization: Required) => {
- this.setState({ organization, step: Step.Plan });
- return Promise.resolve();
- };
-
- handlePaidPlanChoose = () => {
- if (this.state.organization) {
- this.props.onOrgCreated(this.state.organization.key);
- }
- };
-
- handleFreePlanChoose = () => {
- return this.createOrganization().then(key => {
- this.props.onOrgCreated(key);
- });
- };
-
- createOrganization = () => {
- const { organization } = this.state;
- if (organization) {
- return this.props
- .createOrganization({
- avatar: organization.avatar,
- description: organization.description,
- key: organization.key,
- name: organization.name || organization.key,
- url: organization.url
- })
- .then(({ key }) => key);
- } else {
+export default class ManualOrganizationCreate extends React.PureComponent {
+ handleCreateOrganization = () => {
+ const { organization } = this.props;
+ if (!organization) {
return Promise.reject();
}
- };
-
- deleteOrganization = () => {
- const { organization } = this.state;
- if (organization) {
- this.props.deleteOrganization(organization.key).catch(() => {});
- }
+ return this.props.createOrganization(organization);
};
render() {
- const { className, subscriptionPlans } = this.props;
- const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
- const formattedPrice = formatPrice(startedPrice);
-
+ const { className, organization, subscriptionPlans } = this.props;
return (
+ finished={organization !== undefined}
+ onOpen={this.props.handleOrgDetailsStepOpen}
+ open={this.props.step === Step.OrganizationDetails}
+ organization={organization}>
{subscriptionPlans !== undefined && (
)}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
index a3dbc4e49a9..835d31f912b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
@@ -32,8 +32,8 @@ type RequiredOrganization = Required
;
interface Props {
keyReadOnly?: boolean;
- onContinue: (organization: RequiredOrganization) => Promise;
- organization?: T.OrganizationBase & { key: string };
+ onContinue: (organization: T.Organization) => Promise;
+ organization?: T.Organization;
submitText: string;
}
@@ -108,7 +108,7 @@ export default class OrganizationDetailsForm extends React.PureComponent) => {
+ handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const { state } = this;
if (this.canSubmit(state)) {
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
index 978b7faa200..acd98cf23ad 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
@@ -27,7 +27,8 @@ interface Props {
finished: boolean;
onOpen: () => void;
open: boolean;
- organization?: T.OrganizationBase & { key: string };
+ organization?: T.Organization;
+ stepTitle?: string;
}
export default class OrganizationDetailsStep extends React.PureComponent {
renderForm = () => {
@@ -53,7 +54,9 @@ export default class OrganizationDetailsStep extends React.PureComponent
renderForm={this.renderForm}
renderResult={this.renderResult}
stepNumber={1}
- stepTitle={translate('onboarding.create_organization.enter_org_details')}
+ stepTitle={
+ this.props.stepTitle || translate('onboarding.create_organization.enter_org_details')
+ }
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
index ffb191a0e68..59009a44b98 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import BillingFormShim from './BillingFormShim';
import PlanSelect, { Plan } from './PlanSelect';
+import { formatPrice } from './utils';
import Step from '../../tutorials/components/Step';
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { translate } from '../../../helpers/l10n';
@@ -31,12 +32,10 @@ const BillingForm = withCurrentUser(BillingFormShim);
interface Props {
createOrganization: () => Promise;
- deleteOrganization: () => void;
- onFreePlanChoose: () => Promise;
- onPaidPlanChoose: () => void;
+ onDone: () => void;
+ onUpgradeFail?: () => void;
onlyPaid?: boolean;
open: boolean;
- startingPrice: string;
subscriptionPlans: T.SubscriptionPlan[];
}
@@ -84,13 +83,19 @@ export default class PlanStep extends React.PureComponent {
}
};
- handleFreePlanSubmit = () => {
+ handleFreePlanSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
this.setState({ submitting: true });
- this.props.onFreePlanChoose().then(this.stopSubmitting, this.stopSubmitting);
+ return this.props.createOrganization().then(() => {
+ this.props.onDone();
+ this.stopSubmitting();
+ }, this.stopSubmitting);
};
renderForm = () => {
const { submitting } = this.state;
+ const { subscriptionPlans } = this.props;
+ const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
return (
{this.state.ready && (
@@ -99,18 +104,18 @@ export default class PlanStep extends React.PureComponent
{
)}
{this.state.plan === Plan.Paid ? (
{({ onSubmit, renderFormFields, renderSubmitGroup }) => (
-
+
+
+
`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
index 86a76be9310..e69bd5536b6 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
@@ -45,6 +45,9 @@ exports[`should render with auto personal organization bind page 2`] = `
"personal": true,
}
}
+ handleCancelImport={[Function]}
+ handleOrgDetailsFinish={[Function]}
+ handleOrgDetailsStepOpen={[Function]}
importPersonalOrg={
Object {
"actions": Object {
@@ -54,9 +57,21 @@ exports[`should render with auto personal organization bind page 2`] = `
"name": "Foo",
}
}
- onOrgCreated={[Function]}
+ onDone={[Function]}
+ step={0}
+ subscriptionPlans={
+ Array [
+ Object {
+ "maxNcloc": 100000,
+ "price": 10,
+ },
+ Object {
+ "maxNcloc": 250000,
+ "price": 75,
+ },
+ ]
+ }
updateOrganization={[MockFunction]}
- updateUrlQuery={[Function]}
/>
@@ -123,8 +138,11 @@ exports[`should render with auto tab displayed 1`] = `
-
@@ -236,8 +240,11 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
@@ -336,10 +359,12 @@ exports[`should render with manual tab displayed 1`] = `
`;
-exports[`should switch tabs 1`] = `
+exports[`should render with organization bind page 1`] = `
+
+`;
+
+exports[`should render with organization bind page 2`] = `
+
+
+`;
+
+exports[`should switch tabs 1`] = `
+
+
+
+
+
+ onboarding.create_organization.page.header
+
+
+ ,
+ "more":
+ learn_more
+ ,
+ "price": "billing.price_format.10",
+ }
+ }
+ />
+
+
+
+
+
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
index d538064e72c..c9fab655504 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
@@ -4,21 +4,19 @@ exports[`should render and create organization 1`] = `
@@ -84,18 +91,19 @@ exports[`should render and use free plan 2`] = `
-
my_account.create_organization
-
+
@@ -126,13 +134,20 @@ exports[`should upgrade 1`] = `
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
index 92c500b06e7..83c6ba9b429 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
@@ -62,90 +62,101 @@ exports[`should display an alert message 1`] = `
exports[`should display unbound installations 1`] = `
+
+ onboarding.import_organization.import_org_details
+
+
+
-
- onboarding.import_organization.choose_organization_button.github
-
-
-
-
-
- or
-
-
+ onboarding.import_organization.choose_organization_button.github
+
-
-
+
- onboarding.import_organization.choose_unbound_installation.github
-
-
+
-
- continue
-
-
+
+
+ onboarding.import_organization.choose_unbound_installation.github
+
+
+
+
+ continue
+
+
+
@@ -153,31 +164,42 @@ exports[`should display unbound installations 1`] = `
exports[`should render 1`] = `
+
+ onboarding.import_organization.import_org_details
+
+
+
-
- onboarding.import_organization.choose_organization_button.github
-
+
+ onboarding.import_organization.choose_organization_button.github
+
+
diff --git a/server/sonar-web/src/main/js/apps/create/organization/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/utils.ts
index 29d09d82fb1..bc2688be093 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/utils.ts
+++ b/server/sonar-web/src/main/js/apps/create/organization/utils.ts
@@ -35,6 +35,11 @@ export const ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP =
export const ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP =
'sonarcloud.import_org.redirect_to_projects';
+export enum Step {
+ OrganizationDetails,
+ Plan
+}
+
export function formatPrice(price?: number, noSign?: boolean) {
const priceFormatted = formatMeasure(price, 'FLOAT')
.replace(/[.|,]0$/, '')
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index c4cc99db4ad..5bca60ee0ee 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2798,6 +2798,7 @@ onboarding.import_organization.installing=Finalize installation of the ALM appli
onboarding.import_organization.installing.bitbucket=Finalize installation of the Bitbucket application..
onboarding.import_organization.installing.github=Finalize installation of the GitHub application...
onboarding.import_organization.personal.page.header=Bind to your personal organization
+onboarding.import_organization.personal.import_org_details=Import personal organization details
onboarding.import_organization.private.disabled=Selecting private repository is not available yet and will come soon. Meanwhile, you need to create the project manually.
onboarding.import_organization.bitbucket=Import from BitBucket teams
onboarding.import_organization.github=Import from GitHub organizations
--
2.39.5