@@ -29,6 +29,7 @@ import org.sonar.db.Dao; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.db.DatabaseUtils.executeLargeInputs; | |||
public class OrganizationAlmBindingDao implements Dao { | |||
@@ -42,7 +43,7 @@ public class OrganizationAlmBindingDao implements Dao { | |||
} | |||
public Optional<OrganizationAlmBindingDto> selectByOrganization(DbSession dbSession, OrganizationDto organization) { | |||
return Optional.ofNullable(getMapper(dbSession).selectByOrganizationUuid(organization.getUuid())); | |||
return ofNullable(getMapper(dbSession).selectByOrganizationUuid(organization.getUuid())); | |||
} | |||
public List<OrganizationAlmBindingDto> selectByOrganizations(DbSession dbSession, Collection<OrganizationDto> organizations) { | |||
@@ -50,6 +51,10 @@ public class OrganizationAlmBindingDao implements Dao { | |||
organizationUuids -> getMapper(dbSession).selectByOrganizationUuids(organizationUuids)); | |||
} | |||
public Optional<OrganizationAlmBindingDto> selectByAlmAppInstall(DbSession dbSession, AlmAppInstallDto almAppInstall) { | |||
return ofNullable(getMapper(dbSession).selectByInstallationUuid(almAppInstall.getUuid())); | |||
} | |||
public void insert(DbSession dbSession, OrganizationDto organization, AlmAppInstallDto almAppInstall, String url, String userUuid) { | |||
long now = system2.now(); | |||
getMapper(dbSession).insert(new OrganizationAlmBindingDto() |
@@ -31,6 +31,9 @@ public interface OrganizationAlmBindingMapper { | |||
List<OrganizationAlmBindingDto> selectByOrganizationUuids(@Param("organizationUuids") Collection<String> organizationUuids); | |||
@CheckForNull | |||
OrganizationAlmBindingDto selectByInstallationUuid(@Param("installationUuid") String installationUuid); | |||
void insert(@Param("dto") OrganizationAlmBindingDto dto); | |||
void deleteByOrganizationUuid(@Param("organizationUuid") String organizationUuid); |
@@ -109,6 +109,7 @@ public class OrganizationDto { | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getDescription() { | |||
return description; | |||
} | |||
@@ -118,6 +119,7 @@ public class OrganizationDto { | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getUrl() { | |||
return url; | |||
} | |||
@@ -127,6 +129,7 @@ public class OrganizationDto { | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getAvatarUrl() { | |||
return avatarUrl; | |||
} |
@@ -26,12 +26,21 @@ | |||
select | |||
<include refid="columns"/> | |||
from | |||
organization_alm_bindings | |||
organization_alm_bindings | |||
where | |||
organization_uuid in | |||
<foreach collection="organizationUuids" open="(" close=")" item="organizationUuid" separator=","> | |||
#{organizationUuid , jdbcType=VARCHAR} | |||
</foreach> | |||
</select> | |||
<select id="selectByInstallationUuid" parameterType="String" resultType="org.sonar.db.alm.OrganizationAlmBindingDto"> | |||
select | |||
<include refid="columns"/> | |||
from | |||
organization_alm_bindings | |||
where | |||
organization_uuid in | |||
<foreach collection="organizationUuids" open="(" close=")" item="organizationUuid" separator=","> | |||
#{organizationUuid , jdbcType=VARCHAR} | |||
</foreach> | |||
alm_app_install_uuid = #{installationUuid, jdbcType=VARCHAR} | |||
</select> | |||
<insert id="insert" parameterType="Map" useGeneratedKeys="false"> |
@@ -25,14 +25,12 @@ import org.junit.Test; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.api.utils.internal.TestSystem2; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.core.util.Uuids; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.user.UserDto; | |||
import static java.util.Arrays.asList; | |||
import static java.util.Collections.singletonList; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.entry; | |||
import static org.assertj.core.groups.Tuple.tuple; | |||
@@ -75,7 +73,7 @@ public class OrganizationAlmBindingDaoTest { | |||
OrganizationDto organization = db.organizations().insert(); | |||
AlmAppInstallDto almAppInstall = db.alm().insertAlmAppInstall(); | |||
db.alm().insertOrganizationAlmBinding(organization, almAppInstall); | |||
// No binding on other organization | |||
// No binding on other installation | |||
OrganizationDto otherOrganization = db.organizations().insert(); | |||
Optional<OrganizationAlmBindingDto> result = underTest.selectByOrganization(db.getSession(), otherOrganization); | |||
@@ -100,6 +98,36 @@ public class OrganizationAlmBindingDaoTest { | |||
assertThat(underTest.selectByOrganizations(db.getSession(), singletonList(organizationNotBound))).isEmpty(); | |||
} | |||
@Test | |||
public void selectByAlmAppInstall() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
AlmAppInstallDto almAppInstall = db.alm().insertAlmAppInstall(); | |||
OrganizationAlmBindingDto dto = db.alm().insertOrganizationAlmBinding(organization, almAppInstall); | |||
Optional<OrganizationAlmBindingDto> result = underTest.selectByAlmAppInstall(db.getSession(), almAppInstall); | |||
assertThat(result.get()) | |||
.extracting(OrganizationAlmBindingDto::getUuid, OrganizationAlmBindingDto::getOrganizationUuid, OrganizationAlmBindingDto::getAlmAppInstallUuid, | |||
OrganizationAlmBindingDto::getUrl, OrganizationAlmBindingDto::getAlm, | |||
OrganizationAlmBindingDto::getUserUuid, OrganizationAlmBindingDto::getCreatedAt) | |||
.containsExactlyInAnyOrder(dto.getUuid(), organization.getUuid(), dto.getAlmAppInstallUuid(), | |||
dto.getUrl(), ALM.GITHUB, | |||
dto.getUserUuid(), NOW); | |||
} | |||
@Test | |||
public void selectByAlmAppInstall_returns_empty_when_installation_is_not_bound_to_organization() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
AlmAppInstallDto almAppInstall = db.alm().insertAlmAppInstall(); | |||
db.alm().insertOrganizationAlmBinding(organization, almAppInstall); | |||
// No binding on other organization | |||
AlmAppInstallDto otherAlmAppInstall = db.alm().insertAlmAppInstall(); | |||
Optional<OrganizationAlmBindingDto> result = underTest.selectByAlmAppInstall(db.getSession(), otherAlmAppInstall); | |||
assertThat(result).isEmpty(); | |||
} | |||
@Test | |||
public void insert() { | |||
when(uuidFactory.create()).thenReturn("ABCD"); |
@@ -22,7 +22,8 @@ import { | |||
AlmApplication, | |||
AlmOrganization, | |||
AlmRepository, | |||
AlmUnboundApplication | |||
AlmUnboundApplication, | |||
OrganizationBase | |||
} from '../app/types'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
@@ -51,10 +52,20 @@ function fetchAlmOrganization(data: { installationId: string }, remainingTries: | |||
); | |||
} | |||
export function getAlmOrganization(data: { installationId: string }): Promise<AlmOrganization> { | |||
return fetchAlmOrganization(data, 5).then(({ organization }) => ({ | |||
...organization, | |||
name: organization.name || organization.key | |||
export interface GetAlmOrganizationResponse { | |||
almOrganization: AlmOrganization; | |||
boundOrganization?: OrganizationBase; | |||
} | |||
export function getAlmOrganization(data: { | |||
installationId: string; | |||
}): Promise<GetAlmOrganizationResponse> { | |||
return fetchAlmOrganization(data, 5).then(({ almOrganization, boundOrganization }) => ({ | |||
almOrganization: { | |||
...almOrganization, | |||
name: almOrganization.name || almOrganization.key | |||
}, | |||
boundOrganization | |||
})); | |||
} | |||
@@ -64,8 +75,12 @@ export function getRepositories(data: { | |||
return getJSON('/api/alm_integration/list_repositories', data).catch(throwGlobalError); | |||
} | |||
export function listUnboundApplications(): Promise<{ applications: AlmUnboundApplication[] }> { | |||
return getJSON('/api/alm_integration/list_unbound_applications').catch(throwGlobalError); | |||
export function listUnboundApplications(): Promise<AlmUnboundApplication[]> { | |||
return getJSON('/api/alm_integration/list_unbound_applications').then( | |||
({ applications }) => | |||
applications.map((app: AlmUnboundApplication) => ({ ...app, name: app.name || app.key })), | |||
throwGlobalError | |||
); | |||
} | |||
export function provisionProject(data: { |
@@ -42,6 +42,7 @@ export interface AlmRepository { | |||
export interface AlmUnboundApplication { | |||
installationId: string; | |||
key: string; | |||
name: string; | |||
} | |||
@@ -38,8 +38,7 @@ import { getBaseUrl } from '../../../helpers/urls'; | |||
export enum Filters { | |||
Bind = 'bind', | |||
Create = 'create', | |||
None = 'none' | |||
Create = 'create' | |||
} | |||
interface Props { | |||
@@ -47,6 +46,7 @@ interface Props { | |||
almInstallId?: string; | |||
almOrganization?: AlmOrganization; | |||
almUnboundApplications: AlmUnboundApplication[]; | |||
boundOrganization?: OrganizationBase; | |||
createOrganization: ( | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
@@ -55,14 +55,14 @@ interface Props { | |||
} | |||
interface State { | |||
filter: Filters; | |||
filter?: Filters; | |||
} | |||
export default class AutoOrganizationCreate extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
filter: props.unboundOrganizations.length === 0 ? Filters.Create : Filters.None | |||
filter: props.unboundOrganizations.length === 0 ? Filters.Create : undefined | |||
}; | |||
} | |||
@@ -71,19 +71,16 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S | |||
}; | |||
handleCreateOrganization = (organization: Required<OrganizationBase>) => { | |||
if (organization) { | |||
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)); | |||
} | |||
return Promise.reject(); | |||
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)); | |||
}; | |||
handleBindOrganization = (organization: string) => { | |||
@@ -97,8 +94,14 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S | |||
}; | |||
render() { | |||
const { almApplication, almInstallId, almOrganization, unboundOrganizations } = this.props; | |||
if (almInstallId && almOrganization) { | |||
const { | |||
almApplication, | |||
almInstallId, | |||
almOrganization, | |||
boundOrganization, | |||
unboundOrganizations | |||
} = this.props; | |||
if (almInstallId && almOrganization && !boundOrganization) { | |||
const { filter } = this.state; | |||
const hasUnboundOrgs = unboundOrganizations.length > 0; | |||
return ( | |||
@@ -168,7 +171,9 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S | |||
<ChooseRemoteOrganizationStep | |||
almApplication={this.props.almApplication} | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
almUnboundApplications={this.props.almUnboundApplications} | |||
boundOrganization={boundOrganization} | |||
/> | |||
); | |||
} |
@@ -45,20 +45,16 @@ interface Props { | |||
export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> { | |||
handleCreateOrganization = (organization: Required<OrganizationBase>) => { | |||
if (organization) { | |||
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)); | |||
} else { | |||
return Promise.reject(); | |||
} | |||
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)); | |||
}; | |||
render() { | |||
@@ -69,7 +65,7 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr | |||
onOpen={() => {}} | |||
open={true} | |||
organization={importPersonalOrg}> | |||
<p className="huge-spacer-bottom"> | |||
<div className="huge-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.import_personal_organization_x')} | |||
id="onboarding.import_personal_organization_x" | |||
@@ -89,7 +85,7 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr | |||
personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong> | |||
}} | |||
/> | |||
</p> | |||
</div> | |||
<OrganizationDetailsForm | |||
keyReadOnly={true} | |||
onContinue={this.handleCreateOrganization} |
@@ -19,14 +19,21 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { WithRouterProps, withRouter } from 'react-router'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { sortBy } from 'lodash'; | |||
import { serializeQuery } from './utils'; | |||
import IdentityProviderLink from '../../../components/ui/IdentityProviderLink'; | |||
import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; | |||
import Select from '../../../components/controls/Select'; | |||
import Step from '../../tutorials/components/Step'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { SubmitButton } from '../../../components/ui/buttons'; | |||
import { AlmApplication, AlmUnboundApplication } from '../../../app/types'; | |||
import { | |||
AlmApplication, | |||
AlmOrganization, | |||
AlmUnboundApplication, | |||
OrganizationBase | |||
} from '../../../app/types'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
import { sanitizeAlmId } from '../../../helpers/almIntegrations'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -34,7 +41,9 @@ import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
almApplication: AlmApplication; | |||
almInstallId?: string; | |||
almOrganization?: AlmOrganization; | |||
almUnboundApplications: AlmUnboundApplication[]; | |||
boundOrganization?: OrganizationBase; | |||
} | |||
interface State { | |||
@@ -82,19 +91,58 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent< | |||
}; | |||
renderForm = () => { | |||
const { almApplication, almInstallId, almUnboundApplications } = this.props; | |||
const { | |||
almApplication, | |||
almInstallId, | |||
almOrganization, | |||
almUnboundApplications, | |||
boundOrganization | |||
} = this.props; | |||
const { unboundInstallationId } = this.state; | |||
return ( | |||
<div className="boxed-group-inner"> | |||
{almInstallId && ( | |||
<Alert className="markdown big-spacer-bottom width-60" variant="error"> | |||
{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> | |||
</Alert> | |||
)} | |||
{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 abs-width-400"> | |||
<IdentityProviderLink |
@@ -42,6 +42,7 @@ import { | |||
bindAlmOrganization, | |||
getAlmAppInfo, | |||
getAlmOrganization, | |||
GetAlmOrganizationResponse, | |||
listUnboundApplications | |||
} from '../../../api/alm-integration'; | |||
import { getSubscriptionPlans } from '../../../api/billing'; | |||
@@ -59,8 +60,7 @@ import { translate } from '../../../helpers/l10n'; | |||
import { get, remove } from '../../../helpers/storage'; | |||
import { slugify } from '../../../helpers/strings'; | |||
import { getOrganizationUrl } from '../../../helpers/urls'; | |||
import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; | |||
import { skipOnboarding } from '../../../api/users'; | |||
import { skipOnboarding } from '../../../store/users'; | |||
import * as api from '../../../api/organizations'; | |||
import * as actions from '../../../store/organizations'; | |||
import '../../../app/styles/sonarcloud.css'; | |||
@@ -76,7 +76,7 @@ interface Props { | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
userOrganizations: Organization[]; | |||
skipOnboardingAction: () => void; | |||
skipOnboarding: () => void; | |||
} | |||
interface State { | |||
@@ -84,6 +84,7 @@ interface State { | |||
almOrganization?: AlmOrganization; | |||
almOrgLoading: boolean; | |||
almUnboundApplications: AlmUnboundApplication[]; | |||
boundOrganization?: OrganizationBase; | |||
loading: boolean; | |||
organization?: Organization; | |||
subscriptionPlans?: SubscriptionPlan[]; | |||
@@ -127,7 +128,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
if (query.almInstallId) { | |||
this.fetchAlmOrganization(query.almInstallId); | |||
} else { | |||
this.setState({ almOrganization: undefined, loading: true }); | |||
this.setState({ almOrganization: undefined, boundOrganization: undefined, loading: true }); | |||
this.fetchAlmUnboundApplications().then(this.stopLoading, this.stopLoading); | |||
} | |||
} | |||
@@ -136,6 +137,9 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
document.body.classList.remove('white-page'); | |||
if (document.documentElement) { | |||
document.documentElement.classList.remove('white-page'); | |||
} | |||
} | |||
fetchAlmApplication = () => { | |||
@@ -147,14 +151,14 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
}; | |||
fetchAlmUnboundApplications = () => { | |||
return listUnboundApplications().then(({ applications }) => { | |||
return listUnboundApplications().then(almUnboundApplications => { | |||
if (this.mounted) { | |||
this.setState({ almUnboundApplications: applications }); | |||
this.setState({ almUnboundApplications }); | |||
} | |||
}); | |||
}; | |||
fetchValidOrgKey = (almOrganization: AlmOrganization) => { | |||
setValidOrgKey = (almOrganization: AlmOrganization) => { | |||
const key = slugify(almOrganization.key); | |||
const keys = [key, ...times(9, i => `${key}-${i + 1}`)]; | |||
return api | |||
@@ -167,24 +171,31 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
() => key | |||
) | |||
.then(key => { | |||
return { ...almOrganization, key }; | |||
return { almOrganization: { ...almOrganization, key } }; | |||
}); | |||
}; | |||
fetchAlmOrganization = (installationId: string) => { | |||
this.setState({ almOrgLoading: true }); | |||
return getAlmOrganization({ installationId }) | |||
.then(this.fetchValidOrgKey) | |||
.then(almOrganization => { | |||
if (this.mounted) { | |||
this.setState({ almOrganization, almOrgLoading: false }); | |||
.then(({ almOrganization, boundOrganization }) => { | |||
if (boundOrganization) { | |||
return Promise.resolve({ almOrganization, boundOrganization }); | |||
} | |||
return this.setValidOrgKey(almOrganization); | |||
}) | |||
.catch(() => { | |||
if (this.mounted) { | |||
this.setState({ almOrgLoading: false }); | |||
.then( | |||
({ almOrganization, boundOrganization }: GetAlmOrganizationResponse) => { | |||
if (this.mounted) { | |||
this.setState({ almOrganization, almOrgLoading: false, boundOrganization }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ almOrgLoading: false }); | |||
} | |||
} | |||
}); | |||
); | |||
}; | |||
fetchSubscriptionPlans = () => { | |||
@@ -196,8 +207,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
}; | |||
handleOrgCreated = (organization: string, justCreated = true) => { | |||
skipOnboarding().catch(() => {}); | |||
this.props.skipOnboardingAction(); | |||
this.props.skipOnboarding(); | |||
const redirectProjectTimestamp = get(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP); | |||
remove(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP); | |||
if ( | |||
@@ -237,7 +247,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
renderContent = (almInstallId?: string, importPersonalOrg?: Organization) => { | |||
const { currentUser, location } = this.props; | |||
const { almApplication, almOrganization } = this.state; | |||
const state = (location.state || {}) as LocationState; | |||
const state: LocationState = location.state || {}; | |||
if (importPersonalOrg && almOrganization && almApplication) { | |||
return ( | |||
@@ -287,6 +297,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
almUnboundApplications={this.state.almUnboundApplications} | |||
boundOrganization={this.state.boundOrganization} | |||
createOrganization={this.props.createOrganization} | |||
onOrgCreated={this.handleOrgCreated} | |||
unboundOrganizations={this.props.userOrganizations.filter( | |||
@@ -392,7 +403,7 @@ const mapDispatchToProps = { | |||
createOrganization: createOrganization as any, | |||
deleteOrganization: deleteOrganization as any, | |||
updateOrganization: updateOrganization as any, | |||
skipOnboardingAction: skipOnboardingAction as any | |||
skipOnboarding: skipOnboarding as any | |||
}; | |||
export default whenLoggedIn( |
@@ -31,7 +31,7 @@ it('should display an alert message', () => { | |||
}); | |||
it('should display unbound installations', () => { | |||
const installation = { installationId: '12345', name: 'Foo' }; | |||
const installation = { installationId: '12345', key: 'foo', name: 'Foo' }; | |||
const push = jest.fn(); | |||
const wrapper = shallowRender({ | |||
almUnboundApplications: [installation], | |||
@@ -47,6 +47,16 @@ it('should display unbound installations', () => { | |||
}); | |||
}); | |||
it('should display already bound alert message', () => { | |||
expect( | |||
shallowRender({ | |||
almInstallId: 'foo', | |||
almOrganization: { avatar: 'foo-avatar', key: 'foo', name: 'Foo', personal: false }, | |||
boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' } | |||
}).find('Alert') | |||
).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) { | |||
return shallow( | |||
// @ts-ignore avoid passing everything from WithRouterProps |
@@ -50,14 +50,16 @@ jest.mock('../../../../api/alm-integration', () => ({ | |||
} | |||
}), | |||
getAlmOrganization: jest.fn().mockResolvedValue({ | |||
avatar: 'my-avatar', | |||
description: 'Continuous Code Quality', | |||
key: 'sonarsource', | |||
name: 'SonarSource', | |||
personal: false, | |||
url: 'https://www.sonarsource.com' | |||
almOrganization: { | |||
avatar: 'my-avatar', | |||
description: 'Continuous Code Quality', | |||
key: 'sonarsource', | |||
name: 'SonarSource', | |||
personal: false, | |||
url: 'https://www.sonarsource.com' | |||
} | |||
}), | |||
listUnboundApplications: jest.fn().mockResolvedValue({ applications: [] }) | |||
listUnboundApplications: jest.fn().mockResolvedValue([]) | |||
})); | |||
jest.mock('../../../../api/organizations', () => ({ | |||
@@ -127,10 +129,12 @@ it('should render with auto tab selected and manual disabled', async () => { | |||
it('should render with auto personal organization bind page', async () => { | |||
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({ | |||
key: 'foo', | |||
name: 'Foo', | |||
avatar: 'my-avatar', | |||
personal: true | |||
almOrganization: { | |||
key: 'foo', | |||
name: 'Foo', | |||
avatar: 'my-avatar', | |||
personal: true | |||
} | |||
}); | |||
const wrapper = shallowRender({ | |||
currentUser: { ...user, externalProvider: 'github', personalOrganization: 'foo' }, | |||
@@ -143,10 +147,12 @@ it('should render with auto personal organization bind page', async () => { | |||
it('should slugify and find a uniq organization key', async () => { | |||
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({ | |||
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', | |||
key: 'Foo&Bar', | |||
name: 'Foo & Bar', | |||
personal: true | |||
almOrganization: { | |||
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', | |||
key: 'Foo&Bar', | |||
name: 'Foo & Bar', | |||
personal: true | |||
} | |||
}); | |||
(getOrganizations as jest.Mock<any>).mockResolvedValueOnce({ | |||
organizations: [{ key: 'foo-and-bar' }, { key: 'foo-and-bar-1' }] | |||
@@ -246,7 +252,7 @@ function shallowRender(props: Partial<CreateOrganization['props']> = {}) { | |||
// @ts-ignore avoid passing everything from WithRouterProps | |||
location={{}} | |||
router={mockRouter()} | |||
skipOnboardingAction={jest.fn()} | |||
skipOnboarding={jest.fn()} | |||
updateOrganization={jest.fn()} | |||
userOrganizations={[ | |||
{ actions: { admin: true }, key: 'foo', name: 'Foo' }, |
@@ -12,7 +12,7 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
> | |||
<p | |||
<div | |||
className="huge-spacer-bottom" | |||
> | |||
<FormattedMessage | |||
@@ -44,7 +44,7 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
</p> | |||
</div> | |||
<OrganizationDetailsForm | |||
keyReadOnly={true} | |||
onContinue={[Function]} |
@@ -1,19 +1,62 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display already bound alert message 1`] = ` | |||
<Alert | |||
className="big-spacer-bottom width-60" | |||
variant="error" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.import_organization.already_bound_x" | |||
id="onboarding.import_organization.already_bound_x" | |||
values={ | |||
Object { | |||
"avatar": <img | |||
alt="GitHub" | |||
className="little-spacer-left" | |||
src="/images/sonarcloud/github.svg" | |||
width={16} | |||
/>, | |||
"boundAvatar": <OrganizationAvatar | |||
className="little-spacer-left" | |||
organization={ | |||
Object { | |||
"avatar": "bound-avatar", | |||
"key": "bound", | |||
"name": "Bound", | |||
} | |||
} | |||
small={true} | |||
/>, | |||
"boundName": <strong> | |||
Bound | |||
</strong>, | |||
"name": <strong> | |||
Foo | |||
</strong>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
`; | |||
exports[`should display an alert message 1`] = ` | |||
<Alert | |||
className="markdown big-spacer-bottom width-60" | |||
className="big-spacer-bottom width-60" | |||
variant="error" | |||
> | |||
onboarding.import_organization.org_not_found | |||
<ul> | |||
<li> | |||
onboarding.import_organization.org_not_found.tips_1 | |||
</li> | |||
<li> | |||
onboarding.import_organization.org_not_found.tips_2 | |||
</li> | |||
</ul> | |||
<div | |||
className="markdown" | |||
> | |||
onboarding.import_organization.org_not_found | |||
<ul> | |||
<li> | |||
onboarding.import_organization.org_not_found.tips_1 | |||
</li> | |||
<li> | |||
onboarding.import_organization.org_not_found.tips_2 | |||
</li> | |||
</ul> | |||
</div> | |||
</Alert> | |||
`; | |||
@@ -103,6 +146,7 @@ exports[`should display unbound installations 1`] = ` | |||
Array [ | |||
Object { | |||
"installationId": "12345", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
] |
@@ -2775,6 +2775,7 @@ onboarding.import_organization.bitbucket=Import from BitBucket teams | |||
onboarding.import_organization.github=Import from GitHub organizations | |||
onboarding.import_organization.bind_existing=Bind to an existing sonarcloud organization | |||
onboarding.import_organization.create_new=Create new SonarCloud organization from it | |||
onboarding.import_organization.already_bound_x=Your organization {avatar} {name} is already bound to the SonarCloud organization {boundAvatar} {boundName}. Try again and choose a different organization. | |||
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} | |||