]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11324 Bind remote orgs to existing SonarCloud orgs
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 23 Oct 2018 06:17:14 +0000 (08:17 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 16 Nov 2018 19:21:04 +0000 (20:21 +0100)
* Split personal org binding and public org binding
* Remove BETA flag on tabs

73 files changed:
server/sonar-web/src/main/js/app/styles/sonarcloud.css
server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/OrganizationDescriptionInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/OrganizationNameInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/OrganizationUrlInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationAvatarInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationDescriptionInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationUrlInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx [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/ManualOrganizationCreate.tsx
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap [new file with mode: 0644]
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__/ManualOrganizationCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 748afde887ac7f591298d1148975b40c2c394a2f..3ad81d1225adf485afd787efac746660a026a61c 100644 (file)
   font-size: var(--hugeFontSize);
   font-weight: bold;
 }
-
-.beta-badge {
-  display: inline-block;
-  padding: 2px 4px;
-  border: 1px solid var(--alertBorderInfo);
-  border-radius: 2px;
-  background-color: var(--alertBackgroundInfo);
-  color: var(--alertTextInfo);
-  font-size: 10px;
-}
-
-.beta-badge.is-muted {
-  border-color: var(--disableGrayBorder);
-  background-color: var(--disableGrayBg);
-  color: var(--disableGrayText);
-}
diff --git a/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx
new file mode 100644 (file)
index 0000000..c0bcd44
--- /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/components/OrganizationDescriptionInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationDescriptionInput.tsx
new file mode 100644 (file)
index 0000000..567da28
--- /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/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx
new file mode 100644 (file)
index 0000000..b88d638
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * 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;
+  readOnly?: boolean;
+}
+
+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 });
+      if (!this.props.readOnly) {
+        this.validateKey(this.props.initialValue);
+      }
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  checkFreeKey = (key: string) => {
+    this.setState({ validating: true });
+    return getOrganization(key)
+      .then(organization => {
+        if (this.mounted) {
+          if (organization === undefined) {
+            this.setState({ error: undefined, validating: false });
+            this.props.onChange(key);
+          } else {
+            this.setState({
+              error: translate('onboarding.create_organization.organization_name.taken'),
+              touched: true,
+              validating: false
+            });
+            this.props.onChange(undefined);
+          }
+        }
+      })
+      .catch(() => {
+        if (this.mounted) {
+          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={!this.props.readOnly}>
+        <div className="display-inline-flex-baseline">
+          <span className="little-spacer-right">
+            {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
+            {this.props.readOnly && this.state.value}
+          </span>
+          {!this.props.readOnly && (
+            <input
+              autoFocus={true}
+              className={classNames('input-super-large', {
+                '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/components/OrganizationNameInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationNameInput.tsx
new file mode 100644 (file)
index 0000000..3194bef
--- /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/components/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx
new file mode 100644 (file)
index 0000000..8f7defd
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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 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 {
+  onChange: (organization: Organization) => void;
+  organization: string;
+  organizations: Organization[];
+}
+
+export default function OrganizationSelect({ onChange, organization, organizations }: Props) {
+  return (
+    <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())}
+      placeholder={translate('onboarding.import_organization.choose_organization')}
+      required={true}
+      value={organization}
+      valueKey="key"
+      valueRenderer={optionRenderer}
+    />
+  );
+}
+
+export function optionRenderer(organization: Organization) {
+  const icon = organization.alm
+    ? `sonarcloud/${sanitizeAlmId(organization.alm.key)}`
+    : 'sonarcloud-square-logo';
+  return (
+    <span>
+      <img
+        alt={organization.alm ? organization.alm.key : 'SonarCloud'}
+        className="spacer-right"
+        height={14}
+        src={`${getBaseUrl()}/images/${icon}.svg`}
+      />
+      {organization.name}
+      <span className="note little-spacer-left">{organization.key}</span>
+    </span>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/components/OrganizationUrlInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationUrlInput.tsx
new file mode 100644 (file)
index 0000000..098224f
--- /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/components/__tests__/OrganizationAvatarInput-test.tsx b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/OrganizationDescriptionInput-test.tsx b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/OrganizationKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx
new file mode 100644 (file)
index 0000000..ef1b459
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 render correctly with readonly mode', () => {
+  const wrapper = shallow(
+    <OrganizationKeyInput initialValue="key" onChange={jest.fn()} readOnly={true} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should not display any status when the key is not defined', async () => {
+  const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />);
+  await waitAndUpdate(wrapper);
+  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/components/__tests__/OrganizationNameInput-test.tsx b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx
new file mode 100644 (file)
index 0000000..c78fede
--- /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 OrganizationSelect, { optionRenderer } from '../OrganizationSelect';
+
+const organizations = [
+  { key: 'foo', name: 'Foo' },
+  { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
+];
+
+it('should render correctly', () => {
+  expect(
+    shallow(
+      <OrganizationSelect onChange={jest.fn()} organization="bar" organizations={organizations} />
+    )
+  ).toMatchSnapshot();
+});
+
+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/components/__tests__/OrganizationUrlInput-test.tsx b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..c4441f5
--- /dev/null
@@ -0,0 +1,55 @@
+// 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"
+      id="organization-key"
+      maxLength={255}
+      onBlur={[Function]}
+      onChange={[Function]}
+      onFocus={[Function]}
+      type="text"
+      value="key"
+    />
+  </div>
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
+
+exports[`should render correctly with readonly mode 1`] = `
+<ValidationInput
+  id="organization-key"
+  isInvalid={false}
+  isValid={false}
+  label="onboarding.create_organization.organization_name"
+  required={false}
+>
+  <div
+    className="display-inline-flex-baseline"
+  >
+    <span
+      className="little-spacer-right"
+    >
+      localhost/organizations/
+      key
+    </span>
+  </div>
+</ValidationInput>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/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/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
new file mode 100644 (file)
index 0000000..0208b75
--- /dev/null
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Select
+  autoFocus={true}
+  className="input-super-large"
+  clearable={false}
+  id="select-organization"
+  labelKey="name"
+  onChange={[MockFunction]}
+  optionRenderer={[Function]}
+  options={
+    Array [
+      Object {
+        "alm": Object {
+          "key": "github",
+          "url": "",
+        },
+        "key": "bar",
+        "name": "Bar",
+      },
+      Object {
+        "key": "foo",
+        "name": "Foo",
+      },
+    ]
+  }
+  placeholder="onboarding.import_organization.choose_organization"
+  required={true}
+  value="bar"
+  valueKey="key"
+  valueRenderer={[Function]}
+/>
+`;
+
+exports[`should render options correctly 1`] = `
+<span>
+  <img
+    alt="SonarCloud"
+    className="spacer-right"
+    height={14}
+    src="/images/sonarcloud-square-logo.svg"
+  />
+  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/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/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`;
index 72c6dc0197d0e77aac0ec2ede3552938bc7020d4..86b8d778b5f229cd0854263e7708dd0d5978a6ad 100644 (file)
  */
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
 
 export default function AlmApplicationInstalling({ almKey }: { almKey?: string }) {
   return (
-    <div className="sonarcloud page page-limited">
-      <div className="huge-spacer-top text-center">
-        <i className="spinner" />
-        <p className="big-spacer-top">
-          {almKey
-            ? translate('onboarding.import_organization.installing', almKey)
-            : translate('onboarding.import_organization.installing')}
-        </p>
-      </div>
-    </div>
+    <DeferredSpinner
+      customSpinner={
+        <div className="sonarcloud page page-limited">
+          <div className="huge-spacer-top text-center">
+            <i className="spinner" />
+            <p className="big-spacer-top">
+              {almKey
+                ? translate('onboarding.import_organization.installing', almKey)
+                : translate('onboarding.import_organization.installing')}
+            </p>
+          </div>
+        </div>
+      }
+    />
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx
new file mode 100644 (file)
index 0000000..9511f63
--- /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 { Organization } from '../../../app/types';
+import OrganizationSelect from '../components/OrganizationSelect';
+import { SubmitButton } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  onBindOrganization: (organization: string) => Promise<void>;
+  unboundOrganizations: Organization[];
+}
+
+interface State {
+  organization: string;
+  submitting: boolean;
+}
+
+export default class AutoOrganizationBind extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { organization: this.getInitialSelectedOrganization(props), submitting: false };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  getInitialSelectedOrganization(props: Props) {
+    if (props.unboundOrganizations.length === 1) {
+      return props.unboundOrganizations[0].key;
+    }
+    return '';
+  }
+
+  handleChange = ({ key }: Organization) => {
+    this.setState({ organization: key });
+  };
+
+  handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    const { organization } = this.state;
+    if (organization) {
+      this.setState({ submitting: true });
+      this.props.onBindOrganization(organization).then(this.stopSubmitting, this.stopSubmitting);
+    }
+  };
+
+  stopSubmitting = () => {
+    if (this.mounted) {
+      this.setState({ submitting: false });
+    }
+  };
+
+  render() {
+    const { organization, submitting } = this.state;
+    return (
+      <form id="bind-organization-form" onSubmit={this.handleSubmit}>
+        <OrganizationSelect
+          onChange={this.handleChange}
+          organization={organization}
+          organizations={this.props.unboundOrganizations}
+        />
+        <div className="big-spacer-top">
+          <SubmitButton disabled={submitting || !organization}>
+            {translate('onboarding.import_organization.bind')}
+          </SubmitButton>
+        </div>
+      </form>
+    );
+  }
+}
index 14fc63bdbf8f9c2a095e65c220f2b22649a9c553..fc2b246d42842f3ce11a6fd1dd1b4ec3f5b74933 100644 (file)
@@ -19,7 +19,9 @@
  */
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
+import AutoOrganizationBind from './AutoOrganizationBind';
 import ChooseRemoteOrganizationStep from './ChooseRemoteOrganizationStep';
+import OrganizationDetailsForm from './OrganizationDetailsForm';
 import OrganizationDetailsStep from './OrganizationDetailsStep';
 import {
   AlmApplication,
@@ -27,10 +29,17 @@ import {
   OrganizationBase,
   Organization
 } from '../../../app/types';
+import { bindAlmOrganization } from '../../../api/alm-integration';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
 import { getBaseUrl } from '../../../helpers/urls';
 import { translate } from '../../../helpers/l10n';
-import { sanitizeAlmId } from '../../../helpers/almIntegrations';
-import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+import RadioToggle from '../../../components/controls/RadioToggle';
+
+export enum Filters {
+  Bind = 'bind',
+  Create = 'create',
+  None = 'none'
+}
 
 interface Props {
   almApplication: AlmApplication;
@@ -39,59 +48,68 @@ interface Props {
   createOrganization: (
     organization: OrganizationBase & { installationId?: string }
   ) => Promise<Organization>;
-  importPersonalOrg?: Organization;
-  onOrgCreated: (organization: string) => void;
-  updateOrganization: (
-    organization: OrganizationBase & { installationId?: string }
-  ) => Promise<Organization>;
+  onOrgCreated: (organization: string, justCreated?: boolean) => void;
+  unboundOrganizations: Organization[];
+}
+
+interface State {
+  filter: Filters;
 }
 
-export default class AutoOrganizationCreate extends React.PureComponent<Props> {
+export default class AutoOrganizationCreate extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      filter: props.unboundOrganizations.length === 0 ? Filters.Create : Filters.None
+    };
+  }
+
+  handleOptionChange = (filter: Filters) => {
+    this.setState({ filter });
+  };
+
   handleCreateOrganization = (organization: Required<OrganizationBase>) => {
     if (organization) {
-      const { importPersonalOrg } = this.props;
-      let promise: Promise<Organization>;
-      if (importPersonalOrg) {
-        promise = this.props.updateOrganization({
-          avatar: organization.avatar,
-          description: organization.description,
-          installationId: this.props.almInstallId,
-          key: importPersonalOrg.key,
-          name: organization.name || organization.key,
-          url: organization.url
-        });
-      } else {
-        promise = this.props.createOrganization({
+      return this.props
+        .createOrganization({
           avatar: organization.avatar,
           description: organization.description,
           installationId: this.props.almInstallId,
           key: organization.key,
           name: organization.name || organization.key,
           url: organization.url
-        });
-      }
-      return promise.then(({ key }) => this.props.onOrgCreated(key));
-    } else {
-      return Promise.reject();
+        })
+        .then(({ key }) => this.props.onOrgCreated(key));
+    }
+    return Promise.reject();
+  };
+
+  handleBindOrganization = (organization: string) => {
+    if (this.props.almInstallId) {
+      return bindAlmOrganization({
+        organization,
+        installationId: this.props.almInstallId
+      }).then(() => this.props.onOrgCreated(organization, false));
     }
+    return Promise.reject();
   };
 
   render() {
-    const { almApplication, almInstallId, almOrganization, importPersonalOrg } = this.props;
+    const { almApplication, almInstallId, almOrganization, unboundOrganizations } = this.props;
     if (almInstallId && almOrganization) {
-      const description = importPersonalOrg
-        ? translate('onboarding.import_personal_organization_x')
-        : translate('onboarding.import_organization_x');
-      const submitText = importPersonalOrg
-        ? translate('onboarding.import_organization.bind')
-        : translate('my_account.create_organization');
+      const { filter } = this.state;
+      const hasUnboundOrgs = unboundOrganizations.length > 0;
       return (
         <OrganizationDetailsStep
-          description={
-            <p className="huge-spacer-bottom">
+          finished={false}
+          onOpen={() => {}}
+          open={true}
+          organization={almOrganization}>
+          <div className="huge-spacer-bottom">
+            <p className="big-spacer-bottom">
               <FormattedMessage
-                defaultMessage={description}
-                id={description}
+                defaultMessage={translate('onboarding.import_organization_x')}
+                id="onboarding.import_organization_x"
                 values={{
                   avatar: (
                     <img
@@ -103,25 +121,47 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props> {
                       width={16}
                     />
                   ),
-                  name: <strong>{almOrganization.name}</strong>,
-                  personalAvatar: importPersonalOrg && (
-                    <OrganizationAvatar organization={importPersonalOrg} small={true} />
-                  ),
-                  personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong>
+                  name: <strong>{almOrganization.name}</strong>
                 }}
               />
             </p>
-          }
-          finished={false}
-          keyReadOnly={Boolean(importPersonalOrg)}
-          onContinue={this.handleCreateOrganization}
-          onOpen={() => {}}
-          open={true}
-          organization={importPersonalOrg || almOrganization}
-          submitText={submitText}
-        />
+
+            {hasUnboundOrgs && (
+              <RadioToggle
+                name="filter"
+                onCheck={this.handleOptionChange}
+                options={[
+                  {
+                    label: translate('onboarding.import_organization.create_new'),
+                    value: Filters.Create
+                  },
+                  {
+                    label: translate('onboarding.import_organization.bind_existing'),
+                    value: Filters.Bind
+                  }
+                ]}
+                value={filter}
+              />
+            )}
+          </div>
+
+          {filter === Filters.Create && (
+            <OrganizationDetailsForm
+              onContinue={this.handleCreateOrganization}
+              organization={almOrganization}
+              submitText={translate('onboarding.import_organization.import')}
+            />
+          )}
+          {filter === Filters.Bind && (
+            <AutoOrganizationBind
+              onBindOrganization={this.handleBindOrganization}
+              unboundOrganizations={unboundOrganizations}
+            />
+          )}
+        </OrganizationDetailsStep>
       );
     }
+
     return (
       <ChooseRemoteOrganizationStep
         almApplication={this.props.almApplication}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx
new file mode 100644 (file)
index 0000000..47a65c5
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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 { FormattedMessage } from 'react-intl';
+import OrganizationDetailsForm from './OrganizationDetailsForm';
+import OrganizationDetailsStep from './OrganizationDetailsStep';
+import {
+  AlmApplication,
+  AlmOrganization,
+  OrganizationBase,
+  Organization
+} from '../../../app/types';
+import { getBaseUrl } from '../../../helpers/urls';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+
+interface Props {
+  almApplication: AlmApplication;
+  almInstallId?: string;
+  almOrganization: AlmOrganization;
+  importPersonalOrg: Organization;
+  onOrgCreated: (organization: string) => void;
+  updateOrganization: (
+    organization: OrganizationBase & { installationId?: string }
+  ) => Promise<Organization>;
+}
+
+export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> {
+  handleCreateOrganization = (organization: Required<OrganizationBase>) => {
+    if (organization) {
+      return this.props
+        .updateOrganization({
+          avatar: organization.avatar,
+          description: organization.description,
+          installationId: this.props.almInstallId,
+          key: this.props.importPersonalOrg.key,
+          name: organization.name || organization.key,
+          url: organization.url
+        })
+        .then(({ key }) => this.props.onOrgCreated(key));
+    } else {
+      return Promise.reject();
+    }
+  };
+
+  render() {
+    const { almApplication, importPersonalOrg } = this.props;
+    return (
+      <OrganizationDetailsStep
+        finished={false}
+        onOpen={() => {}}
+        open={true}
+        organization={importPersonalOrg}>
+        <p className="huge-spacer-bottom">
+          <FormattedMessage
+            defaultMessage={translate('onboarding.import_personal_organization_x')}
+            id="onboarding.import_personal_organization_x"
+            values={{
+              avatar: (
+                <img
+                  alt={almApplication.name}
+                  className="little-spacer-left"
+                  src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(almApplication.key)}.svg`}
+                  width={16}
+                />
+              ),
+              name: <strong>{this.props.almOrganization.name}</strong>,
+              personalAvatar: importPersonalOrg && (
+                <OrganizationAvatar organization={importPersonalOrg} small={true} />
+              ),
+              personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong>
+            }}
+          />
+        </p>
+        <OrganizationDetailsForm
+          keyReadOnly={true}
+          onContinue={this.handleCreateOrganization}
+          organization={importPersonalOrg}
+          submitText={translate('onboarding.import_organization.bind')}
+        />
+      </OrganizationDetailsStep>
+    );
+  }
+}
index 963182184427eb58216347122364cef1520c8d28..80e80a5f1932748b3b7d696a9e25dfa042e5c7fc 100644 (file)
@@ -36,10 +36,10 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr
       <div className="boxed-group-inner">
         {almInstallId && (
           <Alert className="markdown big-spacer-bottom width-60" variant="error">
-            {translate('onboarding.create_organization.import_org_not_found')}
+            {translate('onboarding.import_organization.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>
+              <li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li>
+              <li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li>
             </ul>
           </Alert>
         )}
@@ -49,7 +49,7 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr
           small={true}
           url={almApplication.installationUrl}>
           {translate(
-            'onboarding.create_organization.choose_organization_button',
+            'onboarding.import_organization.choose_organization_button',
             almApplication.key
           )}
         </IdentityProviderLink>
@@ -70,7 +70,7 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr
         renderForm={this.renderForm}
         renderResult={this.renderResult}
         stepNumber={1}
-        stepTitle={translate('onboarding.create_organization.import_org_details')}
+        stepTitle={translate('onboarding.import_organization.import_org_details')}
       />
     );
   }
index f3301cf3e6b90fe749af81eaa07bee50cea1eb4c..81b219784cdbff598e0a0d8b6d9f7f2e564df54d 100644 (file)
@@ -18,7 +18,6 @@
  * 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 { Dispatch } from 'redux';
 import { Helmet } from 'react-helmet';
@@ -27,6 +26,7 @@ import { Link, withRouter, WithRouterProps } from 'react-router';
 import { formatPrice, parseQuery } from './utils';
 import AlmApplicationInstalling from './AlmApplicationInstalling';
 import AutoOrganizationCreate from './AutoOrganizationCreate';
+import AutoPersonalOrganizationBind from './AutoPersonalOrganizationBind';
 import ManualOrganizationCreate from './ManualOrganizationCreate';
 import DeferredSpinner from '../../../components/common/DeferredSpinner';
 import Tabs from '../../../components/controls/Tabs';
@@ -141,10 +141,10 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
     });
   };
 
-  handleOrgCreated = (organization: string) => {
+  handleOrgCreated = (organization: string, justCreated = true) => {
     this.props.router.push({
       pathname: getOrganizationUrl(organization),
-      state: { justCreated: true }
+      state: { justCreated }
     });
   };
 
@@ -166,16 +166,78 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
     });
   };
 
-  render() {
+  renderContent = (almInstallId?: string, importPersonalOrg?: Organization) => {
     const { currentUser, location } = this.props;
+    const { almApplication, almOrganization } = this.state;
     const state = (location.state || {}) as LocationState;
+
+    if (importPersonalOrg && almOrganization && almApplication) {
+      return (
+        <AutoPersonalOrganizationBind
+          almApplication={almApplication}
+          almInstallId={almInstallId}
+          almOrganization={almOrganization}
+          importPersonalOrg={importPersonalOrg}
+          onOrgCreated={this.handleOrgCreated}
+          updateOrganization={this.props.updateOrganization}
+        />
+      );
+    }
+
+    const showManualTab = state.tab === 'manual' && !almOrganization;
+    return (
+      <>
+        {almApplication && (
+          <Tabs<TabKeys>
+            onChange={this.onTabChange}
+            selected={showManualTab ? 'manual' : 'auto'}
+            tabs={[
+              {
+                key: 'auto',
+                node: translate('onboarding.import_organization', almApplication.key)
+              },
+              {
+                disabled: Boolean(almOrganization),
+                key: 'manual',
+                node: translate('onboarding.create_organization.create_manually')
+              }
+            ]}
+          />
+        )}
+
+        {showManualTab || !almApplication ? (
+          <ManualOrganizationCreate
+            createOrganization={this.props.createOrganization}
+            deleteOrganization={this.props.deleteOrganization}
+            onOrgCreated={this.handleOrgCreated}
+            onlyPaid={state.paid}
+            subscriptionPlans={this.state.subscriptionPlans}
+          />
+        ) : (
+          <AutoOrganizationCreate
+            almApplication={almApplication}
+            almInstallId={almInstallId}
+            almOrganization={almOrganization}
+            createOrganization={this.props.createOrganization}
+            onOrgCreated={this.handleOrgCreated}
+            unboundOrganizations={this.props.userOrganizations.filter(
+              o => !o.alm && o.key !== currentUser.personalOrganization
+            )}
+          />
+        )}
+      </>
+    );
+  };
+
+  render() {
+    const { currentUser, location } = this.props;
     const query = parseQuery(location.query);
 
     if (this.state.almOrgLoading) {
       return <AlmApplicationInstalling almKey={query.almKey} />;
     }
 
-    const { almApplication, almOrganization, subscriptionPlans } = this.state;
+    const { almOrganization, subscriptionPlans } = this.state;
     const importPersonalOrg = isPersonal(almOrganization)
       ? this.props.userOrganizations.find(o => o.key === currentUser.personalOrganization)
       : undefined;
@@ -187,7 +249,6 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
       : translate('onboarding.create_organization.page.description');
     const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
     const formattedPrice = formatPrice(startedPrice);
-    const showManualTab = state.tab === 'manual' && !almOrganization;
 
     return (
       <>
@@ -216,56 +277,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
           {this.state.loading ? (
             <DeferredSpinner />
           ) : (
-            <>
-              {almApplication &&
-                !importPersonalOrg && (
-                  <Tabs<TabKeys>
-                    onChange={this.onTabChange}
-                    selected={showManualTab ? 'manual' : 'auto'}
-                    tabs={[
-                      {
-                        key: 'auto',
-                        node: (
-                          <>
-                            {translate('onboarding.import_organization', almApplication.key)}
-                            <span
-                              className={classNames('beta-badge spacer-left', {
-                                'is-muted': showManualTab
-                              })}>
-                              {translate('beta')}
-                            </span>
-                          </>
-                        )
-                      },
-                      {
-                        disabled: Boolean(almOrganization),
-                        key: 'manual',
-                        node: translate('onboarding.create_organization.create_manually')
-                      }
-                    ]}
-                  />
-                )}
-
-              {showManualTab || !almApplication ? (
-                <ManualOrganizationCreate
-                  createOrganization={this.props.createOrganization}
-                  deleteOrganization={this.props.deleteOrganization}
-                  onOrgCreated={this.handleOrgCreated}
-                  onlyPaid={state.paid}
-                  subscriptionPlans={this.state.subscriptionPlans}
-                />
-              ) : (
-                <AutoOrganizationCreate
-                  almApplication={almApplication}
-                  almInstallId={query.almInstallId}
-                  almOrganization={almOrganization}
-                  createOrganization={this.props.createOrganization}
-                  importPersonalOrg={importPersonalOrg}
-                  onOrgCreated={this.handleOrgCreated}
-                  updateOrganization={this.props.updateOrganization}
-                />
-              )}
-            </>
+            this.renderContent(query.almInstallId, importPersonalOrg)
           )}
         </div>
       </>
index 6bf245e90882c4c345c12a1c1be18c6f57544531..cda7e48af9ee773fc302401bc904f6fa7ae73bfd 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import OrganizationDetailsForm from './OrganizationDetailsForm';
 import OrganizationDetailsStep from './OrganizationDetailsStep';
 import PlanStep from './PlanStep';
 import { formatPrice } from './utils';
@@ -108,12 +109,15 @@ export default class ManualOrganizationCreate extends React.PureComponent<Props,
       <>
         <OrganizationDetailsStep
           finished={this.state.organization !== undefined}
-          onContinue={this.handleOrganizationDetailsFinish}
           onOpen={this.handleOrganizationDetailsStepOpen}
           open={this.state.step === Step.OrganizationDetails}
-          organization={this.state.organization}
-          submitText={translate('continue')}
-        />
+          organization={this.state.organization}>
+          <OrganizationDetailsForm
+            onContinue={this.handleOrganizationDetailsFinish}
+            organization={this.state.organization}
+            submitText={translate('continue')}
+          />
+        </OrganizationDetailsStep>
 
         {subscriptionPlans !== undefined && (
           <PlanStep
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
new file mode 100644 (file)
index 0000000..f6b4b5a
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * 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 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 DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import { OrganizationBase } from '../../../app/types';
+import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+
+type RequiredOrganization = Required<OrganizationBase>;
+
+interface Props {
+  keyReadOnly?: boolean;
+  onContinue: (organization: RequiredOrganization) => Promise<void>;
+  organization?: OrganizationBase & { key: string };
+  submitText: string;
+}
+
+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 OrganizationDetailsForm extends React.PureComponent<Props, State> {
+  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) || ''
+    };
+  }
+
+  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 }));
+  };
+
+  handleKeyUpdate = (key: string | undefined) => {
+    this.setState({ key });
+  };
+
+  handleNameUpdate = (name: string | undefined) => {
+    this.setState({ name });
+  };
+
+  handleDescriptionUpdate = (description: string | undefined) => {
+    this.setState({ description });
+  };
+
+  handleAvatarUpdate = (avatar: string | undefined) => {
+    this.setState({ avatar });
+  };
+
+  handleUrlUpdate = (url: string | undefined) => {
+    this.setState({ url });
+  };
+
+  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);
+    }
+  };
+
+  stopSubmitting = () => {
+    if (this.mounted) {
+      this.setState({ submitting: false });
+    }
+  };
+
+  render() {
+    return (
+      <form id="organization-form" onSubmit={this.handleSubmit}>
+        <OrganizationKeyInput
+          initialValue={this.state.key}
+          onChange={this.handleKeyUpdate}
+          readOnly={this.props.keyReadOnly}
+        />
+        <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">
+            <OrganizationNameInput
+              initialValue={this.state.name}
+              onChange={this.handleNameUpdate}
+            />
+          </div>
+          <div className="big-spacer-top">
+            <OrganizationAvatarInput
+              initialValue={this.state.avatar}
+              name={this.state.name}
+              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">
+          <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}>
+            {this.props.submitText}
+          </SubmitButton>
+        </div>
+      </form>
+    );
+  }
+}
index d6e5f0696a160dd88e03db640a6253b97e249526..b10abe7132311a0d0a0f8cd5a04e88f5c49bd426 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-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 { 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 { OrganizationBase } from '../../../app/types';
 
-type RequiredOrganization = Required<OrganizationBase>;
-
 interface Props {
-  description?: React.ReactNode;
+  children: React.ReactNode;
   finished: boolean;
-  keyReadOnly?: boolean;
-  onContinue: (organization: RequiredOrganization) => Promise<void>;
   onOpen: () => void;
   open: boolean;
   organization?: OrganizationBase & { key: string };
-  submitText: string;
-}
-
-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> {
-  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) || ''
-    };
-  }
-
-  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 }));
-  };
-
-  handleKeyUpdate = (key: string | undefined) => {
-    this.setState({ key });
-  };
-
-  handleNameUpdate = (name: string | undefined) => {
-    this.setState({ name });
-  };
-
-  handleDescriptionUpdate = (description: string | undefined) => {
-    this.setState({ description });
-  };
-
-  handleAvatarUpdate = (avatar: string | undefined) => {
-    this.setState({ avatar });
-  };
-
-  handleUrlUpdate = (url: string | undefined) => {
-    this.setState({ url });
-  };
-
-  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);
-    }
-  };
-
-  stopSubmitting = () => {
-    if (this.mounted) {
-      this.setState({ submitting: false });
-    }
-  };
-
+export default class OrganizationDetailsStep extends React.PureComponent<Props> {
   renderForm = () => {
-    return (
-      <div className="boxed-group-inner">
-        <form id="organization-form" onSubmit={this.handleSubmit}>
-          {this.props.description}
-          <OrganizationKeyInput
-            initialValue={this.state.key}
-            onChange={this.handleKeyUpdate}
-            readOnly={this.props.keyReadOnly}
-          />
-          <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">
-              <OrganizationNameInput
-                initialValue={this.state.name}
-                onChange={this.handleNameUpdate}
-              />
-            </div>
-            <div className="big-spacer-top">
-              <OrganizationAvatarInput
-                initialValue={this.state.avatar}
-                name={this.state.name}
-                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">
-            <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}>
-              {this.props.submitText}
-            </SubmitButton>
-          </div>
-        </form>
-      </div>
-    );
+    return <div className="boxed-group-inner">{this.props.children}</div>;
   };
 
   renderResult = () => {
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx
new file mode 100644 (file)
index 0000000..fc88575
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 AutoOrganizationBind from '../AutoOrganizationBind';
+import { submit } from '../../../../helpers/testUtils';
+
+const organization = {
+  avatar: 'http://example.com/avatar',
+  description: 'description-foo',
+  key: 'key-foo',
+  name: 'name-foo',
+  url: 'http://example.com/foo'
+};
+
+it('should render correctly', () => {
+  const onBindOrganization = jest.fn().mockResolvedValue({});
+  const wrapper = shallowRender({ onBindOrganization });
+  expect(wrapper).toMatchSnapshot();
+
+  submit(wrapper.find('form'));
+  expect(onBindOrganization).toHaveBeenCalled();
+});
+
+function shallowRender(props: Partial<AutoOrganizationBind['props']> = {}) {
+  return shallow(
+    <AutoOrganizationBind
+      onBindOrganization={jest.fn()}
+      unboundOrganizations={[organization]}
+      {...props}
+    />
+  );
+}
index 17e57b70a94668a194d5f188a914ec750f12d7e5..4d6539e500cfeacbcad4b28fd23dda6a07bdfb90 100644 (file)
@@ -21,6 +21,11 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import AutoOrganizationCreate from '../AutoOrganizationCreate';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { bindAlmOrganization } from '../../../../api/alm-integration';
+
+jest.mock('../../../../api/alm-integration', () => ({
+  bindAlmOrganization: jest.fn().mockResolvedValue({})
+}));
 
 const organization = {
   avatar: 'http://example.com/avatar',
@@ -39,42 +44,55 @@ it('should render prefilled and create org', async () => {
   const onOrgCreated = jest.fn();
   const wrapper = shallowRender({
     almInstallId: 'id-foo',
-    almOrganization: {
-      ...organization,
-      type: 'ORGANIZATION'
-    },
+    almOrganization: { ...organization, type: 'ORGANIZATION' },
     createOrganization,
     onOrgCreated
   });
 
   expect(wrapper).toMatchSnapshot();
 
-  wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+  wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization);
   await waitAndUpdate(wrapper);
 
   expect(createOrganization).toBeCalledWith({ ...organization, installationId: 'id-foo' });
   expect(onOrgCreated).toBeCalledWith('foo');
 });
 
-it('should render for personal organizations', async () => {
-  const personalOrg = { key: 'personal-org', name: 'personal-org' };
-  const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key });
+it('should display choice between import or creation', () => {
+  const wrapper = shallowRender({
+    almInstallId: 'id-foo',
+    almOrganization: { ...organization, type: 'ORGANIZATION' },
+    unboundOrganizations: [organization]
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('RadioToggle').prop<Function>('onCheck')('create');
+  wrapper.update();
+  expect(wrapper.find('OrganizationDetailsForm').exists()).toBe(true);
+
+  wrapper.find('RadioToggle').prop<Function>('onCheck')('bind');
+  wrapper.update();
+  expect(wrapper.find('AutoOrganizationBind').exists()).toBe(true);
+});
+
+it('should bind existing organization', async () => {
   const onOrgCreated = jest.fn();
   const wrapper = shallowRender({
     almInstallId: 'id-foo',
-    almOrganization: { ...organization, type: 'USER' },
-    importPersonalOrg: personalOrg,
+    almOrganization: { ...organization, type: 'ORGANIZATION' },
     onOrgCreated,
-    updateOrganization
+    unboundOrganizations: [organization]
   });
 
-  expect(wrapper).toMatchSnapshot();
-
-  wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(personalOrg);
+  wrapper.find('RadioToggle').prop<Function>('onCheck')('bind');
+  wrapper.update();
+  wrapper.find('AutoOrganizationBind').prop<Function>('onBindOrganization')('foo');
+  expect(bindAlmOrganization as jest.Mock<any>).toHaveBeenCalledWith({
+    installationId: 'id-foo',
+    organization: 'foo'
+  });
   await waitAndUpdate(wrapper);
-
-  expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' });
-  expect(onOrgCreated).toBeCalledWith(personalOrg.key);
+  expect(onOrgCreated).toHaveBeenCalledWith('foo', false);
 });
 
 function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) {
@@ -89,7 +107,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) {
       }}
       createOrganization={jest.fn()}
       onOrgCreated={jest.fn()}
-      updateOrganization={jest.fn()}
+      unboundOrganizations={[]}
       {...props}
     />
   );
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx
new file mode 100644 (file)
index 0000000..d735122
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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 AutoPersonalOrganizationBind from '../AutoPersonalOrganizationBind';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+it('should render correctly', async () => {
+  const personalOrg = { key: 'personalorg', name: 'Personal Org' };
+  const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key });
+  const onOrgCreated = jest.fn();
+  const wrapper = shallowRender({
+    almInstallId: 'id-foo',
+    importPersonalOrg: personalOrg,
+    onOrgCreated,
+    updateOrganization
+  });
+
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(personalOrg);
+  await waitAndUpdate(wrapper);
+
+  expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' });
+  expect(onOrgCreated).toBeCalledWith(personalOrg.key);
+});
+
+function shallowRender(props: Partial<AutoPersonalOrganizationBind['props']> = {}) {
+  return shallow(
+    <AutoPersonalOrganizationBind
+      almApplication={{
+        backgroundColor: '#0052CC',
+        iconPath: '"/static/authbitbucket/bitbucket.svg"',
+        installationUrl: 'https://bitbucket.org/install/app',
+        key: 'bitbucket',
+        name: 'BitBucket'
+      }}
+      almOrganization={{
+        avatar: 'http://example.com/avatar',
+        description: 'description-foo',
+        key: 'key-foo',
+        name: 'name-foo',
+        type: 'USER',
+        url: 'http://example.com/foo'
+      }}
+      importPersonalOrg={{ key: 'personalorg', name: 'Personal Org' }}
+      onOrgCreated={jest.fn()}
+      updateOrganization={jest.fn()}
+      {...props}
+    />
+  );
+}
index 7db9ebf2636ffb6d0464956794951a4a6be280e6..8aa237fc1d0e3dbec3756920cb24aac2a7f0dfbb 100644 (file)
@@ -23,6 +23,7 @@ import { shallow } from 'enzyme';
 import { CreateOrganization } from '../CreateOrganization';
 import { mockRouter, waitAndUpdate } from '../../../../helpers/testUtils';
 import { LoggedInUser } from '../../../../app/types';
+import { getAlmOrganization } from '../../../../api/alm-integration';
 
 jest.mock('../../../../api/billing', () => ({
   getSubscriptionPlans: jest
@@ -89,6 +90,22 @@ it('should render with auto tab selected and manual disabled', async () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should render with auto personal organization bind page', async () => {
+  (getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
+    key: 'foo',
+    name: 'Foo',
+    avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4',
+    type: 'USER'
+  });
+  const wrapper = shallowRender({
+    currentUser: { ...user, externalProvider: 'github', personalOrganization: 'foo' },
+    location: { query: { installation_id: 'foo' } } as Location // eslint-disable-line camelcase
+  });
+  expect(wrapper).toMatchSnapshot();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
 it('should switch tabs', async () => {
   const replace = jest.fn();
   const wrapper = shallowRender({
index 6226f8173b7135965aa3c7ae02d84b283b939a71..1682a14601378395e391b8628c816cc5408ff8d4 100644 (file)
@@ -38,7 +38,7 @@ it('should render and create organization', async () => {
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 
-  wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+  wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization);
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 
@@ -52,7 +52,7 @@ it('should preselect paid plan', async () => {
   const wrapper = shallowRender({ onlyPaid: true });
 
   await waitAndUpdate(wrapper);
-  wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+  wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization);
   await waitAndUpdate(wrapper);
   expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true);
 });
@@ -63,7 +63,7 @@ it('should roll back after upgrade failure', async () => {
   const wrapper = shallowRender({ createOrganization, deleteOrganization });
   await waitAndUpdate(wrapper);
 
-  wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+  wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization);
   await waitAndUpdate(wrapper);
 
   wrapper.find('PlanStep').prop<Function>('createOrganization')();
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsForm-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsForm-test.tsx
new file mode 100644 (file)
index 0000000..bc7845d
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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 OrganizationDetailsForm from '../OrganizationDetailsForm';
+import { click, submit } from '../../../../helpers/testUtils';
+
+it('should render form', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('.js-additional-info').prop('hidden')).toBe(true);
+
+  click(wrapper.find('ResetButtonLink'));
+  wrapper.update();
+  expect(wrapper.find('.js-additional-info').prop('hidden')).toBe(false);
+});
+
+it('should validate before submit', () => {
+  const wrapper = shallowRender();
+  const instance = wrapper.instance() as OrganizationDetailsForm;
+
+  expect(
+    instance.canSubmit({
+      additional: false,
+      avatar: '',
+      description: '',
+      name: '',
+      key: 'foo',
+      submitting: false,
+      url: ''
+    })
+  ).toBe(true);
+
+  expect(
+    instance.canSubmit({
+      additional: false,
+      avatar: '',
+      description: '',
+      name: '',
+      key: undefined,
+      submitting: false,
+      url: ''
+    })
+  ).toBe(false);
+
+  expect(
+    instance.canSubmit({
+      additional: false,
+      avatar: undefined,
+      description: '',
+      name: '',
+      key: 'foo',
+      submitting: false,
+      url: ''
+    })
+  ).toBe(false);
+
+  instance.canSubmit = jest.fn() as any;
+  submit(wrapper.find('form'));
+  expect(instance.canSubmit).toHaveBeenCalled();
+});
+
+function shallowRender(props: Partial<OrganizationDetailsForm['props']> = {}) {
+  return shallow(
+    <OrganizationDetailsForm onContinue={jest.fn()} submitText="continue" {...props} />
+  );
+}
index ac756ffdc8bba839a6eadb30a6f8b4daf4361062..e905b769872c37184583243c65a3428b955d695a 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import OrganizationDetailsStep from '../OrganizationDetailsStep';
-import { click, submit } from '../../../../helpers/testUtils';
-import { getOrganization } from '../../../../api/organizations';
-
-jest.mock('../../../../api/organizations', () => ({
-  getOrganization: jest.fn()
-}));
-
-beforeEach(() => {
-  (getOrganization as jest.Mock).mockResolvedValue(undefined);
-});
 
 it('should render form', () => {
   const wrapper = shallow(
-    <OrganizationDetailsStep
-      finished={false}
-      onContinue={jest.fn()}
-      onOpen={jest.fn()}
-      open={true}
-      submitText="continue"
-    />
+    <OrganizationDetailsStep finished={false} onOpen={jest.fn()} open={true}>
+      <form />
+    </OrganizationDetailsStep>
   );
   expect(wrapper).toMatchSnapshot();
-  expect(wrapper.dive()).toMatchSnapshot();
-  expect(
-    wrapper
-      .dive()
-      .find('.js-additional-info')
-      .prop('hidden')
-  ).toBe(true);
-
-  click(wrapper.dive().find('ResetButtonLink'));
-  wrapper.update();
   expect(
     wrapper
       .dive()
-      .find('.js-additional-info')
-      .prop('hidden')
-  ).toBe(false);
-});
-
-it('should validate before submit', () => {
-  const wrapper = shallow(
-    <OrganizationDetailsStep
-      finished={false}
-      onContinue={jest.fn()}
-      onOpen={jest.fn()}
-      open={true}
-      submitText="continue"
-    />
-  );
-  const instance = wrapper.instance() as OrganizationDetailsStep;
-
-  expect(
-    instance.canSubmit({
-      additional: false,
-      avatar: '',
-      description: '',
-      name: '',
-      key: 'foo',
-      submitting: false,
-      url: ''
-    })
+      .find('form')
+      .exists()
   ).toBe(true);
-
-  expect(
-    instance.canSubmit({
-      additional: false,
-      avatar: '',
-      description: '',
-      name: '',
-      key: undefined,
-      submitting: false,
-      url: ''
-    })
-  ).toBe(false);
-
-  expect(
-    instance.canSubmit({
-      additional: false,
-      avatar: undefined,
-      description: '',
-      name: '',
-      key: 'foo',
-      submitting: false,
-      url: ''
-    })
-  ).toBe(false);
-
-  instance.canSubmit = jest.fn() as any;
-  submit(wrapper.dive().find('form'));
-  expect(instance.canSubmit).toHaveBeenCalled();
 });
 
-it.only('should render result', () => {
+it('should render result', () => {
   const wrapper = shallow(
     <OrganizationDetailsStep
       finished={true}
-      onContinue={jest.fn()}
       onOpen={jest.fn()}
       open={false}
-      organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }}
-      submitText="continue"
-    />
+      organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }}>
+      <div />
+    </OrganizationDetailsStep>
   );
   expect(wrapper.dive().find('.boxed-group-actions')).toMatchSnapshot();
   expect(
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap
new file mode 100644 (file)
index 0000000..612312e
--- /dev/null
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<form
+  id="bind-organization-form"
+  onSubmit={[Function]}
+>
+  <OrganizationSelect
+    onChange={[Function]}
+    organization="key-foo"
+    organizations={
+      Array [
+        Object {
+          "avatar": "http://example.com/avatar",
+          "description": "description-foo",
+          "key": "key-foo",
+          "name": "name-foo",
+          "url": "http://example.com/foo",
+        },
+      ]
+    }
+  />
+  <div
+    className="big-spacer-top"
+  >
+    <SubmitButton
+      disabled={false}
+    >
+      onboarding.import_organization.bind
+    </SubmitButton>
+  </div>
+</form>
+`;
index 423b5f2181bbc152a14a56aa5302962d8cfad3cf..f11e164d3a1bd2da5181b3141bead5881df4c9dc 100644 (file)
@@ -1,14 +1,30 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render for personal organizations 1`] = `
+exports[`should display choice between import or creation 1`] = `
 <OrganizationDetailsStep
-  description={
+  finished={false}
+  onOpen={[Function]}
+  open={true}
+  organization={
+    Object {
+      "avatar": "http://example.com/avatar",
+      "description": "description-foo",
+      "key": "key-foo",
+      "name": "name-foo",
+      "type": "ORGANIZATION",
+      "url": "http://example.com/foo",
+    }
+  }
+>
+  <div
+    className="huge-spacer-bottom"
+  >
     <p
-      className="huge-spacer-bottom"
+      className="big-spacer-bottom"
     >
       <FormattedMessage
-        defaultMessage="onboarding.import_personal_organization_x"
-        id="onboarding.import_personal_organization_x"
+        defaultMessage="onboarding.import_organization_x"
+        id="onboarding.import_organization_x"
         values={
           Object {
             "avatar": <img
@@ -20,43 +36,53 @@ exports[`should render for personal organizations 1`] = `
             "name": <strong>
               name-foo
             </strong>,
-            "personalAvatar": <OrganizationAvatar
-              organization={
-                Object {
-                  "key": "personal-org",
-                  "name": "personal-org",
-                }
-              }
-              small={true}
-            />,
-            "personalName": <strong>
-              personal-org
-            </strong>,
           }
         }
       />
     </p>
-  }
+    <RadioToggle
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.import_organization.create_new",
+            "value": "create",
+          },
+          Object {
+            "label": "onboarding.import_organization.bind_existing",
+            "value": "bind",
+          },
+        ]
+      }
+      value="none"
+    />
+  </div>
+</OrganizationDetailsStep>
+`;
+
+exports[`should render prefilled and create org 1`] = `
+<OrganizationDetailsStep
   finished={false}
-  keyReadOnly={true}
-  onContinue={[Function]}
   onOpen={[Function]}
   open={true}
   organization={
     Object {
-      "key": "personal-org",
-      "name": "personal-org",
+      "avatar": "http://example.com/avatar",
+      "description": "description-foo",
+      "key": "key-foo",
+      "name": "name-foo",
+      "type": "ORGANIZATION",
+      "url": "http://example.com/foo",
     }
   }
-  submitText="onboarding.import_organization.bind"
-/>
-`;
-
-exports[`should render prefilled and create org 1`] = `
-<OrganizationDetailsStep
-  description={
+>
+  <div
+    className="huge-spacer-bottom"
+  >
     <p
-      className="huge-spacer-bottom"
+      className="big-spacer-bottom"
     >
       <FormattedMessage
         defaultMessage="onboarding.import_organization_x"
@@ -72,30 +98,26 @@ exports[`should render prefilled and create org 1`] = `
             "name": <strong>
               name-foo
             </strong>,
-            "personalAvatar": undefined,
-            "personalName": undefined,
           }
         }
       />
     </p>
-  }
-  finished={false}
-  keyReadOnly={false}
-  onContinue={[Function]}
-  onOpen={[Function]}
-  open={true}
-  organization={
-    Object {
-      "avatar": "http://example.com/avatar",
-      "description": "description-foo",
-      "key": "key-foo",
-      "name": "name-foo",
-      "type": "ORGANIZATION",
-      "url": "http://example.com/foo",
+  </div>
+  <OrganizationDetailsForm
+    onContinue={[Function]}
+    organization={
+      Object {
+        "avatar": "http://example.com/avatar",
+        "description": "description-foo",
+        "key": "key-foo",
+        "name": "name-foo",
+        "type": "ORGANIZATION",
+        "url": "http://example.com/foo",
+      }
     }
-  }
-  submitText="my_account.create_organization"
-/>
+    submitText="onboarding.import_organization.import"
+  />
+</OrganizationDetailsStep>
 `;
 
 exports[`should render with import org button 1`] = `
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
new file mode 100644 (file)
index 0000000..f29f5f9
--- /dev/null
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<OrganizationDetailsStep
+  finished={false}
+  onOpen={[Function]}
+  open={true}
+  organization={
+    Object {
+      "key": "personalorg",
+      "name": "Personal Org",
+    }
+  }
+>
+  <p
+    className="huge-spacer-bottom"
+  >
+    <FormattedMessage
+      defaultMessage="onboarding.import_personal_organization_x"
+      id="onboarding.import_personal_organization_x"
+      values={
+        Object {
+          "avatar": <img
+            alt="BitBucket"
+            className="little-spacer-left"
+            src="/images/sonarcloud/bitbucket.svg"
+            width={16}
+          />,
+          "name": <strong>
+            name-foo
+          </strong>,
+          "personalAvatar": <OrganizationAvatar
+            organization={
+              Object {
+                "key": "personalorg",
+                "name": "Personal Org",
+              }
+            }
+            small={true}
+          />,
+          "personalName": <strong>
+            Personal Org
+          </strong>,
+        }
+      }
+    />
+  </p>
+  <OrganizationDetailsForm
+    keyReadOnly={true}
+    onContinue={[Function]}
+    organization={
+      Object {
+        "key": "personalorg",
+        "name": "Personal Org",
+      }
+    }
+    submitText="onboarding.import_organization.bind"
+  />
+</OrganizationDetailsStep>
+`;
index 227a1be6a191c10a6d1e23119d2188d7a773ec4b..6a1c633b1a2e01bd5f27f0c29ae7d1c66dc922ca 100644 (file)
@@ -5,13 +5,13 @@ exports[`should display an alert message 1`] = `
   className="markdown big-spacer-bottom width-60"
   variant="error"
 >
-  onboarding.create_organization.import_org_not_found
+  onboarding.import_organization.org_not_found
   <ul>
     <li>
-      onboarding.create_organization.import_org_not_found.tips_1
+      onboarding.import_organization.org_not_found.tips_1
     </li>
     <li>
-      onboarding.create_organization.import_org_not_found.tips_2
+      onboarding.import_organization.org_not_found.tips_2
     </li>
   </ul>
 </Alert>
@@ -30,7 +30,7 @@ exports[`should render 1`] = `
     className="boxed-group-header"
   >
     <h2>
-      onboarding.create_organization.import_org_details
+      onboarding.import_organization.import_org_details
     </h2>
   </div>
   <div
@@ -53,7 +53,7 @@ exports[`should render 1`] = `
         small={true}
         url="https://alm.application.url"
       >
-        onboarding.create_organization.choose_organization_button.github
+        onboarding.import_organization.choose_organization_button.github
       </IdentityProviderLink>
     </div>
   </div>
index 9d3dcf6dbc8027e4638f8ea26f3cef773b390ca2..d43925de3dca88b230f631f649713f95aea29925 100644 (file)
@@ -1,5 +1,84 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`should render with auto personal organization bind page 1`] = `
+<AlmApplicationInstalling
+  almKey="github"
+/>
+`;
+
+exports[`should render with auto personal organization bind page 2`] = `
+<Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.import_organization.personal.page.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title big-spacer-bottom"
+      >
+        onboarding.import_organization.personal.page.header
+      </h1>
+      <p
+        className="page-description"
+      >
+        <FormattedMessage
+          defaultMessage="onboarding.import_organization.personal.page.description"
+          id="onboarding.import_organization.personal.page.description"
+          values={
+            Object {
+              "break": <br />,
+              "more": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                target="_blank"
+                to="/documentation/sonarcloud-pricing/"
+              >
+                learn_more
+              </Link>,
+              "price": "billing.price_format.10",
+            }
+          }
+        />
+      </p>
+    </header>
+    <AutoPersonalOrganizationBind
+      almApplication={
+        Object {
+          "backgroundColor": "blue",
+          "iconPath": "icon/path",
+          "installationUrl": "https://alm.installation.url",
+          "key": "github",
+          "name": "GitHub",
+        }
+      }
+      almInstallId="foo"
+      almOrganization={
+        Object {
+          "avatar": "https://avatars3.githubusercontent.com/u/37629810?v=4",
+          "key": "foo",
+          "name": "Foo",
+          "type": "USER",
+        }
+      }
+      importPersonalOrg={
+        Object {
+          "key": "foo",
+          "name": "Foo",
+        }
+      }
+      onOrgCreated={[Function]}
+    />
+  </div>
+</Fragment>
+`;
+
 exports[`should render with auto tab displayed 1`] = `
 <Fragment>
   <HelmetWrapper
@@ -49,14 +128,7 @@ exports[`should render with auto tab displayed 1`] = `
         Array [
           Object {
             "key": "auto",
-            "node": <React.Fragment>
-              onboarding.import_organization.github
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
+            "node": "onboarding.import_organization.github",
           },
           Object {
             "disabled": false,
@@ -77,6 +149,14 @@ exports[`should render with auto tab displayed 1`] = `
         }
       }
       onOrgCreated={[Function]}
+      unboundOrganizations={
+        Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          },
+        ]
+      }
     />
   </div>
 </Fragment>
@@ -137,14 +217,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
         Array [
           Object {
             "key": "auto",
-            "node": <React.Fragment>
-              onboarding.import_organization.github
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
+            "node": "onboarding.import_organization.github",
           },
           Object {
             "disabled": true,
@@ -176,6 +249,14 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
         }
       }
       onOrgCreated={[Function]}
+      unboundOrganizations={
+        Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          },
+        ]
+      }
     />
   </div>
 </Fragment>
@@ -291,14 +372,7 @@ exports[`should switch tabs 1`] = `
         Array [
           Object {
             "key": "auto",
-            "node": <React.Fragment>
-              onboarding.import_organization.github
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
+            "node": "onboarding.import_organization.github",
           },
           Object {
             "disabled": false,
@@ -319,6 +393,14 @@ exports[`should switch tabs 1`] = `
         }
       }
       onOrgCreated={[Function]}
+      unboundOrganizations={
+        Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          },
+        ]
+      }
     />
   </div>
 </Fragment>
index be548a156d6cd8f9563589dcae9bf2dfe3e74a23..6d36c3862b3d327db9c5e34e0a4506f32fa769b5 100644 (file)
@@ -4,11 +4,14 @@ exports[`should render and create organization 1`] = `
 <Fragment>
   <OrganizationDetailsStep
     finished={false}
-    onContinue={[Function]}
     onOpen={[Function]}
     open={true}
-    submitText="continue"
-  />
+  >
+    <OrganizationDetailsForm
+      onContinue={[Function]}
+      submitText="continue"
+    />
+  </OrganizationDetailsStep>
   <PlanStep
     createOrganization={[Function]}
     deleteOrganization={[Function]}
@@ -36,7 +39,6 @@ exports[`should render and create organization 2`] = `
 <Fragment>
   <OrganizationDetailsStep
     finished={true}
-    onContinue={[Function]}
     onOpen={[Function]}
     open={false}
     organization={
@@ -48,8 +50,21 @@ exports[`should render and create organization 2`] = `
         "url": "http://example.com/foo",
       }
     }
-    submitText="continue"
-  />
+  >
+    <OrganizationDetailsForm
+      onContinue={[Function]}
+      organization={
+        Object {
+          "avatar": "http://example.com/avatar",
+          "description": "description-foo",
+          "key": "key-foo",
+          "name": "name-foo",
+          "url": "http://example.com/foo",
+        }
+      }
+      submitText="continue"
+    />
+  </OrganizationDetailsStep>
   <PlanStep
     createOrganization={[Function]}
     deleteOrganization={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..3acf5cb
--- /dev/null
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render form 1`] = `
+<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
+        initialValue=""
+        onChange={[Function]}
+      />
+    </div>
+    <div
+      className="big-spacer-top"
+    >
+      <OrganizationAvatarInput
+        initialValue=""
+        name=""
+        onChange={[Function]}
+      />
+    </div>
+    <div
+      className="big-spacer-top"
+    >
+      <OrganizationDescriptionInput
+        initialValue=""
+        onChange={[Function]}
+      />
+    </div>
+    <div
+      className="big-spacer-top"
+    >
+      <OrganizationUrlInput
+        initialValue=""
+        onChange={[Function]}
+      />
+    </div>
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <SubmitButton
+      disabled={true}
+    >
+      continue
+    </SubmitButton>
+  </div>
+</form>
+`;
index 169967f9e4f039434fa7c8a756f33f9fb10f7a0b..3b15b770b4013a7fb3d6e95140c5b96b2916a8c3 100644 (file)
@@ -12,241 +12,6 @@ exports[`should render form 1`] = `
 />
 `;
 
-exports[`should render form 2`] = `
-<div
-  className="boxed-group onboarding-step is-open"
->
-  <div
-    className="onboarding-step-number"
-  >
-    1
-  </div>
-  <div
-    className="boxed-group-header"
-  >
-    <h2>
-      onboarding.create_organization.enter_org_details
-    </h2>
-  </div>
-  <div
-    className=""
-  >
-    <div
-      className="boxed-group-inner"
-    >
-<<<<<<< HEAD
-      <ValidationForm
-        initialValues={
-          Object {
-            "avatar": "",
-            "description": "",
-            "key": "",
-            "name": "",
-            "url": "",
-          }
-        }
-        isInitialValid={false}
-        onSubmit={[MockFunction]}
-        validate={[Function]}
-      >
-        <Component />
-      </ValidationForm>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render form 3`] = `
-<form
-  onSubmit={[Function]}
->
-  <OrganizationDetailsInput
-    description="onboarding.create_organization.organization_name.description"
-    dirty={false}
-    id="organization-key"
-    isSubmitting={false}
-    isValidating={false}
-    label="onboarding.create_organization.organization_name"
-    name="key"
-    onBlur={[Function]}
-    onChange={[Function]}
-    required={true}
-    value=""
-  >
-    <Component />
-  </OrganizationDetailsInput>
-  <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"
-    >
-      <OrganizationDetailsInput
-        description="onboarding.create_organization.display_name.description"
-        dirty={false}
-        id="organization-display-name"
-        isSubmitting={false}
-        isValidating={false}
-        label="onboarding.create_organization.display_name"
-        name="name"
-        onBlur={[Function]}
-        onChange={[Function]}
-        value=""
-      >
-        <Component />
-      </OrganizationDetailsInput>
-    </div>
-    <div
-      className="big-spacer-top"
-    >
-      <OrganizationDetailsInput
-        description="onboarding.create_organization.avatar.description"
-        dirty={false}
-        id="organization-avatar"
-        isSubmitting={false}
-        isValidating={false}
-        label="onboarding.create_organization.avatar"
-        name="avatar"
-        onBlur={[Function]}
-        onChange={[Function]}
-        value=""
-      >
-        <Component />
-      </OrganizationDetailsInput>
-    </div>
-    <div
-      className="big-spacer-top"
-    >
-      <OrganizationDetailsInput
-        dirty={false}
-        id="organization-description"
-        isSubmitting={false}
-        isValidating={false}
-        label="description"
-        name="description"
-        onBlur={[Function]}
-        onChange={[Function]}
-        value=""
-      >
-        <Component />
-      </OrganizationDetailsInput>
-    </div>
-    <div
-      className="big-spacer-top"
-    >
-      <OrganizationDetailsInput
-        dirty={false}
-        id="organization-url"
-        isSubmitting={false}
-        isValidating={false}
-        label="onboarding.create_organization.url"
-        name="url"
-        onBlur={[Function]}
-        onChange={[Function]}
-        value=""
-      >
-        <Component />
-      </OrganizationDetailsInput>
-    </div>
-  </div>
-  <div
-    className="big-spacer-top"
-  >
-    <SubmitButton
-      disabled={true}
-    >
-      continue
-    </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-actions display-flex-center"
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
deleted file mode 100644 (file)
index 7d02df3..0000000
+++ /dev/null
@@ -1,111 +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 { 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
deleted file mode 100644 (file)
index eaea25f..0000000
+++ /dev/null
@@ -1,95 +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 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
deleted file mode 100644 (file)
index a4fcc91..0000000
+++ /dev/null
@@ -1,150 +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 { 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;
-  readOnly?: boolean;
-}
-
-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 });
-      if (!this.props.readOnly) {
-        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={!this.props.readOnly}>
-        <div className="display-inline-flex-baseline">
-          <span className="little-spacer-right">
-            {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
-            {this.props.readOnly && this.state.value}
-          </span>
-          {!this.props.readOnly && (
-            <input
-              autoFocus={true}
-              className={classNames('input-super-large', 'text-middle', {
-                'is-invalid': isInvalid,
-                'is-valid': isValid
-              })}
-              id="organization-key"
-              maxLength={255}
-              onBlur={this.handleBlur}
-              onChange={this.handleChange}
-              onFocus={this.handleFocus}
-              type="text"
-              value={this.state.value}
-            />
-          )}
-        </div>
-      </ValidationInput>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx
deleted file mode 100644 (file)
index 9e50b0c..0000000
+++ /dev/null
@@ -1,96 +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 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
deleted file mode 100644 (file)
index a77bdc9..0000000
+++ /dev/null
@@ -1,96 +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 { 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
deleted file mode 100644 (file)
index c7d7c24..0000000
+++ /dev/null
@@ -1,45 +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 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
deleted file mode 100644 (file)
index eab1e2c..0000000
+++ /dev/null
@@ -1,39 +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 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
deleted file mode 100644 (file)
index d559b30..0000000
+++ /dev/null
@@ -1,68 +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 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 render correctly with readonly mode', () => {
-  const wrapper = shallow(
-    <OrganizationKeyInput initialValue="key" onChange={jest.fn()} readOnly={true} />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should not display any status when the key is not defined', async () => {
-  const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />);
-  await waitAndUpdate(wrapper);
-  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
deleted file mode 100644 (file)
index ecbfdb1..0000000
+++ /dev/null
@@ -1,37 +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 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
deleted file mode 100644 (file)
index 357a912..0000000
+++ /dev/null
@@ -1,39 +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 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
deleted file mode 100644 (file)
index 292c7b2..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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
deleted file mode 100644 (file)
index 80e11c0..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-// 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
deleted file mode 100644 (file)
index 05d2e74..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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`;
-
-exports[`should render correctly with readonly mode 1`] = `
-<ValidationInput
-  id="organization-key"
-  isInvalid={false}
-  isValid={false}
-  label="onboarding.create_organization.organization_name"
-  required={false}
->
-  <div
-    className="display-inline-flex-baseline"
-  >
-    <span
-      className="little-spacer-right"
-    >
-      localhost/organizations/
-      key
-    </span>
-  </div>
-</ValidationInput>
-`;
diff --git a/server/sonar-web/src/main/js/apps/create/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
deleted file mode 100644 (file)
index 1af9dc9..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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
deleted file mode 100644 (file)
index d3f571b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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`;
index 4460f222b8f9ae5bca5fe14132769a3c720953f8..ad6f002914ff3ad7ef7b5ac9fff86afb26b967bb 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import RemoteRepositories from './RemoteRepositories';
-import OrganizationSelect from './OrganizationSelect';
+import OrganizationInput from './OrganizationInput';
 import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
 import { AlmApplication, Organization } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
@@ -69,7 +69,7 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
             small={true}
             url={almApplication.installationUrl}>
             {translate(
-              'onboarding.create_organization.choose_organization_button',
+              'onboarding.import_organization.choose_organization_button',
               almApplication.key
             )}
           </IdentityProviderLink>
@@ -80,7 +80,7 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
     const { selectedOrganization } = this.state;
     return (
       <>
-        <OrganizationSelect
+        <OrganizationInput
           autoImport={true}
           onChange={this.handleOrganizationSelect}
           organization={selectedOrganization}
index 53382bfc34ed57025109f86a67eb9e192b08e484..7afb0d1ca1012abbd1225dbb15ac03ea1ada6fcd 100644 (file)
@@ -18,7 +18,6 @@
  * 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';
@@ -142,17 +141,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
                   tabs={[
                     {
                       key: 'auto',
-                      node: (
-                        <>
-                          {translate('onboarding.create_project.select_repositories')}
-                          <span
-                            className={classNames('beta-badge spacer-left', {
-                              'is-muted': showManualTab
-                            })}>
-                            {translate('beta')}
-                          </span>
-                        </>
-                      )
+                      node: translate('onboarding.create_project.select_repositories')
                     },
                     { key: 'manual', node: translate('onboarding.create_project.create_manually') }
                   ]}
index 2820af7c8dc866e5b62b3213808debcf53d6f868..83b0a6ab269a408cc0667c1e16998e351eb91ccd 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import OrganizationSelect from './OrganizationSelect';
+import OrganizationInput from './OrganizationInput';
 import DeferredSpinner from '../../../components/common/DeferredSpinner';
 import { SubmitButton } from '../../../components/ui/buttons';
 import { LoggedInUser, Organization } from '../../../app/types';
@@ -113,7 +113,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
     return (
       <>
         <form onSubmit={this.handleFormSubmit}>
-          <OrganizationSelect
+          <OrganizationInput
             onChange={this.handleOrganizationSelect}
             organization={this.state.selectedOrganization}
             organizations={this.props.userOrganizations}
diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx
new file mode 100644 (file)
index 0000000..b917efd
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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 OrganizationSelect from '../components/OrganizationSelect';
+import { Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  autoImport?: boolean;
+  onChange: (organization: Organization) => void;
+  organization: string;
+  organizations: Organization[];
+}
+
+export default function OrganizationInput({
+  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>
+      <OrganizationSelect
+        onChange={onChange}
+        organization={organization}
+        organizations={organizations}
+      />
+      <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>
+  );
+}
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
deleted file mode 100644 (file)
index ed7cf5d..0000000
+++ /dev/null
@@ -1,86 +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 { 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.alm && (
-        <img
-          alt={organization.alm.key}
-          className="spacer-right"
-          height={14}
-          src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`}
-        />
-      )}
-      {organization.name}
-      <span className="note little-spacer-left">{organization.key}</span>
-    </span>
-  );
-}
index 52d56a87e382843f6750a48be6f6a2fc1c6a47cd..e8e33a81ac1215d9a8a4d3cb1ebd3b601218d885 100644 (file)
@@ -38,7 +38,7 @@ it('should render correctly', () => {
 it('should correctly create a project', async () => {
   const onProjectCreate = jest.fn();
   const wrapper = getWrapper({ onProjectCreate });
-  wrapper.find('OrganizationSelect').prop<Function>('onChange')({ key: 'foo' });
+  wrapper.find('OrganizationInput').prop<Function>('onChange')({ key: 'foo' });
   change(wrapper.find('#project-name'), 'Bar');
   expect(wrapper.find('SubmitButton')).toMatchSnapshot();
 
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx
new file mode 100644 (file)
index 0000000..f6fcd45
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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 OrganizationInput from '../OrganizationInput';
+
+const organizations = [
+  { key: 'foo', name: 'Foo' },
+  { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
+];
+
+it('should render correctly', () => {
+  expect(
+    shallow(
+      <OrganizationInput onChange={jest.fn()} organization="bar" organizations={organizations} />
+    )
+  ).toMatchSnapshot();
+  expect(
+    shallow(
+      <OrganizationInput
+        autoImport={true}
+        onChange={jest.fn()}
+        organization="bar"
+        organizations={organizations}
+      />
+    )
+      .find('.js-new-org')
+      .contains('onboarding.create_project.import_new_org')
+  ).toBe(true);
+});
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
deleted file mode 100644 (file)
index cc7e426..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 { shallow } from 'enzyme';
-import OrganizationSelect, { optionRenderer } from '../OrganizationSelect';
-
-const organizations = [
-  { key: 'foo', name: 'Foo' },
-  { alm: { key: 'github', url: '' }, 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();
-});
index a96a37b53b5754eae3e8d3d7b7994d7f96e0b836..6623016dd4f71185017b09798825548921b7c49d 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`should display the bounded organizations dropdown with the list of repositories 1`] = `
 <Fragment>
-  <OrganizationSelect
+  <OrganizationInput
     autoImport={true}
     onChange={[Function]}
     organization="foo"
@@ -59,7 +59,7 @@ exports[`should display the provider app install button 1`] = `
     small={true}
     url="https://alm.installation.url"
   >
-    onboarding.create_organization.choose_organization_button.github
+    onboarding.import_organization.choose_organization_button.github
   </IdentityProviderLink>
 </Fragment>
 `;
index 5e2c2e1a150f9859687df18f6c0792f70838506b..c0ff4ce75353fd34dcf0a0605ae86a8ec39ffc61 100644 (file)
@@ -54,14 +54,7 @@ exports[`should render correctly 2`] = `
         Array [
           Object {
             "key": "auto",
-            "node": <React.Fragment>
-              onboarding.create_project.select_repositories
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
+            "node": "onboarding.create_project.select_repositories",
           },
           Object {
             "key": "manual",
@@ -178,14 +171,7 @@ exports[`should switch tabs 1`] = `
         Array [
           Object {
             "key": "auto",
-            "node": <React.Fragment>
-              onboarding.create_project.select_repositories
-              <span
-                className="beta-badge spacer-left"
-              >
-                beta
-              </span>
-            </React.Fragment>,
+            "node": "onboarding.create_project.select_repositories",
           },
           Object {
             "key": "manual",
index 53fde97ce314168691ca7fe2d39d1c1da8c2e5ac..4f38672a053904f23e91d742e06bd79c018a3d03 100644 (file)
@@ -21,7 +21,7 @@ exports[`should render correctly 1`] = `
   <form
     onSubmit={[Function]}
   >
-    <OrganizationSelect
+    <OrganizationInput
       onChange={[Function]}
       organization=""
       organizations={
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..fe6c8d4
--- /dev/null
@@ -0,0 +1,46 @@
+// 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>
+  <OrganizationSelect
+    onChange={[MockFunction]}
+    organization="bar"
+    organizations={
+      Array [
+        Object {
+          "key": "foo",
+          "name": "Foo",
+        },
+        Object {
+          "alm": Object {
+            "key": "github",
+            "url": "",
+          },
+          "key": "bar",
+          "name": "Bar",
+        },
+      ]
+    }
+  />
+  <Link
+    className="big-spacer-left js-new-org"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to="/create-organization"
+  >
+    onboarding.create_project.create_new_org
+  </Link>
+</div>
+`;
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
deleted file mode 100644 (file)
index 367f026..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// 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 {
-          "alm": Object {
-            "key": "github",
-            "url": "",
-          },
-          "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>
-`;
index 039574a03fbf4858bdabc6be374a707194fca6c7..ae23029734e37f186ca6526e7199d29a7c6e6b31 100644 (file)
@@ -22,13 +22,12 @@ import * as PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import { InjectedRouter } from 'react-router';
 import OnboardingModal from './OnboardingModal';
-import { skipOnboarding } from '../../../api/users';
-import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
+import { skipOnboarding } from '../../../store/users';
 import TeamOnboardingModal from '../teamOnboarding/TeamOnboardingModal';
 import { Organization } from '../../../app/types';
 
 interface DispatchProps {
-  skipOnboardingAction: () => void;
+  skipOnboarding: () => void;
 }
 
 interface OwnProps {
@@ -52,8 +51,7 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps
   state: State = { modal: ModalKey.onboarding };
 
   closeOnboarding = () => {
-    skipOnboarding();
-    this.props.skipOnboardingAction();
+    this.props.skipOnboarding();
     this.props.router.replace('/');
   };
 
@@ -90,7 +88,7 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps
   }
 }
 
-const mapDispatchToProps: DispatchProps = { skipOnboardingAction };
+const mapDispatchToProps: DispatchProps = { skipOnboarding };
 
 export default connect(
   null,
index dd03cd7aa3e37ab8a2ff8df922cd6923ed24dbb4..4b9175d39f884aaa3a903aa650e5686ed398468e 100644 (file)
@@ -2748,18 +2748,20 @@ onboarding.create_organization.url.error=The value must be a valid url.
 onboarding.create_organization.description=Description
 onboarding.create_organization.enter_org_details=Enter your organization details
 onboarding.create_organization.create_manually=Create manually
-onboarding.create_organization.import_org_details=Import organization details
-onboarding.create_organization.import_org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue:
-onboarding.create_organization.import_org_not_found.tips_1=You must be an administrator of the organization
-onboarding.create_organization.import_org_not_found.tips_2=Try to uninstall and re-install the SonarCloud App (using the button bellow)
-onboarding.create_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket
-onboarding.create_organization.choose_organization_button.github=Choose an organization on GitHub
 onboarding.create_organization.enter_payment_details=Enter payment details
 onboarding.create_organization.choose_plan=Choose a plan
 onboarding.create_organization.enter_your_coupon=Enter your coupon
 onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade
 onboarding.create_organization.ready=All set! Your organization is now ready to go
 onboarding.import_organization.bind=Bind Organization
+onboarding.import_organization.import=Import Organization
+onboarding.import_organization.import_org_details=Import organization details
+onboarding.import_organization.org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue:
+onboarding.import_organization.org_not_found.tips_1=You must be an administrator of the organization
+onboarding.import_organization.org_not_found.tips_2=Try to uninstall and re-install the SonarCloud App (using the button bellow)
+onboarding.import_organization.choose_organization=Choose an organization...
+onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket
+onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub
 onboarding.import_organization.installing=Installation of the ALM application in progress...
 onboarding.import_organization.installing.bitbucket=Installation of the Bitbucket application in progress..
 onboarding.import_organization.installing.github=Installation of the GitHub application in progress...
@@ -2767,6 +2769,8 @@ onboarding.import_organization.personal.page.header=Bind to your personal organi
 onboarding.import_organization.personal.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.
 onboarding.import_organization.bitbucket=Import from BitBucket teams
 onboarding.import_organization.github=Import from GitHub organizations
+onboarding.import_organization.bind_existing=Bind to an existing sonarcloud organization
+onboarding.import_organization.create_new=Create new SonarCloud organization from it
 onboarding.import_organization_x=Import {avatar} {name} into SonarCloud organization
 onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName}