@@ -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<T.Organization>; | |||
organization: T.Organization & { installationId?: string } | |||
) => Promise<string>; | |||
handleCancelImport: () => void; | |||
handleOrgDetailsFinish: (organization: T.Organization) => Promise<void>; | |||
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<Query>) => void; | |||
} | |||
interface State { | |||
@@ -64,121 +69,117 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S | |||
} | |||
handleBindOrganization = (organization: string) => { | |||
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<T.OrganizationBase>) => { | |||
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 ( | |||
<div className="boxed-group-inner"> | |||
<div className="huge-spacer-bottom"> | |||
<p className="display-flex-center big-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.import_organization_x')} | |||
id="onboarding.import_organization_x" | |||
values={{ | |||
avatar: ( | |||
<img | |||
alt={almApplication.name} | |||
className="little-spacer-left" | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId( | |||
almApplication.key | |||
)}.svg`} | |||
width={16} | |||
/> | |||
), | |||
name: <strong>{almOrganization.name}</strong> | |||
}} | |||
/> | |||
<DeleteButton className="little-spacer-left" onClick={this.handleCancelImport} /> | |||
</p> | |||
<div className={className}> | |||
<OrganizationDetailsStep | |||
finished={organization !== undefined} | |||
onOpen={this.props.handleOrgDetailsStepOpen} | |||
open={step === Step.OrganizationDetails} | |||
organization={organization} | |||
stepTitle={translate('onboarding.import_organization.import_org_details')}> | |||
<div className="huge-spacer-bottom"> | |||
<p className="display-flex-center big-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.import_organization_x')} | |||
id="onboarding.import_organization_x" | |||
values={{ | |||
avatar: ( | |||
<img | |||
alt={almApplication.name} | |||
className="little-spacer-left" | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId( | |||
almApplication.key | |||
)}.svg`} | |||
width={16} | |||
/> | |||
), | |||
name: <strong>{almOrganization.name}</strong> | |||
}} | |||
/> | |||
<DeleteButton | |||
className="little-spacer-left" | |||
onClick={this.props.handleCancelImport} | |||
/> | |||
</p> | |||
{hasUnboundOrgs && ( | |||
<RadioToggle | |||
name="filter" | |||
onCheck={this.handleOptionChange} | |||
options={[ | |||
{ | |||
label: translate('onboarding.import_organization.create_new'), | |||
value: Filters.Create | |||
}, | |||
{ | |||
label: translate('onboarding.import_organization.bind_existing'), | |||
value: Filters.Bind | |||
} | |||
]} | |||
value={filter} | |||
{hasUnboundOrgs && ( | |||
<RadioToggle | |||
name="filter" | |||
onCheck={this.handleOptionChange} | |||
options={[ | |||
{ | |||
label: translate('onboarding.import_organization.create_new'), | |||
value: Filters.Create | |||
}, | |||
{ | |||
label: translate('onboarding.import_organization.bind_existing'), | |||
value: Filters.Bind | |||
} | |||
]} | |||
value={filter} | |||
/> | |||
)} | |||
</div> | |||
{filter === Filters.Create && ( | |||
<OrganizationDetailsForm | |||
onContinue={this.props.handleOrgDetailsFinish} | |||
organization={almOrganization} | |||
submitText={translate('continue')} | |||
/> | |||
)} | |||
</div> | |||
{filter === Filters.Create && ( | |||
<OrganizationDetailsForm | |||
onContinue={this.handleCreateOrganization} | |||
organization={almOrganization} | |||
submitText={translate('onboarding.import_organization.import')} | |||
/> | |||
)} | |||
{filter === Filters.Bind && ( | |||
<AutoOrganizationBind | |||
onBindOrganization={this.handleBindOrganization} | |||
unboundOrganizations={unboundOrganizations} | |||
/> | |||
)} | |||
</div> | |||
); | |||
}; | |||
render() { | |||
const { almInstallId, almOrganization, boundOrganization, className } = this.props; | |||
return ( | |||
<div className={classNames('boxed-group', className)}> | |||
<div className="boxed-group-header"> | |||
<h2>{translate('onboarding.import_organization.import_org_details')}</h2> | |||
</div> | |||
{filter === Filters.Bind && ( | |||
<AutoOrganizationBind | |||
onBindOrganization={this.handleBindOrganization} | |||
unboundOrganizations={unboundOrganizations} | |||
/> | |||
)} | |||
</OrganizationDetailsStep> | |||
{almInstallId && almOrganization && !boundOrganization ? ( | |||
this.renderContent(almOrganization) | |||
) : ( | |||
<RemoteOrganizationChoose | |||
almApplication={this.props.almApplication} | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
almUnboundApplications={this.props.almUnboundApplications} | |||
boundOrganization={boundOrganization} | |||
/> | |||
)} | |||
{subscriptionPlans !== undefined && | |||
filter !== Filters.Bind && ( | |||
<PlanStep | |||
createOrganization={this.handleCreateOrganization} | |||
onDone={this.props.onDone} | |||
onUpgradeFail={this.props.onUpgradeFail} | |||
onlyPaid={false /* TODO */} | |||
open={step === Step.Plan} | |||
subscriptionPlans={subscriptionPlans} | |||
/> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -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<void>; | |||
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<T.Organization>; | |||
updateUrlQuery: (query: Partial<Query>) => void; | |||
organization: T.Organization & { installationId?: string } | |||
) => Promise<string>; | |||
} | |||
export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> { | |||
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<T.OrganizationBase>) => { | |||
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 ( | |||
<div className="boxed-group"> | |||
<div className="boxed-group-inner"> | |||
<> | |||
<OrganizationDetailsStep | |||
finished={organization !== undefined} | |||
onOpen={this.props.handleOrgDetailsStepOpen} | |||
open={step === Step.OrganizationDetails} | |||
organization={organization} | |||
stepTitle={translate('onboarding.import_organization.personal.import_org_details')}> | |||
<div className="display-flex-center big-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.import_personal_organization_x')} | |||
@@ -84,16 +97,25 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr | |||
personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong> | |||
}} | |||
/> | |||
<DeleteButton className="little-spacer-left" onClick={this.handleCancelImport} /> | |||
<DeleteButton className="little-spacer-left" onClick={this.props.handleCancelImport} /> | |||
</div> | |||
<OrganizationDetailsForm | |||
keyReadOnly={true} | |||
onContinue={this.handleCreateOrganization} | |||
onContinue={this.handleOrgDetailsFinish} | |||
organization={importPersonalOrg} | |||
submitText={translate('onboarding.import_organization.bind')} | |||
submitText={translate('continue')} | |||
/> | |||
</div> | |||
</div> | |||
</OrganizationDetailsStep> | |||
{subscriptionPlans !== undefined && ( | |||
<PlanStep | |||
createOrganization={this.handleCreateOrganization} | |||
onDone={this.props.onDone} | |||
onlyPaid={false /* TODO */} | |||
open={step === Step.Plan} | |||
subscriptionPlans={subscriptionPlans} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -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<T.Organization>; | |||
organization: T.Organization & { installationId?: string } | |||
) => Promise<string>; | |||
currentUser: T.LoggedInUser; | |||
deleteOrganization: (key: string) => Promise<void>; | |||
updateOrganization: ( | |||
organization: T.OrganizationBase & { installationId?: string } | |||
) => Promise<T.Organization>; | |||
organization: T.Organization & { installationId?: string } | |||
) => Promise<string>; | |||
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<Props & WithRouterProps, State> { | |||
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<Props & WithRouterPr | |||
} | |||
} | |||
deleteOrganization = () => { | |||
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<Props & WithRouterPr | |||
}); | |||
}; | |||
fetchAlmUnboundApplications = () => { | |||
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<Props & WithRouterPr | |||
); | |||
}; | |||
fetchAlmUnboundApplications = () => { | |||
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<Props & WithRouterPr | |||
}); | |||
}; | |||
handleCancelImport = () => { | |||
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<Props & WithRouterPr | |||
} | |||
}; | |||
handleOrgDetailsFinish = (organization: T.Organization) => { | |||
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<Props & WithRouterPr | |||
this.updateUrlState({ tab }); | |||
}; | |||
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 } }; | |||
}); | |||
}; | |||
stopLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
@@ -267,66 +300,97 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
renderContent = (almInstallId?: string, importPersonalOrg?: T.Organization) => { | |||
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 ( | |||
<ManualOrganizationCreate | |||
{...commonProps} | |||
createOrganization={this.props.createOrganization} | |||
onUpgradeFail={this.deleteOrganization} | |||
onlyPaid={paid} | |||
organization={this.state.organization} | |||
step={this.state.step} | |||
/> | |||
); | |||
} | |||
const { almApplication, almOrganization, boundOrganization } = state; | |||
if (importPersonalOrg && almOrganization && almApplication) { | |||
return ( | |||
<AutoPersonalOrganizationBind | |||
almApplication={state.almApplication} | |||
{...commonProps} | |||
almApplication={almApplication} | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
handleCancelImport={this.handleCancelImport} | |||
importPersonalOrg={importPersonalOrg} | |||
onOrgCreated={this.handleOrgCreated} | |||
subscriptionPlans={subscriptionPlans} | |||
updateOrganization={this.props.updateOrganization} | |||
updateUrlQuery={this.updateUrlQuery} | |||
/> | |||
); | |||
} | |||
return ( | |||
<> | |||
{this.hasAutoImport(state, paid) && ( | |||
<Tabs<TabKeys> | |||
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') | |||
} | |||
]} | |||
/> | |||
)} | |||
<Tabs<TabKeys> | |||
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') | |||
} | |||
]} | |||
/> | |||
<ManualOrganizationCreate | |||
className={classNames({ hidden: tab !== 'manual' && this.hasAutoImport(state, paid) })} | |||
{...commonProps} | |||
className={classNames({ hidden: tab !== 'manual' && this.hasAutoImport(state) })} | |||
createOrganization={this.props.createOrganization} | |||
deleteOrganization={this.props.deleteOrganization} | |||
onOrgCreated={this.handleOrgCreated} | |||
onUpgradeFail={this.deleteOrganization} | |||
onlyPaid={paid} | |||
subscriptionPlans={this.state.subscriptionPlans} | |||
/> | |||
{this.hasAutoImport(state, paid) && ( | |||
{almInstallId && almOrganization && !boundOrganization ? ( | |||
<AutoOrganizationCreate | |||
almApplication={state.almApplication} | |||
{...commonProps} | |||
almApplication={almApplication} | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
almUnboundApplications={this.state.almUnboundApplications} | |||
boundOrganization={this.state.boundOrganization} | |||
className={classNames({ hidden: tab !== 'auto' })} | |||
createOrganization={this.props.createOrganization} | |||
handleCancelImport={this.handleCancelImport} | |||
onOrgCreated={this.handleOrgCreated} | |||
onUpgradeFail={this.deleteOrganization} | |||
unboundOrganizations={this.props.userOrganizations.filter( | |||
({ actions = {}, alm, key }) => | |||
!alm && key !== currentUser.personalOrganization && actions.admin | |||
)} | |||
updateUrlQuery={this.updateUrlQuery} | |||
/> | |||
) : ( | |||
<RemoteOrganizationChoose | |||
almApplication={almApplication} | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
almUnboundApplications={state.almUnboundApplications} | |||
boundOrganization={boundOrganization} | |||
className={classNames({ hidden: tab !== 'auto' })} | |||
/> | |||
)} | |||
</> | |||
@@ -387,18 +451,18 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
} | |||
} | |||
function createOrganization(organization: T.OrganizationBase & { installationId?: string }) { | |||
function createOrganization(organization: T.Organization & { installationId?: string }) { | |||
return (dispatch: Dispatch) => { | |||
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; | |||
}); | |||
}; | |||
} |
@@ -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<T.Organization>; | |||
createOrganization: (organization: T.Organization) => Promise<string>; | |||
className?: string; | |||
deleteOrganization: (key: string) => Promise<void>; | |||
onOrgCreated: (organization: string) => void; | |||
onUpgradeFail: () => void; | |||
handleOrgDetailsFinish: (organization: T.Organization) => Promise<void>; | |||
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<Props, State> { | |||
mounted = false; | |||
state: State = { step: Step.OrganizationDetails }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleOrganizationDetailsStepOpen = () => { | |||
this.setState({ step: Step.OrganizationDetails }); | |||
}; | |||
handleOrganizationDetailsFinish = (organization: Required<T.OrganizationBase>) => { | |||
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<Props> { | |||
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 ( | |||
<div className={className}> | |||
<OrganizationDetailsStep | |||
finished={this.state.organization !== undefined} | |||
onOpen={this.handleOrganizationDetailsStepOpen} | |||
open={this.state.step === Step.OrganizationDetails} | |||
organization={this.state.organization}> | |||
finished={organization !== undefined} | |||
onOpen={this.props.handleOrgDetailsStepOpen} | |||
open={this.props.step === Step.OrganizationDetails} | |||
organization={organization}> | |||
<OrganizationDetailsForm | |||
onContinue={this.handleOrganizationDetailsFinish} | |||
organization={this.state.organization} | |||
onContinue={this.props.handleOrgDetailsFinish} | |||
organization={organization} | |||
submitText={translate('continue')} | |||
/> | |||
</OrganizationDetailsStep> | |||
{subscriptionPlans !== undefined && ( | |||
<PlanStep | |||
createOrganization={this.createOrganization} | |||
deleteOrganization={this.deleteOrganization} | |||
onFreePlanChoose={this.handleFreePlanChoose} | |||
onPaidPlanChoose={this.handlePaidPlanChoose} | |||
createOrganization={this.handleCreateOrganization} | |||
onDone={this.props.onDone} | |||
onUpgradeFail={this.props.onUpgradeFail} | |||
onlyPaid={this.props.onlyPaid} | |||
open={this.state.step === Step.Plan} | |||
startingPrice={formattedPrice} | |||
open={this.props.step === Step.Plan} | |||
subscriptionPlans={subscriptionPlans} | |||
/> | |||
)} |
@@ -32,8 +32,8 @@ type RequiredOrganization = Required<T.OrganizationBase>; | |||
interface Props { | |||
keyReadOnly?: boolean; | |||
onContinue: (organization: RequiredOrganization) => Promise<void>; | |||
organization?: T.OrganizationBase & { key: string }; | |||
onContinue: (organization: T.Organization) => Promise<void>; | |||
organization?: T.Organization; | |||
submitText: string; | |||
} | |||
@@ -108,7 +108,7 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props, | |||
this.setState({ url }); | |||
}; | |||
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { | |||
handleSubmit = (event: React.FormEvent) => { | |||
event.preventDefault(); | |||
const { state } = this; | |||
if (this.canSubmit(state)) { |
@@ -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<Props> { | |||
renderForm = () => { | |||
@@ -53,7 +54,9 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props> | |||
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') | |||
} | |||
/> | |||
); | |||
} |
@@ -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<string>; | |||
deleteOrganization: () => void; | |||
onFreePlanChoose: () => Promise<void>; | |||
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<Props, State> { | |||
} | |||
}; | |||
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 ( | |||
<div className="boxed-group-inner"> | |||
{this.state.ready && ( | |||
@@ -99,18 +104,18 @@ export default class PlanStep extends React.PureComponent<Props, State> { | |||
<PlanSelect | |||
onChange={this.handlePlanChange} | |||
plan={this.state.plan} | |||
startingPrice={this.props.startingPrice} | |||
startingPrice={formatPrice(startedPrice)} | |||
/> | |||
)} | |||
{this.state.plan === Plan.Paid ? ( | |||
<BillingForm | |||
onCommit={this.props.onPaidPlanChoose} | |||
onFailToUpgrade={this.props.deleteOrganization} | |||
onCommit={this.props.onDone} | |||
onFailToUpgrade={this.props.onUpgradeFail} | |||
organizationKey={this.props.createOrganization} | |||
subscriptionPlans={this.props.subscriptionPlans}> | |||
{({ onSubmit, renderFormFields, renderSubmitGroup }) => ( | |||
<form onSubmit={onSubmit}> | |||
<form id="organization-paid-plan-form" onSubmit={onSubmit}> | |||
{renderFormFields()} | |||
<div className="billing-input-large big-spacer-top"> | |||
{renderSubmitGroup( | |||
@@ -121,12 +126,15 @@ export default class PlanStep extends React.PureComponent<Props, State> { | |||
)} | |||
</BillingForm> | |||
) : ( | |||
<div className="display-flex-center big-spacer-top"> | |||
<SubmitButton disabled={submitting} onClick={this.handleFreePlanSubmit}> | |||
<form | |||
className="display-flex-center big-spacer-top" | |||
id="organization-free-plan-form" | |||
onSubmit={this.handleFreePlanSubmit}> | |||
<SubmitButton disabled={submitting}> | |||
{translate('my_account.create_organization')} | |||
</SubmitButton> | |||
{submitting && <DeferredSpinner className="spacer-left" />} | |||
</div> | |||
</form> | |||
)} | |||
</> | |||
)} |
@@ -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<Props & WithRo | |||
almInstallId, | |||
almOrganization, | |||
almUnboundApplications, | |||
boundOrganization | |||
boundOrganization, | |||
className | |||
} = this.props; | |||
const { unboundInstallationId } = this.state; | |||
return ( | |||
<div className="boxed-group-inner"> | |||
{almInstallId && | |||
!almOrganization && ( | |||
<Alert className="big-spacer-bottom width-60" variant="error"> | |||
<div className="markdown"> | |||
{translate('onboarding.import_organization.org_not_found')} | |||
<ul> | |||
<li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li> | |||
<li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li> | |||
</ul> | |||
</div> | |||
</Alert> | |||
)} | |||
{almOrganization && | |||
boundOrganization && ( | |||
<Alert className="big-spacer-bottom width-60" variant="error"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.import_organization.already_bound_x')} | |||
id="onboarding.import_organization.already_bound_x" | |||
values={{ | |||
avatar: ( | |||
<img | |||
alt={almApplication.name} | |||
className="little-spacer-left" | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId( | |||
<div className={classNames('boxed-group', className)}> | |||
<div className="boxed-group-header"> | |||
<h2>{translate('onboarding.import_organization.import_org_details')}</h2> | |||
</div> | |||
<div className="boxed-group-inner"> | |||
{almInstallId && | |||
!almOrganization && ( | |||
<Alert className="big-spacer-bottom width-60" variant="error"> | |||
<div className="markdown"> | |||
{translate('onboarding.import_organization.org_not_found')} | |||
<ul> | |||
<li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li> | |||
<li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li> | |||
</ul> | |||
</div> | |||
</Alert> | |||
)} | |||
{almOrganization && | |||
boundOrganization && ( | |||
<Alert className="big-spacer-bottom width-60" variant="error"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.import_organization.already_bound_x')} | |||
id="onboarding.import_organization.already_bound_x" | |||
values={{ | |||
avatar: ( | |||
<img | |||
alt={almApplication.name} | |||
className="little-spacer-left" | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId( | |||
almApplication.key | |||
)}.svg`} | |||
width={16} | |||
/> | |||
), | |||
name: <strong>{almOrganization.name}</strong>, | |||
boundAvatar: ( | |||
<OrganizationAvatar | |||
className="little-spacer-left" | |||
organization={boundOrganization} | |||
small={true} | |||
/> | |||
), | |||
boundName: <strong>{boundOrganization.name}</strong> | |||
}} | |||
/> | |||
</Alert> | |||
)} | |||
<div className="display-flex-center"> | |||
<div className="display-inline-block"> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={almApplication} | |||
onClick={this.handleInstallAppClick} | |||
small={true} | |||
url={almApplication.installationUrl}> | |||
{translate( | |||
'onboarding.import_organization.choose_organization_button', | |||
almApplication.key | |||
)} | |||
</IdentityProviderLink> | |||
</div> | |||
{almUnboundApplications.length > 0 && ( | |||
<div className="display-flex-stretch"> | |||
<div className="vertical-pipe-separator"> | |||
<div className="vertical-separator " /> | |||
<span className="note">{translate('or')}</span> | |||
<div className="vertical-separator" /> | |||
</div> | |||
<form className="big-spacer-top big-spacer-bottom" onSubmit={this.handleSubmit}> | |||
<div className="form-field abs-width-400"> | |||
<label htmlFor="select-unbound-installation"> | |||
{translate( | |||
'onboarding.import_organization.choose_unbound_installation', | |||
almApplication.key | |||
)}.svg`} | |||
width={16} | |||
)} | |||
</label> | |||
<Select | |||
className="input-super-large" | |||
clearable={false} | |||
id="select-unbound-installation" | |||
labelKey="name" | |||
onChange={this.handleInstallationChange} | |||
optionRenderer={this.renderOption} | |||
options={sortBy(almUnboundApplications, o => o.name.toLowerCase())} | |||
placeholder={translate('onboarding.import_organization.choose_organization')} | |||
value={unboundInstallationId} | |||
valueKey="installationId" | |||
valueRenderer={this.renderOption} | |||
/> | |||
), | |||
name: <strong>{almOrganization.name}</strong>, | |||
boundAvatar: ( | |||
<OrganizationAvatar | |||
className="little-spacer-left" | |||
organization={boundOrganization} | |||
small={true} | |||
/> | |||
), | |||
boundName: <strong>{boundOrganization.name}</strong> | |||
}} | |||
/> | |||
</Alert> | |||
)} | |||
<div className="display-flex-center"> | |||
<div className="display-inline-block"> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={almApplication} | |||
onClick={this.handleInstallAppClick} | |||
small={true} | |||
url={almApplication.installationUrl}> | |||
{translate( | |||
'onboarding.import_organization.choose_organization_button', | |||
almApplication.key | |||
)} | |||
</IdentityProviderLink> | |||
</div> | |||
{almUnboundApplications.length > 0 && ( | |||
<div className="display-flex-stretch"> | |||
<div className="vertical-pipe-separator"> | |||
<div className="vertical-separator " /> | |||
<span className="note">{translate('or')}</span> | |||
<div className="vertical-separator" /> | |||
</div> | |||
<SubmitButton disabled={!unboundInstallationId}> | |||
{translate('continue')} | |||
</SubmitButton> | |||
</form> | |||
</div> | |||
<form className="big-spacer-top big-spacer-bottom" onSubmit={this.handleSubmit}> | |||
<div className="form-field abs-width-400"> | |||
<label htmlFor="select-unbound-installation"> | |||
{translate( | |||
'onboarding.import_organization.choose_unbound_installation', | |||
almApplication.key | |||
)} | |||
</label> | |||
<Select | |||
className="input-super-large" | |||
clearable={false} | |||
id="select-unbound-installation" | |||
labelKey="name" | |||
onChange={this.handleInstallationChange} | |||
optionRenderer={this.renderOption} | |||
options={sortBy(almUnboundApplications, o => o.name.toLowerCase())} | |||
placeholder={translate('onboarding.import_organization.choose_organization')} | |||
value={unboundInstallationId} | |||
valueKey="installationId" | |||
valueRenderer={this.renderOption} | |||
/> | |||
</div> | |||
<SubmitButton disabled={!unboundInstallationId}> | |||
{translate('continue')} | |||
</SubmitButton> | |||
</form> | |||
</div> | |||
)} | |||
)} | |||
</div> | |||
</div> | |||
</div> | |||
); |
@@ -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<Function>('onContinue')(organization); | |||
await waitAndUpdate(wrapper); | |||
expect(handleOrgDetailsFinish).toBeCalled(); | |||
wrapper.setProps({ organization }); | |||
wrapper.find('PlanStep').prop<Function>('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<Function>('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<Function>('onCheck')('bind'); | |||
wrapper.update(); | |||
@@ -117,11 +98,18 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { | |||
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} | |||
/> | |||
); |
@@ -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<Function>('onContinue')(personalOrg); | |||
await waitAndUpdate(wrapper); | |||
expect(handleOrgDetailsFinish).toBeCalled(); | |||
wrapper.setProps({ organization: personalOrg }); | |||
wrapper.find('PlanStep').prop<Function>('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<AutoPersonalOrganizationBind['props']> = {}) { | |||
@@ -65,18 +76,15 @@ function shallowRender(props: Partial<AutoPersonalOrganizationBind['props']> = { | |||
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} | |||
/> | |||
); |
@@ -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<any>).mockClear(); | |||
(getAlmOrganization as jest.Mock<any>).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<any>).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<any>).mockResolvedValueOnce({ almOrganization }); | |||
(getOrganizations as jest.Mock<any>).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<Function>('onOrgCreated')('foo'); | |||
wrapper.setState({ organization: boundOrganization }); | |||
wrapper.instance().handleOrgCreated('foo'); | |||
expect(push).toHaveBeenCalledWith({ | |||
pathname: '/organizations/foo', | |||
state: { justCreated: true } | |||
}); | |||
(get as jest.Mock<any>).mockReturnValueOnce('0'); | |||
wrapper.find('ManualOrganizationCreate').prop<Function>('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<any>).mockReturnValueOnce(Date.now().toString()); | |||
wrapper.find('ManualOrganizationCreate').prop<Function>('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<any>).mockReturnValueOnce(Date.now().toString()); | |||
wrapper.find('ManualOrganizationCreate').prop<Function>('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<any>).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<any>).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<any>).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<Function>('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<CreateOrganization['props']> = {}) { | |||
return shallow( | |||
return shallow<CreateOrganization>( | |||
<CreateOrganization | |||
createOrganization={jest.fn()} | |||
currentUser={user} |
@@ -21,6 +21,7 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ManualOrganizationCreate from '../ManualOrganizationCreate'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { Step } from '../utils'; | |||
const organization = { | |||
avatar: 'http://example.com/avatar', | |||
@@ -32,20 +33,18 @@ const organization = { | |||
it('should render and create organization', async () => { | |||
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<Function>('onContinue')(organization); | |||
await waitAndUpdate(wrapper); | |||
expect(handleOrgDetailsFinish).toHaveBeenCalled(); | |||
wrapper.setProps({ step: Step.Plan }); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find('PlanStep').prop<Function>('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<Function>('onContinue')(organization); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('PlanStep').prop<Function>('createOrganization')(); | |||
expect(createOrganization).toBeCalledWith(organization); | |||
wrapper.find('PlanStep').prop<Function>('deleteOrganization')(); | |||
expect(deleteOrganization).toBeCalledWith(organization.key); | |||
}); | |||
function shallowRender(props: Partial<ManualOrganizationCreate['props']> = {}) { | |||
return shallow( | |||
<ManualOrganizationCreate | |||
createOrganization={jest.fn()} | |||
deleteOrganization={jest.fn()} | |||
onOrgCreated={jest.fn()} | |||
handleOrgDetailsFinish={jest.fn()} | |||
handleOrgDetailsStepOpen={jest.fn()} | |||
onDone={jest.fn()} | |||
onUpgradeFail={jest.fn()} | |||
step={Step.OrganizationDetails} | |||
subscriptionPlans={[{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]} | |||
{...props} | |||
/> |
@@ -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( | |||
<PlanStep | |||
createOrganization={jest.fn().mockResolvedValue('org')} | |||
deleteOrganization={jest.fn().mockResolvedValue(undefined)} | |||
onFreePlanChoose={onFreePlanChoose} | |||
onPaidPlanChoose={jest.fn()} | |||
createOrganization={createOrganization} | |||
onDone={onDone} | |||
onUpgradeFail={jest.fn()} | |||
open={true} | |||
startingPrice="10" | |||
subscriptionPlans={[]} | |||
subscriptionPlans={subscriptionPlans} | |||
/> | |||
); | |||
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( | |||
<PlanStep | |||
createOrganization={jest.fn().mockResolvedValue('org')} | |||
deleteOrganization={jest.fn().mockResolvedValue(undefined)} | |||
onFreePlanChoose={jest.fn().mockResolvedValue(undefined)} | |||
onPaidPlanChoose={onPaidPlanChoose} | |||
onDone={onDone} | |||
onUpgradeFail={jest.fn()} | |||
open={true} | |||
startingPrice="10" | |||
subscriptionPlans={[]} | |||
subscriptionPlans={subscriptionPlans} | |||
/> | |||
); | |||
await waitAndUpdate(wrapper); | |||
@@ -73,20 +74,18 @@ it('should upgrade', async () => { | |||
.dive() | |||
.find('Connect(withCurrentUser(BillingFormShim))') | |||
.prop<Function>('onCommit')(); | |||
expect(onPaidPlanChoose).toBeCalled(); | |||
expect(onDone).toBeCalled(); | |||
}); | |||
it('should preselect paid plan', async () => { | |||
const wrapper = shallow( | |||
<PlanStep | |||
createOrganization={jest.fn()} | |||
deleteOrganization={jest.fn().mockResolvedValue(undefined)} | |||
onFreePlanChoose={jest.fn().mockResolvedValue(undefined)} | |||
onPaidPlanChoose={jest.fn()} | |||
onDone={jest.fn()} | |||
onUpgradeFail={jest.fn()} | |||
onlyPaid={true} | |||
open={true} | |||
startingPrice="10" | |||
subscriptionPlans={[]} | |||
subscriptionPlans={subscriptionPlans} | |||
/> | |||
); | |||
await waitAndUpdate(wrapper); |
@@ -1,18 +1,12 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display choice between import or creation 1`] = ` | |||
<div | |||
className="boxed-group" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.import_organization.import_org_details | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
<div> | |||
<OrganizationDetailsStep | |||
finished={false} | |||
onOpen={[MockFunction]} | |||
open={true} | |||
stepTitle="onboarding.import_organization.import_org_details" | |||
> | |||
<div | |||
className="huge-spacer-bottom" | |||
@@ -39,7 +33,7 @@ exports[`should display choice between import or creation 1`] = ` | |||
/> | |||
<DeleteButton | |||
className="little-spacer-left" | |||
onClick={[Function]} | |||
onClick={[MockFunction]} | |||
/> | |||
</p> | |||
<RadioToggle | |||
@@ -61,23 +55,36 @@ exports[`should display choice between import or creation 1`] = ` | |||
value={null} | |||
/> | |||
</div> | |||
</div> | |||
</OrganizationDetailsStep> | |||
<PlanStep | |||
createOrganization={[Function]} | |||
onDone={[MockFunction]} | |||
onUpgradeFail={[MockFunction]} | |||
onlyPaid={false} | |||
open={false} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 100000, | |||
"price": 10, | |||
}, | |||
Object { | |||
"maxNcloc": 250000, | |||
"price": 75, | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render prefilled and create org 1`] = ` | |||
<div | |||
className="boxed-group" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.import_organization.import_org_details | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
<div> | |||
<OrganizationDetailsStep | |||
finished={false} | |||
onOpen={[MockFunction]} | |||
open={true} | |||
stepTitle="onboarding.import_organization.import_org_details" | |||
> | |||
<div | |||
className="huge-spacer-bottom" | |||
@@ -104,12 +111,12 @@ exports[`should render prefilled and create org 1`] = ` | |||
/> | |||
<DeleteButton | |||
className="little-spacer-left" | |||
onClick={[Function]} | |||
onClick={[MockFunction]} | |||
/> | |||
</p> | |||
</div> | |||
<OrganizationDetailsForm | |||
onContinue={[Function]} | |||
onContinue={[MockFunction]} | |||
organization={ | |||
Object { | |||
"avatar": "http://example.com/avatar", | |||
@@ -120,34 +127,27 @@ exports[`should render prefilled and create org 1`] = ` | |||
"url": "http://example.com/foo", | |||
} | |||
} | |||
submitText="onboarding.import_organization.import" | |||
submitText="continue" | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render with import org button 1`] = ` | |||
<div | |||
className="boxed-group" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.import_organization.import_org_details | |||
</h2> | |||
</div> | |||
<withRouter(RemoteOrganizationChoose) | |||
almApplication={ | |||
Object { | |||
"backgroundColor": "#0052CC", | |||
"iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"", | |||
"installationUrl": "https://bitbucket.org/install/app", | |||
"key": "bitbucket", | |||
"name": "BitBucket", | |||
} | |||
</OrganizationDetailsStep> | |||
<PlanStep | |||
createOrganization={[Function]} | |||
onDone={[MockFunction]} | |||
onUpgradeFail={[MockFunction]} | |||
onlyPaid={false} | |||
open={false} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 100000, | |||
"price": 10, | |||
}, | |||
Object { | |||
"maxNcloc": 250000, | |||
"price": 75, | |||
}, | |||
] | |||
} | |||
almUnboundApplications={Array []} | |||
/> | |||
</div> | |||
`; |
@@ -1,11 +1,12 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="boxed-group" | |||
> | |||
<div | |||
className="boxed-group-inner" | |||
<Fragment> | |||
<OrganizationDetailsStep | |||
finished={false} | |||
onOpen={[MockFunction]} | |||
open={true} | |||
stepTitle="onboarding.import_organization.personal.import_org_details" | |||
> | |||
<div | |||
className="display-flex-center big-spacer-bottom" | |||
@@ -41,7 +42,7 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
<DeleteButton | |||
className="little-spacer-left" | |||
onClick={[Function]} | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
<OrganizationDetailsForm | |||
@@ -53,8 +54,26 @@ exports[`should render correctly 1`] = ` | |||
"name": "Personal Org", | |||
} | |||
} | |||
submitText="onboarding.import_organization.bind" | |||
submitText="continue" | |||
/> | |||
</div> | |||
</div> | |||
</OrganizationDetailsStep> | |||
<PlanStep | |||
createOrganization={[Function]} | |||
onDone={[MockFunction]} | |||
onlyPaid={false} | |||
open={false} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 100000, | |||
"price": 10, | |||
}, | |||
Object { | |||
"maxNcloc": 250000, | |||
"price": 75, | |||
}, | |||
] | |||
} | |||
/> | |||
</Fragment> | |||
`; |
@@ -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]} | |||
/> | |||
</div> | |||
</Fragment> | |||
@@ -123,8 +138,11 @@ exports[`should render with auto tab displayed 1`] = ` | |||
<ManualOrganizationCreate | |||
className="hidden" | |||
createOrganization={[MockFunction]} | |||
deleteOrganization={[MockFunction]} | |||
onOrgCreated={[Function]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
@@ -138,7 +156,7 @@ exports[`should render with auto tab displayed 1`] = ` | |||
] | |||
} | |||
/> | |||
<AutoOrganizationCreate | |||
<withRouter(RemoteOrganizationChoose) | |||
almApplication={ | |||
Object { | |||
"backgroundColor": "blue", | |||
@@ -150,20 +168,6 @@ exports[`should render with auto tab displayed 1`] = ` | |||
} | |||
almUnboundApplications={Array []} | |||
className="" | |||
createOrganization={[MockFunction]} | |||
onOrgCreated={[Function]} | |||
unboundOrganizations={ | |||
Array [ | |||
Object { | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
] | |||
} | |||
updateUrlQuery={[Function]} | |||
/> | |||
</div> | |||
</Fragment> | |||
@@ -236,8 +240,11 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` | |||
<ManualOrganizationCreate | |||
className="hidden" | |||
createOrganization={[MockFunction]} | |||
deleteOrganization={[MockFunction]} | |||
onOrgCreated={[Function]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
@@ -272,10 +279,27 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` | |||
"url": "https://www.sonarsource.com", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
className="" | |||
createOrganization={[MockFunction]} | |||
handleCancelImport={[Function]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onOrgCreated={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 100000, | |||
"price": 10, | |||
}, | |||
Object { | |||
"maxNcloc": 250000, | |||
"price": 75, | |||
}, | |||
] | |||
} | |||
unboundOrganizations={ | |||
Array [ | |||
Object { | |||
@@ -287,7 +311,6 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` | |||
}, | |||
] | |||
} | |||
updateUrlQuery={[Function]} | |||
/> | |||
</div> | |||
</Fragment> | |||
@@ -336,10 +359,12 @@ exports[`should render with manual tab displayed 1`] = ` | |||
</p> | |||
</header> | |||
<ManualOrganizationCreate | |||
className="" | |||
createOrganization={[MockFunction]} | |||
deleteOrganization={[MockFunction]} | |||
onOrgCreated={[Function]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
@@ -357,7 +382,13 @@ exports[`should render with manual tab displayed 1`] = ` | |||
</Fragment> | |||
`; | |||
exports[`should switch tabs 1`] = ` | |||
exports[`should render with organization bind page 1`] = ` | |||
<AlmApplicationInstalling | |||
almKey="github" | |||
/> | |||
`; | |||
exports[`should render with organization bind page 2`] = ` | |||
<Fragment> | |||
<HelmetWrapper | |||
defer={true} | |||
@@ -418,8 +449,11 @@ exports[`should switch tabs 1`] = ` | |||
<ManualOrganizationCreate | |||
className="hidden" | |||
createOrganization={[MockFunction]} | |||
deleteOrganization={[MockFunction]} | |||
onOrgCreated={[Function]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
@@ -443,10 +477,36 @@ exports[`should switch tabs 1`] = ` | |||
"name": "GitHub", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
almInstallId="foo" | |||
almOrganization={ | |||
Object { | |||
"avatar": "my-avatar", | |||
"key": "foo", | |||
"name": "Foo", | |||
"personal": false, | |||
} | |||
} | |||
className="" | |||
createOrganization={[MockFunction]} | |||
handleCancelImport={[Function]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onOrgCreated={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 100000, | |||
"price": 10, | |||
}, | |||
Object { | |||
"maxNcloc": 250000, | |||
"price": 75, | |||
}, | |||
] | |||
} | |||
unboundOrganizations={ | |||
Array [ | |||
Object { | |||
@@ -458,7 +518,102 @@ exports[`should switch tabs 1`] = ` | |||
}, | |||
] | |||
} | |||
updateUrlQuery={[Function]} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should switch tabs 1`] = ` | |||
<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> | |||
<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 {}} | |||
target="_blank" | |||
to="/documentation/sonarcloud-pricing/" | |||
> | |||
learn_more | |||
</Link>, | |||
"price": "billing.price_format.10", | |||
} | |||
} | |||
/> | |||
</p> | |||
</header> | |||
<Tabs | |||
onChange={[Function]} | |||
selected="auto" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"key": "auto", | |||
"node": "onboarding.import_organization.github", | |||
}, | |||
Object { | |||
"key": "manual", | |||
"node": "onboarding.create_organization.create_manually", | |||
}, | |||
] | |||
} | |||
/> | |||
<ManualOrganizationCreate | |||
className="hidden" | |||
createOrganization={[MockFunction]} | |||
handleOrgDetailsFinish={[Function]} | |||
handleOrgDetailsStepOpen={[Function]} | |||
onDone={[Function]} | |||
onUpgradeFail={[Function]} | |||
step={0} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 100000, | |||
"price": 10, | |||
}, | |||
Object { | |||
"maxNcloc": 250000, | |||
"price": 75, | |||
}, | |||
] | |||
} | |||
/> | |||
<withRouter(RemoteOrganizationChoose) | |||
almApplication={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.installation.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
className="" | |||
/> | |||
</div> | |||
</Fragment> |
@@ -4,21 +4,19 @@ exports[`should render and create organization 1`] = ` | |||
<div> | |||
<OrganizationDetailsStep | |||
finished={false} | |||
onOpen={[Function]} | |||
onOpen={[MockFunction]} | |||
open={true} | |||
> | |||
<OrganizationDetailsForm | |||
onContinue={[Function]} | |||
onContinue={[MockFunction]} | |||
submitText="continue" | |||
/> | |||
</OrganizationDetailsStep> | |||
<PlanStep | |||
createOrganization={[Function]} | |||
deleteOrganization={[Function]} | |||
onFreePlanChoose={[Function]} | |||
onPaidPlanChoose={[Function]} | |||
onDone={[MockFunction]} | |||
onUpgradeFail={[MockFunction]} | |||
open={false} | |||
startingPrice="billing.price_format.10" | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
@@ -38,28 +36,30 @@ exports[`should render and create organization 1`] = ` | |||
exports[`should render and create organization 2`] = ` | |||
<div> | |||
<OrganizationDetailsStep | |||
finished={true} | |||
onOpen={[Function]} | |||
finished={false} | |||
onOpen={[MockFunction]} | |||
open={false} | |||
organization={ | |||
Object { | |||
"avatar": "http://example.com/avatar", | |||
"description": "description-foo", | |||
"key": "key-foo", | |||
"name": "name-foo", | |||
"url": "http://example.com/foo", | |||
} | |||
} | |||
> | |||
<OrganizationDetailsForm | |||
onContinue={[Function]} | |||
organization={ | |||
Object { | |||
"avatar": "http://example.com/avatar", | |||
"description": "description-foo", | |||
"key": "key-foo", | |||
"name": "name-foo", | |||
"url": "http://example.com/foo", | |||
onContinue={ | |||
[MockFunction] { | |||
"calls": Array [ | |||
Array [ | |||
Object { | |||
"avatar": "http://example.com/avatar", | |||
"description": "description-foo", | |||
"key": "key-foo", | |||
"name": "name-foo", | |||
"url": "http://example.com/foo", | |||
}, | |||
], | |||
], | |||
"results": Array [ | |||
Object { | |||
"isThrow": false, | |||
"value": undefined, | |||
}, | |||
], | |||
} | |||
} | |||
submitText="continue" | |||
@@ -67,11 +67,9 @@ exports[`should render and create organization 2`] = ` | |||
</OrganizationDetailsStep> | |||
<PlanStep | |||
createOrganization={[Function]} | |||
deleteOrganization={[Function]} | |||
onFreePlanChoose={[Function]} | |||
onPaidPlanChoose={[Function]} | |||
onDone={[MockFunction]} | |||
onUpgradeFail={[MockFunction]} | |||
open={true} | |||
startingPrice="billing.price_format.10" | |||
subscriptionPlans={ | |||
Array [ | |||
Object { |
@@ -38,7 +38,14 @@ exports[`should preselect paid plan 2`] = ` | |||
onCommit={[MockFunction]} | |||
onFailToUpgrade={[MockFunction]} | |||
organizationKey={[MockFunction]} | |||
subscriptionPlans={Array []} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 1000, | |||
"price": 100, | |||
}, | |||
] | |||
} | |||
> | |||
<Component /> | |||
</Connect(withCurrentUser(BillingFormShim))> | |||
@@ -84,18 +91,19 @@ exports[`should render and use free plan 2`] = ` | |||
<PlanSelect | |||
onChange={[Function]} | |||
plan="free" | |||
startingPrice="10" | |||
startingPrice="billing.price_format.100" | |||
/> | |||
<div | |||
<form | |||
className="display-flex-center big-spacer-top" | |||
id="organization-free-plan-form" | |||
onSubmit={[Function]} | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
my_account.create_organization | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -126,13 +134,20 @@ exports[`should upgrade 1`] = ` | |||
<PlanSelect | |||
onChange={[Function]} | |||
plan="paid" | |||
startingPrice="10" | |||
startingPrice="billing.price_format.100" | |||
/> | |||
<Connect(withCurrentUser(BillingFormShim)) | |||
onCommit={[MockFunction]} | |||
onFailToUpgrade={[MockFunction]} | |||
organizationKey={[MockFunction]} | |||
subscriptionPlans={Array []} | |||
subscriptionPlans={ | |||
Array [ | |||
Object { | |||
"maxNcloc": 1000, | |||
"price": 100, | |||
}, | |||
] | |||
} | |||
> | |||
<Component /> | |||
</Connect(withCurrentUser(BillingFormShim))> |
@@ -62,90 +62,101 @@ exports[`should display an alert message 1`] = ` | |||
exports[`should display unbound installations 1`] = ` | |||
<div | |||
className="boxed-group-inner" | |||
className="boxed-group" | |||
> | |||
<div | |||
className="display-flex-center" | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.import_organization.import_org_details | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<div | |||
className="display-inline-block" | |||
> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
onClick={[Function]} | |||
small={true} | |||
url="https://alm.application.url" | |||
> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
</div> | |||
<div | |||
className="display-flex-stretch" | |||
className="display-flex-center" | |||
> | |||
<div | |||
className="vertical-pipe-separator" | |||
className="display-inline-block" | |||
> | |||
<div | |||
className="vertical-separator " | |||
/> | |||
<span | |||
className="note" | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
onClick={[Function]} | |||
small={true} | |||
url="https://alm.application.url" | |||
> | |||
or | |||
</span> | |||
<div | |||
className="vertical-separator" | |||
/> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
</div> | |||
<form | |||
className="big-spacer-top big-spacer-bottom" | |||
onSubmit={[Function]} | |||
<div | |||
className="display-flex-stretch" | |||
> | |||
<div | |||
className="form-field abs-width-400" | |||
className="vertical-pipe-separator" | |||
> | |||
<label | |||
htmlFor="select-unbound-installation" | |||
<div | |||
className="vertical-separator " | |||
/> | |||
<span | |||
className="note" | |||
> | |||
onboarding.import_organization.choose_unbound_installation.github | |||
</label> | |||
<Select | |||
className="input-super-large" | |||
clearable={false} | |||
id="select-unbound-installation" | |||
labelKey="name" | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"installationId": "12345", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
] | |||
} | |||
placeholder="onboarding.import_organization.choose_organization" | |||
value="" | |||
valueKey="installationId" | |||
valueRenderer={[Function]} | |||
or | |||
</span> | |||
<div | |||
className="vertical-separator" | |||
/> | |||
</div> | |||
<SubmitButton | |||
disabled={true} | |||
<form | |||
className="big-spacer-top big-spacer-bottom" | |||
onSubmit={[Function]} | |||
> | |||
continue | |||
</SubmitButton> | |||
</form> | |||
<div | |||
className="form-field abs-width-400" | |||
> | |||
<label | |||
htmlFor="select-unbound-installation" | |||
> | |||
onboarding.import_organization.choose_unbound_installation.github | |||
</label> | |||
<Select | |||
className="input-super-large" | |||
clearable={false} | |||
id="select-unbound-installation" | |||
labelKey="name" | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"installationId": "12345", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
] | |||
} | |||
placeholder="onboarding.import_organization.choose_organization" | |||
value="" | |||
valueKey="installationId" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
continue | |||
</SubmitButton> | |||
</form> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -153,31 +164,42 @@ exports[`should display unbound installations 1`] = ` | |||
exports[`should render 1`] = ` | |||
<div | |||
className="boxed-group-inner" | |||
className="boxed-group" | |||
> | |||
<div | |||
className="display-flex-center" | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.import_organization.import_org_details | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<div | |||
className="display-inline-block" | |||
className="display-flex-center" | |||
> | |||
<IdentityProviderLink | |||
<div | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
onClick={[Function]} | |||
small={true} | |||
url="https://alm.application.url" | |||
> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
onClick={[Function]} | |||
small={true} | |||
url="https://alm.application.url" | |||
> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
</div> | |||
</div> | |||
</div> | |||
</div> |
@@ -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$/, '') |
@@ -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 |