]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11323 Ease workflow to bind personal organizations
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 19 Oct 2018 15:25:13 +0000 (17:25 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 16 Nov 2018 19:21:04 +0000 (20:21 +0100)
* Create withUserOrganizations and use it in create Orgs/Projects page
* Update ALM object format in api/navigation/component and api/organizations/search

36 files changed:
server/sonar-web/src/main/js/api/alm-integration.ts
server/sonar-web/src/main/js/api/organizations.ts
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx
server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx
server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx
server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx
server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
server/sonar-web/src/main/js/helpers/almIntegrations.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5805eea72b973b2f19c2f4d528e95c78db91ae67..568eb2407ffc571dfde46e84918d2dd7ce195aeb 100644 (file)
  * 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);
 }
index 1b72037ce832fb01edb9b1bd79c6036c9e543178..3ed1fe0c87c53d7230b0ad17d4544727ee3633ce 100644 (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);
 }
index f32e3dfa30875c955a451af61eb805e064f9254a..f3301290cd36356fc4038faa98538b175c5fd864 100644 (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>
index 4c3eb39bf08030e091d5db06e4807234ec457a3f..2d1dd8d5f65a53f8054f7a12ea59a7610bbfc692 100644 (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}
index 803ba673ad2083ec60543ef0a765256a8aac4b70..df5915bbfd03a1ed6fffda049283ae00b48557d0 100644 (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;
index f7882de2ba95f3ccd7266e952cc99860a9105bcb..14fc63bdbf8f9c2a095e65c220f2b22649a9c553 100644 (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}
         />
       );
     }
index 56e401bd103c9d63c22d7c634d1002b582000e1d..17c0e3a83eef6395352e2e46a1d767fcd4263c23 100644 (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)
+    )
   )
 );
index 2b31d8a23791235c3b27e53ae9cd32bec16c7d4c..d6e5f0696a160dd88e03db640a6253b97e249526 100644 (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>
index 8beb62e897c0e60b33da3cbdd4700ad534c88b37..17e57b70a94668a194d5f188a914ec750f12d7e5 100644 (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}
     />
   );
index 9c1e58367fb206c2e74d30500efaa079e86fe7ad..330f8062730ca42d684dcb7a219214cca6459241 100644 (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}
     />
   );
index a57042c6f50abed77714197c6634dff8cedf2ebb..423b5f2181bbc152a14a56aa5302962d8cfad3cf 100644 (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}
index c4b506bc1f3eedea4eacd14fd569d643bdd898b2..c25c32f3e2caf2f6b6b325fbb632884153a0b3f2 100644 (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"
               >
index 0fd3c61b35a280f5d23216131bac21f954e45841..a4fcc91d9790d52812b9fd692c12998f6059317f 100644 (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>
     );
index a6bcde51a7e4ccec51187acae5e6c967875486b8..d559b30e4d010063c08ef91321658ec8f6562900 100644 (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);
index 8cba7d969a312939e6afefdbc94eaf8e3f502a89..05d2e74dd683545c06fa1165dd84e8575256bf0c 100644 (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>
+`;
index d46f7bbaa36f1d8bea74e304b87f0dfde6e9f9a1..53382bfc34ed57025109f86a67eb9e192b08e484 100644 (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)
+  )
 );
index a1d52e0af64485b6d147fd15a116a96f2dcdec39..ed7cf5d30ee45a5ce4adba548dbc37ae8ffd864c 100644 (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}
index 3364a73a344a77c4165d65aaf43d7be3542ddfca..a7664c362be180f3e3586dbb0af4b312ec3dde1f 100644 (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=""
index 6c9acb9d77cc9fa4edbbcefa2360acc31aa27b12..12af30eb2b8fbbddbd0c80da9aea48d1c7ce7466 100644 (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}
     />
index 4224b152a3871b0af63eecb5d9dde598483fcd59..cc7e426bbc463c21d7a5a49a519131f18dc4425e 100644 (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(
index 147427d62a654c635a0c7e6db8de02be7c3326ef..a96a37b53b5754eae3e8d3d7b7994d7f96e0b836 100644 (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",
         },
index 6e1f9059e897949797cb407346d5306be8f3aebd..5e2c2e1a150f9859687df18f6c0792f70838506b 100644 (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",
           },
index 50cd939ec7ee31233d549b45ff8fa47cc5370a0b..367f0265e728fdd2b819896669380aff74bd9eab 100644 (file)
@@ -25,7 +25,10 @@ exports[`should render correctly 1`] = `
     options={
       Array [
         Object {
-          "almId": "github",
+          "alm": Object {
+            "key": "github",
+            "url": "",
+          },
           "key": "bar",
           "name": "Bar",
         },
index d376085573a44303ed562bae56f2d8ad5e5d2426..4d7bb7f71e1408d2630d62087b6fa4d47bb93104 100644 (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>
index 021b80766e405e68b001cd6003cbaa068363f931..2d6853a617d72d6b88a81112f787cac0049802f8 100644 (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
index cf3e383e57384de3444bf1081f1d7089444ff2c1..ca3bd2d87cdc6edcb7945c8b1c5ca1994aaf1a02 100644 (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",
index d930fb3d064fccbffcef4b843056bb14f8af6bc0..031af9848cd91b78c7dcd28d9049ccfe829da3e9 100644 (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}
index d433da91112e74a1ca7454a74a976d199379af49..65b4f31b5108e806edbd6660e7043e6c302930fa 100644 (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
index bc4a6d7b55b6b0d5db096273dfd6eeda8a108c91..22c182361a1497553447738b5e207dc06101b7c1 100644 (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();
 });
index e75dad4cdd8c41937d0ec68450af034909836fa7..0bed5354f4efe747bafaf0f4f142c9b2d665a254 100644 (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();
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 (file)
index 0000000..dee2f7f
--- /dev/null
@@ -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]);
+});
index 00dd040670e1e87fd54c4d4ba87d8ab162337c07..2ce4c8894c09accef006f694193e95b542fd0c15 100644 (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);
 }
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 (file)
index 0000000..bedc033
--- /dev/null
@@ -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);
+}
index 02353fe12e279842107293afe0b3f176792d110d..7a54b6b68bd7017f2360dc0674415960d4b6a3df 100644 (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');
index c943f67b90e5e4c8c4d1b03fc5f1adba0a628579..fdfe7abd17c110311f6eb3d5679dae7d41be1ca3 100644 (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;
 }
index b798554254fda48a36208a7091b6c65dcb828e2f..1f34203174bfd7358c0013074aef9f9dbf20ed32 100644 (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!