Browse Source

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
tags/7.5
Grégoire Aubert 5 years ago
parent
commit
07546d5e1f
36 changed files with 488 additions and 174 deletions
  1. 5
    1
      server/sonar-web/src/main/js/api/alm-integration.ts
  2. 1
    1
      server/sonar-web/src/main/js/api/organizations.ts
  3. 4
    4
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
  4. 1
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
  5. 3
    4
      server/sonar-web/src/main/js/app/types.ts
  6. 40
    12
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  7. 84
    46
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  8. 7
    1
      server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
  9. 23
    1
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
  10. 4
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
  11. 57
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
  12. 3
    3
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
  13. 22
    16
      server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
  14. 7
    0
      server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
  15. 21
    0
      server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
  16. 11
    27
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  17. 3
    3
      server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
  18. 2
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
  19. 1
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
  20. 4
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
  21. 8
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
  22. 12
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  23. 4
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
  24. 4
    4
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
  25. 1
    2
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
  26. 4
    2
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
  27. 3
    3
      server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx
  28. 4
    4
      server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx
  29. 4
    4
      server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx
  30. 1
    4
      server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx
  31. 49
    0
      server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx
  32. 2
    3
      server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
  33. 61
    0
      server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx
  34. 6
    1
      server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
  35. 14
    10
      server/sonar-web/src/main/js/helpers/almIntegrations.ts
  36. 8
    3
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 5
- 1
server/sonar-web/src/main/js/api/alm-integration.ts View File

@@ -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);
}

+ 1
- 1
server/sonar-web/src/main/js/api/organizations.ts View File

@@ -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);
}

+ 4
- 4
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx View File

@@ -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>

+ 1
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx View File

@@ -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}

+ 3
- 4
server/sonar-web/src/main/js/app/types.ts View File

@@ -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;

+ 40
- 12
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx View File

@@ -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}
/>
);
}

+ 84
- 46
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx View File

@@ -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)
)
)
);

+ 7
- 1
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx View File

@@ -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>

+ 23
- 1
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx View File

@@ -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}
/>
);

+ 4
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx View File

@@ -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}
/>
);

+ 57
- 2
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap View File

@@ -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}

+ 3
- 3
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap View File

@@ -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"
>

+ 22
- 16
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx View File

@@ -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>
);

+ 7
- 0
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx View File

@@ -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);

+ 21
- 0
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap View File

@@ -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>
`;

+ 11
- 27
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View File

@@ -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)
)
);

+ 3
- 3
server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx View File

@@ -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}

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx View File

@@ -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=""

+ 1
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx View File

@@ -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}
/>

+ 4
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx View File

@@ -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(

+ 8
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap View File

@@ -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",
},

+ 12
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap View File

@@ -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",
},

+ 4
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap View File

@@ -25,7 +25,10 @@ exports[`should render correctly 1`] = `
options={
Array [
Object {
"almId": "github",
"alm": Object {
"key": "github",
"url": "",
},
"key": "bar",
"name": "Bar",
},

+ 4
- 4
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx View File

@@ -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>

+ 1
- 2
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx View File

@@ -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

+ 4
- 2
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap View File

@@ -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",

+ 3
- 3
server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx View File

@@ -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}

+ 4
- 4
server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx View File

@@ -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

+ 4
- 4
server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx View File

@@ -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();
});

+ 1
- 4
server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx View File

@@ -20,7 +20,6 @@
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { createStore } from 'redux';
import { mockRouter } from '../../../helpers/testUtils';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import { whenLoggedIn } from '../whenLoggedIn';

@@ -44,15 +43,13 @@ it('should render for logged in user', () => {

it('should not render for anonymous user', () => {
const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } });
const router = mockRouter({ replace: jest.fn() });
const wrapper = shallow(<UnderTest />, { context: { store, router } });
const wrapper = shallow(<UnderTest />, { context: { store } });
expect(getRenderedType(wrapper)).toBe(null);
expect(handleRequiredAuthentication).toBeCalled();
});

function getRenderedType(wrapper: ShallowWrapper) {
return wrapper
.dive()
.dive()
.dive()
.type();

+ 49
- 0
server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx View File

@@ -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]);
});

+ 2
- 3
server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx View File

@@ -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);
}

+ 61
- 0
server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx View File

@@ -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);
}

+ 6
- 1
server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts View File

@@ -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');

+ 14
- 10
server/sonar-web/src/main/js/helpers/almIntegrations.ts View File

@@ -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;
}

+ 8
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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!

Loading…
Cancel
Save