* Create withUserOrganizations and use it in create Orgs/Projects page * Update ALM object format in api/navigation/component and api/organizations/searchtags/7.5
@@ -17,10 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON, postJSON } from '../helpers/request'; | |||
import { getJSON, postJSON, post } from '../helpers/request'; | |||
import { AlmRepository, AlmApplication, AlmOrganization } from '../app/types'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function bindAlmOrganization(data: { installationId: string; organization: string }) { | |||
return post('/api/alm_integration/bind_organization', data).catch(throwGlobalError); | |||
} | |||
export function getAlmAppInfo(): Promise<{ application: AlmApplication }> { | |||
return getJSON('/api/alm_integration/show_app_info').catch(throwGlobalError); | |||
} |
@@ -55,7 +55,7 @@ export function getOrganizationNavigation(key: string): Promise<GetOrganizationN | |||
} | |||
export function createOrganization( | |||
data: OrganizationBase & { installId?: string } | |||
data: OrganizationBase & { installationId?: string } | |||
): Promise<Organization> { | |||
return postJSON('/api/organizations/create', data).then(r => r.organization, throwGlobalError); | |||
} |
@@ -73,17 +73,17 @@ export function ComponentNavHeader(props: Props) { | |||
)} | |||
{renderBreadcrumbs(component.breadcrumbs)} | |||
{isSonarCloud() && | |||
component.almRepoUrl && ( | |||
component.alm && ( | |||
<a | |||
className="link-no-underline" | |||
href={component.almRepoUrl} | |||
href={component.alm.url} | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
<img | |||
alt={sanitizeAlmId(component.almId)} | |||
alt={sanitizeAlmId(component.alm.key)} | |||
className="text-text-top spacer-left" | |||
height={16} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.almId)}.svg`} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.alm.key)}.svg`} | |||
width={16} | |||
/> | |||
</a> |
@@ -77,8 +77,7 @@ it('should render alm links', () => { | |||
branchLikes={[]} | |||
component={{ | |||
...component, | |||
almId: 'bitbucketcloud', | |||
almRepoUrl: 'https://bitbucket.org/foo' | |||
alm: { key: 'bitbucketcloud', url: 'https://bitbucket.org/foo' } | |||
}} | |||
currentBranchLike={undefined} | |||
organization={organization} |
@@ -89,8 +89,7 @@ export interface Breadcrumb { | |||
} | |||
export interface Component extends LightComponent { | |||
almId?: string; | |||
almRepoUrl?: string; | |||
alm?: { key: string; url: string }; | |||
analysisDate?: string; | |||
breadcrumbs: Breadcrumb[]; | |||
configuration?: ComponentConfiguration; | |||
@@ -412,6 +411,7 @@ export interface LoggedInUser extends CurrentUser { | |||
local?: boolean; | |||
login: string; | |||
name: string; | |||
personalOrganization?: string; | |||
scmAccounts: string[]; | |||
} | |||
@@ -480,8 +480,7 @@ export interface Notification { | |||
} | |||
export interface Organization extends OrganizationBase { | |||
almId?: string; | |||
almRepoUrl?: string; | |||
alm?: { key: string; url: string }; | |||
adminPages?: Extension[]; | |||
canAdmin?: boolean; | |||
canDelete?: boolean; |
@@ -30,45 +30,68 @@ import { | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeAlmId } from '../../../helpers/almIntegrations'; | |||
import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; | |||
interface Props { | |||
almApplication: AlmApplication; | |||
almInstallId?: string; | |||
almOrganization?: AlmOrganization; | |||
createOrganization: ( | |||
organization: OrganizationBase & { installId?: string } | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
importPersonalOrg?: Organization; | |||
onOrgCreated: (organization: string) => void; | |||
updateOrganization: ( | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
} | |||
export default class AutoOrganizationCreate extends React.PureComponent<Props> { | |||
handleCreateOrganization = (organization: Required<OrganizationBase>) => { | |||
if (organization) { | |||
return this.props | |||
.createOrganization({ | |||
const { importPersonalOrg } = this.props; | |||
let promise: Promise<Organization>; | |||
if (importPersonalOrg) { | |||
promise = this.props.updateOrganization({ | |||
avatar: organization.avatar, | |||
description: organization.description, | |||
installationId: this.props.almInstallId, | |||
key: importPersonalOrg.key, | |||
name: organization.name || organization.key, | |||
url: organization.url | |||
}); | |||
} else { | |||
promise = this.props.createOrganization({ | |||
avatar: organization.avatar, | |||
description: organization.description, | |||
installId: this.props.almInstallId, | |||
installationId: this.props.almInstallId, | |||
key: organization.key, | |||
name: organization.name || organization.key, | |||
url: organization.url | |||
}) | |||
.then(({ key }) => this.props.onOrgCreated(key)); | |||
}); | |||
} | |||
return promise.then(({ key }) => this.props.onOrgCreated(key)); | |||
} else { | |||
return Promise.reject(); | |||
} | |||
}; | |||
render() { | |||
const { almApplication, almInstallId, almOrganization } = this.props; | |||
const { almApplication, almInstallId, almOrganization, importPersonalOrg } = this.props; | |||
if (almInstallId && almOrganization) { | |||
const description = importPersonalOrg | |||
? translate('onboarding.import_personal_organization_x') | |||
: translate('onboarding.import_organization_x'); | |||
const submitText = importPersonalOrg | |||
? translate('onboarding.import_organization.bind') | |||
: translate('my_account.create_organization'); | |||
return ( | |||
<OrganizationDetailsStep | |||
description={ | |||
<p className="huge-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.create_organization.import_organization_x')} | |||
id="onboarding.create_organization.import_organization_x" | |||
defaultMessage={description} | |||
id={description} | |||
values={{ | |||
avatar: ( | |||
<img | |||
@@ -80,17 +103,22 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props> { | |||
width={16} | |||
/> | |||
), | |||
name: <strong>{almOrganization.name}</strong> | |||
name: <strong>{almOrganization.name}</strong>, | |||
personalAvatar: importPersonalOrg && ( | |||
<OrganizationAvatar organization={importPersonalOrg} small={true} /> | |||
), | |||
personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong> | |||
}} | |||
/> | |||
</p> | |||
} | |||
finished={false} | |||
keyReadOnly={Boolean(importPersonalOrg)} | |||
onContinue={this.handleCreateOrganization} | |||
onOpen={() => {}} | |||
open={true} | |||
organization={almOrganization} | |||
submitText={translate('my_account.create_organization')} | |||
organization={importPersonalOrg || almOrganization} | |||
submitText={submitText} | |||
/> | |||
); | |||
} |
@@ -30,7 +30,12 @@ import ManualOrganizationCreate from './ManualOrganizationCreate'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import Tabs from '../../../components/controls/Tabs'; | |||
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; | |||
import { getAlmAppInfo, getAlmOrganization } from '../../../api/alm-integration'; | |||
import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations'; | |||
import { | |||
getAlmAppInfo, | |||
getAlmOrganization, | |||
bindAlmOrganization | |||
} from '../../../api/alm-integration'; | |||
import { getSubscriptionPlans } from '../../../api/billing'; | |||
import { | |||
LoggedInUser, | |||
@@ -40,7 +45,7 @@ import { | |||
AlmOrganization, | |||
OrganizationBase | |||
} from '../../../app/types'; | |||
import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations'; | |||
import { hasAdvancedALMIntegration, isPersonal } from '../../../helpers/almIntegrations'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getOrganizationUrl } from '../../../helpers/urls'; | |||
import * as api from '../../../api/organizations'; | |||
@@ -49,9 +54,15 @@ import '../../../app/styles/sonarcloud.css'; | |||
import '../../tutorials/styles.css'; // TODO remove me | |||
interface Props { | |||
createOrganization: (organization: OrganizationBase) => Promise<Organization>; | |||
createOrganization: ( | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
currentUser: LoggedInUser; | |||
deleteOrganization: (key: string) => Promise<void>; | |||
updateOrganization: ( | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
userOrganizations: Organization[]; | |||
} | |||
interface State { | |||
@@ -146,11 +157,19 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
}; | |||
render() { | |||
const { location } = this.props; | |||
const { almApplication, loading, subscriptionPlans } = this.state; | |||
const { currentUser, location } = this.props; | |||
const { almApplication, almOrganization, loading, subscriptionPlans } = this.state; | |||
const state = (location.state || {}) as LocationState; | |||
const query = parseQuery(location.query); | |||
const header = translate('onboarding.create_organization.page.header'); | |||
const importPersonalOrg = isPersonal(almOrganization) | |||
? this.props.userOrganizations.find(o => o.key === currentUser.personalOrganization) | |||
: undefined; | |||
const header = importPersonalOrg | |||
? translate('onboarding.import_organization.personal.page.header') | |||
: translate('onboarding.create_organization.page.header'); | |||
const description = importPersonalOrg | |||
? translate('onboarding.import_organization.personal.page.description') | |||
: translate('onboarding.create_organization.page.description'); | |||
const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; | |||
const formattedPrice = formatPrice(startedPrice); | |||
const showManualTab = state.tab === 'manual' && !query.almInstallId; | |||
@@ -164,8 +183,8 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
{startedPrice !== undefined && ( | |||
<p className="page-description"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.create_organization.page.description')} | |||
id="onboarding.create_organization.page.description" | |||
defaultMessage={description} | |||
id={description} | |||
values={{ | |||
break: <br />, | |||
price: formattedPrice, | |||
@@ -184,36 +203,34 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
<DeferredSpinner /> | |||
) : ( | |||
<> | |||
{almApplication && ( | |||
<Tabs<TabKeys> | |||
onChange={this.onTabChange} | |||
selected={showManualTab ? 'manual' : 'auto'} | |||
tabs={[ | |||
{ | |||
key: 'auto', | |||
node: ( | |||
<> | |||
{translate( | |||
'onboarding.create_organization.import_organization', | |||
almApplication.key | |||
)} | |||
<span | |||
className={classNames('beta-badge spacer-left', { | |||
'is-muted': showManualTab | |||
})}> | |||
{translate('beta')} | |||
</span> | |||
</> | |||
) | |||
}, | |||
{ | |||
disabled: Boolean(query.almInstallId), | |||
key: 'manual', | |||
node: translate('onboarding.create_organization.create_manually') | |||
} | |||
]} | |||
/> | |||
)} | |||
{almApplication && | |||
!importPersonalOrg && ( | |||
<Tabs<TabKeys> | |||
onChange={this.onTabChange} | |||
selected={showManualTab ? 'manual' : 'auto'} | |||
tabs={[ | |||
{ | |||
key: 'auto', | |||
node: ( | |||
<> | |||
{translate('onboarding.import_organization', almApplication.key)} | |||
<span | |||
className={classNames('beta-badge spacer-left', { | |||
'is-muted': showManualTab | |||
})}> | |||
{translate('beta')} | |||
</span> | |||
</> | |||
) | |||
}, | |||
{ | |||
disabled: Boolean(query.almInstallId), | |||
key: 'manual', | |||
node: translate('onboarding.create_organization.create_manually') | |||
} | |||
]} | |||
/> | |||
)} | |||
{showManualTab || !almApplication ? ( | |||
<ManualOrganizationCreate | |||
@@ -227,9 +244,11 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
<AutoOrganizationCreate | |||
almApplication={almApplication} | |||
almInstallId={query.almInstallId} | |||
almOrganization={this.state.almOrganization} | |||
almOrganization={almOrganization} | |||
createOrganization={this.props.createOrganization} | |||
importPersonalOrg={importPersonalOrg} | |||
onOrgCreated={this.handleOrgCreated} | |||
updateOrganization={this.props.updateOrganization} | |||
/> | |||
)} | |||
</> | |||
@@ -240,7 +259,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
} | |||
} | |||
function createOrganization(organization: OrganizationBase & { installId?: string }) { | |||
function createOrganization(organization: OrganizationBase & { installationId?: string }) { | |||
return (dispatch: Dispatch) => { | |||
return api.createOrganization(organization).then((organization: Organization) => { | |||
dispatch(actions.createOrganization(organization)); | |||
@@ -249,6 +268,22 @@ function createOrganization(organization: OrganizationBase & { installId?: strin | |||
}; | |||
} | |||
function updateOrganization( | |||
organization: OrganizationBase & { key: string; installationId?: string } | |||
) { | |||
return (dispatch: Dispatch) => { | |||
const { key, installationId, ...changes } = organization; | |||
const promises = [api.updateOrganization(key, changes)]; | |||
if (installationId) { | |||
promises.push(bindAlmOrganization({ organization: key, installationId })); | |||
} | |||
return Promise.all(promises).then(() => { | |||
dispatch(actions.updateOrganization(key, changes)); | |||
return organization; | |||
}); | |||
}; | |||
} | |||
function deleteOrganization(key: string) { | |||
return (dispatch: Dispatch) => { | |||
return api.deleteOrganization(key).then(() => { | |||
@@ -259,14 +294,17 @@ function deleteOrganization(key: string) { | |||
const mapDispatchToProps = { | |||
createOrganization: createOrganization as any, | |||
deleteOrganization: deleteOrganization as any | |||
deleteOrganization: deleteOrganization as any, | |||
updateOrganization: updateOrganization as any | |||
}; | |||
export default whenLoggedIn( | |||
withRouter( | |||
connect( | |||
null, | |||
mapDispatchToProps | |||
)(CreateOrganization) | |||
withUserOrganizations( | |||
withRouter( | |||
connect( | |||
null, | |||
mapDispatchToProps | |||
)(CreateOrganization) | |||
) | |||
) | |||
); |
@@ -35,6 +35,7 @@ type RequiredOrganization = Required<OrganizationBase>; | |||
interface Props { | |||
description?: React.ReactNode; | |||
finished: boolean; | |||
keyReadOnly?: boolean; | |||
onContinue: (organization: RequiredOrganization) => Promise<void>; | |||
onOpen: () => void; | |||
open: boolean; | |||
@@ -141,7 +142,11 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props, | |||
<div className="boxed-group-inner"> | |||
<form id="organization-form" onSubmit={this.handleSubmit}> | |||
{this.props.description} | |||
<OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} /> | |||
<OrganizationKeyInput | |||
initialValue={this.state.key} | |||
onChange={this.handleKeyUpdate} | |||
readOnly={this.props.keyReadOnly} | |||
/> | |||
<div className="big-spacer-top"> | |||
<ResetButtonLink onClick={this.handleAdditionalClick}> | |||
{translate( | |||
@@ -162,6 +167,7 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props, | |||
<div className="big-spacer-top"> | |||
<OrganizationAvatarInput | |||
initialValue={this.state.avatar} | |||
name={this.state.name} | |||
onChange={this.handleDescriptionUpdate} | |||
/> | |||
</div> |
@@ -52,10 +52,31 @@ it('should render prefilled and create org', async () => { | |||
wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); | |||
await waitAndUpdate(wrapper); | |||
expect(createOrganization).toBeCalledWith({ ...organization, installId: 'id-foo' }); | |||
expect(createOrganization).toBeCalledWith({ ...organization, installationId: 'id-foo' }); | |||
expect(onOrgCreated).toBeCalledWith('foo'); | |||
}); | |||
it('should render for personal organizations', async () => { | |||
const personalOrg = { key: 'personal-org', name: 'personal-org' }; | |||
const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key }); | |||
const onOrgCreated = jest.fn(); | |||
const wrapper = shallowRender({ | |||
almInstallId: 'id-foo', | |||
almOrganization: { ...organization, type: 'USER' }, | |||
importPersonalOrg: personalOrg, | |||
onOrgCreated, | |||
updateOrganization | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(personalOrg); | |||
await waitAndUpdate(wrapper); | |||
expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' }); | |||
expect(onOrgCreated).toBeCalledWith(personalOrg.key); | |||
}); | |||
function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { | |||
return shallow( | |||
<AutoOrganizationCreate | |||
@@ -68,6 +89,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { | |||
}} | |||
createOrganization={jest.fn()} | |||
onOrgCreated={jest.fn()} | |||
updateOrganization={jest.fn()} | |||
{...props} | |||
/> | |||
); |
@@ -116,6 +116,10 @@ function shallowRender(props: Partial<CreateOrganization['props']> = {}) { | |||
// @ts-ignore avoid passing everything from WithRouterProps | |||
location={{}} | |||
router={mockRouter()} | |||
userOrganizations={[ | |||
{ key: 'foo', name: 'Foo' }, | |||
{ alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } | |||
]} | |||
{...props} | |||
/> | |||
); |
@@ -1,5 +1,57 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render for personal organizations 1`] = ` | |||
<OrganizationDetailsStep | |||
description={ | |||
<p | |||
className="huge-spacer-bottom" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.import_personal_organization_x" | |||
id="onboarding.import_personal_organization_x" | |||
values={ | |||
Object { | |||
"avatar": <img | |||
alt="BitBucket" | |||
className="little-spacer-left" | |||
src="/images/sonarcloud/bitbucket.svg" | |||
width={16} | |||
/>, | |||
"name": <strong> | |||
name-foo | |||
</strong>, | |||
"personalAvatar": <OrganizationAvatar | |||
organization={ | |||
Object { | |||
"key": "personal-org", | |||
"name": "personal-org", | |||
} | |||
} | |||
small={true} | |||
/>, | |||
"personalName": <strong> | |||
personal-org | |||
</strong>, | |||
} | |||
} | |||
/> | |||
</p> | |||
} | |||
finished={false} | |||
keyReadOnly={true} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
open={true} | |||
organization={ | |||
Object { | |||
"key": "personal-org", | |||
"name": "personal-org", | |||
} | |||
} | |||
submitText="onboarding.import_organization.bind" | |||
/> | |||
`; | |||
exports[`should render prefilled and create org 1`] = ` | |||
<OrganizationDetailsStep | |||
description={ | |||
@@ -7,8 +59,8 @@ exports[`should render prefilled and create org 1`] = ` | |||
className="huge-spacer-bottom" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.create_organization.import_organization_x" | |||
id="onboarding.create_organization.import_organization_x" | |||
defaultMessage="onboarding.import_organization_x" | |||
id="onboarding.import_organization_x" | |||
values={ | |||
Object { | |||
"avatar": <img | |||
@@ -20,12 +72,15 @@ exports[`should render prefilled and create org 1`] = ` | |||
"name": <strong> | |||
name-foo | |||
</strong>, | |||
"personalAvatar": undefined, | |||
"personalName": undefined, | |||
} | |||
} | |||
/> | |||
</p> | |||
} | |||
finished={false} | |||
keyReadOnly={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
open={true} |
@@ -50,7 +50,7 @@ exports[`should render with auto tab displayed 1`] = ` | |||
Object { | |||
"key": "auto", | |||
"node": <React.Fragment> | |||
onboarding.create_organization.import_organization.github | |||
onboarding.import_organization.github | |||
<span | |||
className="beta-badge spacer-left" | |||
> | |||
@@ -132,7 +132,7 @@ exports[`should render with auto tab selected and manual disabled 1`] = ` | |||
Object { | |||
"key": "auto", | |||
"node": <React.Fragment> | |||
onboarding.create_organization.import_organization.github | |||
onboarding.import_organization.github | |||
<span | |||
className="beta-badge spacer-left" | |||
> | |||
@@ -286,7 +286,7 @@ exports[`should switch tabs 1`] = ` | |||
Object { | |||
"key": "auto", | |||
"node": <React.Fragment> | |||
onboarding.create_organization.import_organization.github | |||
onboarding.import_organization.github | |||
<span | |||
className="beta-badge spacer-left" | |||
> |
@@ -28,6 +28,7 @@ import { getHostUrl } from '../../../../helpers/urls'; | |||
interface Props { | |||
initialValue?: string; | |||
onChange: (value: string | undefined) => void; | |||
readOnly?: boolean; | |||
} | |||
interface State { | |||
@@ -50,7 +51,9 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta | |||
this.mounted = true; | |||
if (this.props.initialValue !== undefined) { | |||
this.setState({ value: this.props.initialValue }); | |||
this.validateKey(this.props.initialValue); | |||
if (!this.props.readOnly) { | |||
this.validateKey(this.props.initialValue); | |||
} | |||
} | |||
} | |||
@@ -118,25 +121,28 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta | |||
isInvalid={isInvalid} | |||
isValid={isValid} | |||
label={translate('onboarding.create_organization.organization_name')} | |||
required={true}> | |||
required={!this.props.readOnly}> | |||
<div className="display-inline-flex-baseline"> | |||
<span className="little-spacer-right"> | |||
{getHostUrl().replace(/https*:\/\//, '') + '/organizations/'} | |||
{this.props.readOnly && this.state.value} | |||
</span> | |||
<input | |||
autoFocus={true} | |||
className={classNames('input-super-large', 'text-middle', { | |||
'is-invalid': isInvalid, | |||
'is-valid': isValid | |||
})} | |||
id="organization-key" | |||
maxLength={255} | |||
onBlur={this.handleBlur} | |||
onChange={this.handleChange} | |||
onFocus={this.handleFocus} | |||
type="text" | |||
value={this.state.value} | |||
/> | |||
{!this.props.readOnly && ( | |||
<input | |||
autoFocus={true} | |||
className={classNames('input-super-large', 'text-middle', { | |||
'is-invalid': isInvalid, | |||
'is-valid': isValid | |||
})} | |||
id="organization-key" | |||
maxLength={255} | |||
onBlur={this.handleBlur} | |||
onChange={this.handleChange} | |||
onFocus={this.handleFocus} | |||
type="text" | |||
value={this.state.value} | |||
/> | |||
)} | |||
</div> | |||
</ValidationInput> | |||
); |
@@ -38,6 +38,13 @@ it('should render correctly', () => { | |||
expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with readonly mode', () => { | |||
const wrapper = shallow( | |||
<OrganizationKeyInput initialValue="key" onChange={jest.fn()} readOnly={true} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should not display any status when the key is not defined', async () => { | |||
const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />); | |||
await waitAndUpdate(wrapper); |
@@ -32,3 +32,24 @@ exports[`should render correctly 1`] = ` | |||
`; | |||
exports[`should render correctly 2`] = `true`; | |||
exports[`should render correctly with readonly mode 1`] = ` | |||
<ValidationInput | |||
id="organization-key" | |||
isInvalid={false} | |||
isValid={false} | |||
label="onboarding.create_organization.organization_name" | |||
required={false} | |||
> | |||
<div | |||
className="display-inline-flex-baseline" | |||
> | |||
<span | |||
className="little-spacer-right" | |||
> | |||
localhost/organizations/ | |||
key | |||
</span> | |||
</div> | |||
</ValidationInput> | |||
`; |
@@ -27,8 +27,7 @@ import ManualProjectCreate from './ManualProjectCreate'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import Tabs from '../../../components/controls/Tabs'; | |||
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; | |||
import { fetchMyOrganizations } from '../../account/organizations/actions'; | |||
import { getMyOrganizations, Store } from '../../../store/rootReducer'; | |||
import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations'; | |||
import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; | |||
import { LoggedInUser, AlmApplication, Organization } from '../../../app/types'; | |||
import { getAlmAppInfo } from '../../../api/alm-integration'; | |||
@@ -38,14 +37,10 @@ import { translate } from '../../../helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import '../../../app/styles/sonarcloud.css'; | |||
interface StateProps { | |||
userOrganizations: Organization[]; | |||
} | |||
interface Props { | |||
currentUser: LoggedInUser; | |||
fetchMyOrganizations: () => Promise<void>; | |||
skipOnboardingAction: () => void; | |||
userOrganizations: Organization[]; | |||
} | |||
interface State { | |||
@@ -60,16 +55,12 @@ interface LocationState { | |||
tab?: TabKeys; | |||
} | |||
export class CreateProjectPage extends React.PureComponent< | |||
Props & StateProps & WithRouterProps, | |||
State | |||
> { | |||
export class CreateProjectPage extends React.PureComponent<Props & WithRouterProps, State> { | |||
mounted = false; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.props.fetchMyOrganizations(); | |||
if (hasAdvancedALMIntegration(this.props.currentUser)) { | |||
this.fetchAlmApplication(); | |||
} else { | |||
@@ -178,7 +169,7 @@ export class CreateProjectPage extends React.PureComponent< | |||
) : ( | |||
<AutoProjectCreate | |||
almApplication={almApplication} | |||
boundOrganizations={userOrganizations.filter(o => o.almId)} | |||
boundOrganizations={userOrganizations.filter(o => o.alm)} | |||
onProjectCreate={this.handleProjectCreate} | |||
organization={state.organization} | |||
/> | |||
@@ -191,20 +182,13 @@ export class CreateProjectPage extends React.PureComponent< | |||
} | |||
} | |||
const mapDispatchToProps = { | |||
fetchMyOrganizations, | |||
skipOnboardingAction | |||
}; | |||
const mapStateToProps = (state: Store) => { | |||
return { | |||
userOrganizations: getMyOrganizations(state) | |||
}; | |||
}; | |||
const mapDispatchToProps = { skipOnboardingAction }; | |||
export default whenLoggedIn( | |||
connect<StateProps>( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(CreateProjectPage) | |||
withUserOrganizations( | |||
connect( | |||
null, | |||
mapDispatchToProps | |||
)(CreateProjectPage) | |||
) | |||
); |
@@ -71,12 +71,12 @@ export default function OrganizationSelect({ | |||
export function optionRenderer(organization: Organization) { | |||
return ( | |||
<span> | |||
{organization.almId && ( | |||
{organization.alm && ( | |||
<img | |||
alt={organization.almId} | |||
alt={organization.alm.key} | |||
className="spacer-right" | |||
height={14} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`} | |||
/> | |||
)} | |||
{organization.name} |
@@ -42,8 +42,8 @@ function shallowRender(props: Partial<AutoProjectCreate['props']> = {}) { | |||
<AutoProjectCreate | |||
almApplication={almApplication} | |||
boundOrganizations={[ | |||
{ almId: 'github', key: 'foo', name: 'Foo' }, | |||
{ almId: 'github', key: 'bar', name: 'Bar' } | |||
{ alm: { key: 'github', url: '' }, key: 'foo', name: 'Foo' }, | |||
{ alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } | |||
]} | |||
onProjectCreate={jest.fn()} | |||
organization="" |
@@ -81,14 +81,13 @@ function getWrapper(props = {}) { | |||
<CreateProjectPage | |||
addGlobalErrorMessage={jest.fn()} | |||
currentUser={user} | |||
fetchMyOrganizations={jest.fn()} | |||
// @ts-ignore avoid passing everything from WithRouterProps | |||
location={{}} | |||
router={mockRouter()} | |||
skipOnboardingAction={jest.fn()} | |||
userOrganizations={[ | |||
{ key: 'foo', name: 'Foo' }, | |||
{ almId: 'github', key: 'bar', name: 'Bar' } | |||
{ alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } | |||
]} | |||
{...props} | |||
/> |
@@ -21,7 +21,10 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import OrganizationSelect, { optionRenderer } from '../OrganizationSelect'; | |||
const organizations = [{ key: 'foo', name: 'Foo' }, { almId: 'github', key: 'bar', name: 'Bar' }]; | |||
const organizations = [ | |||
{ key: 'foo', name: 'Foo' }, | |||
{ alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } | |||
]; | |||
it('should render correctly', () => { | |||
expect( |
@@ -9,12 +9,18 @@ exports[`should display the bounded organizations dropdown with the list of repo | |||
organizations={ | |||
Array [ | |||
Object { | |||
"almId": "github", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"almId": "github", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "bar", | |||
"name": "Bar", | |||
}, |
@@ -83,7 +83,10 @@ exports[`should render correctly 2`] = ` | |||
boundOrganizations={ | |||
Array [ | |||
Object { | |||
"almId": "github", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
@@ -134,7 +137,10 @@ exports[`should render with Manual creation only 1`] = ` | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"almId": "github", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
@@ -201,7 +207,10 @@ exports[`should switch tabs 1`] = ` | |||
boundOrganizations={ | |||
Array [ | |||
Object { | |||
"almId": "github", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "bar", | |||
"name": "Bar", | |||
}, |
@@ -25,7 +25,10 @@ exports[`should render correctly 1`] = ` | |||
options={ | |||
Array [ | |||
Object { | |||
"almId": "github", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "bar", | |||
"name": "Bar", | |||
}, |
@@ -58,17 +58,17 @@ export default function OrganizationNavigationHeader({ organization, organizatio | |||
) : ( | |||
<span className="spacer-left">{organization.name}</span> | |||
)} | |||
{organization.almRepoUrl && ( | |||
{organization.alm && ( | |||
<a | |||
className="link-no-underline" | |||
href={organization.almRepoUrl} | |||
href={organization.alm.url} | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
<img | |||
alt={sanitizeAlmId(organization.almId)} | |||
alt={sanitizeAlmId(organization.alm.key)} | |||
className="text-text-top spacer-left" | |||
height={16} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`} | |||
width={16} | |||
/> | |||
</a> |
@@ -38,8 +38,7 @@ it('renders with alm integration', () => { | |||
shallow( | |||
<OrganizationNavigationHeader | |||
organization={{ | |||
almId: 'github', | |||
almRepoUrl: 'https://github.com/foo', | |||
alm: { key: 'github', url: 'https://github.com/foo' }, | |||
key: 'foo', | |||
name: 'Foo', | |||
projectVisibility: Visibility.Public |
@@ -70,8 +70,10 @@ exports[`renders with alm integration 1`] = ` | |||
<OrganizationAvatar | |||
organization={ | |||
Object { | |||
"almId": "github", | |||
"almRepoUrl": "https://github.com/foo", | |||
"alm": Object { | |||
"key": "github", | |||
"url": "https://github.com/foo", | |||
}, | |||
"key": "foo", | |||
"name": "Foo", | |||
"projectVisibility": "public", |
@@ -56,7 +56,7 @@ export default class AnalyzeTutorial extends React.PureComponent<Props, State> { | |||
const { component, currentUser } = this.props; | |||
const { step, token } = this.state; | |||
const almId = component.almId || currentUser.externalProvider; | |||
const almKey = (component.alm && component.alm.key) || currentUser.externalProvider; | |||
return ( | |||
<> | |||
<div className="page-header big-spacer-bottom"> | |||
@@ -64,9 +64,9 @@ export default class AnalyzeTutorial extends React.PureComponent<Props, State> { | |||
<p className="page-description">{translate('onboarding.project_analysis.description')}</p> | |||
</div> | |||
<AnalyzeTutorialSuggestion almId={almId} /> | |||
<AnalyzeTutorialSuggestion almKey={almKey} /> | |||
{!isVSTS(almId) && ( | |||
{!isVSTS(almKey) && ( | |||
<> | |||
<TokenStep | |||
currentUser={currentUser} |
@@ -24,8 +24,8 @@ import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) { | |||
if (isBitbucket(almId)) { | |||
export default function AnalyzeTutorialSuggestion({ almKey }: { almKey?: string }) { | |||
if (isBitbucket(almKey)) { | |||
return ( | |||
<Alert className="big-spacer-bottom" variant="info"> | |||
<p>{translate('onboarding.project_analysis.commands_for_analysis')}</p> | |||
@@ -49,7 +49,7 @@ export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) | |||
/> | |||
</Alert> | |||
); | |||
} else if (isGithub(almId)) { | |||
} else if (isGithub(almKey)) { | |||
return ( | |||
<Alert className="big-spacer-bottom" variant="info"> | |||
<p>{translate('onboarding.project_analysis.commands_for_analysis')} </p> | |||
@@ -70,7 +70,7 @@ export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) | |||
/> | |||
</Alert> | |||
); | |||
} else if (isVSTS(almId)) { | |||
} else if (isVSTS(almKey)) { | |||
return ( | |||
<Alert className="big-spacer-bottom" variant="info"> | |||
<FormattedMessage |
@@ -22,17 +22,17 @@ import { shallow } from 'enzyme'; | |||
import AnalyzeTutorialSuggestion from '../AnalyzeTutorialSuggestion'; | |||
it('should not render', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId={undefined} />).type()).toBeNull(); | |||
expect(shallow(<AnalyzeTutorialSuggestion almKey={undefined} />).type()).toBeNull(); | |||
}); | |||
it('renders bitbucket suggestions correctly', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId="bitbucket" />)).toMatchSnapshot(); | |||
expect(shallow(<AnalyzeTutorialSuggestion almKey="bitbucket" />)).toMatchSnapshot(); | |||
}); | |||
it('renders github suggestions correctly', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId="github" />)).toMatchSnapshot(); | |||
expect(shallow(<AnalyzeTutorialSuggestion almKey="github" />)).toMatchSnapshot(); | |||
}); | |||
it('renders vsts suggestions correctly', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId="microsoft" />)).toMatchSnapshot(); | |||
expect(shallow(<AnalyzeTutorialSuggestion almKey="microsoft" />)).toMatchSnapshot(); | |||
}); |
@@ -20,7 +20,6 @@ | |||
import * as React from 'react'; | |||
import { shallow, ShallowWrapper } from 'enzyme'; | |||
import { createStore } from 'redux'; | |||
import { mockRouter } from '../../../helpers/testUtils'; | |||
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; | |||
import { whenLoggedIn } from '../whenLoggedIn'; | |||
@@ -44,15 +43,13 @@ it('should render for logged in user', () => { | |||
it('should not render for anonymous user', () => { | |||
const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } }); | |||
const router = mockRouter({ replace: jest.fn() }); | |||
const wrapper = shallow(<UnderTest />, { context: { store, router } }); | |||
const wrapper = shallow(<UnderTest />, { context: { store } }); | |||
expect(getRenderedType(wrapper)).toBe(null); | |||
expect(handleRequiredAuthentication).toBeCalled(); | |||
}); | |||
function getRenderedType(wrapper: ShallowWrapper) { | |||
return wrapper | |||
.dive() | |||
.dive() | |||
.dive() | |||
.type(); |
@@ -0,0 +1,49 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { createStore } from 'redux'; | |||
import { Organization } from '../../../app/types'; | |||
import { withUserOrganizations } from '../withUserOrganizations'; | |||
jest.mock('../../../api/organizations', () => ({ getOrganizations: jest.fn() })); | |||
class X extends React.Component<{ userOrganizations: Organization[] }> { | |||
render() { | |||
return <div />; | |||
} | |||
} | |||
const UnderTest = withUserOrganizations(X); | |||
// TODO Find a way to make this work, currently getting the following error : Actions must be plain objects. Use custom middleware for async actions. | |||
it.skip('should pass user organizations and logged in user', () => { | |||
const org = { key: 'my-org', name: 'My Organization' }; | |||
const store = createStore(state => state, { | |||
organizations: { byKey: { 'my-org': org }, my: ['my-org'] } | |||
}); | |||
const wrapper = shallow(<UnderTest />, { context: { store } }); | |||
const wrappedComponent = wrapper | |||
.dive() | |||
.dive() | |||
.dive(); | |||
expect(wrappedComponent.type()).toBe(X); | |||
expect(wrappedComponent.prop('userOrganizations')).toEqual([org]); | |||
}); |
@@ -18,7 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { withRouter, WithRouterProps } from 'react-router'; | |||
import { withCurrentUser } from './withCurrentUser'; | |||
import { CurrentUser } from '../../app/types'; | |||
import { isLoggedIn } from '../../helpers/users'; | |||
@@ -27,7 +26,7 @@ import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthenti | |||
export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) { | |||
const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; | |||
class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> { | |||
class Wrapper extends React.Component<P & { currentUser: CurrentUser }> { | |||
static displayName = `whenLoggedIn(${wrappedDisplayName})`; | |||
componentDidMount() { | |||
@@ -45,5 +44,5 @@ export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) { | |||
} | |||
} | |||
return withCurrentUser(withRouter(Wrapper)); | |||
return withCurrentUser(Wrapper); | |||
} |
@@ -0,0 +1,61 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { Store, getMyOrganizations } from '../../store/rootReducer'; | |||
import { fetchMyOrganizations } from '../../apps/account/organizations/actions'; | |||
import { Organization } from '../../app/types'; | |||
export function withUserOrganizations<P>( | |||
WrappedComponent: React.ComponentClass< | |||
P & { | |||
personalOrganization?: Organization; | |||
userOrganizations: Organization[]; | |||
} | |||
> | |||
) { | |||
type Props = P & { fetchMyOrganizations: () => Promise<void>; userOrganizations: Organization[] }; | |||
const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; | |||
class Wrapper extends React.Component<Props> { | |||
static displayName = `withUserOrganizations(${wrappedDisplayName})`; | |||
componentDidMount() { | |||
this.props.fetchMyOrganizations(); | |||
} | |||
render() { | |||
// @ts-ignore Rest operator not supported yet by TS for generics | |||
const { fetchMyOrganizations, ...other } = this.props; | |||
return <WrappedComponent {...other} />; | |||
} | |||
} | |||
const mapDispatchToProps = { fetchMyOrganizations }; | |||
function mapStateToProps(state: Store) { | |||
return { userOrganizations: getMyOrganizations(state) }; | |||
} | |||
return connect( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(Wrapper); | |||
} |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { isBitbucket, isGithub, isVSTS, sanitizeAlmId } from '../almIntegrations'; | |||
import { isBitbucket, isGithub, isPersonal, isVSTS, sanitizeAlmId } from '../almIntegrations'; | |||
it('#isBitbucket', () => { | |||
expect(isBitbucket('bitbucket')).toBeTruthy(); | |||
@@ -35,6 +35,11 @@ it('#isVSTS', () => { | |||
expect(isVSTS('github')).toBeFalsy(); | |||
}); | |||
it('#isPersonal', () => { | |||
expect(isPersonal({ key: 'foo', name: 'Foo', type: 'USER' })).toBeTruthy(); | |||
expect(isPersonal({ key: 'foo', name: 'Foo', type: 'ORGANIZATION' })).toBeFalsy(); | |||
}); | |||
it('#sanitizeAlmId', () => { | |||
expect(sanitizeAlmId('bitbucketcloud')).toBe('bitbucket'); | |||
expect(sanitizeAlmId('bitbucket')).toBe('bitbucket'); |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { isLoggedIn } from './users'; | |||
import { CurrentUser } from '../app/types'; | |||
import { CurrentUser, AlmOrganization } from '../app/types'; | |||
export function hasAdvancedALMIntegration(user: CurrentUser) { | |||
return ( | |||
@@ -26,21 +26,25 @@ export function hasAdvancedALMIntegration(user: CurrentUser) { | |||
); | |||
} | |||
export function isBitbucket(almId?: string) { | |||
return almId && almId.startsWith('bitbucket'); | |||
export function isBitbucket(almKey?: string) { | |||
return almKey && almKey.startsWith('bitbucket'); | |||
} | |||
export function isGithub(almId?: string) { | |||
return almId === 'github'; | |||
export function isGithub(almKey?: string) { | |||
return almKey === 'github'; | |||
} | |||
export function isVSTS(almId?: string) { | |||
return almId === 'microsoft'; | |||
export function isVSTS(almKey?: string) { | |||
return almKey === 'microsoft'; | |||
} | |||
export function sanitizeAlmId(almId?: string) { | |||
if (isBitbucket(almId)) { | |||
export function isPersonal(organization?: AlmOrganization) { | |||
return Boolean(organization && organization.type === 'USER'); | |||
} | |||
export function sanitizeAlmId(almKey?: string) { | |||
if (isBitbucket(almKey)) { | |||
return 'bitbucket'; | |||
} | |||
return almId; | |||
return almKey; | |||
} |
@@ -2748,9 +2748,6 @@ onboarding.create_organization.url.error=The value must be a valid url. | |||
onboarding.create_organization.description=Description | |||
onboarding.create_organization.enter_org_details=Enter your organization details | |||
onboarding.create_organization.create_manually=Create manually | |||
onboarding.create_organization.import_organization.bitbucket=Import from BitBucket teams | |||
onboarding.create_organization.import_organization.github=Import from GitHub organizations | |||
onboarding.create_organization.import_organization_x=Import {avatar} {name} into SonarCloud organization | |||
onboarding.create_organization.import_org_details=Import organization details | |||
onboarding.create_organization.import_org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue: | |||
onboarding.create_organization.import_org_not_found.tips_1=You must be an administrator of the organization | |||
@@ -2762,6 +2759,14 @@ onboarding.create_organization.choose_plan=Choose a plan | |||
onboarding.create_organization.enter_your_coupon=Enter your coupon | |||
onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade | |||
onboarding.create_organization.ready=All set! Your organization is now ready to go | |||
onboarding.import_organization.bind=Bind Organization | |||
onboarding.import_organization.personal.page.header=Bind to your personal organization | |||
onboarding.import_organization.personal.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. | |||
onboarding.import_organization.bitbucket=Import from BitBucket teams | |||
onboarding.import_organization.github=Import from GitHub organizations | |||
onboarding.import_organization_x=Import {avatar} {name} into SonarCloud organization | |||
onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName} | |||
onboarding.team.header=Join a team | |||
onboarding.team.first_step=Well congrats, the first step is done! |