From 07546d5e1f4047a1030a91d0ffaa39fb96e66a41 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Fri, 19 Oct 2018 17:25:13 +0200 Subject: [PATCH] SONAR-11323 Ease workflow to bind personal organizations * Create withUserOrganizations and use it in create Orgs/Projects page * Update ALM object format in api/navigation/component and api/organizations/search --- .../src/main/js/api/alm-integration.ts | 6 +- .../src/main/js/api/organizations.ts | 2 +- .../nav/component/ComponentNavHeader.tsx | 8 +- .../__tests__/ComponentNavHeader-test.tsx | 3 +- server/sonar-web/src/main/js/app/types.ts | 7 +- .../organization/AutoOrganizationCreate.tsx | 52 +++++-- .../organization/CreateOrganization.tsx | 130 +++++++++++------- .../organization/OrganizationDetailsStep.tsx | 8 +- .../__tests__/AutoOrganizationCreate-test.tsx | 24 +++- .../__tests__/CreateOrganization-test.tsx | 4 + .../AutoOrganizationCreate-test.tsx.snap | 59 +++++++- .../CreateOrganization-test.tsx.snap | 6 +- .../components/OrganizationKeyInput.tsx | 38 ++--- .../__tests__/OrganizationKeyInput-test.tsx | 7 + .../OrganizationKeyInput-test.tsx.snap | 21 +++ .../apps/create/project/CreateProjectPage.tsx | 38 ++--- .../create/project/OrganizationSelect.tsx | 6 +- .../__tests__/AutoProjectCreate-test.tsx | 4 +- .../__tests__/CreateProjectPage-test.tsx | 3 +- .../__tests__/OrganizationSelect-test.tsx | 5 +- .../AutoProjectCreate-test.tsx.snap | 10 +- .../CreateProjectPage-test.tsx.snap | 15 +- .../OrganizationSelect-test.tsx.snap | 5 +- .../OrganizationNavigationHeader.tsx | 8 +- .../OrganizationNavigationHeader-test.tsx | 3 +- ...OrganizationNavigationHeader-test.tsx.snap | 6 +- .../analyzeProject/AnalyzeTutorial.tsx | 6 +- .../AnalyzeTutorialSuggestion.tsx | 8 +- .../AnalyzeTutorialSuggestion-test.tsx | 8 +- .../hoc/__tests__/whenLoggedIn-test.tsx | 5 +- .../__tests__/withUserOrganizations-test.tsx | 49 +++++++ .../main/js/components/hoc/whenLoggedIn.tsx | 5 +- .../components/hoc/withUserOrganizations.tsx | 61 ++++++++ .../helpers/__tests__/almIntegrations-test.ts | 7 +- .../src/main/js/helpers/almIntegrations.ts | 24 ++-- .../resources/org/sonar/l10n/core.properties | 11 +- 36 files changed, 488 insertions(+), 174 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx create mode 100644 server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx diff --git a/server/sonar-web/src/main/js/api/alm-integration.ts b/server/sonar-web/src/main/js/api/alm-integration.ts index 5805eea72b9..568eb2407ff 100644 --- a/server/sonar-web/src/main/js/api/alm-integration.ts +++ b/server/sonar-web/src/main/js/api/alm-integration.ts @@ -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); } diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts index 1b72037ce83..3ed1fe0c87c 100644 --- a/server/sonar-web/src/main/js/api/organizations.ts +++ b/server/sonar-web/src/main/js/api/organizations.ts @@ -55,7 +55,7 @@ export function getOrganizationNavigation(key: string): Promise { return postJSON('/api/organizations/create', data).then(r => r.organization, throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx index f32e3dfa308..f3301290cd3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx @@ -73,17 +73,17 @@ export function ComponentNavHeader(props: Props) { )} {renderBreadcrumbs(component.breadcrumbs)} {isSonarCloud() && - component.almRepoUrl && ( + component.alm && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx index 4c3eb39bf08..2d1dd8d5f65 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 803ba673ad2..df5915bbfd0 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -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; diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx index f7882de2ba9..14fc63bdbf8 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx @@ -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; + importPersonalOrg?: Organization; onOrgCreated: (organization: string) => void; + updateOrganization: ( + organization: OrganizationBase & { installationId?: string } + ) => Promise; } export default class AutoOrganizationCreate extends React.PureComponent { handleCreateOrganization = (organization: Required) => { if (organization) { - return this.props - .createOrganization({ + const { importPersonalOrg } = this.props; + let promise: Promise; + 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 ( { width={16} /> ), - name: {almOrganization.name} + name: {almOrganization.name}, + personalAvatar: importPersonalOrg && ( + + ), + personalName: importPersonalOrg && {importPersonalOrg.name} }} />
{translate('onboarding.project_analysis.description')}
{translate('onboarding.project_analysis.commands_for_analysis')}
(WrappedComponent: React.ComponentClass
) { const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; - class Wrapper extends React.Component
{ + class Wrapper extends React.Component
{ static displayName = `whenLoggedIn(${wrappedDisplayName})`; componentDidMount() { @@ -45,5 +44,5 @@ export function whenLoggedIn
) { } } - return withCurrentUser(withRouter(Wrapper)); + return withCurrentUser(Wrapper); } diff --git a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx new file mode 100644 index 00000000000..bedc033bc09 --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx @@ -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
( + WrappedComponent: React.ComponentClass< + P & { + personalOrganization?: Organization; + userOrganizations: Organization[]; + } + > +) { + type Props = P & { fetchMyOrganizations: () => Promise; userOrganizations: Organization[] }; + const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + class Wrapper extends React.Component { + 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 ; + } + } + + const mapDispatchToProps = { fetchMyOrganizations }; + + function mapStateToProps(state: Store) { + return { userOrganizations: getMyOrganizations(state) }; + } + + return connect( + mapStateToProps, + mapDispatchToProps + )(Wrapper); +} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts index 02353fe12e2..7a54b6b68bd 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts @@ -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'); diff --git a/server/sonar-web/src/main/js/helpers/almIntegrations.ts b/server/sonar-web/src/main/js/helpers/almIntegrations.ts index c943f67b90e..fdfe7abd17c 100644 --- a/server/sonar-web/src/main/js/helpers/almIntegrations.ts +++ b/server/sonar-web/src/main/js/helpers/almIntegrations.ts @@ -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; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b798554254f..1f34203174b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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! -- 2.39.5