aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-10-19 17:25:13 +0200
committerSonarTech <sonartech@sonarsource.com>2018-11-16 20:21:04 +0100
commit07546d5e1f4047a1030a91d0ffaa39fb96e66a41 (patch)
tree442327150154571ec1be84a47cf4c012f0ba50bc
parent3ea9808248000c145f53a4f1cdb8711d63b97da4 (diff)
downloadsonarqube-07546d5e1f4047a1030a91d0ffaa39fb96e66a41.tar.gz
sonarqube-07546d5e1f4047a1030a91d0ffaa39fb96e66a41.zip
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
-rw-r--r--server/sonar-web/src/main/js/api/alm-integration.ts6
-rw-r--r--server/sonar-web/src/main/js/api/organizations.ts2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/types.ts7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx130
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap59
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap21
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap5
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx61
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts7
-rw-r--r--server/sonar-web/src/main/js/helpers/almIntegrations.ts24
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties11
36 files changed, 488 insertions, 174 deletions
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<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);
}
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 && (
<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>
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<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}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
index 56e401bd103..17c0e3a83ee 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
@@ -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)
+ )
)
);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
index 2b31d8a2379..d6e5f0696a1 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
index 8beb62e897c..17e57b70a94 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
@@ -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}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
index 9c1e58367fb..330f8062730 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
@@ -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}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
index a57042c6f50..423b5f2181b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
@@ -1,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}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
index c4b506bc1f3..c25c32f3e2c 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
@@ -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"
>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
index 0fd3c61b35a..a4fcc91d979 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
index a6bcde51a7e..d559b30e4d0 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
index 8cba7d969a3..05d2e74dd68 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index d46f7bbaa36..53382bfc34e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -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)
+ )
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
index a1d52e0af64..ed7cf5d30ee 100644
--- a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
index 3364a73a344..a7664c362be 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
@@ -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=""
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
index 6c9acb9d77c..12af30eb2b8 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
@@ -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}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
index 4224b152a38..cc7e426bbc4 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
@@ -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(
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
index 147427d62a6..a96a37b53b5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
@@ -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",
},
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index 6e1f9059e89..5e2c2e1a150 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -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",
},
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
index 50cd939ec7e..367f0265e72 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
@@ -25,7 +25,10 @@ exports[`should render correctly 1`] = `
options={
Array [
Object {
- "almId": "github",
+ "alm": Object {
+ "key": "github",
+ "url": "",
+ },
"key": "bar",
"name": "Bar",
},
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
index d376085573a..4d7bb7f71e1 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
index 021b80766e4..2d6853a617d 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
index cf3e383e573..ca3bd2d87cd 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
@@ -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",
diff --git a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx
index d930fb3d064..031af9848cd 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx
index d433da91112..65b4f31b510 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx
index bc4a6d7b55b..22c182361a1 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx
index e75dad4cdd8..0bed5354f4e 100644
--- a/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx
+++ b/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx
@@ -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,8 +43,7 @@ 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();
});
@@ -54,6 +52,5 @@ function getRenderedType(wrapper: ShallowWrapper) {
return wrapper
.dive()
.dive()
- .dive()
.type();
}
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx
new file mode 100644
index 00000000000..dee2f7fec03
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx
@@ -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]);
+});
diff --git a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
index 00dd040670e..2ce4c8894c0 100644
--- a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
+++ b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
@@ -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);
}
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<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);
+}
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!