]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11322 Import repos from bound organizations
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 16 Oct 2018 15:03:32 +0000 (17:03 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 16 Nov 2018 19:21:04 +0000 (20:21 +0100)
* Move project create page in the create folder
* Move HOCs to components
* Update create project page
* Move getting user organizations in CreateProjectPage (1 level higher)
* Creact OrganizationSelect component
* Create RemoteRepositories component
* Use OrganizationSelect in ManualProjectCreate
* Add OrganizationSelect and RemoteRepositories in AutoProjectCreate page
* Rework validation of the create organization page
* Add 'organization' param on list_repositories and provision_projects

79 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/alm/AlmAppInstallMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/alm/AlmAppInstallDaoTest.java
server/sonar-web/src/main/js/api/alm-integration.ts
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/utils/__test__/handleRequiredAuthentication-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-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/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/AlmRepositoryItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/create/utils.ts [deleted file]
server/sonar-web/src/main/js/apps/projects/routes.ts
server/sonar-web/src/main/js/components/controls/Tabs.tsx
server/sonar-web/src/main/js/components/controls/ValidationInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/__tests__/withCurrentUser-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 005017eefd8d93aef84784218e1c37bda81c8e7d..0b4c5ca42f6469eb5434b4eb56bb49469c4aeda2 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.db.alm;
 
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import javax.annotation.Nullable;
 import org.sonar.api.utils.System2;
@@ -45,6 +44,11 @@ public class AlmAppInstallDao implements Dao {
     this.uuidFactory = uuidFactory;
   }
 
+  public Optional<AlmAppInstallDto> selectByUuid(DbSession dbSession, String uuid) {
+    AlmAppInstallMapper mapper = getMapper(dbSession);
+    return Optional.ofNullable(mapper.selectByUuid(uuid));
+  }
+
   public Optional<AlmAppInstallDto> selectByOwnerId(DbSession dbSession, ALM alm, String ownerId) {
     checkAlm(alm);
     checkOwnerId(ownerId);
index f840a5aa07bf0e0ed07f5ad1de29d5cc1b7d9636..4c18e9bfa4a280e6b37bd09ad92eff25443468cc 100644 (file)
@@ -32,6 +32,9 @@ public interface AlmAppInstallMapper {
   @CheckForNull
   AlmAppInstallDto selectByInstallationId(@Param("almId") String almId, @Param("installId") String installId);
 
+  @CheckForNull
+  AlmAppInstallDto selectByUuid(@Param("uuid") String uuid);
+
   List<AlmAppInstallDto> selectAllWithNoOwnerType();
 
   void insert(@Param("uuid") String uuid, @Param("almId") String almId, @Param("ownerId") String ownerId,
index 2fe08a4dd5ef8080d10f0c72721051d80f772c8f..0d539aad6cf3ed10ce2bab2be6fa7f8eaa7e16d2 100644 (file)
@@ -23,8 +23,7 @@
   </select>
 
   <select id="selectByInstallationId" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
-    select
-    <include refid="sqlColumns"/>
+    select <include refid="sqlColumns"/>
     from
       alm_app_installs
     where
       and install_id = #{installId, jdbcType=VARCHAR}
   </select>
 
+  <select id="selectByUuid" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
+    select <include refid="sqlColumns"/>
+    from
+      alm_app_installs
+    where
+      uuid = #{uuid, jdbcType=VARCHAR}
+  </select>
+
   <select id="selectAllWithNoOwnerType" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
     select <include refid="sqlColumns" />
     from
-    alm_app_installs
+      alm_app_installs
     where
-    is_owner_user is null
+      is_owner_user is null
   </select>
 
   <insert id="insert" parameterType="Map" useGeneratedKeys="false">
index ddc76bdf31650f612a8066505a24535b1de8b3b0..0e61ee69324e39b83739e5b8eabd9bc3485b0888 100644 (file)
@@ -59,6 +59,20 @@ public class AlmAppInstallDaoTest {
   private UuidFactory uuidFactory = mock(UuidFactory.class);
   private AlmAppInstallDao underTest = new AlmAppInstallDao(system2, uuidFactory);
 
+  @Test
+  public void selectByUuid() {
+    when(uuidFactory.create()).thenReturn(A_UUID);
+    when(system2.now()).thenReturn(DATE);
+    underTest.insertOrUpdate(dbSession, GITHUB, A_OWNER, true, AN_INSTALL);
+
+    assertThat(underTest.selectByUuid(dbSession, A_UUID).get())
+      .extracting(AlmAppInstallDto::getUuid, AlmAppInstallDto::getAlm, AlmAppInstallDto::getInstallId, AlmAppInstallDto::getOwnerId,
+        AlmAppInstallDto::getCreatedAt, AlmAppInstallDto::getUpdatedAt)
+      .contains(A_UUID, GITHUB, A_OWNER, AN_INSTALL, DATE, DATE);
+
+    assertThat(underTest.selectByUuid(dbSession, "foo")).isNotPresent();
+  }
+
   @Test
   public void selectByOwnerId() {
     when(uuidFactory.create()).thenReturn(A_UUID);
index c3f560044f236d9e05493d10ab2fc9adcb4f7fb9..5805eea72b973b2f19c2f4d528e95c78db91ae67 100644 (file)
@@ -35,18 +35,15 @@ export function getAlmOrganization(data: { installationId: string }): Promise<Al
   );
 }
 
-export function getRepositories(): Promise<{
-  almIntegration: {
-    installed: boolean;
-    installationUrl: string;
-  };
-  repositories: AlmRepository[];
-}> {
-  return getJSON('/api/alm_integration/list_repositories').catch(throwGlobalError);
+export function getRepositories(data: {
+  organization: string;
+}): Promise<{ repositories: AlmRepository[] }> {
+  return getJSON('/api/alm_integration/list_repositories', data).catch(throwGlobalError);
 }
 
 export function provisionProject(data: {
   installationKeys: string[];
+  organization: string;
 }): Promise<{ projects: Array<{ projectKey: string }> }> {
   return postJSON('/api/alm_integration/provision_projects', {
     ...data,
index c0393198c15717e740f801b245f5b4ac7038536e..155c2984e5d9bb56447c2b0a2a16f1ea9f43d4b6 100644 (file)
@@ -124,7 +124,7 @@ export class StartupModal extends React.PureComponent<Props, State> {
   openProjectOnboarding = (organization?: string) => {
     if (isSonarCloud()) {
       this.setState({ automatic: false, modal: undefined });
-      this.props.router.push({ pathname: `/projects/create`, query: { organization } });
+      this.props.router.push({ pathname: `/projects/create`, state: { organization } });
     } else {
       this.setState({ modal: ModalKey.projectOnboarding });
     }
diff --git a/server/sonar-web/src/main/js/app/utils/__test__/handleRequiredAuthentication-test.ts b/server/sonar-web/src/main/js/app/utils/__test__/handleRequiredAuthentication-test.ts
new file mode 100644 (file)
index 0000000..4c04265
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 handleRequiredAuthentication from '../handleRequiredAuthentication';
+import getHistory from '../getHistory';
+
+jest.mock('../getHistory', () => ({
+  default: jest.fn()
+}));
+
+it('should not render for anonymous user', () => {
+  const replace = jest.fn();
+  (getHistory as jest.Mock<any>).mockReturnValue({ replace });
+  handleRequiredAuthentication();
+  expect(replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' }));
+});
index 25a091e924687b507ddc4654d43671c818ddf0d2..602d3139d47270c40376002a8f67898b3bb27441 100644 (file)
@@ -22,6 +22,7 @@ import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
 import Step from '../../tutorials/components/Step';
 import { translate } from '../../../helpers/l10n';
 import { AlmApplication } from '../../../app/types';
+import { Alert } from '../../../components/ui/Alert';
 
 interface Props {
   almApplication: AlmApplication;
@@ -34,13 +35,13 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr
     return (
       <div className="boxed-group-inner">
         {almInstallId && (
-          <span className="alert alert-warning markdown big-spacer-bottom width-60">
+          <Alert className="markdown big-spacer-bottom width-60" variant="warning">
             {translate('onboarding.create_organization.import_org_not_found')}
             <ul>
               <li>{translate('onboarding.create_organization.import_org_not_found.tips_1')}</li>
               <li>{translate('onboarding.create_organization.import_org_not_found.tips_2')}</li>
             </ul>
-          </span>
+          </Alert>
         )}
         <IdentityProviderLink
           className="display-inline-block"
index 2767cdfa399a4d1d43d30a88e6f9a0ef0b98fa87..56e401bd103c9d63c22d7c634d1002b582000e1d 100644 (file)
@@ -25,11 +25,11 @@ import { Helmet } from 'react-helmet';
 import { FormattedMessage } from 'react-intl';
 import { Link, withRouter, WithRouterProps } from 'react-router';
 import { formatPrice, parseQuery } from './utils';
-import { whenLoggedIn } from './whenLoggedIn';
 import AutoOrganizationCreate from './AutoOrganizationCreate';
 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 { getSubscriptionPlans } from '../../../api/billing';
 import {
@@ -62,9 +62,11 @@ interface State {
   subscriptionPlans?: SubscriptionPlan[];
 }
 
+type TabKeys = 'auto' | 'manual';
+
 interface LocationState {
   paid?: boolean;
-  tab?: 'auto' | 'manual';
+  tab?: TabKeys;
 }
 
 export class CreateOrganization extends React.PureComponent<Props & WithRouterProps, State> {
@@ -125,7 +127,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
     });
   };
 
-  onTabChange = (tab: 'auto' | 'manual') => {
+  onTabChange = (tab: TabKeys) => {
     this.updateUrl({ tab });
   };
 
@@ -138,6 +140,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
   updateUrl = (state: Partial<LocationState> = {}) => {
     this.props.router.replace({
       pathname: this.props.location.pathname,
+      query: this.props.location.query,
       state: { ...(this.props.location.state || {}), ...state }
     });
   };
@@ -182,7 +185,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
           ) : (
             <>
               {almApplication && (
-                <Tabs
+                <Tabs<TabKeys>
                   onChange={this.onTabChange}
                   selected={showManualTab ? 'manual' : 'auto'}
                   tabs={[
@@ -195,13 +198,9 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
                             almApplication.key
                           )}
                           <span
-                            className={classNames(
-                              'rounded alert alert-small spacer-left display-inline-block',
-                              {
-                                'alert-info': !showManualTab,
-                                'alert-muted': showManualTab
-                              }
-                            )}>
+                            className={classNames('beta-badge spacer-left', {
+                              'is-muted': showManualTab
+                            })}>
                             {translate('beta')}
                           </span>
                         </>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
deleted file mode 100644 (file)
index 9526e06..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 * as classNames from 'classnames';
-import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
-
-interface Props {
-  description?: string;
-  dirty: boolean;
-  children: (inputProps: React.InputHTMLAttributes<Element>) => React.ReactElement<any>;
-  error: string | undefined;
-  id: string;
-  isSubmitting: boolean;
-  isValidating: boolean;
-  label: React.ReactNode;
-  name: string;
-  onBlur: React.FocusEventHandler;
-  onChange: React.ChangeEventHandler;
-  required?: boolean;
-  touched?: boolean;
-  value: string;
-}
-
-export default function OrganizationDetailsInput(props: Props) {
-  const hasError = props.dirty && props.touched && !props.isValidating && props.error !== undefined;
-  const isValid = props.dirty && props.touched && props.error === undefined;
-  return (
-    <div>
-      <label htmlFor={props.id}>
-        <strong>{props.label}</strong>
-        {props.required && <em className="mandatory">*</em>}
-      </label>
-      <div className="little-spacer-top spacer-bottom">
-        {props.children({
-          className: classNames('input-super-large', 'text-middle', {
-            'is-invalid': hasError,
-            'is-valid': isValid
-          }),
-          disabled: props.isSubmitting,
-          id: props.id,
-          name: props.name,
-          onBlur: props.onBlur,
-          onChange: props.onChange,
-          type: 'text',
-          value: props.value
-        })}
-        {hasError && (
-          <>
-            <AlertErrorIcon className="spacer-left text-middle" />
-            <span className="little-spacer-left text-danger text-middle">{props.error}</span>
-          </>
-        )}
-        {isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
-      </div>
-      {props.description && <div className="note abs-width-400">{props.description}</div>}
-    </div>
-  );
-}
index 75f1b75d17bceda6a29c2f5ca2db56018d6335e9..2b31d8a23791235c3b27e53ae9cd32bec16c7d4c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { isWebUri } from 'valid-url';
-import OrganizationDetailsInput from './OrganizationDetailsInput';
+import OrganizationAvatarInput from './components/OrganizationAvatarInput';
+import OrganizationDescriptionInput from './components/OrganizationDescriptionInput';
+import OrganizationKeyInput from './components/OrganizationKeyInput';
+import OrganizationNameInput from './components/OrganizationNameInput';
+import OrganizationUrlInput from './components/OrganizationUrlInput';
 import Step from '../../tutorials/components/Step';
-import ValidationForm, { ChildrenProps } from '../../../components/controls/ValidationForm';
 import { translate } from '../../../helpers/l10n';
 import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
 import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
 import DropdownIcon from '../../../components/icons-components/DropdownIcon';
-import { getHostUrl } from '../../../helpers/urls';
 import { OrganizationBase } from '../../../app/types';
-import { getOrganization } from '../../../api/organizations';
 
-type Values = Required<OrganizationBase>;
-
-const initialValues: Values = {
-  avatar: '',
-  description: '',
-  name: '',
-  key: '',
-  url: ''
-};
+type RequiredOrganization = Required<OrganizationBase>;
 
 interface Props {
   description?: React.ReactNode;
   finished: boolean;
-  onContinue: (organization: Required<OrganizationBase>) => Promise<void>;
+  onContinue: (organization: RequiredOrganization) => Promise<void>;
   onOpen: () => void;
   open: boolean;
   organization?: OrganizationBase & { key: string };
@@ -52,199 +44,143 @@ interface Props {
 
 interface State {
   additional: boolean;
+  avatar?: string;
+  description?: string;
+  key?: string;
+  name?: string;
+  submitting: boolean;
+  url?: string;
 }
 
+type ValidState = Pick<State, Exclude<keyof State, RequiredOrganization>> & RequiredOrganization;
+
 export default class OrganizationDetailsStep extends React.PureComponent<Props, State> {
-  state: State = { additional: false };
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    const { organization } = props;
+    this.state = {
+      additional: false,
+      avatar: (organization && organization.avatar) || '',
+      description: (organization && organization.description) || '',
+      key: (organization && organization.key) || undefined,
+      name: (organization && organization.name) || '',
+      submitting: false,
+      url: (organization && organization.url) || ''
+    };
+  }
 
-  getInitialValues = (): Values => {
-    const { organization } = this.props;
-    if (organization) {
-      return {
-        avatar: organization.avatar || '',
-        description: organization.description || '',
-        name: organization.name,
-        key: organization.key,
-        url: organization.url || ''
-      };
-    } else {
-      return initialValues;
-    }
-  };
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  canSubmit(state: State): state is ValidState {
+    return Boolean(
+      state.key !== undefined &&
+        state.name !== undefined &&
+        state.description !== undefined &&
+        state.avatar !== undefined &&
+        state.url !== undefined
+    );
+  }
 
   handleAdditionalClick = () => {
     this.setState(state => ({ additional: !state.additional }));
   };
 
-  checkFreeKey = (key: string) => {
-    return getOrganization(key).then(organization => organization === undefined, () => true);
+  handleKeyUpdate = (key: string | undefined) => {
+    this.setState({ key });
   };
 
-  handleValidate = ({ avatar, name, key, url }: Values) => {
-    const errors: { [P in keyof Values]?: string } = {};
+  handleNameUpdate = (name: string | undefined) => {
+    this.setState({ name });
+  };
 
-    if (avatar.length > 0 && !isWebUri(avatar)) {
-      errors.avatar = translate('onboarding.create_organization.avatar.error');
-    }
+  handleDescriptionUpdate = (description: string | undefined) => {
+    this.setState({ description });
+  };
 
-    if (name.length > 255) {
-      errors.name = translate('onboarding.create_organization.display_name.error');
-    }
+  handleAvatarUpdate = (avatar: string | undefined) => {
+    this.setState({ avatar });
+  };
 
-    if (key.length > 255 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]?$/.test(key)) {
-      errors.key = translate('onboarding.create_organization.organization_name.error');
-    }
+  handleUrlUpdate = (url: string | undefined) => {
+    this.setState({ url });
+  };
 
-    if (url.length > 0 && !isWebUri(url)) {
-      errors.url = translate('onboarding.create_organization.url.error');
+  handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    const { state } = this;
+    if (this.canSubmit(state)) {
+      this.setState({ submitting: true });
+      this.props
+        .onContinue({
+          avatar: state.avatar,
+          description: state.description,
+          key: state.key,
+          name: state.name,
+          url: state.url
+        })
+        .then(this.stopSubmitting, this.stopSubmitting);
     }
+  };
 
-    // don't try to check if the organization key is already taken if the key is invalid
-    if (errors.key) {
-      return Promise.reject(errors);
+  stopSubmitting = () => {
+    if (this.mounted) {
+      this.setState({ submitting: false });
     }
-
-    // TODO debounce
-    return this.checkFreeKey(key).then(free => {
-      if (!free) {
-        errors.key = translate('onboarding.create_organization.organization_name.taken');
-      }
-      return Object.keys(errors).length ? Promise.reject(errors) : Promise.resolve(errors);
-    });
   };
 
-  renderInnerForm = (props: ChildrenProps<Values>) => {
-    const {
-      dirty,
-      errors,
-      handleBlur,
-      handleChange,
-      isSubmitting,
-      isValid,
-      isValidating,
-      touched,
-      values
-    } = props;
-    const commonProps = {
-      dirty,
-      isValidating,
-      isSubmitting,
-      onBlur: handleBlur,
-      onChange: handleChange
-    };
+  renderForm = () => {
     return (
-      <>
-        <OrganizationDetailsInput
-          {...commonProps}
-          description={translate('onboarding.create_organization.organization_name.description')}
-          error={errors.key}
-          id="organization-key"
-          label={translate('onboarding.create_organization.organization_name')}
-          name="key"
-          required={true}
-          touched={touched.key}
-          value={values.key}>
-          {props => (
-            <div className="display-inline-flex-baseline">
-              <span className="little-spacer-right">
-                {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
-              </span>
-              <input autoFocus={true} maxLength={255} {...props} />
-            </div>
-          )}
-        </OrganizationDetailsInput>
-        <div className="big-spacer-top">
-          <ResetButtonLink onClick={this.handleAdditionalClick}>
-            {translate(
-              this.state.additional
-                ? 'onboarding.create_organization.hide_additional_info'
-                : 'onboarding.create_organization.add_additional_info'
-            )}
-            <DropdownIcon className="little-spacer-left" turned={this.state.additional} />
-          </ResetButtonLink>
-        </div>
-        <div className="js-additional-info" hidden={!this.state.additional}>
-          <div className="big-spacer-top">
-            <OrganizationDetailsInput
-              {...commonProps}
-              description={translate('onboarding.create_organization.display_name.description')}
-              error={errors.name}
-              id="organization-display-name"
-              label={translate('onboarding.create_organization.display_name')}
-              name="name"
-              touched={touched.name && values.name !== ''}
-              value={values.name}>
-              {props => <input {...props} />}
-            </OrganizationDetailsInput>
-          </div>
+      <div className="boxed-group-inner">
+        <form id="organization-form" onSubmit={this.handleSubmit}>
+          {this.props.description}
+          <OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} />
           <div className="big-spacer-top">
-            <OrganizationDetailsInput
-              {...commonProps}
-              description={translate('onboarding.create_organization.avatar.description')}
-              error={errors.avatar}
-              id="organization-avatar"
-              label={translate('onboarding.create_organization.avatar')}
-              name="avatar"
-              touched={touched.avatar && values.avatar !== ''}
-              value={values.avatar}>
-              {props => (
-                <>
-                  {values.avatar && (
-                    <img
-                      alt=""
-                      className="display-block spacer-bottom rounded"
-                      src={values.avatar}
-                      width={48}
-                    />
-                  )}
-                  <input {...props} />
-                </>
+            <ResetButtonLink onClick={this.handleAdditionalClick}>
+              {translate(
+                this.state.additional
+                  ? 'onboarding.create_organization.hide_additional_info'
+                  : 'onboarding.create_organization.add_additional_info'
               )}
-            </OrganizationDetailsInput>
+              <DropdownIcon className="little-spacer-left" turned={this.state.additional} />
+            </ResetButtonLink>
           </div>
-          <div className="big-spacer-top">
-            <OrganizationDetailsInput
-              {...commonProps}
-              error={errors.description}
-              id="organization-description"
-              label={translate('description')}
-              name="description"
-              touched={touched.description && values.description !== ''}
-              value={values.description}>
-              {props => <textarea {...props} maxLength={256} rows={3} />}
-            </OrganizationDetailsInput>
+          <div className="js-additional-info" hidden={!this.state.additional}>
+            <div className="big-spacer-top">
+              <OrganizationNameInput
+                initialValue={this.state.name}
+                onChange={this.handleNameUpdate}
+              />
+            </div>
+            <div className="big-spacer-top">
+              <OrganizationAvatarInput
+                initialValue={this.state.avatar}
+                onChange={this.handleDescriptionUpdate}
+              />
+            </div>
+            <div className="big-spacer-top">
+              <OrganizationDescriptionInput
+                initialValue={this.state.description}
+                onChange={this.handleAvatarUpdate}
+              />
+            </div>
+            <div className="big-spacer-top">
+              <OrganizationUrlInput initialValue={this.state.url} onChange={this.handleUrlUpdate} />
+            </div>
           </div>
           <div className="big-spacer-top">
-            <OrganizationDetailsInput
-              {...commonProps}
-              error={errors.url}
-              id="organization-url"
-              label={translate('onboarding.create_organization.url')}
-              name="url"
-              touched={touched.url && values.url !== ''}
-              value={values.url}>
-              {props => <input {...props} />}
-            </OrganizationDetailsInput>
+            <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}>
+              {this.props.submitText}
+            </SubmitButton>
           </div>
-        </div>
-        <div className="big-spacer-top">
-          <SubmitButton disabled={isSubmitting || !isValid}>{this.props.submitText}</SubmitButton>
-        </div>
-      </>
-    );
-  };
-
-  renderForm = () => {
-    return (
-      <div className="boxed-group-inner">
-        {this.props.description}
-        <ValidationForm<Values>
-          initialValues={this.getInitialValues()}
-          isInitialValid={this.props.organization !== undefined}
-          onSubmit={this.props.onContinue}
-          validate={this.handleValidate}>
-          {this.renderInnerForm}
-        </ValidationForm>
+        </form>
       </div>
     );
   };
index 5118540ce8184b16aa8d8cea73c5bc8157867a21..f7b7a0f6a42bc5bed36bc3222a59a316082dc92e 100644 (file)
@@ -19,9 +19,9 @@
  */
 import * as React from 'react';
 import BillingFormShim from './BillingFormShim';
-import { withCurrentUser } from './withCurrentUser';
 import PlanSelect, { Plan } from './PlanSelect';
 import Step from '../../tutorials/components/Step';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
 import { translate } from '../../../helpers/l10n';
 import { getExtensionStart } from '../../../app/components/extensions/utils';
 import { SubscriptionPlan } from '../../../app/types';
index a86ab8dfd1149fcbf7ba3dea43002c2cbd34ca5a..6721e089b15f3d1f90e8d39ff837123c81f3647b 100644 (file)
@@ -26,7 +26,7 @@ it('should render', () => {
 });
 
 it('should display a warning message', () => {
-  expect(shallowRender({ almInstallId: 'foo' }).find('.alert-warning')).toMatchSnapshot();
+  expect(shallowRender({ almInstallId: 'foo' }).find('Alert')).toMatchSnapshot();
 });
 
 function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) {
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx
deleted file mode 100644 (file)
index 4ddeac5..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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 OrganizationDetailsInput from '../OrganizationDetailsInput';
-
-it('should render', () => {
-  const render = jest.fn().mockReturnValue(<div />);
-  expect(
-    shallow(
-      <OrganizationDetailsInput
-        dirty={true}
-        error="This field is bad!"
-        id="field"
-        isSubmitting={true}
-        isValidating={false}
-        label="Label"
-        name="field"
-        onBlur={jest.fn()}
-        onChange={jest.fn()}
-        required={true}
-        touched={true}
-        value="foo">
-        {render}
-      </OrganizationDetailsInput>
-    )
-  ).toMatchSnapshot();
-  expect(render).toBeCalledWith(
-    expect.objectContaining({
-      className: 'input-super-large text-middle is-invalid',
-      disabled: true,
-      id: 'field',
-      name: 'field',
-      type: 'text',
-      value: 'foo'
-    })
-  );
-});
index f8748b45aecfd00416973a41486a2ea6842e689e..ac756ffdc8bba839a6eadb30a6f8b4daf4361062 100644 (file)
@@ -18,9 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
 import OrganizationDetailsStep from '../OrganizationDetailsStep';
-import { click } from '../../../../helpers/testUtils';
+import { click, submit } from '../../../../helpers/testUtils';
 import { getOrganization } from '../../../../api/organizations';
 
 jest.mock('../../../../api/organizations', () => ({
@@ -43,23 +43,24 @@ it('should render form', () => {
   );
   expect(wrapper).toMatchSnapshot();
   expect(wrapper.dive()).toMatchSnapshot();
-  expect(getForm(wrapper)).toMatchSnapshot();
   expect(
-    getForm(wrapper)
+    wrapper
+      .dive()
       .find('.js-additional-info')
       .prop('hidden')
   ).toBe(true);
 
-  click(getForm(wrapper).find('ResetButtonLink'));
+  click(wrapper.dive().find('ResetButtonLink'));
   wrapper.update();
   expect(
-    getForm(wrapper)
+    wrapper
+      .dive()
       .find('.js-additional-info')
       .prop('hidden')
   ).toBe(false);
 });
 
-it('should validate', async () => {
+it('should validate before submit', () => {
   const wrapper = shallow(
     <OrganizationDetailsStep
       finished={false}
@@ -71,77 +72,48 @@ it('should validate', async () => {
   );
   const instance = wrapper.instance() as OrganizationDetailsStep;
 
-  await expect(
-    instance.handleValidate({
+  expect(
+    instance.canSubmit({
+      additional: false,
       avatar: '',
       description: '',
       name: '',
       key: 'foo',
+      submitting: false,
       url: ''
     })
-  ).resolves.toEqual({});
+  ).toBe(true);
 
-  await expect(
-    instance.handleValidate({
+  expect(
+    instance.canSubmit({
+      additional: false,
       avatar: '',
       description: '',
       name: '',
-      key: 'x'.repeat(256),
+      key: undefined,
+      submitting: false,
       url: ''
     })
-  ).rejects.toEqual({
-    key: 'onboarding.create_organization.organization_name.error'
-  });
+  ).toBe(false);
 
-  await expect(
-    instance.handleValidate({
-      avatar: 'bla',
+  expect(
+    instance.canSubmit({
+      additional: false,
+      avatar: undefined,
       description: '',
       name: '',
       key: 'foo',
+      submitting: false,
       url: ''
     })
-  ).rejects.toEqual({ avatar: 'onboarding.create_organization.avatar.error' });
-
-  await expect(
-    instance.handleValidate({
-      avatar: '',
-      description: '',
-      name: 'x'.repeat(256),
-      key: 'foo',
-      url: ''
-    })
-  ).rejects.toEqual({
-    name: 'onboarding.create_organization.display_name.error'
-  });
-
-  await expect(
-    instance.handleValidate({
-      avatar: '',
-      description: '',
-      name: '',
-      key: 'foo',
-      url: 'bla'
-    })
-  ).rejects.toEqual({
-    url: 'onboarding.create_organization.url.error'
-  });
+  ).toBe(false);
 
-  (getOrganization as jest.Mock).mockResolvedValue({});
-  await expect(
-    instance.handleValidate({
-      avatar: '',
-      description: '',
-      name: '',
-      key: 'foo',
-      url: ''
-    })
-  ).rejects.toEqual({
-    key: 'onboarding.create_organization.organization_name.taken'
-  });
+  instance.canSubmit = jest.fn() as any;
+  submit(wrapper.dive().find('form'));
+  expect(instance.canSubmit).toHaveBeenCalled();
 });
 
-it('should render result', () => {
+it.only('should render result', () => {
   const wrapper = shallow(
     <OrganizationDetailsStep
       finished={true}
@@ -152,14 +124,11 @@ it('should render result', () => {
       submitText="continue"
     />
   );
-  expect(wrapper.dive()).toMatchSnapshot();
+  expect(wrapper.dive().find('.boxed-group-actions')).toMatchSnapshot();
+  expect(
+    wrapper
+      .dive()
+      .find('.hidden')
+      .exists()
+  ).toBe(true);
 });
-
-function getForm(wrapper: ShallowWrapper) {
-  return wrapper
-    .dive()
-    .find('ValidationForm')
-    .dive()
-    .dive()
-    .children();
-}
index ec99dad98eaeec664e5458a980076ac0a8d20eb1..3a79e945db16d2ad4e4270740a6de97adad1ab8a 100644 (file)
@@ -1,8 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should display a warning message 1`] = `
-<span
-  className="alert alert-warning markdown big-spacer-bottom width-60"
+<Alert
+  className="markdown big-spacer-bottom width-60"
+  variant="warning"
 >
   onboarding.create_organization.import_org_not_found
   <ul>
@@ -13,7 +14,7 @@ exports[`should display a warning message 1`] = `
       onboarding.create_organization.import_org_not_found.tips_2
     </li>
   </ul>
-</span>
+</Alert>
 `;
 
 exports[`should render 1`] = `
index f2da7e6a62c3156ebd2e6632bac7d68712f7d8ea..c4b506bc1f3eedea4eacd14fd569d643bdd898b2 100644 (file)
@@ -52,7 +52,7 @@ exports[`should render with auto tab displayed 1`] = `
             "node": <React.Fragment>
               onboarding.create_organization.import_organization.github
               <span
-                className="rounded alert alert-small spacer-left display-inline-block alert-info"
+                className="beta-badge spacer-left"
               >
                 beta
               </span>
@@ -134,7 +134,7 @@ exports[`should render with auto tab selected and manual disabled 1`] = `
             "node": <React.Fragment>
               onboarding.create_organization.import_organization.github
               <span
-                className="rounded alert alert-small spacer-left display-inline-block alert-info"
+                className="beta-badge spacer-left"
               >
                 beta
               </span>
@@ -288,7 +288,7 @@ exports[`should switch tabs 1`] = `
             "node": <React.Fragment>
               onboarding.create_organization.import_organization.github
               <span
-                className="rounded alert alert-small spacer-left display-inline-block alert-info"
+                className="beta-badge spacer-left"
               >
                 beta
               </span>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
deleted file mode 100644 (file)
index b8bd98a..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div>
-  <label
-    htmlFor="field"
-  >
-    <strong>
-      Label
-    </strong>
-    <em
-      className="mandatory"
-    >
-      *
-    </em>
-  </label>
-  <div
-    className="little-spacer-top spacer-bottom"
-  >
-    <div />
-    <AlertErrorIcon
-      className="spacer-left text-middle"
-    />
-    <span
-      className="little-spacer-left text-danger text-middle"
-    >
-      This field is bad!
-    </span>
-  </div>
-</div>
-`;
index a52c598379da9842be6765a198c00ea730b1bf75..169967f9e4f039434fa7c8a756f33f9fb10f7a0b 100644 (file)
@@ -34,6 +34,7 @@ exports[`should render form 2`] = `
     <div
       className="boxed-group-inner"
     >
+<<<<<<< HEAD
       <ValidationForm
         initialValues={
           Object {
@@ -172,65 +173,91 @@ exports[`should render form 3`] = `
     </SubmitButton>
   </div>
 </form>
+=======
+      <form
+        id="organization-form"
+        onSubmit={[Function]}
+      >
+        <OrganizationKeyInput
+          onChange={[Function]}
+        />
+        <div
+          className="big-spacer-top"
+        >
+          <ResetButtonLink
+            onClick={[Function]}
+          >
+            onboarding.create_organization.add_additional_info
+            <DropdownIcon
+              className="little-spacer-left"
+              turned={false}
+            />
+          </ResetButtonLink>
+        </div>
+        <div
+          className="js-additional-info"
+          hidden={true}
+        >
+          <div
+            className="big-spacer-top"
+          >
+            <OrganizationNameInput
+              initialOrgName=""
+              onChange={[Function]}
+            />
+          </div>
+          <div
+            className="big-spacer-top"
+          >
+            <OrganizationAvatarInput
+              initialOrgAvatar=""
+              onChange={[Function]}
+            />
+          </div>
+          <div
+            className="big-spacer-top"
+          >
+            <OrganizationDescriptionInput
+              initialOrgDescription=""
+              onChange={[Function]}
+            />
+          </div>
+          <div
+            className="big-spacer-top"
+          >
+            <OrganizationUrlInput
+              initialOrgUrl=""
+              onChange={[Function]}
+            />
+          </div>
+        </div>
+        <div
+          className="big-spacer-top"
+        >
+          <SubmitButton
+            disabled={true}
+          >
+            continue
+          </SubmitButton>
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
+>>>>>>> 116a4ec872... SONAR-11322 Import repos from bound organizations
 `;
 
 exports[`should render result 1`] = `
 <div
-  className="boxed-group onboarding-step is-finished"
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
+  className="boxed-group-actions display-flex-center"
 >
-  <div
-    className="onboarding-step-number"
-  >
-    1
-  </div>
-  <div
-    className="boxed-group-actions display-flex-center"
-  >
-    <AlertSuccessIcon
-      className="spacer-right"
-    />
-    <strong
-      className="text-limited"
-    >
-      org
-    </strong>
-  </div>
-  <div
-    className="boxed-group-header"
-  >
-    <h2>
-      onboarding.create_organization.enter_org_details
-    </h2>
-  </div>
-  <div
-    className="boxed-group-inner"
+  <AlertSuccessIcon
+    className="spacer-right"
   />
-  <div
-    className="hidden"
+  <strong
+    className="text-limited"
   >
-    <div
-      className="boxed-group-inner"
-    >
-      <ValidationForm
-        initialValues={
-          Object {
-            "avatar": "",
-            "description": "",
-            "key": "org",
-            "name": "Organization",
-            "url": "",
-          }
-        }
-        isInitialValid={true}
-        onSubmit={[MockFunction]}
-        validate={[Function]}
-      >
-        <Component />
-      </ValidationForm>
-    </div>
-  </div>
+    org
+  </strong>
 </div>
 `;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx
deleted file mode 100644 (file)
index 4fc1ee2..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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, ShallowWrapper } from 'enzyme';
-import { createStore } from 'redux';
-import { whenLoggedIn } from '../whenLoggedIn';
-import { mockRouter } from '../../../../helpers/testUtils';
-
-class X extends React.Component {
-  render() {
-    return <div />;
-  }
-}
-
-const UnderTest = whenLoggedIn(X);
-
-it('should render for logged in user', () => {
-  const store = createStore(state => state, { users: { currentUser: { isLoggedIn: true } } });
-  const wrapper = shallow(<UnderTest />, { context: { store } });
-  expect(getRenderedType(wrapper)).toBe(X);
-});
-
-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 } });
-  expect(getRenderedType(wrapper)).toBe(null);
-  expect(router.replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' }));
-});
-
-function getRenderedType(wrapper: ShallowWrapper) {
-  return wrapper
-    .dive()
-    .dive()
-    .dive()
-    .type();
-}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx
deleted file mode 100644 (file)
index 142f6e9..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 { withCurrentUser } from '../withCurrentUser';
-import { CurrentUser } from '../../../../app/types';
-
-class X extends React.Component<{ currentUser: CurrentUser }> {
-  render() {
-    return <div />;
-  }
-}
-
-const UnderTest = withCurrentUser(X);
-
-it('should pass logged in user', () => {
-  const currentUser = { isLoggedIn: false };
-  const store = createStore(state => state, { users: { currentUser } });
-  const wrapper = shallow(<UnderTest />, { context: { store } });
-  expect(wrapper.dive().type()).toBe(X);
-  expect(wrapper.dive().prop('currentUser')).toBe(currentUser);
-});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx
new file mode 100644 (file)
index 0000000..7d02df3
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * 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 * as classNames from 'classnames';
+import { isWebUri } from 'valid-url';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+
+interface Props {
+  initialValue?: string;
+  name?: string;
+  onChange: (value: string | undefined) => void;
+}
+
+interface State {
+  editing: boolean;
+  error?: string;
+  touched: boolean;
+  value: string;
+}
+
+export default class OrganizationAvatarInput extends React.PureComponent<Props, State> {
+  state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+  componentDidMount() {
+    if (this.props.initialValue) {
+      const value = this.props.initialValue;
+      const error = this.validateUrl(value);
+      this.setState({ error, touched: Boolean(error), value });
+    }
+  }
+
+  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const value = event.currentTarget.value.trim();
+    const error = this.validateUrl(value);
+    this.setState({ error, touched: true, value });
+    this.props.onChange(error === undefined ? value : undefined);
+  };
+
+  handleBlur = () => {
+    this.setState({ editing: false });
+  };
+
+  handleFocus = () => {
+    this.setState({ editing: true });
+  };
+
+  validateUrl(url: string) {
+    if (url.length > 0 && !isWebUri(url)) {
+      return translate('onboarding.create_organization.url.error');
+    }
+    return undefined;
+  }
+
+  render() {
+    const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+    const isValidUrl = this.state.error === undefined && this.state.value !== '';
+    const isValid = this.state.touched && isValidUrl;
+    return (
+      <ValidationInput
+        description={translate('onboarding.create_organization.avatar.description')}
+        error={this.state.error}
+        id="organization-avatar"
+        isInvalid={isInvalid}
+        isValid={isValid}
+        label={translate('onboarding.create_organization.avatar')}>
+        <>
+          {(isValidUrl || this.props.name) && (
+            <OrganizationAvatar
+              className="display-block spacer-bottom"
+              organization={{
+                avatar: isValidUrl ? this.state.value : undefined,
+                name: this.props.name || ''
+              }}
+            />
+          )}
+          <input
+            className={classNames('input-super-large', 'text-middle', {
+              'is-invalid': isInvalid,
+              'is-valid': isValid
+            })}
+            id="organization-display-name"
+            onBlur={this.handleBlur}
+            onChange={this.handleChange}
+            onFocus={this.handleFocus}
+            type="text"
+            value={this.state.value}
+          />
+        </>
+      </ValidationInput>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx
new file mode 100644 (file)
index 0000000..eaea25f
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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 * as classNames from 'classnames';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  initialValue?: string;
+  onChange: (value: string | undefined) => void;
+}
+
+interface State {
+  editing: boolean;
+  error?: string;
+  touched: boolean;
+  value: string;
+}
+
+export default class OrganizationDescriptionInput extends React.PureComponent<Props, State> {
+  state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+  componentDidMount() {
+    if (this.props.initialValue) {
+      const error = this.validateDescription(this.props.initialValue);
+      this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+    }
+  }
+
+  handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+    const { value } = event.currentTarget;
+    const error = this.validateDescription(value);
+    this.setState({ error, touched: true, value });
+    this.props.onChange(error === undefined ? value : undefined);
+  };
+
+  handleBlur = () => {
+    this.setState({ editing: false });
+  };
+
+  handleFocus = () => {
+    this.setState({ editing: true });
+  };
+
+  validateDescription(description: string) {
+    if (description.length > 256) {
+      return translate('onboarding.create_organization.description.error');
+    }
+    return undefined;
+  }
+
+  render() {
+    const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+    const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+    return (
+      <ValidationInput
+        error={this.state.error}
+        id="organization-display-name"
+        isInvalid={isInvalid}
+        isValid={isValid}
+        label={translate('onboarding.create_organization.description')}>
+        <textarea
+          className={classNames('input-super-large', 'text-middle', {
+            'is-invalid': isInvalid,
+            'is-valid': isValid
+          })}
+          id="organization-description"
+          maxLength={256}
+          onBlur={this.handleBlur}
+          onChange={this.handleChange}
+          onFocus={this.handleFocus}
+          rows={3}
+          value={this.state.value}
+        />
+      </ValidationInput>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
new file mode 100644 (file)
index 0000000..0fd3c61
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * 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 * as classNames from 'classnames';
+import { debounce } from 'lodash';
+import { getOrganization } from '../../../../api/organizations';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+import { getHostUrl } from '../../../../helpers/urls';
+
+interface Props {
+  initialValue?: string;
+  onChange: (value: string | undefined) => void;
+}
+
+interface State {
+  editing: boolean;
+  error?: string;
+  touched: boolean;
+  validating: boolean;
+  value: string;
+}
+
+export default class OrganizationKeyInput extends React.PureComponent<Props, State> {
+  mounted = false;
+  constructor(props: Props) {
+    super(props);
+    this.state = { error: undefined, editing: false, touched: false, validating: false, value: '' };
+    this.checkFreeKey = debounce(this.checkFreeKey, 250);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    if (this.props.initialValue !== undefined) {
+      this.setState({ value: this.props.initialValue });
+      this.validateKey(this.props.initialValue);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  checkFreeKey = (key: string) => {
+    this.setState({ validating: true });
+    return getOrganization(key)
+      .then(organization => organization === undefined, () => true)
+      .then(
+        free => {
+          if (this.mounted) {
+            if (!free) {
+              this.setState({
+                error: translate('onboarding.create_organization.organization_name.taken'),
+                touched: true,
+                validating: false
+              });
+              this.props.onChange(undefined);
+            } else {
+              this.setState({ error: undefined, validating: false });
+              this.props.onChange(key);
+            }
+          }
+        },
+        () => {}
+      );
+  };
+
+  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const { value } = event.currentTarget;
+    this.setState({ touched: true, value });
+    this.validateKey(value);
+  };
+
+  handleBlur = () => {
+    this.setState({ editing: false });
+  };
+
+  handleFocus = () => {
+    this.setState({ editing: true });
+  };
+
+  validateKey(key: string) {
+    if (key.length > 255 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]?$/.test(key)) {
+      this.setState({
+        error: translate('onboarding.create_organization.organization_name.error'),
+        touched: true
+      });
+      this.props.onChange(undefined);
+    } else {
+      this.checkFreeKey(key);
+    }
+  }
+
+  render() {
+    const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+    const isValid = this.state.touched && !this.state.validating && this.state.error === undefined;
+    return (
+      <ValidationInput
+        error={this.state.error}
+        id="organization-key"
+        isInvalid={isInvalid}
+        isValid={isValid}
+        label={translate('onboarding.create_organization.organization_name')}
+        required={true}>
+        <div className="display-inline-flex-baseline">
+          <span className="little-spacer-right">
+            {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
+          </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}
+          />
+        </div>
+      </ValidationInput>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx
new file mode 100644 (file)
index 0000000..9e50b0c
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 * as classNames from 'classnames';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  initialValue?: string;
+  onChange: (value: string | undefined) => void;
+}
+
+interface State {
+  editing: boolean;
+  error?: string;
+  touched: boolean;
+  value: string;
+}
+
+export default class OrganizationNameInput extends React.PureComponent<Props, State> {
+  state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+  componentDidMount() {
+    if (this.props.initialValue) {
+      const error = this.validateName(this.props.initialValue);
+      this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+    }
+  }
+
+  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const { value } = event.currentTarget;
+    const error = this.validateName(value);
+    this.setState({ error, touched: true, value });
+    this.props.onChange(error === undefined ? value : undefined);
+  };
+
+  handleBlur = () => {
+    this.setState({ editing: false });
+  };
+
+  handleFocus = () => {
+    this.setState({ editing: true });
+  };
+
+  validateName(name: string) {
+    if (name.length > 255) {
+      return translate('onboarding.create_organization.display_name.error');
+    }
+    return undefined;
+  }
+
+  render() {
+    const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+    const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+    return (
+      <ValidationInput
+        description={translate('onboarding.create_organization.display_name.description')}
+        error={this.state.error}
+        id="organization-display-name"
+        isInvalid={isInvalid}
+        isValid={isValid}
+        label={translate('onboarding.create_organization.display_name')}>
+        <input
+          className={classNames('input-super-large', 'text-middle', {
+            'is-invalid': isInvalid,
+            'is-valid': isValid
+          })}
+          id="organization-display-name"
+          maxLength={255}
+          onBlur={this.handleBlur}
+          onChange={this.handleChange}
+          onFocus={this.handleFocus}
+          type="text"
+          value={this.state.value}
+        />
+      </ValidationInput>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx
new file mode 100644 (file)
index 0000000..a77bdc9
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 * as classNames from 'classnames';
+import { isWebUri } from 'valid-url';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  initialValue?: string;
+  onChange: (value: string | undefined) => void;
+}
+
+interface State {
+  editing: boolean;
+  error?: string;
+  touched: boolean;
+  value: string;
+}
+
+export default class OrganizationUrlInput extends React.PureComponent<Props, State> {
+  state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+  componentDidMount() {
+    if (this.props.initialValue) {
+      const value = this.props.initialValue;
+      const error = this.validateUrl(value);
+      this.setState({ error, touched: Boolean(error), value });
+    }
+  }
+
+  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const value = event.currentTarget.value.trim();
+    const error = this.validateUrl(value);
+    this.setState({ error, touched: true, value });
+    this.props.onChange(error === undefined ? value : undefined);
+  };
+
+  handleBlur = () => {
+    this.setState({ editing: false });
+  };
+
+  handleFocus = () => {
+    this.setState({ editing: true });
+  };
+
+  validateUrl(url: string) {
+    if (url.length > 0 && !isWebUri(url)) {
+      return translate('onboarding.create_organization.url.error');
+    }
+    return undefined;
+  }
+
+  render() {
+    const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+    const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+    return (
+      <ValidationInput
+        error={this.state.error}
+        id="organization-url"
+        isInvalid={isInvalid}
+        isValid={isValid}
+        label={translate('onboarding.create_organization.url')}>
+        <input
+          className={classNames('input-super-large', 'text-middle', {
+            'is-invalid': isInvalid,
+            'is-valid': isValid
+          })}
+          id="organization-url"
+          onBlur={this.handleBlur}
+          onChange={this.handleChange}
+          onFocus={this.handleFocus}
+          type="text"
+          value={this.state.value}
+        />
+      </ValidationInput>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx
new file mode 100644 (file)
index 0000000..c7d7c24
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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 OrganizationAvatarInput from '../OrganizationAvatarInput';
+
+it('should render correctly', () => {
+  const wrapper = shallow(
+    <OrganizationAvatarInput initialValue="https://my.avatar" onChange={jest.fn()} />
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({ touched: true });
+  expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when the avatar url is not valid', () => {
+  expect(
+    shallow(<OrganizationAvatarInput initialValue="whatever" onChange={jest.fn()} />)
+      .find('ValidationInput')
+      .prop('isInvalid')
+  ).toBe(true);
+});
+
+it('should display the fallback avatar when there is no url', () => {
+  expect(
+    shallow(<OrganizationAvatarInput initialValue="" name="Luke Skywalker" onChange={jest.fn()} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx
new file mode 100644 (file)
index 0000000..eab1e2c
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 OrganizationDescriptionInput from '../OrganizationDescriptionInput';
+
+it('should render correctly', () => {
+  const wrapper = shallow(
+    <OrganizationDescriptionInput initialValue="My description" onChange={jest.fn()} />
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({ touched: true });
+  expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when description is too long', () => {
+  expect(
+    shallow(<OrganizationDescriptionInput initialValue={'x'.repeat(260)} onChange={jest.fn()} />)
+      .find('ValidationInput')
+      .prop('isInvalid')
+  ).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
new file mode 100644 (file)
index 0000000..a6bcde5
--- /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 { shallow } from 'enzyme';
+import OrganizationKeyInput from '../OrganizationKeyInput';
+import { getOrganization } from '../../../../../api/organizations';
+import { waitAndUpdate } from '../../../../../helpers/testUtils';
+
+jest.mock('../../../../../api/organizations', () => ({
+  getOrganization: jest.fn().mockResolvedValue(undefined)
+}));
+
+beforeEach(() => {
+  (getOrganization as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+  const wrapper = shallow(<OrganizationKeyInput initialValue="key" onChange={jest.fn()} />);
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({ touched: true });
+  expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should not display any status when the key is not defined', async () => {
+  const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(false);
+  expect(wrapper.find('ValidationInput').prop('isValid')).toBe(false);
+});
+
+it('should have an error when the key is invalid', async () => {
+  const wrapper = shallow(
+    <OrganizationKeyInput initialValue="KEy-with#speci@l_char" onChange={jest.fn()} />
+  );
+  await waitAndUpdate(wrapper);
+  expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
+
+it('should have an error when the key already exists', async () => {
+  (getOrganization as jest.Mock<any>).mockResolvedValue({});
+  const wrapper = shallow(<OrganizationKeyInput initialValue="" onChange={jest.fn()} />);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx
new file mode 100644 (file)
index 0000000..ecbfdb1
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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 OrganizationNameInput from '../OrganizationNameInput';
+
+it('should render correctly', () => {
+  const wrapper = shallow(<OrganizationNameInput initialValue="Org Name" onChange={jest.fn()} />);
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({ touched: true });
+  expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when description is too long', () => {
+  expect(
+    shallow(<OrganizationNameInput initialValue={'x'.repeat(256)} onChange={jest.fn()} />)
+      .find('ValidationInput')
+      .prop('isInvalid')
+  ).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx
new file mode 100644 (file)
index 0000000..357a912
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 OrganizationUrlInput from '../OrganizationUrlInput';
+
+it('should render correctly', () => {
+  const wrapper = shallow(
+    <OrganizationUrlInput initialValue="http://my.website" onChange={jest.fn()} />
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({ touched: true });
+  expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when the url is invalid', () => {
+  expect(
+    shallow(<OrganizationUrlInput initialValue="whatever" onChange={jest.fn()} />)
+      .find('ValidationInput')
+      .prop('isInvalid')
+  ).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..292c7b2
--- /dev/null
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the fallback avatar when there is no url 1`] = `
+<ValidationInput
+  description="onboarding.create_organization.avatar.description"
+  id="organization-avatar"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.avatar"
+>
+  <OrganizationAvatar
+    className="display-block spacer-bottom"
+    organization={
+      Object {
+        "avatar": undefined,
+        "name": "Luke Skywalker",
+      }
+    }
+  />
+  <input
+    className="input-super-large text-middle"
+    id="organization-display-name"
+    onBlur={[Function]}
+    onChange={[Function]}
+    onFocus={[Function]}
+    type="text"
+    value=""
+  />
+</ValidationInput>
+`;
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+  description="onboarding.create_organization.avatar.description"
+  id="organization-avatar"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.avatar"
+>
+  <OrganizationAvatar
+    className="display-block spacer-bottom"
+    organization={
+      Object {
+        "avatar": "https://my.avatar",
+        "name": "",
+      }
+    }
+  />
+  <input
+    className="input-super-large text-middle"
+    id="organization-display-name"
+    onBlur={[Function]}
+    onChange={[Function]}
+    onFocus={[Function]}
+    type="text"
+    value="https://my.avatar"
+  />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..80e11c0
--- /dev/null
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+  id="organization-display-name"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.description"
+>
+  <textarea
+    className="input-super-large text-middle"
+    id="organization-description"
+    maxLength={256}
+    onBlur={[Function]}
+    onChange={[Function]}
+    onFocus={[Function]}
+    rows={3}
+    value="My description"
+  />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..8cba7d9
--- /dev/null
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+  id="organization-key"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.organization_name"
+  required={true}
+>
+  <div
+    className="display-inline-flex-baseline"
+  >
+    <span
+      className="little-spacer-right"
+    >
+      localhost/organizations/
+    </span>
+    <input
+      autoFocus={true}
+      className="input-super-large text-middle"
+      id="organization-key"
+      maxLength={255}
+      onBlur={[Function]}
+      onChange={[Function]}
+      onFocus={[Function]}
+      type="text"
+      value="key"
+    />
+  </div>
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..1af9dc9
--- /dev/null
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+  description="onboarding.create_organization.display_name.description"
+  id="organization-display-name"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.display_name"
+>
+  <input
+    className="input-super-large text-middle"
+    id="organization-display-name"
+    maxLength={255}
+    onBlur={[Function]}
+    onChange={[Function]}
+    onFocus={[Function]}
+    type="text"
+    value="Org Name"
+  />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..d3f571b
--- /dev/null
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+  id="organization-url"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.url"
+>
+  <input
+    className="input-super-large text-middle"
+    id="organization-url"
+    onBlur={[Function]}
+    onChange={[Function]}
+    onFocus={[Function]}
+    type="text"
+    value="http://my.website"
+  />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx b/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
deleted file mode 100644 (file)
index be69f2c..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 { withRouter, WithRouterProps } from 'react-router';
-import { withCurrentUser } from './withCurrentUser';
-import { CurrentUser } from '../../../app/types';
-import { isLoggedIn } from '../../../helpers/users';
-
-export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
-  const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
-
-  class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> {
-    static displayName = `whenLoggedIn(${wrappedDisplayName})`;
-
-    componentDidMount() {
-      if (!isLoggedIn(this.props.currentUser)) {
-        const returnTo = window.location.pathname + window.location.search + window.location.hash;
-        this.props.router.replace({
-          pathname: '/sessions/new',
-          query: { return_to: returnTo } // eslint-disable-line camelcase
-        });
-      }
-    }
-
-    render() {
-      if (isLoggedIn(this.props.currentUser)) {
-        return <WrappedComponent {...this.props} />;
-      } else {
-        return null;
-      }
-    }
-  }
-
-  return withCurrentUser(withRouter(Wrapper));
-}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx b/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx
deleted file mode 100644 (file)
index 117af66..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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 { CurrentUser } from '../../../app/types';
-import { Store, getCurrentUser } from '../../../store/rootReducer';
-
-export function withCurrentUser<P>(
-  WrappedComponent: React.ComponentClass<P & { currentUser: CurrentUser }>
-) {
-  const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
-
-  class Wrapper extends React.Component<P & { currentUser: CurrentUser }> {
-    static displayName = `withCurrentUser(${wrappedDisplayName})`;
-
-    render() {
-      return <WrappedComponent {...this.props} />;
-    }
-  }
-
-  function mapStateToProps(state: Store) {
-    return { currentUser: getCurrentUser(state) };
-  }
-
-  return connect(mapStateToProps)(Wrapper);
-}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
new file mode 100644 (file)
index 0000000..8fd083f
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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 { Link } from 'react-router';
+import * as theme from '../../../app/theme';
+import Checkbox from '../../../components/controls/Checkbox';
+import CheckIcon from '../../../components/icons-components/CheckIcon';
+import { AlmRepository, IdentityProvider } from '../../../app/types';
+import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  identityProvider: IdentityProvider;
+  repository: AlmRepository;
+  selected: boolean;
+  toggleRepository: (repository: AlmRepository) => void;
+}
+
+export default class AlmRepositoryItem extends React.PureComponent<Props> {
+  handleChange = () => {
+    this.props.toggleRepository(this.props.repository);
+  };
+
+  render() {
+    const { identityProvider, repository, selected } = this.props;
+    const alreadyImported = Boolean(repository.linkedProjectKey);
+    return (
+      <>
+        <Checkbox
+          checked={selected || alreadyImported}
+          disabled={alreadyImported}
+          onCheck={this.handleChange}>
+          <img
+            alt={identityProvider.name}
+            className="spacer-left"
+            height={14}
+            src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
+            style={{ opacity: alreadyImported ? 0.5 : 1 }}
+            width={14}
+          />
+          <span className="spacer-left">{this.props.repository.label}</span>
+        </Checkbox>
+        {repository.linkedProjectKey && (
+          <span className="big-spacer-left">
+            <CheckIcon className="little-spacer-right" fill={theme.green} />
+            <Link to={getProjectUrl(repository.linkedProjectKey)}>
+              {translate('onboarding.create_project.already_imported')}
+            </Link>
+          </span>
+        )}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx
new file mode 100644 (file)
index 0000000..4460f22
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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 RemoteRepositories from './RemoteRepositories';
+import OrganizationSelect from './OrganizationSelect';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { AlmApplication, Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  almApplication: AlmApplication;
+  boundOrganizations: Organization[];
+  onProjectCreate: (projectKeys: string[]) => void;
+  organization?: string;
+}
+
+interface State {
+  selectedOrganization: string;
+}
+
+export default class AutoProjectCreate extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = { selectedOrganization: this.getInitialSelectedOrganization(props) };
+  }
+
+  getInitialSelectedOrganization(props: Props) {
+    const organization =
+      props.organization && props.boundOrganizations.find(o => o.key === props.organization);
+    if (organization) {
+      return organization.key;
+    }
+    if (props.boundOrganizations.length === 1) {
+      return props.boundOrganizations[0].key;
+    }
+    return '';
+  }
+
+  handleOrganizationSelect = ({ key }: Organization) => {
+    this.setState({ selectedOrganization: key });
+  };
+
+  render() {
+    const { almApplication, boundOrganizations, onProjectCreate } = this.props;
+
+    if (boundOrganizations.length === 0) {
+      return (
+        <>
+          <IdentityProviderLink
+            className="display-inline-block"
+            identityProvider={almApplication}
+            small={true}
+            url={almApplication.installationUrl}>
+            {translate(
+              'onboarding.create_organization.choose_organization_button',
+              almApplication.key
+            )}
+          </IdentityProviderLink>
+        </>
+      );
+    }
+
+    const { selectedOrganization } = this.state;
+    return (
+      <>
+        <OrganizationSelect
+          autoImport={true}
+          onChange={this.handleOrganizationSelect}
+          organization={selectedOrganization}
+          organizations={this.props.boundOrganizations}
+        />
+        {selectedOrganization && (
+          <RemoteRepositories
+            almApplication={almApplication}
+            onProjectCreate={onProjectCreate}
+            organization={selectedOrganization}
+          />
+        )}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
new file mode 100644 (file)
index 0000000..d46f7bb
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * 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 * as classNames from 'classnames';
+import { connect } from 'react-redux';
+import { WithRouterProps } from 'react-router';
+import Helmet from 'react-helmet';
+import AutoProjectCreate from './AutoProjectCreate';
+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 { skipOnboarding as skipOnboardingAction } from '../../../store/users';
+import { LoggedInUser, AlmApplication, Organization } from '../../../app/types';
+import { getAlmAppInfo } from '../../../api/alm-integration';
+import { skipOnboarding } from '../../../api/users';
+import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
+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;
+}
+
+interface State {
+  almApplication?: AlmApplication;
+  loading: boolean;
+}
+
+type TabKeys = 'auto' | 'manual';
+
+interface LocationState {
+  organization?: string;
+  tab?: TabKeys;
+}
+
+export class CreateProjectPage extends React.PureComponent<
+  Props & StateProps & WithRouterProps,
+  State
+> {
+  mounted = false;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.props.fetchMyOrganizations();
+    if (hasAdvancedALMIntegration(this.props.currentUser)) {
+      this.fetchAlmApplication();
+    } else {
+      this.setState({ loading: false });
+    }
+    document.body.classList.add('white-page');
+    if (document.documentElement) {
+      document.documentElement.classList.add('white-page');
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    document.body.classList.remove('white-page');
+    if (document.documentElement) {
+      document.documentElement.classList.remove('white-page');
+    }
+  }
+
+  handleProjectCreate = (projectKeys: string[]) => {
+    skipOnboarding().catch(() => {});
+    this.props.skipOnboardingAction();
+    if (projectKeys.length > 1) {
+      this.props.router.push({ pathname: '/projects' });
+    } else if (projectKeys.length === 1) {
+      this.props.router.push(getProjectUrl(projectKeys[0]));
+    }
+  };
+
+  fetchAlmApplication = () => {
+    return getAlmAppInfo().then(
+      ({ application }) => {
+        if (this.mounted) {
+          this.setState({ almApplication: application, loading: false });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  onTabChange = (tab: TabKeys) => {
+    this.updateUrl({ tab });
+  };
+
+  updateUrl = (state: Partial<LocationState> = {}) => {
+    this.props.router.replace({
+      pathname: this.props.location.pathname,
+      query: this.props.location.query,
+      state: { ...(this.props.location.state || {}), ...state }
+    });
+  };
+
+  render() {
+    const { currentUser, location, userOrganizations } = this.props;
+    const { almApplication, loading } = this.state;
+    const state: LocationState = location.state || {};
+    const header = translate('onboarding.create_project.header');
+    const showManualTab = state.tab === 'manual';
+
+    return (
+      <>
+        <Helmet title={header} titleTemplate="%s" />
+        <div className="sonarcloud page page-limited">
+          <header className="page-header">
+            <h1 className="page-title">{header}</h1>
+          </header>
+          {loading ? (
+            <DeferredSpinner />
+          ) : (
+            <>
+              {almApplication && (
+                <Tabs<TabKeys>
+                  onChange={this.onTabChange}
+                  selected={state.tab || 'auto'}
+                  tabs={[
+                    {
+                      key: 'auto',
+                      node: (
+                        <>
+                          {translate('onboarding.create_project.select_repositories')}
+                          <span
+                            className={classNames('beta-badge spacer-left', {
+                              'is-muted': showManualTab
+                            })}>
+                            {translate('beta')}
+                          </span>
+                        </>
+                      )
+                    },
+                    { key: 'manual', node: translate('onboarding.create_project.create_manually') }
+                  ]}
+                />
+              )}
+
+              {showManualTab || !almApplication ? (
+                <ManualProjectCreate
+                  currentUser={currentUser}
+                  onProjectCreate={this.handleProjectCreate}
+                  organization={state.organization}
+                  userOrganizations={userOrganizations}
+                />
+              ) : (
+                <AutoProjectCreate
+                  almApplication={almApplication}
+                  boundOrganizations={userOrganizations.filter(o => o.almId)}
+                  onProjectCreate={this.handleProjectCreate}
+                  organization={state.organization}
+                />
+              )}
+            </>
+          )}
+        </div>
+      </>
+    );
+  }
+}
+
+const mapDispatchToProps = {
+  fetchMyOrganizations,
+  skipOnboardingAction
+};
+
+const mapStateToProps = (state: Store) => {
+  return {
+    userOrganizations: getMyOrganizations(state)
+  };
+};
+
+export default whenLoggedIn(
+  connect<StateProps>(
+    mapStateToProps,
+    mapDispatchToProps
+  )(CreateProjectPage)
+);
diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
new file mode 100644 (file)
index 0000000..2820af7
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * 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 OrganizationSelect from './OrganizationSelect';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton } from '../../../components/ui/buttons';
+import { LoggedInUser, Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { createProject } from '../../../api/components';
+
+interface Props {
+  currentUser: LoggedInUser;
+  onProjectCreate: (projectKeys: string[]) => void;
+  organization?: string;
+  userOrganizations: Organization[];
+}
+
+interface State {
+  projectName: string;
+  projectKey: string;
+  selectedOrganization: string;
+  submitting: boolean;
+}
+
+export default class ManualProjectCreate extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      projectName: '',
+      projectKey: '',
+      selectedOrganization: this.getInitialSelectedOrganization(props),
+      submitting: false
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  getInitialSelectedOrganization(props: Props) {
+    if (props.organization) {
+      return props.organization;
+    } else if (props.userOrganizations.length === 1) {
+      return props.userOrganizations[0].key;
+    } else {
+      return '';
+    }
+  }
+
+  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    if (this.isValid()) {
+      const { projectKey, projectName, selectedOrganization } = this.state;
+      this.setState({ submitting: true });
+      createProject({
+        project: projectKey,
+        name: projectName,
+        organization: selectedOrganization
+      }).then(
+        ({ project }) => this.props.onProjectCreate([project.key]),
+        () => {
+          if (this.mounted) {
+            this.setState({ submitting: false });
+          }
+        }
+      );
+    }
+  };
+
+  handleOrganizationSelect = ({ key }: Organization) => {
+    this.setState({ selectedOrganization: key });
+  };
+
+  handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({ projectName: event.currentTarget.value });
+  };
+
+  handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({ projectKey: event.currentTarget.value });
+  };
+
+  isValid = () => {
+    const { projectKey, projectName, selectedOrganization } = this.state;
+    return Boolean(projectKey && projectName && selectedOrganization);
+  };
+
+  render() {
+    const { submitting } = this.state;
+    return (
+      <>
+        <form onSubmit={this.handleFormSubmit}>
+          <OrganizationSelect
+            onChange={this.handleOrganizationSelect}
+            organization={this.state.selectedOrganization}
+            organizations={this.props.userOrganizations}
+          />
+          <div className="form-field">
+            <label htmlFor="project-name">
+              {translate('onboarding.create_project.project_name')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              className="input-super-large"
+              id="project-name"
+              maxLength={400}
+              minLength={1}
+              onChange={this.handleProjectNameChange}
+              required={true}
+              type="text"
+              value={this.state.projectName}
+            />
+          </div>
+          <div className="form-field">
+            <label htmlFor="project-key">
+              {translate('onboarding.create_project.project_key')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              className="input-super-large"
+              id="project-key"
+              maxLength={400}
+              minLength={1}
+              onChange={this.handleProjectKeyChange}
+              required={true}
+              type="text"
+              value={this.state.projectKey}
+            />
+          </div>
+          <SubmitButton disabled={!this.isValid() || submitting}>
+            {translate('create')}
+          </SubmitButton>
+          <DeferredSpinner className="spacer-left" loading={submitting} />
+        </form>
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
new file mode 100644 (file)
index 0000000..a1d52e0
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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 { Link } from 'react-router';
+import { sortBy } from 'lodash';
+import Select from '../../../components/controls/Select';
+import { Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+  autoImport?: boolean;
+  onChange: (organization: Organization) => void;
+  organization: string;
+  organizations: Organization[];
+}
+
+export default function OrganizationSelect({
+  autoImport,
+  onChange,
+  organization,
+  organizations
+}: Props) {
+  return (
+    <div className="form-field spacer-bottom">
+      <label htmlFor="select-organization">
+        {translate('onboarding.create_project.organization')}
+        <em className="mandatory">*</em>
+      </label>
+      <Select
+        autoFocus={true}
+        className="input-super-large"
+        clearable={false}
+        id="select-organization"
+        labelKey="name"
+        onChange={onChange}
+        optionRenderer={optionRenderer}
+        options={sortBy(organizations, o => o.name.toLowerCase())}
+        required={true}
+        value={organization}
+        valueKey="key"
+        valueRenderer={optionRenderer}
+      />
+      <Link className="big-spacer-left js-new-org" to="/create-organization">
+        {autoImport
+          ? translate('onboarding.create_project.import_new_org')
+          : translate('onboarding.create_project.create_new_org')}
+      </Link>
+    </div>
+  );
+}
+
+export function optionRenderer(organization: Organization) {
+  return (
+    <span>
+      {organization.almId && (
+        <img
+          alt={organization.almId}
+          className="spacer-right"
+          height={14}
+          src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`}
+        />
+      )}
+      {organization.name}
+      <span className="note little-spacer-left">{organization.key}</span>
+    </span>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx
new file mode 100644 (file)
index 0000000..66f3aef
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * 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 AlmRepositoryItem from './AlmRepositoryItem';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { getRepositories, provisionProject } from '../../../api/alm-integration';
+import { AlmApplication, AlmRepository } from '../../../app/types';
+import { SubmitButton } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  almApplication: AlmApplication;
+  onProjectCreate: (projectKeys: string[]) => void;
+  organization: string;
+}
+
+type SelectedRepositories = { [key: string]: AlmRepository | undefined };
+
+interface State {
+  loading: boolean;
+  repositories: AlmRepository[];
+  selectedRepositories: SelectedRepositories;
+  submitting: boolean;
+}
+
+export default class RemoteRepositories extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: true, repositories: [], selectedRepositories: {}, submitting: false };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchRepositories();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    const { organization } = this.props;
+    if (prevProps.organization !== organization) {
+      this.setState({ loading: true });
+      this.fetchRepositories();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchRepositories = () => {
+    const { organization } = this.props;
+    return getRepositories({
+      organization
+    }).then(
+      ({ repositories }) => {
+        if (this.mounted) {
+          this.setState({ loading: false, repositories });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    if (this.isValid()) {
+      const { selectedRepositories } = this.state;
+      this.setState({ submitting: true });
+      provisionProject({
+        installationKeys: Object.keys(selectedRepositories).filter(key =>
+          Boolean(selectedRepositories[key])
+        ),
+        organization: this.props.organization
+      }).then(
+        ({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
+        this.handleProvisionFail
+      );
+    }
+  };
+
+  handleProvisionFail = () => {
+    return this.fetchRepositories().then(() => {
+      if (this.mounted) {
+        this.setState(({ repositories, selectedRepositories }) => {
+          const updateSelectedRepositories: SelectedRepositories = {};
+          Object.keys(selectedRepositories).forEach(installationKey => {
+            const newRepository = repositories.find(r => r.installationKey === installationKey);
+            if (newRepository && !newRepository.linkedProjectKey) {
+              updateSelectedRepositories[newRepository.installationKey] = newRepository;
+            }
+          });
+          return { selectedRepositories: updateSelectedRepositories, submitting: false };
+        });
+      }
+    });
+  };
+
+  isValid = () => {
+    return this.state.repositories.some(repo =>
+      Boolean(this.state.selectedRepositories[repo.installationKey])
+    );
+  };
+
+  toggleRepository = (repository: AlmRepository) => {
+    this.setState(({ selectedRepositories }) => ({
+      selectedRepositories: {
+        ...selectedRepositories,
+        [repository.installationKey]: selectedRepositories[repository.installationKey]
+          ? undefined
+          : repository
+      }
+    }));
+  };
+
+  render() {
+    const { loading, selectedRepositories, submitting } = this.state;
+    const { almApplication } = this.props;
+    return (
+      <DeferredSpinner loading={loading}>
+        <form onSubmit={this.handleFormSubmit}>
+          <div className="form-field">
+            <ul>
+              {this.state.repositories.map(repo => (
+                <li className="big-spacer-bottom" key={repo.installationKey}>
+                  <AlmRepositoryItem
+                    identityProvider={almApplication}
+                    repository={repo}
+                    selected={Boolean(selectedRepositories[repo.installationKey])}
+                    toggleRepository={this.toggleRepository}
+                  />
+                </li>
+              ))}
+            </ul>
+          </div>
+          <SubmitButton disabled={!this.isValid() || submitting}>
+            {translate('create')}
+          </SubmitButton>
+          <DeferredSpinner className="spacer-left" loading={submitting} />
+        </form>
+      </DeferredSpinner>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx
new file mode 100644 (file)
index 0000000..72b25cb
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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 AlmRepositoryItem from '../AlmRepositoryItem';
+
+const identityProviders = {
+  backgroundColor: 'blue',
+  iconPath: 'icon/path',
+  key: 'foo',
+  name: 'Foo Provider'
+};
+
+const repositories = [
+  {
+    label: 'Cool Project',
+    installationKey: 'github/cool',
+    linkedProjectKey: 'proj_cool',
+    linkedProjectName: 'Proj Cool'
+  },
+  {
+    label: 'Awesome Project',
+    installationKey: 'github/awesome'
+  }
+];
+
+it('should render correctly', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should render selected', () => {
+  expect(getWrapper({ selected: true })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+  expect(getWrapper({ repository: repositories[0] })).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <AlmRepositoryItem
+      identityProvider={identityProviders}
+      repository={repositories[1]}
+      selected={false}
+      toggleRepository={jest.fn()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
new file mode 100644 (file)
index 0000000..3364a73
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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 AutoProjectCreate from '../AutoProjectCreate';
+
+const almApplication = {
+  backgroundColor: 'blue',
+  iconPath: 'icon/path',
+  installationUrl: 'https://alm.installation.url',
+  key: 'github',
+  name: 'GitHub'
+};
+
+it('should display the provider app install button', () => {
+  expect(shallowRender({ boundOrganizations: [] })).toMatchSnapshot();
+});
+
+it('should display the bounded organizations dropdown with the list of repositories', () => {
+  expect(shallowRender({ organization: 'foo' })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<AutoProjectCreate['props']> = {}) {
+  return shallow(
+    <AutoProjectCreate
+      almApplication={almApplication}
+      boundOrganizations={[
+        { almId: 'github', key: 'foo', name: 'Foo' },
+        { almId: 'github', key: 'bar', name: 'Bar' }
+      ]}
+      onProjectCreate={jest.fn()}
+      organization=""
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
new file mode 100644 (file)
index 0000000..6c9acb9
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 { CreateProjectPage } from '../CreateProjectPage';
+import { LoggedInUser } from '../../../../app/types';
+import { waitAndUpdate, mockRouter } from '../../../../helpers/testUtils';
+import { getAlmAppInfo } from '../../../../api/alm-integration';
+
+jest.mock('../../../../api/alm-integration', () => ({
+  getAlmAppInfo: jest.fn().mockResolvedValue({
+    application: {
+      backgroundColor: 'blue',
+      iconPath: 'icon/path',
+      installationUrl: 'https://alm.installation.url',
+      key: 'github',
+      name: 'GitHub'
+    }
+  })
+}));
+
+const user: LoggedInUser = {
+  externalProvider: 'github',
+  groups: [],
+  isLoggedIn: true,
+  login: 'foo',
+  name: 'Foo',
+  scmAccounts: []
+};
+
+beforeEach(() => {
+  (getAlmAppInfo as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', async () => {
+  const wrapper = getWrapper();
+  expect(wrapper).toMatchSnapshot();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render with Manual creation only', () => {
+  expect(getWrapper({ currentUser: { ...user, externalProvider: 'microsoft' } })).toMatchSnapshot();
+});
+
+it('should switch tabs', async () => {
+  const replace = jest.fn();
+  const wrapper = getWrapper({ router: { replace } });
+  replace.mockImplementation(location => {
+    wrapper.setProps({ location }).update();
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('Tabs').prop<Function>('onChange')('manual');
+  expect(wrapper.find('ManualProjectCreate').exists()).toBeTruthy();
+  wrapper.find('Tabs').prop<Function>('onChange')('auto');
+  expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <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' }
+      ]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
new file mode 100644 (file)
index 0000000..52d56a8
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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 ManualProjectCreate from '../ManualProjectCreate';
+import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
+import { createProject } from '../../../../api/components';
+
+jest.mock('../../../../api/components', () => ({
+  createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
+}));
+
+beforeEach(() => {
+  (createProject as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should correctly create a project', async () => {
+  const onProjectCreate = jest.fn();
+  const wrapper = getWrapper({ onProjectCreate });
+  wrapper.find('OrganizationSelect').prop<Function>('onChange')({ key: 'foo' });
+  change(wrapper.find('#project-name'), 'Bar');
+  expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+  change(wrapper.find('#project-key'), 'bar');
+  expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+  submit(wrapper.find('form'));
+  expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
+
+  await waitAndUpdate(wrapper);
+  expect(onProjectCreate).toBeCalledWith(['bar']);
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <ManualProjectCreate
+      currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
+      onProjectCreate={jest.fn()}
+      userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
new file mode 100644 (file)
index 0000000..4224b15
--- /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 OrganizationSelect, { optionRenderer } from '../OrganizationSelect';
+
+const organizations = [{ key: 'foo', name: 'Foo' }, { almId: 'github', key: 'bar', name: 'Bar' }];
+
+it('should render correctly', () => {
+  expect(
+    shallow(
+      <OrganizationSelect onChange={jest.fn()} organization="bar" organizations={organizations} />
+    )
+  ).toMatchSnapshot();
+  expect(
+    shallow(
+      <OrganizationSelect
+        autoImport={true}
+        onChange={jest.fn()}
+        organization="bar"
+        organizations={organizations}
+      />
+    )
+      .find('.js-new-org')
+      .contains('onboarding.create_project.import_new_org')
+  ).toBe(true);
+});
+
+it('should render options correctly', () => {
+  expect(shallow(optionRenderer(organizations[0]))).toMatchSnapshot();
+  expect(shallow(optionRenderer(organizations[1]))).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
new file mode 100644 (file)
index 0000000..eefe188
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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 RemoteRepositories from '../RemoteRepositories';
+import { getRepositories, provisionProject } from '../../../../api/alm-integration';
+import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/alm-integration', () => ({
+  getRepositories: jest.fn().mockResolvedValue({
+    repositories: [
+      {
+        label: 'Cool Project',
+        installationKey: 'github/cool',
+        linkedProjectKey: 'proj_cool',
+        linkedProjectName: 'Proj Cool'
+      },
+      {
+        label: 'Awesome Project',
+        installationKey: 'github/awesome'
+      }
+    ]
+  }),
+  provisionProject: jest.fn().mockResolvedValue({ projects: [{ projectKey: 'awesome' }] })
+}));
+
+const almApplication = {
+  backgroundColor: 'blue',
+  iconPath: 'icon/path',
+  installationUrl: 'https://alm.installation.url',
+  key: 'github',
+  name: 'GitHub'
+};
+
+beforeEach(() => {
+  (getRepositories as jest.Mock<any>).mockClear();
+  (provisionProject as jest.Mock<any>).mockClear();
+});
+
+it('should display the list of repositories', async () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  await waitAndUpdate(wrapper);
+  expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should correctly create a project', async () => {
+  const onProjectCreate = jest.fn();
+  const wrapper = shallowRender({ onProjectCreate });
+  (wrapper.instance() as RemoteRepositories).toggleRepository({
+    label: 'Awesome Project',
+    installationKey: 'github/awesome'
+  });
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+  submit(wrapper.find('form'));
+  expect(provisionProject).toBeCalledWith({
+    installationKeys: ['github/awesome'],
+    organization: 'sonarsource'
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(onProjectCreate).toBeCalledWith(['awesome']);
+});
+
+function shallowRender(props: Partial<RemoteRepositories['props']> = {}) {
+  return shallow(
+    <RemoteRepositories
+      almApplication={almApplication}
+      onProjectCreate={jest.fn()}
+      organization="sonarsource"
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
new file mode 100644 (file)
index 0000000..7ed1eed
--- /dev/null
@@ -0,0 +1,111 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+  <Checkbox
+    checked={false}
+    disabled={false}
+    onCheck={[Function]}
+    thirdState={false}
+  >
+    <img
+      alt="Foo Provider"
+      className="spacer-left"
+      height={14}
+      src="/images/sonarcloud/foo.svg"
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+      width={14}
+    />
+    <span
+      className="spacer-left"
+    >
+      Awesome Project
+    </span>
+  </Checkbox>
+</Fragment>
+`;
+
+exports[`should render disabled 1`] = `
+<Fragment>
+  <Checkbox
+    checked={true}
+    disabled={true}
+    onCheck={[Function]}
+    thirdState={false}
+  >
+    <img
+      alt="Foo Provider"
+      className="spacer-left"
+      height={14}
+      src="/images/sonarcloud/foo.svg"
+      style={
+        Object {
+          "opacity": 0.5,
+        }
+      }
+      width={14}
+    />
+    <span
+      className="spacer-left"
+    >
+      Cool Project
+    </span>
+  </Checkbox>
+  <span
+    className="big-spacer-left"
+  >
+    <CheckIcon
+      className="little-spacer-right"
+      fill="#00aa00"
+    />
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/dashboard",
+          "query": Object {
+            "branch": undefined,
+            "id": "proj_cool",
+          },
+        }
+      }
+    >
+      onboarding.create_project.already_imported
+    </Link>
+  </span>
+</Fragment>
+`;
+
+exports[`should render selected 1`] = `
+<Fragment>
+  <Checkbox
+    checked={true}
+    disabled={false}
+    onCheck={[Function]}
+    thirdState={false}
+  >
+    <img
+      alt="Foo Provider"
+      className="spacer-left"
+      height={14}
+      src="/images/sonarcloud/foo.svg"
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+      width={14}
+    />
+    <span
+      className="spacer-left"
+    >
+      Awesome Project
+    </span>
+  </Checkbox>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
new file mode 100644 (file)
index 0000000..147427d
--- /dev/null
@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the bounded organizations dropdown with the list of repositories 1`] = `
+<Fragment>
+  <OrganizationSelect
+    autoImport={true}
+    onChange={[Function]}
+    organization="foo"
+    organizations={
+      Array [
+        Object {
+          "almId": "github",
+          "key": "foo",
+          "name": "Foo",
+        },
+        Object {
+          "almId": "github",
+          "key": "bar",
+          "name": "Bar",
+        },
+      ]
+    }
+  />
+  <RemoteRepositories
+    almApplication={
+      Object {
+        "backgroundColor": "blue",
+        "iconPath": "icon/path",
+        "installationUrl": "https://alm.installation.url",
+        "key": "github",
+        "name": "GitHub",
+      }
+    }
+    onProjectCreate={[MockFunction]}
+    organization="foo"
+  />
+</Fragment>
+`;
+
+exports[`should display the provider app install button 1`] = `
+<Fragment>
+  <IdentityProviderLink
+    className="display-inline-block"
+    identityProvider={
+      Object {
+        "backgroundColor": "blue",
+        "iconPath": "icon/path",
+        "installationUrl": "https://alm.installation.url",
+        "key": "github",
+        "name": "GitHub",
+      }
+    }
+    small={true}
+    url="https://alm.installation.url"
+  >
+    onboarding.create_organization.choose_organization_button.github
+  </IdentityProviderLink>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
new file mode 100644 (file)
index 0000000..6e1f905
--- /dev/null
@@ -0,0 +1,214 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.create_project.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        onboarding.create_project.header
+      </h1>
+    </header>
+    <DeferredSpinner
+      timeout={100}
+    />
+  </div>
+</Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.create_project.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        onboarding.create_project.header
+      </h1>
+    </header>
+    <Tabs
+      onChange={[Function]}
+      selected="auto"
+      tabs={
+        Array [
+          Object {
+            "key": "auto",
+            "node": <React.Fragment>
+              onboarding.create_project.select_repositories
+              <span
+                className="beta-badge spacer-left"
+              >
+                beta
+              </span>
+            </React.Fragment>,
+          },
+          Object {
+            "key": "manual",
+            "node": "onboarding.create_project.create_manually",
+          },
+        ]
+      }
+    />
+    <AutoProjectCreate
+      almApplication={
+        Object {
+          "backgroundColor": "blue",
+          "iconPath": "icon/path",
+          "installationUrl": "https://alm.installation.url",
+          "key": "github",
+          "name": "GitHub",
+        }
+      }
+      boundOrganizations={
+        Array [
+          Object {
+            "almId": "github",
+            "key": "bar",
+            "name": "Bar",
+          },
+        ]
+      }
+      onProjectCreate={[Function]}
+    />
+  </div>
+</Fragment>
+`;
+
+exports[`should render with Manual creation only 1`] = `
+<Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.create_project.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        onboarding.create_project.header
+      </h1>
+    </header>
+    <ManualProjectCreate
+      currentUser={
+        Object {
+          "externalProvider": "microsoft",
+          "groups": Array [],
+          "isLoggedIn": true,
+          "login": "foo",
+          "name": "Foo",
+          "scmAccounts": Array [],
+        }
+      }
+      onProjectCreate={[Function]}
+      userOrganizations={
+        Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          },
+          Object {
+            "almId": "github",
+            "key": "bar",
+            "name": "Bar",
+          },
+        ]
+      }
+    />
+  </div>
+</Fragment>
+`;
+
+exports[`should switch tabs 1`] = `
+<Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.create_project.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        onboarding.create_project.header
+      </h1>
+    </header>
+    <Tabs
+      onChange={[Function]}
+      selected="auto"
+      tabs={
+        Array [
+          Object {
+            "key": "auto",
+            "node": <React.Fragment>
+              onboarding.create_project.select_repositories
+              <span
+                className="beta-badge spacer-left"
+              >
+                beta
+              </span>
+            </React.Fragment>,
+          },
+          Object {
+            "key": "manual",
+            "node": "onboarding.create_project.create_manually",
+          },
+        ]
+      }
+    />
+    <AutoProjectCreate
+      almApplication={
+        Object {
+          "backgroundColor": "blue",
+          "iconPath": "icon/path",
+          "installationUrl": "https://alm.installation.url",
+          "key": "github",
+          "name": "GitHub",
+        }
+      }
+      boundOrganizations={
+        Array [
+          Object {
+            "almId": "github",
+            "key": "bar",
+            "name": "Bar",
+          },
+        ]
+      }
+      onProjectCreate={[Function]}
+    />
+  </div>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
new file mode 100644 (file)
index 0000000..53fde97
--- /dev/null
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+  disabled={true}
+>
+  create
+</SubmitButton>
+`;
+
+exports[`should correctly create a project 2`] = `
+<SubmitButton
+  disabled={false}
+>
+  create
+</SubmitButton>
+`;
+
+exports[`should render correctly 1`] = `
+<Fragment>
+  <form
+    onSubmit={[Function]}
+  >
+    <OrganizationSelect
+      onChange={[Function]}
+      organization=""
+      organizations={
+        Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          },
+          Object {
+            "key": "bar",
+            "name": "Bar",
+          },
+        ]
+      }
+    />
+    <div
+      className="form-field"
+    >
+      <label
+        htmlFor="project-name"
+      >
+        onboarding.create_project.project_name
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <input
+        className="input-super-large"
+        id="project-name"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value=""
+      />
+    </div>
+    <div
+      className="form-field"
+    >
+      <label
+        htmlFor="project-key"
+      >
+        onboarding.create_project.project_key
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <input
+        className="input-super-large"
+        id="project-key"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value=""
+      />
+    </div>
+    <SubmitButton
+      disabled={true}
+    >
+      create
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
new file mode 100644 (file)
index 0000000..50cd939
--- /dev/null
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="form-field spacer-bottom"
+>
+  <label
+    htmlFor="select-organization"
+  >
+    onboarding.create_project.organization
+    <em
+      className="mandatory"
+    >
+      *
+    </em>
+  </label>
+  <Select
+    autoFocus={true}
+    className="input-super-large"
+    clearable={false}
+    id="select-organization"
+    labelKey="name"
+    onChange={[MockFunction]}
+    optionRenderer={[Function]}
+    options={
+      Array [
+        Object {
+          "almId": "github",
+          "key": "bar",
+          "name": "Bar",
+        },
+        Object {
+          "key": "foo",
+          "name": "Foo",
+        },
+      ]
+    }
+    required={true}
+    value="bar"
+    valueKey="key"
+    valueRenderer={[Function]}
+  />
+  <Link
+    className="big-spacer-left js-new-org"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to="/create-organization"
+  >
+    onboarding.create_project.create_new_org
+  </Link>
+</div>
+`;
+
+exports[`should render options correctly 1`] = `
+<span>
+  Foo
+  <span
+    className="note little-spacer-left"
+  >
+    foo
+  </span>
+</span>
+`;
+
+exports[`should render options correctly 2`] = `
+<span>
+  <img
+    alt="github"
+    className="spacer-right"
+    height={14}
+    src="/images/sonarcloud/github.svg"
+  />
+  Bar
+  <span
+    className="note little-spacer-left"
+  >
+    bar
+  </span>
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
new file mode 100644 (file)
index 0000000..01359b6
--- /dev/null
@@ -0,0 +1,114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+  disabled={false}
+>
+  create
+</SubmitButton>
+`;
+
+exports[`should display the list of repositories 1`] = `
+<DeferredSpinner
+  loading={true}
+  timeout={100}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="form-field"
+    >
+      <ul />
+    </div>
+    <SubmitButton
+      disabled={true}
+    >
+      create
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+</DeferredSpinner>
+`;
+
+exports[`should display the list of repositories 2`] = `
+<DeferredSpinner
+  loading={false}
+  timeout={100}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <div
+      className="form-field"
+    >
+      <ul>
+        <li
+          className="big-spacer-bottom"
+          key="github/cool"
+        >
+          <AlmRepositoryItem
+            identityProvider={
+              Object {
+                "backgroundColor": "blue",
+                "iconPath": "icon/path",
+                "installationUrl": "https://alm.installation.url",
+                "key": "github",
+                "name": "GitHub",
+              }
+            }
+            repository={
+              Object {
+                "installationKey": "github/cool",
+                "label": "Cool Project",
+                "linkedProjectKey": "proj_cool",
+                "linkedProjectName": "Proj Cool",
+              }
+            }
+            selected={false}
+            toggleRepository={[Function]}
+          />
+        </li>
+        <li
+          className="big-spacer-bottom"
+          key="github/awesome"
+        >
+          <AlmRepositoryItem
+            identityProvider={
+              Object {
+                "backgroundColor": "blue",
+                "iconPath": "icon/path",
+                "installationUrl": "https://alm.installation.url",
+                "key": "github",
+                "name": "GitHub",
+              }
+            }
+            repository={
+              Object {
+                "installationKey": "github/awesome",
+                "label": "Awesome Project",
+              }
+            }
+            selected={false}
+            toggleRepository={[Function]}
+          />
+        </li>
+      </ul>
+    </div>
+    <SubmitButton
+      disabled={true}
+    >
+      create
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+</DeferredSpinner>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx b/server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx
deleted file mode 100644 (file)
index 8fd083f..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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 { Link } from 'react-router';
-import * as theme from '../../../app/theme';
-import Checkbox from '../../../components/controls/Checkbox';
-import CheckIcon from '../../../components/icons-components/CheckIcon';
-import { AlmRepository, IdentityProvider } from '../../../app/types';
-import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  identityProvider: IdentityProvider;
-  repository: AlmRepository;
-  selected: boolean;
-  toggleRepository: (repository: AlmRepository) => void;
-}
-
-export default class AlmRepositoryItem extends React.PureComponent<Props> {
-  handleChange = () => {
-    this.props.toggleRepository(this.props.repository);
-  };
-
-  render() {
-    const { identityProvider, repository, selected } = this.props;
-    const alreadyImported = Boolean(repository.linkedProjectKey);
-    return (
-      <>
-        <Checkbox
-          checked={selected || alreadyImported}
-          disabled={alreadyImported}
-          onCheck={this.handleChange}>
-          <img
-            alt={identityProvider.name}
-            className="spacer-left"
-            height={14}
-            src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
-            style={{ opacity: alreadyImported ? 0.5 : 1 }}
-            width={14}
-          />
-          <span className="spacer-left">{this.props.repository.label}</span>
-        </Checkbox>
-        {repository.linkedProjectKey && (
-          <span className="big-spacer-left">
-            <CheckIcon className="little-spacer-right" fill={theme.green} />
-            <Link to={getProjectUrl(repository.linkedProjectKey)}>
-              {translate('onboarding.create_project.already_imported')}
-            </Link>
-          </span>
-        )}
-      </>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx
deleted file mode 100644 (file)
index 7467760..0000000
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * 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 AlmRepositoryItem from './AlmRepositoryItem';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
-import { getRepositories, provisionProject } from '../../../api/alm-integration';
-import { IdentityProvider, AlmRepository } from '../../../app/types';
-import { SubmitButton } from '../../../components/ui/buttons';
-import { translateWithParameters, translate } from '../../../helpers/l10n';
-import { Alert } from '../../../components/ui/Alert';
-
-interface Props {
-  identityProvider: IdentityProvider;
-  onProjectCreate: (projectKeys: string[]) => void;
-}
-
-interface State {
-  installationUrl?: string;
-  installed?: boolean;
-  loading: boolean;
-  repositories: AlmRepository[];
-  selectedRepositories: { [key: string]: AlmRepository | undefined };
-  submitting: boolean;
-}
-
-export default class AutoProjectCreate extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    loading: true,
-    repositories: [],
-    selectedRepositories: {},
-    submitting: false
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchRepositories();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchRepositories = () => {
-    getRepositories().then(
-      ({ almIntegration, repositories }) => {
-        if (this.mounted) {
-          this.setState({ ...almIntegration, loading: false, repositories });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    if (this.isValid()) {
-      const { selectedRepositories } = this.state;
-      this.setState({ submitting: true });
-      provisionProject({
-        installationKeys: Object.keys(selectedRepositories).filter(key =>
-          Boolean(selectedRepositories[key])
-        )
-      }).then(
-        ({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
-        () => {
-          if (this.mounted) {
-            this.setState({ loading: true, submitting: false });
-            this.fetchRepositories();
-          }
-        }
-      );
-    }
-  };
-
-  isValid = () => {
-    return this.state.repositories.some(repo =>
-      Boolean(this.state.selectedRepositories[repo.installationKey])
-    );
-  };
-
-  toggleRepository = (repository: AlmRepository) => {
-    this.setState(({ selectedRepositories }) => ({
-      selectedRepositories: {
-        ...selectedRepositories,
-        [repository.installationKey]: selectedRepositories[repository.installationKey]
-          ? undefined
-          : repository
-      }
-    }));
-  };
-
-  renderContent = () => {
-    const { identityProvider } = this.props;
-    const { selectedRepositories, submitting } = this.state;
-
-    if (this.state.installed) {
-      return (
-        <form onSubmit={this.handleFormSubmit}>
-          <ul>
-            {this.state.repositories.map(repo => (
-              <li className="big-spacer-bottom" key={repo.installationKey}>
-                <AlmRepositoryItem
-                  identityProvider={identityProvider}
-                  repository={repo}
-                  selected={Boolean(selectedRepositories[repo.installationKey])}
-                  toggleRepository={this.toggleRepository}
-                />
-              </li>
-            ))}
-          </ul>
-          <SubmitButton disabled={!this.isValid() || submitting}>
-            {translate('create')}
-          </SubmitButton>
-          <DeferredSpinner className="spacer-left" loading={submitting} />
-        </form>
-      );
-    }
-    return (
-      <div>
-        <p className="spacer-bottom">
-          {translateWithParameters(
-            'onboarding.create_project.install_app_x',
-            identityProvider.name
-          )}
-        </p>
-        <IdentityProviderLink
-          className="display-inline-block"
-          identityProvider={identityProvider}
-          small={true}
-          url={this.state.installationUrl}>
-          {translateWithParameters(
-            'onboarding.create_project.install_app_x.button',
-            identityProvider.name
-          )}
-        </IdentityProviderLink>
-      </div>
-    );
-  };
-
-  render() {
-    const { identityProvider } = this.props;
-    const { loading } = this.state;
-
-    return (
-      <>
-        <Alert className="width-60 big-spacer-bottom" variant="info">
-          {translateWithParameters(
-            'onboarding.create_project.beta_feature_x',
-            identityProvider.name
-          )}
-        </Alert>
-        {loading ? <DeferredSpinner /> : this.renderContent()}
-      </>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx
deleted file mode 100644 (file)
index f45dccd..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * 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 * as classNames from 'classnames';
-import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
-import { Location } from 'history';
-import Helmet from 'react-helmet';
-import AutoProjectCreate from './AutoProjectCreate';
-import ManualProjectCreate from './ManualProjectCreate';
-import { serializeQuery, Query, parseQuery } from './utils';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import Tabs from '../../../components/controls/Tabs';
-import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
-import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
-import { CurrentUser, IdentityProvider, LoggedInUser } from '../../../app/types';
-import { skipOnboarding, getIdentityProviders } from '../../../api/users';
-import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
-import { translate } from '../../../helpers/l10n';
-import { getProjectUrl } from '../../../helpers/urls';
-import { isLoggedIn } from '../../../helpers/users';
-import '../../../app/styles/sonarcloud.css';
-
-interface OwnProps {
-  location: Location;
-  router: Pick<InjectedRouter, 'push' | 'replace'>;
-}
-
-interface StateProps {
-  currentUser: CurrentUser;
-}
-
-interface DispatchProps {
-  addGlobalErrorMessage: (message: string) => void;
-  skipOnboardingAction: () => void;
-}
-
-type Props = StateProps & DispatchProps & OwnProps;
-
-interface State {
-  identityProvider?: IdentityProvider;
-  loading: boolean;
-}
-
-export class CreateProjectPage extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: true };
-
-  componentDidMount() {
-    if (isLoggedIn(this.props.currentUser)) {
-      this.mounted = true;
-      const query = parseQuery(this.props.location.query);
-      if (query.error) {
-        this.props.addGlobalErrorMessage(query.error);
-      }
-      if (!hasAdvancedALMIntegration(this.props.currentUser)) {
-        this.setState({ loading: false });
-        this.updateQuery({ manual: true });
-      } else {
-        this.fetchIdentityProviders();
-      }
-      document.body.classList.add('white-page');
-      if (document.documentElement) {
-        document.documentElement.classList.add('white-page');
-      }
-    } else {
-      handleRequiredAuthentication();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-    document.body.classList.remove('white-page');
-    if (document.documentElement) {
-      document.documentElement.classList.remove('white-page');
-    }
-  }
-
-  handleProjectCreate = (projectKeys: string[]) => {
-    skipOnboarding().catch(() => {});
-    this.props.skipOnboardingAction();
-    if (projectKeys.length > 1) {
-      this.props.router.push({ pathname: '/projects' });
-    } else if (projectKeys.length === 1) {
-      this.props.router.push(getProjectUrl(projectKeys[0]));
-    }
-  };
-
-  fetchIdentityProviders = () => {
-    getIdentityProviders().then(
-      ({ identityProviders }) => {
-        if (this.mounted) {
-          this.setState({
-            identityProvider: identityProviders.find(
-              identityProvider =>
-                identityProvider.key === (this.props.currentUser as LoggedInUser).externalProvider
-            ),
-            loading: false
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  onTabChange = (tab: 'auto' | 'manual') => {
-    this.updateQuery({ manual: tab === 'manual' });
-  };
-
-  updateQuery = (changes: Partial<Query>) => {
-    this.props.router.replace({
-      pathname: this.props.location.pathname,
-      query: serializeQuery({ ...parseQuery(this.props.location.query), ...changes })
-    });
-  };
-
-  render() {
-    const { currentUser } = this.props;
-
-    if (!isLoggedIn(currentUser)) {
-      return null;
-    }
-
-    const { identityProvider, loading } = this.state;
-    const query = parseQuery(this.props.location.query);
-    const header = translate('onboarding.create_project.header');
-    const hasAutoProvisioning = hasAdvancedALMIntegration(currentUser) && identityProvider;
-    return (
-      <>
-        <Helmet title={header} titleTemplate="%s" />
-        <div className="sonarcloud page page-limited">
-          <header className="page-header">
-            <h1 className="page-title">{header}</h1>
-          </header>
-          {loading ? (
-            <DeferredSpinner />
-          ) : (
-            <>
-              {hasAutoProvisioning && (
-                <Tabs
-                  onChange={this.onTabChange}
-                  selected={query.manual ? 'manual' : 'auto'}
-                  tabs={[
-                    {
-                      key: 'auto',
-                      node: (
-                        <>
-                          {translate('onboarding.create_project.select_repositories')}
-                          <span
-                            className={classNames('beta-badge spacer-left', {
-                              'is-muted': query.manual
-                            })}>
-                            {translate('beta')}
-                          </span>
-                        </>
-                      )
-                    },
-                    { key: 'manual', node: translate('onboarding.create_project.create_manually') }
-                  ]}
-                />
-              )}
-
-              {query.manual || !hasAutoProvisioning || !identityProvider ? (
-                <ManualProjectCreate
-                  currentUser={currentUser}
-                  onProjectCreate={this.handleProjectCreate}
-                  organization={query.organization}
-                />
-              ) : (
-                <AutoProjectCreate
-                  identityProvider={identityProvider}
-                  onProjectCreate={this.handleProjectCreate}
-                />
-              )}
-            </>
-          )}
-        </div>
-      </>
-    );
-  }
-}
-
-const mapStateToProps = (state: Store): StateProps => ({
-  currentUser: getCurrentUser(state)
-});
-
-const mapDispatchToProps: DispatchProps = { addGlobalErrorMessage, skipOnboardingAction };
-
-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(CreateProjectPage);
diff --git a/server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx
deleted file mode 100644 (file)
index 1101f64..0000000
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * 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 { sortBy } from 'lodash';
-import { connect } from 'react-redux';
-import { Link } from 'react-router';
-import Select from '../../../components/controls/Select';
-import { SubmitButton } from '../../../components/ui/buttons';
-import { LoggedInUser, Organization } from '../../../app/types';
-import { fetchMyOrganizations } from '../../account/organizations/actions';
-import { getMyOrganizations, Store } from '../../../store/rootReducer';
-import { translate } from '../../../helpers/l10n';
-import { createProject } from '../../../api/components';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-
-interface StateProps {
-  userOrganizations: Organization[];
-}
-
-interface DispatchProps {
-  fetchMyOrganizations: () => Promise<void>;
-}
-
-interface OwnProps {
-  currentUser: LoggedInUser;
-  onProjectCreate: (projectKeys: string[]) => void;
-  organization?: string;
-}
-
-type Props = OwnProps & StateProps & DispatchProps;
-
-interface State {
-  projectName: string;
-  projectKey: string;
-  selectedOrganization: string;
-  submitting: boolean;
-}
-
-export class ManualProjectCreate extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      projectName: '',
-      projectKey: '',
-      selectedOrganization: this.getInitialSelectedOrganization(props),
-      submitting: false
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  getInitialSelectedOrganization(props: Props) {
-    if (props.organization) {
-      return props.organization;
-    } else if (props.userOrganizations.length === 1) {
-      return props.userOrganizations[0].key;
-    } else {
-      return '';
-    }
-  }
-
-  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    if (this.isValid()) {
-      const { projectKey, projectName, selectedOrganization } = this.state;
-      this.setState({ submitting: true });
-      createProject({
-        project: projectKey,
-        name: projectName,
-        organization: selectedOrganization
-      }).then(
-        ({ project }) => this.props.onProjectCreate([project.key]),
-        () => {
-          if (this.mounted) {
-            this.setState({ submitting: false });
-          }
-        }
-      );
-    }
-  };
-
-  handleOrganizationSelect = ({ value }: { value: string }) => {
-    this.setState({ selectedOrganization: value });
-  };
-
-  handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({ projectName: event.currentTarget.value });
-  };
-
-  handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({ projectKey: event.currentTarget.value });
-  };
-
-  isValid = () => {
-    const { projectKey, projectName, selectedOrganization } = this.state;
-    return Boolean(projectKey && projectName && selectedOrganization);
-  };
-
-  render() {
-    const { submitting } = this.state;
-    return (
-      <>
-        <form onSubmit={this.handleFormSubmit}>
-          <div className="form-field">
-            <label htmlFor="select-organization">
-              {translate('onboarding.create_project.organization')}
-              <em className="mandatory">*</em>
-            </label>
-            <Select
-              autoFocus={true}
-              className="input-super-large"
-              clearable={false}
-              id="select-organization"
-              onChange={this.handleOrganizationSelect}
-              options={sortBy(this.props.userOrganizations, o => o.name.toLowerCase()).map(
-                organization => ({
-                  label: organization.name,
-                  value: organization.key
-                })
-              )}
-              required={true}
-              value={this.state.selectedOrganization}
-            />
-            <Link className="big-spacer-left js-new-org" to="/create-organization">
-              {translate('onboarding.create_project.create_new_org')}
-            </Link>
-          </div>
-          <div className="form-field">
-            <label htmlFor="project-name">
-              {translate('onboarding.create_project.project_name')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              className="input-super-large"
-              id="project-name"
-              maxLength={400}
-              minLength={1}
-              onChange={this.handleProjectNameChange}
-              required={true}
-              type="text"
-              value={this.state.projectName}
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="project-key">
-              {translate('onboarding.create_project.project_key')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              className="input-super-large"
-              id="project-key"
-              maxLength={400}
-              minLength={1}
-              onChange={this.handleProjectKeyChange}
-              required={true}
-              type="text"
-              value={this.state.projectKey}
-            />
-          </div>
-          <SubmitButton disabled={!this.isValid() || submitting}>
-            {translate('create')}
-          </SubmitButton>
-          <DeferredSpinner className="spacer-left" loading={submitting} />
-        </form>
-      </>
-    );
-  }
-}
-
-const mapDispatchToProps = ({
-  fetchMyOrganizations
-} as any) as DispatchProps;
-
-const mapStateToProps = (state: Store): StateProps => {
-  return {
-    userOrganizations: getMyOrganizations(state)
-  };
-};
-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(ManualProjectCreate);
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/AlmRepositoryItem-test.tsx b/server/sonar-web/src/main/js/apps/projects/create/__tests__/AlmRepositoryItem-test.tsx
deleted file mode 100644 (file)
index 72b25cb..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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 AlmRepositoryItem from '../AlmRepositoryItem';
-
-const identityProviders = {
-  backgroundColor: 'blue',
-  iconPath: 'icon/path',
-  key: 'foo',
-  name: 'Foo Provider'
-};
-
-const repositories = [
-  {
-    label: 'Cool Project',
-    installationKey: 'github/cool',
-    linkedProjectKey: 'proj_cool',
-    linkedProjectName: 'Proj Cool'
-  },
-  {
-    label: 'Awesome Project',
-    installationKey: 'github/awesome'
-  }
-];
-
-it('should render correctly', () => {
-  expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should render selected', () => {
-  expect(getWrapper({ selected: true })).toMatchSnapshot();
-});
-
-it('should render disabled', () => {
-  expect(getWrapper({ repository: repositories[0] })).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <AlmRepositoryItem
-      identityProvider={identityProviders}
-      repository={repositories[1]}
-      selected={false}
-      toggleRepository={jest.fn()}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx
deleted file mode 100644 (file)
index 91e189f..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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 AutoProjectCreate from '../AutoProjectCreate';
-import { getRepositories } from '../../../../api/alm-integration';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-
-jest.mock('../../../../api/alm-integration', () => ({
-  getRepositories: jest.fn().mockResolvedValue({
-    almIntegration: {
-      installationUrl: 'https://alm.foo.com/install',
-      installed: false
-    },
-    repositories: []
-  }),
-  provisionProject: jest.fn().mockResolvedValue({ projects: [] })
-}));
-
-const identityProvider = {
-  backgroundColor: 'blue',
-  iconPath: 'icon/path',
-  key: 'foo',
-  name: 'Foo Provider'
-};
-
-const repositories = [
-  {
-    label: 'Cool Project',
-    installationKey: 'github/cool',
-    linkedProjectKey: 'proj_cool',
-    linkedProjectName: 'Proj Cool'
-  },
-  {
-    label: 'Awesome Project',
-    installationKey: 'github/awesome'
-  }
-];
-
-beforeEach(() => {
-  (getRepositories as jest.Mock<any>).mockClear();
-});
-
-it('should display the provider app install button', async () => {
-  const wrapper = getWrapper();
-  expect(wrapper).toMatchSnapshot();
-  expect(getRepositories).toHaveBeenCalled();
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should display the list of repositories', async () => {
-  (getRepositories as jest.Mock<any>).mockResolvedValue({
-    almIntegration: {
-      installationUrl: 'https://alm.foo.com/install',
-      installed: true
-    },
-    repositories
-  });
-  const wrapper = getWrapper();
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <AutoProjectCreate identityProvider={identityProvider} onProjectCreate={jest.fn()} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx
deleted file mode 100644 (file)
index fba7c58..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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 { Location } from 'history';
-import { CreateProjectPage } from '../CreateProjectPage';
-import { getIdentityProviders } from '../../../../api/users';
-import { LoggedInUser } from '../../../../app/types';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-
-jest.mock('../../../../api/users', () => ({
-  getIdentityProviders: jest.fn().mockResolvedValue({
-    identityProviders: [
-      {
-        backgroundColor: 'blue',
-        iconPath: 'icon/path',
-        key: 'github',
-        name: 'GitHub'
-      }
-    ]
-  })
-}));
-
-const user: LoggedInUser = {
-  externalProvider: 'github',
-  groups: [],
-  isLoggedIn: true,
-  login: 'foo',
-  name: 'Foo',
-  scmAccounts: []
-};
-
-beforeEach(() => {
-  (getIdentityProviders as jest.Mock<any>).mockClear();
-});
-
-it('should render correctly', async () => {
-  const wrapper = getWrapper();
-  expect(wrapper).toMatchSnapshot();
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render with Manual creation only', () => {
-  expect(getWrapper({ currentUser: { ...user, externalProvider: 'microsoft' } })).toMatchSnapshot();
-});
-
-it('should switch tabs', async () => {
-  const replace = jest.fn();
-  const wrapper = getWrapper({ router: { replace } });
-  replace.mockImplementation(location => {
-    wrapper.setProps({ location }).update();
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-
-  wrapper.find('Tabs').prop<Function>('onChange')('manual');
-  expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
-  wrapper.find('Tabs').prop<Function>('onChange')('auto');
-  expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
-});
-
-it('should display an error message on load', () => {
-  const addGlobalErrorMessage = jest.fn();
-  getWrapper({
-    addGlobalErrorMessage,
-    location: { pathname: 'foo', query: { error: 'Foo error' } }
-  });
-  expect(addGlobalErrorMessage).toHaveBeenCalledWith('Foo error');
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <CreateProjectPage
-      addGlobalErrorMessage={jest.fn()}
-      currentUser={user}
-      location={{ pathname: 'foo', query: { manual: 'false' } } as Location}
-      router={{ push: jest.fn(), replace: jest.fn() }}
-      skipOnboardingAction={jest.fn()}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx
deleted file mode 100644 (file)
index 5777d22..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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 { ManualProjectCreate } from '../ManualProjectCreate';
-import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
-import { createProject } from '../../../../api/components';
-
-jest.mock('../../../../api/components', () => ({
-  createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
-}));
-
-beforeEach(() => {
-  (createProject as jest.Mock<any>).mockClear();
-});
-
-it('should render correctly', () => {
-  expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should correctly create a project', async () => {
-  const onProjectCreate = jest.fn();
-  const wrapper = getWrapper({ onProjectCreate });
-  wrapper.find('Select').prop<Function>('onChange')({ value: 'foo' });
-  change(wrapper.find('#project-name'), 'Bar');
-  expect(wrapper.find('SubmitButton')).toMatchSnapshot();
-
-  change(wrapper.find('#project-key'), 'bar');
-  expect(wrapper.find('SubmitButton')).toMatchSnapshot();
-
-  submit(wrapper.find('form'));
-  expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
-
-  await waitAndUpdate(wrapper);
-  expect(onProjectCreate).toBeCalledWith(['bar']);
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <ManualProjectCreate
-      currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
-      fetchMyOrganizations={jest.fn()}
-      onProjectCreate={jest.fn()}
-      userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
deleted file mode 100644 (file)
index 7ed1eed..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
-  <Checkbox
-    checked={false}
-    disabled={false}
-    onCheck={[Function]}
-    thirdState={false}
-  >
-    <img
-      alt="Foo Provider"
-      className="spacer-left"
-      height={14}
-      src="/images/sonarcloud/foo.svg"
-      style={
-        Object {
-          "opacity": 1,
-        }
-      }
-      width={14}
-    />
-    <span
-      className="spacer-left"
-    >
-      Awesome Project
-    </span>
-  </Checkbox>
-</Fragment>
-`;
-
-exports[`should render disabled 1`] = `
-<Fragment>
-  <Checkbox
-    checked={true}
-    disabled={true}
-    onCheck={[Function]}
-    thirdState={false}
-  >
-    <img
-      alt="Foo Provider"
-      className="spacer-left"
-      height={14}
-      src="/images/sonarcloud/foo.svg"
-      style={
-        Object {
-          "opacity": 0.5,
-        }
-      }
-      width={14}
-    />
-    <span
-      className="spacer-left"
-    >
-      Cool Project
-    </span>
-  </Checkbox>
-  <span
-    className="big-spacer-left"
-  >
-    <CheckIcon
-      className="little-spacer-right"
-      fill="#00aa00"
-    />
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to={
-        Object {
-          "pathname": "/dashboard",
-          "query": Object {
-            "branch": undefined,
-            "id": "proj_cool",
-          },
-        }
-      }
-    >
-      onboarding.create_project.already_imported
-    </Link>
-  </span>
-</Fragment>
-`;
-
-exports[`should render selected 1`] = `
-<Fragment>
-  <Checkbox
-    checked={true}
-    disabled={false}
-    onCheck={[Function]}
-    thirdState={false}
-  >
-    <img
-      alt="Foo Provider"
-      className="spacer-left"
-      height={14}
-      src="/images/sonarcloud/foo.svg"
-      style={
-        Object {
-          "opacity": 1,
-        }
-      }
-      width={14}
-    />
-    <span
-      className="spacer-left"
-    >
-      Awesome Project
-    </span>
-  </Checkbox>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
deleted file mode 100644 (file)
index 619285b..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display the list of repositories 1`] = `
-<Fragment>
-  <Alert
-    className="width-60 big-spacer-bottom"
-    variant="info"
-  >
-    onboarding.create_project.beta_feature_x.Foo Provider
-  </Alert>
-  <form
-    onSubmit={[Function]}
-  >
-    <ul>
-      <li
-        className="big-spacer-bottom"
-        key="github/cool"
-      >
-        <AlmRepositoryItem
-          identityProvider={
-            Object {
-              "backgroundColor": "blue",
-              "iconPath": "icon/path",
-              "key": "foo",
-              "name": "Foo Provider",
-            }
-          }
-          repository={
-            Object {
-              "installationKey": "github/cool",
-              "label": "Cool Project",
-              "linkedProjectKey": "proj_cool",
-              "linkedProjectName": "Proj Cool",
-            }
-          }
-          selected={false}
-          toggleRepository={[Function]}
-        />
-      </li>
-      <li
-        className="big-spacer-bottom"
-        key="github/awesome"
-      >
-        <AlmRepositoryItem
-          identityProvider={
-            Object {
-              "backgroundColor": "blue",
-              "iconPath": "icon/path",
-              "key": "foo",
-              "name": "Foo Provider",
-            }
-          }
-          repository={
-            Object {
-              "installationKey": "github/awesome",
-              "label": "Awesome Project",
-            }
-          }
-          selected={false}
-          toggleRepository={[Function]}
-        />
-      </li>
-    </ul>
-    <SubmitButton
-      disabled={true}
-    >
-      create
-    </SubmitButton>
-    <DeferredSpinner
-      className="spacer-left"
-      loading={false}
-      timeout={100}
-    />
-  </form>
-</Fragment>
-`;
-
-exports[`should display the provider app install button 1`] = `
-<Fragment>
-  <Alert
-    className="width-60 big-spacer-bottom"
-    variant="info"
-  >
-    onboarding.create_project.beta_feature_x.Foo Provider
-  </Alert>
-  <DeferredSpinner
-    timeout={100}
-  />
-</Fragment>
-`;
-
-exports[`should display the provider app install button 2`] = `
-<Fragment>
-  <Alert
-    className="width-60 big-spacer-bottom"
-    variant="info"
-  >
-    onboarding.create_project.beta_feature_x.Foo Provider
-  </Alert>
-  <div>
-    <p
-      className="spacer-bottom"
-    >
-      onboarding.create_project.install_app_x.Foo Provider
-    </p>
-    <IdentityProviderLink
-      className="display-inline-block"
-      identityProvider={
-        Object {
-          "backgroundColor": "blue",
-          "iconPath": "icon/path",
-          "key": "foo",
-          "name": "Foo Provider",
-        }
-      }
-      small={true}
-      url="https://alm.foo.com/install"
-    >
-      onboarding.create_project.install_app_x.button.Foo Provider
-    </IdentityProviderLink>
-  </div>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
deleted file mode 100644 (file)
index 69731b0..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="onboarding.create_project.header"
-    titleTemplate="%s"
-  />
-  <div
-    className="sonarcloud page page-limited"
-  >
-    <header
-      className="page-header"
-    >
-      <h1
-        className="page-title"
-      >
-        onboarding.create_project.header
-      </h1>
-    </header>
-    <DeferredSpinner
-      timeout={100}
-    />
-  </div>
-</Fragment>
-`;
-
-exports[`should render correctly 2`] = `
-<Fragment>
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="onboarding.create_project.header"
-    titleTemplate="%s"
-  />
-  <div
-    className="sonarcloud page page-limited"
-  >
-    <header
-      className="page-header"
-    >
-      <h1
-        className="page-title"
-      >
-        onboarding.create_project.header
-      </h1>
-    </header>
-    <Tabs
-      onChange={[Function]}
-      selected="auto"
-      tabs={
-        Array [
-          Object {
-            "key": "auto",
-            "node": <React.Fragment>
-              onboarding.create_project.select_repositories
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
-          },
-          Object {
-            "key": "manual",
-            "node": "onboarding.create_project.create_manually",
-          },
-        ]
-      }
-    />
-    <AutoProjectCreate
-      identityProvider={
-        Object {
-          "backgroundColor": "blue",
-          "iconPath": "icon/path",
-          "key": "github",
-          "name": "GitHub",
-        }
-      }
-      onProjectCreate={[Function]}
-    />
-  </div>
-</Fragment>
-`;
-
-exports[`should render with Manual creation only 1`] = `
-<Fragment>
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="onboarding.create_project.header"
-    titleTemplate="%s"
-  />
-  <div
-    className="sonarcloud page page-limited"
-  >
-    <header
-      className="page-header"
-    >
-      <h1
-        className="page-title"
-      >
-        onboarding.create_project.header
-      </h1>
-    </header>
-    <Connect(ManualProjectCreate)
-      currentUser={
-        Object {
-          "externalProvider": "microsoft",
-          "groups": Array [],
-          "isLoggedIn": true,
-          "login": "foo",
-          "name": "Foo",
-          "scmAccounts": Array [],
-        }
-      }
-      onProjectCreate={[Function]}
-    />
-  </div>
-</Fragment>
-`;
-
-exports[`should switch tabs 1`] = `
-<Fragment>
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="onboarding.create_project.header"
-    titleTemplate="%s"
-  />
-  <div
-    className="sonarcloud page page-limited"
-  >
-    <header
-      className="page-header"
-    >
-      <h1
-        className="page-title"
-      >
-        onboarding.create_project.header
-      </h1>
-    </header>
-    <Tabs
-      onChange={[Function]}
-      selected="auto"
-      tabs={
-        Array [
-          Object {
-            "key": "auto",
-            "node": <React.Fragment>
-              onboarding.create_project.select_repositories
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
-          },
-          Object {
-            "key": "manual",
-            "node": "onboarding.create_project.create_manually",
-          },
-        ]
-      }
-    />
-    <AutoProjectCreate
-      identityProvider={
-        Object {
-          "backgroundColor": "blue",
-          "iconPath": "icon/path",
-          "key": "github",
-          "name": "GitHub",
-        }
-      }
-      onProjectCreate={[Function]}
-    />
-  </div>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
deleted file mode 100644 (file)
index 6993ed1..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly create a project 1`] = `
-<SubmitButton
-  disabled={true}
->
-  create
-</SubmitButton>
-`;
-
-exports[`should correctly create a project 2`] = `
-<SubmitButton
-  disabled={false}
->
-  create
-</SubmitButton>
-`;
-
-exports[`should render correctly 1`] = `
-<Fragment>
-  <form
-    onSubmit={[Function]}
-  >
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="select-organization"
-      >
-        onboarding.create_project.organization
-        <em
-          className="mandatory"
-        >
-          *
-        </em>
-      </label>
-      <Select
-        autoFocus={true}
-        className="input-super-large"
-        clearable={false}
-        id="select-organization"
-        onChange={[Function]}
-        options={
-          Array [
-            Object {
-              "label": "Bar",
-              "value": "bar",
-            },
-            Object {
-              "label": "Foo",
-              "value": "foo",
-            },
-          ]
-        }
-        required={true}
-        value=""
-      />
-      <Link
-        className="big-spacer-left js-new-org"
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/create-organization"
-      >
-        onboarding.create_project.create_new_org
-      </Link>
-    </div>
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="project-name"
-      >
-        onboarding.create_project.project_name
-        <em
-          className="mandatory"
-        >
-          *
-        </em>
-      </label>
-      <input
-        className="input-super-large"
-        id="project-name"
-        maxLength={400}
-        minLength={1}
-        onChange={[Function]}
-        required={true}
-        type="text"
-        value=""
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="project-key"
-      >
-        onboarding.create_project.project_key
-        <em
-          className="mandatory"
-        >
-          *
-        </em>
-      </label>
-      <input
-        className="input-super-large"
-        id="project-key"
-        maxLength={400}
-        minLength={1}
-        onChange={[Function]}
-        required={true}
-        type="text"
-        value=""
-      />
-    </div>
-    <SubmitButton
-      disabled={true}
-    >
-      create
-    </SubmitButton>
-    <DeferredSpinner
-      className="spacer-left"
-      loading={false}
-      timeout={100}
-    />
-  </form>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/utils.ts b/server/sonar-web/src/main/js/apps/projects/create/utils.ts
deleted file mode 100644 (file)
index ed3f0b1..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 { memoize } from 'lodash';
-import {
-  cleanQuery,
-  RawQuery,
-  parseAsBoolean,
-  serializeOptionalBoolean,
-  parseAsOptionalString,
-  serializeString
-} from '../../../helpers/query';
-
-export interface Query {
-  error?: string;
-  manual: boolean;
-  organization?: string;
-}
-
-export const parseQuery = memoize(
-  (urlQuery: RawQuery): Query => {
-    return {
-      error: parseAsOptionalString(urlQuery['error']),
-      manual: parseAsBoolean(urlQuery['manual'], false),
-      organization: parseAsOptionalString(urlQuery['organization'])
-    };
-  }
-);
-
-export const serializeQuery = memoize(
-  (query: Query): RawQuery =>
-    cleanQuery({
-      manual: serializeOptionalBoolean(query.manual || undefined),
-      organization: serializeString(query.organization)
-    })
-);
index 061627c206a8eb379b11bbf7eaea49a181c205dd..5478d3728237416128f4287456561b30621fc195 100644 (file)
@@ -37,7 +37,7 @@ const routes = [
   { path: 'favorite', component: FavoriteProjectsContainer },
   isSonarCloud() && {
     path: 'create',
-    component: lazyLoad(() => import('./create/CreateProjectPage'))
+    component: lazyLoad(() => import('../create/project/CreateProjectPage'))
   }
 ].filter(Boolean);
 
index 7678b54a2c8b4fc2a3606e4be4f897c533133b9f..0e37e914653d1e2e1bf0ad6d25a9c8b4c2fe8fa3 100644 (file)
@@ -21,13 +21,13 @@ import * as React from 'react';
 import * as classNames from 'classnames';
 import './Tabs.css';
 
-interface Props {
-  onChange: (tab: string) => void;
-  selected?: string;
-  tabs: Array<{ disabled?: boolean; key: string; node: React.ReactNode }>;
+interface Props<T extends string> {
+  onChange: (tab: T) => void;
+  selected?: T;
+  tabs: Array<{ disabled?: boolean; key: T; node: React.ReactNode }>;
 }
 
-export default function Tabs({ onChange, selected, tabs }: Props) {
+export default function Tabs<T extends string>({ onChange, selected, tabs }: Props<T>) {
   return (
     <ul className="flex-tabs">
       {tabs.map(tab => (
@@ -44,15 +44,15 @@ export default function Tabs({ onChange, selected, tabs }: Props) {
   );
 }
 
-interface TabProps {
+interface TabProps<T> {
   children: React.ReactNode;
   disabled?: boolean;
-  name: string;
-  onSelect: (tab: string) => void;
+  name: T;
+  onSelect: (tab: T) => void;
   selected: boolean;
 }
 
-export class Tab extends React.PureComponent<TabProps> {
+export class Tab<T> extends React.PureComponent<TabProps<T>> {
   handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
     event.preventDefault();
     event.stopPropagation();
diff --git a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
new file mode 100644 (file)
index 0000000..f95fb29
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 AlertErrorIcon from '../icons-components/AlertErrorIcon';
+import AlertSuccessIcon from '../icons-components/AlertSuccessIcon';
+
+interface Props {
+  description?: string;
+  children: React.ReactNode;
+  error: string | undefined;
+  id: string;
+  isInvalid: boolean;
+  isValid: boolean;
+  label: React.ReactNode;
+  required?: boolean;
+}
+
+export default function ValidationInput(props: Props) {
+  const hasError = props.isInvalid && props.error !== undefined;
+  return (
+    <div>
+      <label htmlFor={props.id}>
+        <strong>{props.label}</strong>
+        {props.required && <em className="mandatory">*</em>}
+      </label>
+      <div className="little-spacer-top spacer-bottom">
+        {props.children}
+        {props.isInvalid && <AlertErrorIcon className="spacer-left text-middle" />}
+        {hasError && (
+          <span className="little-spacer-left text-danger text-middle">{props.error}</span>
+        )}
+        {props.isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
+      </div>
+      {props.description && <div className="note abs-width-400">{props.description}</div>}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx
new file mode 100644 (file)
index 0000000..37b3d8e
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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 ValidationInput from '../ValidationInput';
+
+it('should render', () => {
+  expect(
+    shallow(
+      <ValidationInput
+        description="My description"
+        error={undefined}
+        id="field-id"
+        isInvalid={false}
+        isValid={false}
+        label="Field label"
+        required={true}>
+        <div />
+      </ValidationInput>
+    )
+  ).toMatchSnapshot();
+});
+
+it('should render with error', () => {
+  expect(
+    shallow(
+      <ValidationInput
+        description="My description"
+        error="Field error message"
+        id="field-id"
+        isInvalid={true}
+        isValid={false}
+        label="Field label">
+        <div />
+      </ValidationInput>
+    )
+  ).toMatchSnapshot();
+});
+
+it('should render when valid', () => {
+  expect(
+    shallow(
+      <ValidationInput
+        description="My description"
+        error={undefined}
+        id="field-id"
+        isInvalid={false}
+        isValid={true}
+        label="Field label"
+        required={true}>
+        <div />
+      </ValidationInput>
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..f49d27a
--- /dev/null
@@ -0,0 +1,88 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div>
+  <label
+    htmlFor="field-id"
+  >
+    <strong>
+      Field label
+    </strong>
+    <em
+      className="mandatory"
+    >
+      *
+    </em>
+  </label>
+  <div
+    className="little-spacer-top spacer-bottom"
+  >
+    <div />
+  </div>
+  <div
+    className="note abs-width-400"
+  >
+    My description
+  </div>
+</div>
+`;
+
+exports[`should render when valid 1`] = `
+<div>
+  <label
+    htmlFor="field-id"
+  >
+    <strong>
+      Field label
+    </strong>
+    <em
+      className="mandatory"
+    >
+      *
+    </em>
+  </label>
+  <div
+    className="little-spacer-top spacer-bottom"
+  >
+    <div />
+    <AlertSuccessIcon
+      className="spacer-left text-middle"
+    />
+  </div>
+  <div
+    className="note abs-width-400"
+  >
+    My description
+  </div>
+</div>
+`;
+
+exports[`should render with error 1`] = `
+<div>
+  <label
+    htmlFor="field-id"
+  >
+    <strong>
+      Field label
+    </strong>
+  </label>
+  <div
+    className="little-spacer-top spacer-bottom"
+  >
+    <div />
+    <AlertErrorIcon
+      className="spacer-left text-middle"
+    />
+    <span
+      className="little-spacer-left text-danger text-middle"
+    >
+      Field error message
+    </span>
+  </div>
+  <div
+    className="note abs-width-400"
+  >
+    My description
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx
new file mode 100644 (file)
index 0000000..e75dad4
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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, ShallowWrapper } from 'enzyme';
+import { createStore } from 'redux';
+import { mockRouter } from '../../../helpers/testUtils';
+import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import { whenLoggedIn } from '../whenLoggedIn';
+
+jest.mock('../../../app/utils/handleRequiredAuthentication', () => ({
+  default: jest.fn()
+}));
+
+class X extends React.Component {
+  render() {
+    return <div />;
+  }
+}
+
+const UnderTest = whenLoggedIn(X);
+
+it('should render for logged in user', () => {
+  const store = createStore(state => state, { users: { currentUser: { isLoggedIn: true } } });
+  const wrapper = shallow(<UnderTest />, { context: { store } });
+  expect(getRenderedType(wrapper)).toBe(X);
+});
+
+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 } });
+  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__/withCurrentUser-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withCurrentUser-test.tsx
new file mode 100644 (file)
index 0000000..84c292a
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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 { CurrentUser } from '../../../app/types';
+import { withCurrentUser } from '../withCurrentUser';
+
+class X extends React.Component<{ currentUser: CurrentUser }> {
+  render() {
+    return <div />;
+  }
+}
+
+const UnderTest = withCurrentUser(X);
+
+it('should pass logged in user', () => {
+  const currentUser = { isLoggedIn: false };
+  const store = createStore(state => state, { users: { currentUser } });
+  const wrapper = shallow(<UnderTest />, { context: { store } });
+  expect(wrapper.dive().type()).toBe(X);
+  expect(wrapper.dive().prop('currentUser')).toBe(currentUser);
+});
diff --git a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
new file mode 100644 (file)
index 0000000..00dd040
--- /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 { withRouter, WithRouterProps } from 'react-router';
+import { withCurrentUser } from './withCurrentUser';
+import { CurrentUser } from '../../app/types';
+import { isLoggedIn } from '../../helpers/users';
+import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
+
+export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
+  const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
+  class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> {
+    static displayName = `whenLoggedIn(${wrappedDisplayName})`;
+
+    componentDidMount() {
+      if (!isLoggedIn(this.props.currentUser)) {
+        handleRequiredAuthentication();
+      }
+    }
+
+    render() {
+      if (isLoggedIn(this.props.currentUser)) {
+        return <WrappedComponent {...this.props} />;
+      } else {
+        return null;
+      }
+    }
+  }
+
+  return withCurrentUser(withRouter(Wrapper));
+}
diff --git a/server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx b/server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx
new file mode 100644 (file)
index 0000000..b1933e1
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 { CurrentUser } from '../../app/types';
+import { Store, getCurrentUser } from '../../store/rootReducer';
+
+export function withCurrentUser<P>(
+  WrappedComponent: React.ComponentClass<P & { currentUser: CurrentUser }>
+) {
+  const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
+  class Wrapper extends React.Component<P & { currentUser: CurrentUser }> {
+    static displayName = `withCurrentUser(${wrappedDisplayName})`;
+
+    render() {
+      return <WrappedComponent {...this.props} />;
+    }
+  }
+
+  function mapStateToProps(state: Store) {
+    return { currentUser: getCurrentUser(state) };
+  }
+
+  return connect(mapStateToProps)(Wrapper);
+}
index e098be7449eaa4ee194cae2265422c10732093f6..b798554254fda48a36208a7091b6c65dcb828e2f 100644 (file)
@@ -2718,10 +2718,9 @@ onboarding.project_analysis.suggestions.github=If you are using Travis CI, the S
 
 onboarding.create_project.header=Create project(s)
 onboarding.create_project.already_imported=Repository already imported
-onboarding.create_project.beta_feature_x=This feature is being beta tested. We offer to create projects from your {0} repositories only for public personal projects on your personal SonarCloud organization. For other kind of projects please create them manually.
 onboarding.create_project.create_manually=Create manually
 onboarding.create_project.create_new_org=I want to create another organization
-onboarding.create_project.install_app_x=We need you to install the Sonarcloud {0} application in order to select which repositories you want to analyze.
+onboarding.create_project.import_new_org=I want to import another organization
 onboarding.create_project.install_app_x.button=Install SonarCloud {0} application
 onboarding.create_project.organization=Organization
 onboarding.create_project.project_key=Project key
@@ -2736,6 +2735,8 @@ onboarding.create_organization.organization_name.error=The provided value doesn'
 onboarding.create_organization.organization_name.taken=This name is already taken.
 onboarding.create_organization.add_additional_info=Add additional info
 onboarding.create_organization.hide_additional_info=Hide additional info
+onboarding.create_organization.description=Description
+onboarding.create_organization.description.error=The provided value doesn't match the expected format.
 onboarding.create_organization.display_name=Display Name
 onboarding.create_organization.display_name.description=Up to 255 characters
 onboarding.create_organization.display_name.error=The provided value doesn't match the expected format.