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 }) => ( -
+ {renderFormFields()}
{renderSubmitGroup( @@ -121,12 +126,15 @@ export default class PlanStep extends React.PureComponent { )} ) : ( -
- + + {translate('my_account.create_organization')} {submitting && } -
+ )} )} diff --git a/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx index 199fa8899e9..fcf1b9c415f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import { WithRouterProps, withRouter } from 'react-router'; import { FormattedMessage } from 'react-intl'; import { sortBy } from 'lodash'; @@ -38,6 +39,7 @@ interface Props { almOrganization?: T.AlmOrganization; almUnboundApplications: T.AlmUnboundApplication[]; boundOrganization?: T.OrganizationBase; + className?: string; } interface State { @@ -91,102 +93,108 @@ export class RemoteOrganizationChoose extends React.PureComponent - {almInstallId && - !almOrganization && ( - -
- {translate('onboarding.import_organization.org_not_found')} -
    -
  • {translate('onboarding.import_organization.org_not_found.tips_1')}
  • -
  • {translate('onboarding.import_organization.org_not_found.tips_2')}
  • -
-
-
- )} - {almOrganization && - boundOrganization && ( - - +
+

{translate('onboarding.import_organization.import_org_details')}

+
+
+ {almInstallId && + !almOrganization && ( + +
+ {translate('onboarding.import_organization.org_not_found')} +
    +
  • {translate('onboarding.import_organization.org_not_found.tips_1')}
  • +
  • {translate('onboarding.import_organization.org_not_found.tips_2')}
  • +
+
+
+ )} + {almOrganization && + boundOrganization && ( + + + ), + name: {almOrganization.name}, + boundAvatar: ( + + ), + boundName: {boundOrganization.name} + }} + /> + + )} +
+
+ + {translate( + 'onboarding.import_organization.choose_organization_button', + almApplication.key + )} + +
+ {almUnboundApplications.length > 0 && ( +
+
+
+ {translate('or')} +
+
+
+
+ + o.name.toLowerCase())} - placeholder={translate('onboarding.import_organization.choose_organization')} - value={unboundInstallationId} - valueKey="installationId" - valueRenderer={this.renderOption} - /> -
- - {translate('continue')} - -
-
- )} + )} +
); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx index aaed1cd3a24..d4c4ed73bc8 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx @@ -22,6 +22,7 @@ import { shallow } from 'enzyme'; import AutoOrganizationCreate from '../AutoOrganizationCreate'; import { waitAndUpdate, click } from '../../../../helpers/testUtils'; import { bindAlmOrganization } from '../../../../api/alm-integration'; +import { Step } from '../utils'; jest.mock('../../../../api/alm-integration', () => ({ bindAlmOrganization: jest.fn().mockResolvedValue({}) @@ -35,47 +36,32 @@ const organization = { url: 'http://example.com/foo' }; -it('should render with import org button', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - it('should render prefilled and create org', async () => { const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' }); - const onOrgCreated = jest.fn(); - const wrapper = shallowRender({ - almInstallId: 'id-foo', - almOrganization: { ...organization, personal: false }, - createOrganization, - onOrgCreated - }); + const handleOrgDetailsFinish = jest.fn(); + const wrapper = shallowRender({ createOrganization, handleOrgDetailsFinish }); expect(wrapper).toMatchSnapshot(); wrapper.find('OrganizationDetailsForm').prop('onContinue')(organization); await waitAndUpdate(wrapper); + expect(handleOrgDetailsFinish).toBeCalled(); + wrapper.setProps({ organization }); + wrapper.find('PlanStep').prop('createOrganization')(); expect(createOrganization).toBeCalledWith({ ...organization, installationId: 'id-foo' }); - expect(onOrgCreated).toBeCalledWith('foo'); }); it('should allow to cancel org import', () => { - const updateUrlQuery = jest.fn().mockResolvedValue({ key: 'foo' }); - const wrapper = shallowRender({ - almInstallId: 'id-foo', - almOrganization: { ...organization, personal: false }, - updateUrlQuery - }); + const handleCancelImport = jest.fn().mockResolvedValue({ key: 'foo' }); + const wrapper = shallowRender({ handleCancelImport }); click(wrapper.find('DeleteButton')); - expect(updateUrlQuery).toBeCalledWith({ almInstallId: undefined, almKey: undefined }); + expect(handleCancelImport).toBeCalled(); }); it('should display choice between import or creation', () => { - const wrapper = shallowRender({ - almInstallId: 'id-foo', - almOrganization: { ...organization, personal: false }, - unboundOrganizations: [organization] - }); + const wrapper = shallowRender({ unboundOrganizations: [organization] }); expect(wrapper).toMatchSnapshot(); wrapper.find('RadioToggle').prop('onCheck')('create'); @@ -89,12 +75,7 @@ it('should display choice between import or creation', () => { it('should bind existing organization', async () => { const onOrgCreated = jest.fn(); - const wrapper = shallowRender({ - almInstallId: 'id-foo', - almOrganization: { ...organization, personal: false }, - onOrgCreated, - unboundOrganizations: [organization] - }); + const wrapper = shallowRender({ onOrgCreated, unboundOrganizations: [organization] }); wrapper.find('RadioToggle').prop('onCheck')('bind'); wrapper.update(); @@ -117,11 +98,18 @@ function shallowRender(props: Partial = {}) { key: 'bitbucket', name: 'BitBucket' }} - almUnboundApplications={[]} + almInstallId="id-foo" + almOrganization={{ ...organization, personal: false }} createOrganization={jest.fn()} + handleCancelImport={jest.fn()} + handleOrgDetailsFinish={jest.fn()} + handleOrgDetailsStepOpen={jest.fn()} + onDone={jest.fn()} onOrgCreated={jest.fn()} + onUpgradeFail={jest.fn()} + step={Step.OrganizationDetails} + subscriptionPlans={[{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]} unboundOrganizations={[]} - updateUrlQuery={jest.fn()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx index eeb1de2935d..fbb17c78fe2 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx @@ -21,16 +21,25 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import AutoPersonalOrganizationBind from '../AutoPersonalOrganizationBind'; import { waitAndUpdate, click } from '../../../../helpers/testUtils'; +import { Step } from '../utils'; const personalOrg = { key: 'personalorg', name: 'Personal Org' }; +const almOrganization = { + avatar: 'http://example.com/avatar', + description: 'description-foo', + key: 'key-foo', + name: 'name-foo', + personal: true, + url: 'http://example.com/foo' +}; it('should render correctly', async () => { const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key }); - const onOrgCreated = jest.fn(); + const handleOrgDetailsFinish = jest.fn(); const wrapper = shallowRender({ almInstallId: 'id-foo', importPersonalOrg: personalOrg, - onOrgCreated, + handleOrgDetailsFinish, updateOrganization }); @@ -38,21 +47,23 @@ it('should render correctly', async () => { wrapper.find('OrganizationDetailsForm').prop('onContinue')(personalOrg); await waitAndUpdate(wrapper); + expect(handleOrgDetailsFinish).toBeCalled(); + wrapper.setProps({ organization: personalOrg }); + wrapper.find('PlanStep').prop('createOrganization')(); expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' }); - expect(onOrgCreated).toBeCalledWith(personalOrg.key); }); it('should allow to cancel org import', () => { - const updateUrlQuery = jest.fn(); + const handleCancelImport = jest.fn(); const wrapper = shallowRender({ almInstallId: 'id-foo', importPersonalOrg: personalOrg, - updateUrlQuery + handleCancelImport }); click(wrapper.find('DeleteButton')); - expect(updateUrlQuery).toBeCalledWith({ almInstallId: undefined, almKey: undefined }); + expect(handleCancelImport).toBeCalled(); }); function shallowRender(props: Partial = {}) { @@ -65,18 +76,15 @@ function shallowRender(props: Partial = { key: 'bitbucket', name: 'BitBucket' }} - almOrganization={{ - avatar: 'http://example.com/avatar', - description: 'description-foo', - key: 'key-foo', - name: 'name-foo', - personal: true, - url: 'http://example.com/foo' - }} + almOrganization={almOrganization} + handleCancelImport={jest.fn()} + handleOrgDetailsFinish={jest.fn()} + handleOrgDetailsStepOpen={jest.fn()} importPersonalOrg={{ key: 'personalorg', name: 'Personal Org' }} - onOrgCreated={jest.fn()} + onDone={jest.fn()} + step={Step.OrganizationDetails} + subscriptionPlans={[{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]} updateOrganization={jest.fn()} - updateUrlQuery={jest.fn()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index f4471222b68..8e7cbd3ec60 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -79,6 +79,15 @@ const user: T.LoggedInUser = { showOnboardingTutorial: false }; +const almOrganization = { + avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', + key: 'Foo&Bar', + name: 'Foo & Bar', + personal: true +}; + +const boundOrganization = { key: 'foobar', name: 'Foo & Bar' }; + beforeEach(() => { (getAlmAppInfo as jest.Mock).mockClear(); (getAlmOrganization as jest.Mock).mockClear(); @@ -144,15 +153,26 @@ it('should render with auto personal organization bind page', async () => { expect(wrapper).toMatchSnapshot(); }); -it('should slugify and find a uniq organization key', async () => { +it('should render with organization bind page', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ almOrganization: { - avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', - key: 'Foo&Bar', - name: 'Foo & Bar', - personal: true + key: 'foo', + name: 'Foo', + avatar: 'my-avatar', + personal: false } }); + const wrapper = shallowRender({ + currentUser: { ...user, externalProvider: 'github' }, + location: { query: { installation_id: 'foo' } } as Location + }); + expect(wrapper).toMatchSnapshot(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +it('should slugify and find a uniq organization key', async () => { + (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ almOrganization }); (getOrganizations as jest.Mock).mockResolvedValueOnce({ organizations: [{ key: 'foo-and-bar' }, { key: 'foo-and-bar-1' }] }); @@ -185,9 +205,9 @@ it('should switch tabs', async () => { (wrapper.find('Tabs').prop('onChange') as Function)('manual'); expect(wrapper.find('ManualOrganizationCreate').hasClass('hidden')).toBeFalsy(); - expect(wrapper.find('AutoOrganizationCreate').hasClass('hidden')).toBeTruthy(); + expect(wrapper.find('withRouter(RemoteOrganizationChoose)').hasClass('hidden')).toBeTruthy(); (wrapper.find('Tabs').prop('onChange') as Function)('auto'); - expect(wrapper.find('AutoOrganizationCreate').hasClass('hidden')).toBeFalsy(); + expect(wrapper.find('withRouter(RemoteOrganizationChoose)').hasClass('hidden')).toBeFalsy(); expect(wrapper.find('ManualOrganizationCreate').hasClass('hidden')).toBeTruthy(); }); @@ -195,9 +215,9 @@ it('should reload the alm organization when the url query changes', async () => const wrapper = shallowRender({ currentUser: { ...user, externalProvider: 'github' } }); await waitAndUpdate(wrapper); expect(getAlmOrganization).not.toHaveBeenCalled(); - wrapper.setProps({ location: { query: { installation_id: 'foo' } } }); + wrapper.setProps({ location: { query: { installation_id: 'foo' } } as Location }); expect(getAlmOrganization).toHaveBeenCalledWith({ installationId: 'foo' }); - wrapper.setProps({ location: { query: {} } }); + wrapper.setProps({ location: { query: {} } as Location }); expect(wrapper.state('almOrganization')).toBeUndefined(); expect(listUnboundApplications).toHaveBeenCalledTimes(2); }); @@ -207,14 +227,14 @@ it('should redirect to organization page after creation', async () => { const wrapper = shallowRender({ router: mockRouter({ push }) }); await waitAndUpdate(wrapper); - wrapper.find('ManualOrganizationCreate').prop('onOrgCreated')('foo'); + wrapper.setState({ organization: boundOrganization }); + wrapper.instance().handleOrgCreated('foo'); expect(push).toHaveBeenCalledWith({ pathname: '/organizations/foo', state: { justCreated: true } }); - (get as jest.Mock).mockReturnValueOnce('0'); - wrapper.find('ManualOrganizationCreate').prop('onOrgCreated')('foo', false); + wrapper.instance().handleOrgCreated('foo', false); expect(push).toHaveBeenCalledWith({ pathname: '/organizations/foo', state: { justCreated: false } @@ -227,7 +247,7 @@ it('should redirect to projects creation page after creation', async () => { await waitAndUpdate(wrapper); (get as jest.Mock).mockReturnValueOnce(Date.now().toString()); - wrapper.find('ManualOrganizationCreate').prop('onOrgCreated')('foo'); + wrapper.instance().handleOrgCreated('foo'); expect(get).toHaveBeenCalled(); expect(remove).toHaveBeenCalled(); expect(push).toHaveBeenCalledWith({ @@ -235,9 +255,11 @@ it('should redirect to projects creation page after creation', async () => { state: { organization: 'foo', tab: 'manual' } }); - wrapper.setState({ almOrganization: { key: 'foo', name: 'Foo', avatar: 'my-avatar' } }); + wrapper.setState({ + almOrganization: { key: 'foo', name: 'Foo', avatar: 'my-avatar', personal: false } + }); (get as jest.Mock).mockReturnValueOnce(Date.now().toString()); - wrapper.find('ManualOrganizationCreate').prop('onOrgCreated')('foo'); + wrapper.instance().handleOrgCreated('foo'); expect(push).toHaveBeenCalledWith({ pathname: '/projects/create', state: { organization: 'foo', tab: 'auto' } @@ -246,13 +268,8 @@ it('should redirect to projects creation page after creation', async () => { it('should display AutoOrganizationCreate with already bound organization', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ - almOrganization: { - avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', - key: 'Foo&Bar', - name: 'Foo & Bar', - personal: true - }, - boundOrganization: { key: 'foobar', name: 'Foo & Bar' } + almOrganization: { ...almOrganization, personal: false }, + boundOrganization }); (get as jest.Mock).mockReturnValueOnce(Date.now().toString()); const push = jest.fn(); @@ -266,7 +283,7 @@ it('should display AutoOrganizationCreate with already bound organization', asyn expect(remove).toHaveBeenCalled(); expect(getAlmOrganization).toHaveBeenCalledWith({ installationId: 'foo' }); expect(push).not.toHaveBeenCalled(); - expect(wrapper.find('AutoOrganizationCreate').prop('boundOrganization')).toEqual({ + expect(wrapper.find('withRouter(RemoteOrganizationChoose)').prop('boundOrganization')).toEqual({ key: 'foobar', name: 'Foo & Bar' }); @@ -274,13 +291,8 @@ it('should display AutoOrganizationCreate with already bound organization', asyn it('should redirect to org page when already bound and no binding in progress', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ - almOrganization: { - avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', - key: 'Foo&Bar', - name: 'Foo & Bar', - personal: true - }, - boundOrganization: { key: 'foobar', name: 'Foo & Bar' } + almOrganization, + boundOrganization }); const push = jest.fn(); const wrapper = shallowRender({ @@ -293,8 +305,25 @@ it('should redirect to org page when already bound and no binding in progress', expect(push).toHaveBeenCalledWith({ pathname: '/organizations/foobar' }); }); +it('should roll back after upgrade failure', async () => { + const deleteOrganization = jest.fn(); + const wrapper = shallowRender({ deleteOrganization }); + await waitAndUpdate(wrapper); + wrapper.setState({ organization: boundOrganization }); + wrapper.find('ManualOrganizationCreate').prop('onUpgradeFail')(); + expect(deleteOrganization).toBeCalled(); +}); + +it('should cancel imports', async () => { + const push = jest.fn(); + const wrapper = shallowRender({ router: mockRouter({ push }) }); + await waitAndUpdate(wrapper); + wrapper.instance().handleCancelImport(); + expect(push).toBeCalledWith({ query: {} }); +}); + function shallowRender(props: Partial = {}) { - return shallow( + return shallow( { const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' }); - const onOrgCreated = jest.fn(); - const wrapper = shallowRender({ createOrganization, onOrgCreated }); + const onDone = jest.fn(); + const handleOrgDetailsFinish = jest.fn(); + const wrapper = shallowRender({ createOrganization, handleOrgDetailsFinish, onDone }); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); wrapper.find('OrganizationDetailsForm').prop('onContinue')(organization); await waitAndUpdate(wrapper); + expect(handleOrgDetailsFinish).toHaveBeenCalled(); + wrapper.setProps({ step: Step.Plan }); expect(wrapper).toMatchSnapshot(); - - wrapper.find('PlanStep').prop('onFreePlanChoose')(); - await waitAndUpdate(wrapper); - expect(createOrganization).toBeCalledWith(organization); - expect(onOrgCreated).toBeCalledWith('foo'); }); it('should preselect paid plan', async () => { @@ -57,28 +56,15 @@ it('should preselect paid plan', async () => { expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true); }); -it('should roll back after upgrade failure', async () => { - const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' }); - const deleteOrganization = jest.fn().mockResolvedValue(undefined); - const wrapper = shallowRender({ createOrganization, deleteOrganization }); - await waitAndUpdate(wrapper); - - wrapper.find('OrganizationDetailsForm').prop('onContinue')(organization); - await waitAndUpdate(wrapper); - - wrapper.find('PlanStep').prop('createOrganization')(); - expect(createOrganization).toBeCalledWith(organization); - - wrapper.find('PlanStep').prop('deleteOrganization')(); - expect(deleteOrganization).toBeCalledWith(organization.key); -}); - function shallowRender(props: Partial = {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx index 876a20f0bfd..2334b42871f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx @@ -20,45 +20,46 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import PlanStep from '../PlanStep'; -import { waitAndUpdate, click } from '../../../../helpers/testUtils'; +import { waitAndUpdate, submit } from '../../../../helpers/testUtils'; import { Plan } from '../PlanSelect'; jest.mock('../../../../app/components/extensions/utils', () => ({ getExtensionStart: jest.fn().mockResolvedValue(undefined) })); +const subscriptionPlans = [{ maxNcloc: 1000, price: 100 }]; + it('should render and use free plan', async () => { - const onFreePlanChoose = jest.fn().mockResolvedValue(undefined); + const onDone = jest.fn(); + const createOrganization = jest.fn().mockResolvedValue('org'); const wrapper = shallow( ); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); expect(wrapper.dive()).toMatchSnapshot(); - click(wrapper.dive().find('SubmitButton')); - expect(onFreePlanChoose).toBeCalled(); + submit(wrapper.dive().find('form')); + await waitAndUpdate(wrapper); + expect(createOrganization).toBeCalled(); + expect(onDone).toBeCalled(); }); it('should upgrade', async () => { - const onPaidPlanChoose = jest.fn(); + const onDone = jest.fn(); const wrapper = shallow( ); await waitAndUpdate(wrapper); @@ -73,20 +74,18 @@ it('should upgrade', async () => { .dive() .find('Connect(withCurrentUser(BillingFormShim))') .prop('onCommit')(); - expect(onPaidPlanChoose).toBeCalled(); + expect(onDone).toBeCalled(); }); it('should preselect paid plan', async () => { const wrapper = shallow( ); await waitAndUpdate(wrapper); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap index 985f767d6c7..91da2875d70 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap @@ -1,18 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should display choice between import or creation 1`] = ` -
-
-

- onboarding.import_organization.import_org_details -

-
-
+

-
+ +
`; exports[`should render prefilled and create org 1`] = ` -
-
-

- onboarding.import_organization.import_org_details -

-
-
+

-
-
-`; - -exports[`should render with import org button 1`] = ` -
-
-

- onboarding.import_organization.import_org_details -

-
- +
`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap index b1487a7738b..cece5c58e9a 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap @@ -1,11 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -
-
+
-
-
+ + + `; 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 +
-
- - +
+ + 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