aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-10-16 17:03:32 +0200
committerSonarTech <sonartech@sonarsource.com>2018-11-16 20:21:04 +0100
commit3ea9808248000c145f53a4f1cdb8711d63b97da4 (patch)
tree3b18082347d14718b90b3409b28740ca971b1568 /server/sonar-web/src/main/js/apps
parentbcddd9658c7eea3bb5576bece3a0c2b985a7d506 (diff)
downloadsonarqube-3ea9808248000c145f53a4f1cdb8711d63b97da4.tar.gz
sonarqube-3ea9808248000c145f53a4f1cdb8711d63b97da4.zip
SONAR-11322 Import repos from bound organizations
* Move project create page in the create folder * Move HOCs to components * Update create project page * Move getting user organizations in CreateProjectPage (1 level higher) * Creact OrganizationSelect component * Create RemoteRepositories component * Use OrganizationSelect in ManualProjectCreate * Add OrganizationSelect and RemoteRepositories in AutoProjectCreate page * Rework validation of the create organization page * Add 'organization' param on list_repositories and provision_projects
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx298
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx103
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap31
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap133
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx111
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx144
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx96
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx96
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx (renamed from server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx)38
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx (renamed from server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx)29
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/utils.ts)49
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap61
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap34
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx99
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx)149
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx)71
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx86
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx)144
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/__tests__/AlmRepositoryItem-test.tsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx)47
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx (renamed from server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx)5
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx (renamed from server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx)50
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx94
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap59
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap)39
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap)59
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap80
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap114
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap123
-rw-r--r--server/sonar-web/src/main/js/apps/projects/routes.ts2
49 files changed, 1945 insertions, 1134 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
index 25a091e9246..602d3139d47 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
@@ -22,6 +22,7 @@ import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
import Step from '../../tutorials/components/Step';
import { translate } from '../../../helpers/l10n';
import { AlmApplication } from '../../../app/types';
+import { Alert } from '../../../components/ui/Alert';
interface Props {
almApplication: AlmApplication;
@@ -34,13 +35,13 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr
return (
<div className="boxed-group-inner">
{almInstallId && (
- <span className="alert alert-warning markdown big-spacer-bottom width-60">
+ <Alert className="markdown big-spacer-bottom width-60" variant="warning">
{translate('onboarding.create_organization.import_org_not_found')}
<ul>
<li>{translate('onboarding.create_organization.import_org_not_found.tips_1')}</li>
<li>{translate('onboarding.create_organization.import_org_not_found.tips_2')}</li>
</ul>
- </span>
+ </Alert>
)}
<IdentityProviderLink
className="display-inline-block"
diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
index 2767cdfa399..56e401bd103 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
@@ -25,11 +25,11 @@ import { Helmet } from 'react-helmet';
import { FormattedMessage } from 'react-intl';
import { Link, withRouter, WithRouterProps } from 'react-router';
import { formatPrice, parseQuery } from './utils';
-import { whenLoggedIn } from './whenLoggedIn';
import AutoOrganizationCreate from './AutoOrganizationCreate';
import ManualOrganizationCreate from './ManualOrganizationCreate';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tabs from '../../../components/controls/Tabs';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { getAlmAppInfo, getAlmOrganization } from '../../../api/alm-integration';
import { getSubscriptionPlans } from '../../../api/billing';
import {
@@ -62,9 +62,11 @@ interface State {
subscriptionPlans?: SubscriptionPlan[];
}
+type TabKeys = 'auto' | 'manual';
+
interface LocationState {
paid?: boolean;
- tab?: 'auto' | 'manual';
+ tab?: TabKeys;
}
export class CreateOrganization extends React.PureComponent<Props & WithRouterProps, State> {
@@ -125,7 +127,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
});
};
- onTabChange = (tab: 'auto' | 'manual') => {
+ onTabChange = (tab: TabKeys) => {
this.updateUrl({ tab });
};
@@ -138,6 +140,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
updateUrl = (state: Partial<LocationState> = {}) => {
this.props.router.replace({
pathname: this.props.location.pathname,
+ query: this.props.location.query,
state: { ...(this.props.location.state || {}), ...state }
});
};
@@ -182,7 +185,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
) : (
<>
{almApplication && (
- <Tabs
+ <Tabs<TabKeys>
onChange={this.onTabChange}
selected={showManualTab ? 'manual' : 'auto'}
tabs={[
@@ -195,13 +198,9 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
almApplication.key
)}
<span
- className={classNames(
- 'rounded alert alert-small spacer-left display-inline-block',
- {
- 'alert-info': !showManualTab,
- 'alert-muted': showManualTab
- }
- )}>
+ className={classNames('beta-badge spacer-left', {
+ 'is-muted': showManualTab
+ })}>
{translate('beta')}
</span>
</>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
deleted file mode 100644
index 9526e064562..00000000000
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import * as classNames from 'classnames';
-import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
-
-interface Props {
- description?: string;
- dirty: boolean;
- children: (inputProps: React.InputHTMLAttributes<Element>) => React.ReactElement<any>;
- error: string | undefined;
- id: string;
- isSubmitting: boolean;
- isValidating: boolean;
- label: React.ReactNode;
- name: string;
- onBlur: React.FocusEventHandler;
- onChange: React.ChangeEventHandler;
- required?: boolean;
- touched?: boolean;
- value: string;
-}
-
-export default function OrganizationDetailsInput(props: Props) {
- const hasError = props.dirty && props.touched && !props.isValidating && props.error !== undefined;
- const isValid = props.dirty && props.touched && props.error === undefined;
- return (
- <div>
- <label htmlFor={props.id}>
- <strong>{props.label}</strong>
- {props.required && <em className="mandatory">*</em>}
- </label>
- <div className="little-spacer-top spacer-bottom">
- {props.children({
- className: classNames('input-super-large', 'text-middle', {
- 'is-invalid': hasError,
- 'is-valid': isValid
- }),
- disabled: props.isSubmitting,
- id: props.id,
- name: props.name,
- onBlur: props.onBlur,
- onChange: props.onChange,
- type: 'text',
- value: props.value
- })}
- {hasError && (
- <>
- <AlertErrorIcon className="spacer-left text-middle" />
- <span className="little-spacer-left text-danger text-middle">{props.error}</span>
- </>
- )}
- {isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
- </div>
- {props.description && <div className="note abs-width-400">{props.description}</div>}
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
index 75f1b75d17b..2b31d8a2379 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
@@ -18,32 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { isWebUri } from 'valid-url';
-import OrganizationDetailsInput from './OrganizationDetailsInput';
+import OrganizationAvatarInput from './components/OrganizationAvatarInput';
+import OrganizationDescriptionInput from './components/OrganizationDescriptionInput';
+import OrganizationKeyInput from './components/OrganizationKeyInput';
+import OrganizationNameInput from './components/OrganizationNameInput';
+import OrganizationUrlInput from './components/OrganizationUrlInput';
import Step from '../../tutorials/components/Step';
-import ValidationForm, { ChildrenProps } from '../../../components/controls/ValidationForm';
import { translate } from '../../../helpers/l10n';
import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
-import { getHostUrl } from '../../../helpers/urls';
import { OrganizationBase } from '../../../app/types';
-import { getOrganization } from '../../../api/organizations';
-type Values = Required<OrganizationBase>;
-
-const initialValues: Values = {
- avatar: '',
- description: '',
- name: '',
- key: '',
- url: ''
-};
+type RequiredOrganization = Required<OrganizationBase>;
interface Props {
description?: React.ReactNode;
finished: boolean;
- onContinue: (organization: Required<OrganizationBase>) => Promise<void>;
+ onContinue: (organization: RequiredOrganization) => Promise<void>;
onOpen: () => void;
open: boolean;
organization?: OrganizationBase & { key: string };
@@ -52,199 +44,143 @@ interface Props {
interface State {
additional: boolean;
+ avatar?: string;
+ description?: string;
+ key?: string;
+ name?: string;
+ submitting: boolean;
+ url?: string;
}
+type ValidState = Pick<State, Exclude<keyof State, RequiredOrganization>> & RequiredOrganization;
+
export default class OrganizationDetailsStep extends React.PureComponent<Props, State> {
- state: State = { additional: false };
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ const { organization } = props;
+ this.state = {
+ additional: false,
+ avatar: (organization && organization.avatar) || '',
+ description: (organization && organization.description) || '',
+ key: (organization && organization.key) || undefined,
+ name: (organization && organization.name) || '',
+ submitting: false,
+ url: (organization && organization.url) || ''
+ };
+ }
- getInitialValues = (): Values => {
- const { organization } = this.props;
- if (organization) {
- return {
- avatar: organization.avatar || '',
- description: organization.description || '',
- name: organization.name,
- key: organization.key,
- url: organization.url || ''
- };
- } else {
- return initialValues;
- }
- };
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ canSubmit(state: State): state is ValidState {
+ return Boolean(
+ state.key !== undefined &&
+ state.name !== undefined &&
+ state.description !== undefined &&
+ state.avatar !== undefined &&
+ state.url !== undefined
+ );
+ }
handleAdditionalClick = () => {
this.setState(state => ({ additional: !state.additional }));
};
- checkFreeKey = (key: string) => {
- return getOrganization(key).then(organization => organization === undefined, () => true);
+ handleKeyUpdate = (key: string | undefined) => {
+ this.setState({ key });
};
- handleValidate = ({ avatar, name, key, url }: Values) => {
- const errors: { [P in keyof Values]?: string } = {};
+ handleNameUpdate = (name: string | undefined) => {
+ this.setState({ name });
+ };
- if (avatar.length > 0 && !isWebUri(avatar)) {
- errors.avatar = translate('onboarding.create_organization.avatar.error');
- }
+ handleDescriptionUpdate = (description: string | undefined) => {
+ this.setState({ description });
+ };
- if (name.length > 255) {
- errors.name = translate('onboarding.create_organization.display_name.error');
- }
+ handleAvatarUpdate = (avatar: string | undefined) => {
+ this.setState({ avatar });
+ };
- if (key.length > 255 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]?$/.test(key)) {
- errors.key = translate('onboarding.create_organization.organization_name.error');
- }
+ handleUrlUpdate = (url: string | undefined) => {
+ this.setState({ url });
+ };
- if (url.length > 0 && !isWebUri(url)) {
- errors.url = translate('onboarding.create_organization.url.error');
+ handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ const { state } = this;
+ if (this.canSubmit(state)) {
+ this.setState({ submitting: true });
+ this.props
+ .onContinue({
+ avatar: state.avatar,
+ description: state.description,
+ key: state.key,
+ name: state.name,
+ url: state.url
+ })
+ .then(this.stopSubmitting, this.stopSubmitting);
}
+ };
- // don't try to check if the organization key is already taken if the key is invalid
- if (errors.key) {
- return Promise.reject(errors);
+ stopSubmitting = () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
}
-
- // TODO debounce
- return this.checkFreeKey(key).then(free => {
- if (!free) {
- errors.key = translate('onboarding.create_organization.organization_name.taken');
- }
- return Object.keys(errors).length ? Promise.reject(errors) : Promise.resolve(errors);
- });
};
- renderInnerForm = (props: ChildrenProps<Values>) => {
- const {
- dirty,
- errors,
- handleBlur,
- handleChange,
- isSubmitting,
- isValid,
- isValidating,
- touched,
- values
- } = props;
- const commonProps = {
- dirty,
- isValidating,
- isSubmitting,
- onBlur: handleBlur,
- onChange: handleChange
- };
+ renderForm = () => {
return (
- <>
- <OrganizationDetailsInput
- {...commonProps}
- description={translate('onboarding.create_organization.organization_name.description')}
- error={errors.key}
- id="organization-key"
- label={translate('onboarding.create_organization.organization_name')}
- name="key"
- required={true}
- touched={touched.key}
- value={values.key}>
- {props => (
- <div className="display-inline-flex-baseline">
- <span className="little-spacer-right">
- {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
- </span>
- <input autoFocus={true} maxLength={255} {...props} />
- </div>
- )}
- </OrganizationDetailsInput>
- <div className="big-spacer-top">
- <ResetButtonLink onClick={this.handleAdditionalClick}>
- {translate(
- this.state.additional
- ? 'onboarding.create_organization.hide_additional_info'
- : 'onboarding.create_organization.add_additional_info'
- )}
- <DropdownIcon className="little-spacer-left" turned={this.state.additional} />
- </ResetButtonLink>
- </div>
- <div className="js-additional-info" hidden={!this.state.additional}>
- <div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- description={translate('onboarding.create_organization.display_name.description')}
- error={errors.name}
- id="organization-display-name"
- label={translate('onboarding.create_organization.display_name')}
- name="name"
- touched={touched.name && values.name !== ''}
- value={values.name}>
- {props => <input {...props} />}
- </OrganizationDetailsInput>
- </div>
+ <div className="boxed-group-inner">
+ <form id="organization-form" onSubmit={this.handleSubmit}>
+ {this.props.description}
+ <OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} />
<div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- description={translate('onboarding.create_organization.avatar.description')}
- error={errors.avatar}
- id="organization-avatar"
- label={translate('onboarding.create_organization.avatar')}
- name="avatar"
- touched={touched.avatar && values.avatar !== ''}
- value={values.avatar}>
- {props => (
- <>
- {values.avatar && (
- <img
- alt=""
- className="display-block spacer-bottom rounded"
- src={values.avatar}
- width={48}
- />
- )}
- <input {...props} />
- </>
+ <ResetButtonLink onClick={this.handleAdditionalClick}>
+ {translate(
+ this.state.additional
+ ? 'onboarding.create_organization.hide_additional_info'
+ : 'onboarding.create_organization.add_additional_info'
)}
- </OrganizationDetailsInput>
+ <DropdownIcon className="little-spacer-left" turned={this.state.additional} />
+ </ResetButtonLink>
</div>
- <div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- error={errors.description}
- id="organization-description"
- label={translate('description')}
- name="description"
- touched={touched.description && values.description !== ''}
- value={values.description}>
- {props => <textarea {...props} maxLength={256} rows={3} />}
- </OrganizationDetailsInput>
+ <div className="js-additional-info" hidden={!this.state.additional}>
+ <div className="big-spacer-top">
+ <OrganizationNameInput
+ initialValue={this.state.name}
+ onChange={this.handleNameUpdate}
+ />
+ </div>
+ <div className="big-spacer-top">
+ <OrganizationAvatarInput
+ initialValue={this.state.avatar}
+ onChange={this.handleDescriptionUpdate}
+ />
+ </div>
+ <div className="big-spacer-top">
+ <OrganizationDescriptionInput
+ initialValue={this.state.description}
+ onChange={this.handleAvatarUpdate}
+ />
+ </div>
+ <div className="big-spacer-top">
+ <OrganizationUrlInput initialValue={this.state.url} onChange={this.handleUrlUpdate} />
+ </div>
</div>
<div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- error={errors.url}
- id="organization-url"
- label={translate('onboarding.create_organization.url')}
- name="url"
- touched={touched.url && values.url !== ''}
- value={values.url}>
- {props => <input {...props} />}
- </OrganizationDetailsInput>
+ <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}>
+ {this.props.submitText}
+ </SubmitButton>
</div>
- </div>
- <div className="big-spacer-top">
- <SubmitButton disabled={isSubmitting || !isValid}>{this.props.submitText}</SubmitButton>
- </div>
- </>
- );
- };
-
- renderForm = () => {
- return (
- <div className="boxed-group-inner">
- {this.props.description}
- <ValidationForm<Values>
- initialValues={this.getInitialValues()}
- isInitialValid={this.props.organization !== undefined}
- onSubmit={this.props.onContinue}
- validate={this.handleValidate}>
- {this.renderInnerForm}
- </ValidationForm>
+ </form>
</div>
);
};
diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
index 5118540ce81..f7b7a0f6a42 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
@@ -19,9 +19,9 @@
*/
import * as React from 'react';
import BillingFormShim from './BillingFormShim';
-import { withCurrentUser } from './withCurrentUser';
import PlanSelect, { Plan } from './PlanSelect';
import Step from '../../tutorials/components/Step';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { translate } from '../../../helpers/l10n';
import { getExtensionStart } from '../../../app/components/extensions/utils';
import { SubscriptionPlan } from '../../../app/types';
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
index a86ab8dfd11..6721e089b15 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
@@ -26,7 +26,7 @@ it('should render', () => {
});
it('should display a warning message', () => {
- expect(shallowRender({ almInstallId: 'foo' }).find('.alert-warning')).toMatchSnapshot();
+ expect(shallowRender({ almInstallId: 'foo' }).find('Alert')).toMatchSnapshot();
});
function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) {
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
index f8748b45aec..ac756ffdc8b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
@@ -18,9 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
import OrganizationDetailsStep from '../OrganizationDetailsStep';
-import { click } from '../../../../helpers/testUtils';
+import { click, submit } from '../../../../helpers/testUtils';
import { getOrganization } from '../../../../api/organizations';
jest.mock('../../../../api/organizations', () => ({
@@ -43,23 +43,24 @@ it('should render form', () => {
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.dive()).toMatchSnapshot();
- expect(getForm(wrapper)).toMatchSnapshot();
expect(
- getForm(wrapper)
+ wrapper
+ .dive()
.find('.js-additional-info')
.prop('hidden')
).toBe(true);
- click(getForm(wrapper).find('ResetButtonLink'));
+ click(wrapper.dive().find('ResetButtonLink'));
wrapper.update();
expect(
- getForm(wrapper)
+ wrapper
+ .dive()
.find('.js-additional-info')
.prop('hidden')
).toBe(false);
});
-it('should validate', async () => {
+it('should validate before submit', () => {
const wrapper = shallow(
<OrganizationDetailsStep
finished={false}
@@ -71,77 +72,48 @@ it('should validate', async () => {
);
const instance = wrapper.instance() as OrganizationDetailsStep;
- await expect(
- instance.handleValidate({
+ expect(
+ instance.canSubmit({
+ additional: false,
avatar: '',
description: '',
name: '',
key: 'foo',
+ submitting: false,
url: ''
})
- ).resolves.toEqual({});
+ ).toBe(true);
- await expect(
- instance.handleValidate({
+ expect(
+ instance.canSubmit({
+ additional: false,
avatar: '',
description: '',
name: '',
- key: 'x'.repeat(256),
+ key: undefined,
+ submitting: false,
url: ''
})
- ).rejects.toEqual({
- key: 'onboarding.create_organization.organization_name.error'
- });
+ ).toBe(false);
- await expect(
- instance.handleValidate({
- avatar: 'bla',
+ expect(
+ instance.canSubmit({
+ additional: false,
+ avatar: undefined,
description: '',
name: '',
key: 'foo',
+ submitting: false,
url: ''
})
- ).rejects.toEqual({ avatar: 'onboarding.create_organization.avatar.error' });
-
- await expect(
- instance.handleValidate({
- avatar: '',
- description: '',
- name: 'x'.repeat(256),
- key: 'foo',
- url: ''
- })
- ).rejects.toEqual({
- name: 'onboarding.create_organization.display_name.error'
- });
-
- await expect(
- instance.handleValidate({
- avatar: '',
- description: '',
- name: '',
- key: 'foo',
- url: 'bla'
- })
- ).rejects.toEqual({
- url: 'onboarding.create_organization.url.error'
- });
+ ).toBe(false);
- (getOrganization as jest.Mock).mockResolvedValue({});
- await expect(
- instance.handleValidate({
- avatar: '',
- description: '',
- name: '',
- key: 'foo',
- url: ''
- })
- ).rejects.toEqual({
- key: 'onboarding.create_organization.organization_name.taken'
- });
+ instance.canSubmit = jest.fn() as any;
+ submit(wrapper.dive().find('form'));
+ expect(instance.canSubmit).toHaveBeenCalled();
});
-it('should render result', () => {
+it.only('should render result', () => {
const wrapper = shallow(
<OrganizationDetailsStep
finished={true}
@@ -152,14 +124,11 @@ it('should render result', () => {
submitText="continue"
/>
);
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(wrapper.dive().find('.boxed-group-actions')).toMatchSnapshot();
+ expect(
+ wrapper
+ .dive()
+ .find('.hidden')
+ .exists()
+ ).toBe(true);
});
-
-function getForm(wrapper: ShallowWrapper) {
- return wrapper
- .dive()
- .find('ValidationForm')
- .dive()
- .dive()
- .children();
-}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
index ec99dad98ea..3a79e945db1 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
@@ -1,8 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should display a warning message 1`] = `
-<span
- className="alert alert-warning markdown big-spacer-bottom width-60"
+<Alert
+ className="markdown big-spacer-bottom width-60"
+ variant="warning"
>
onboarding.create_organization.import_org_not_found
<ul>
@@ -13,7 +14,7 @@ exports[`should display a warning message 1`] = `
onboarding.create_organization.import_org_not_found.tips_2
</li>
</ul>
-</span>
+</Alert>
`;
exports[`should render 1`] = `
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
index f2da7e6a62c..c4b506bc1f3 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
@@ -52,7 +52,7 @@ exports[`should render with auto tab displayed 1`] = `
"node": <React.Fragment>
onboarding.create_organization.import_organization.github
<span
- className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ className="beta-badge spacer-left"
>
beta
</span>
@@ -134,7 +134,7 @@ exports[`should render with auto tab selected and manual disabled 1`] = `
"node": <React.Fragment>
onboarding.create_organization.import_organization.github
<span
- className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ className="beta-badge spacer-left"
>
beta
</span>
@@ -288,7 +288,7 @@ exports[`should switch tabs 1`] = `
"node": <React.Fragment>
onboarding.create_organization.import_organization.github
<span
- className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ className="beta-badge spacer-left"
>
beta
</span>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
deleted file mode 100644
index b8bd98adf5b..00000000000
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
+++ /dev/null
@@ -1,31 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div>
- <label
- htmlFor="field"
- >
- <strong>
- Label
- </strong>
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <div
- className="little-spacer-top spacer-bottom"
- >
- <div />
- <AlertErrorIcon
- className="spacer-left text-middle"
- />
- <span
- className="little-spacer-left text-danger text-middle"
- >
- This field is bad!
- </span>
- </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
index a52c598379d..169967f9e4f 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
@@ -34,6 +34,7 @@ exports[`should render form 2`] = `
<div
className="boxed-group-inner"
>
+<<<<<<< HEAD
<ValidationForm
initialValues={
Object {
@@ -172,65 +173,91 @@ exports[`should render form 3`] = `
</SubmitButton>
</div>
</form>
+=======
+ <form
+ id="organization-form"
+ onSubmit={[Function]}
+ >
+ <OrganizationKeyInput
+ onChange={[Function]}
+ />
+ <div
+ className="big-spacer-top"
+ >
+ <ResetButtonLink
+ onClick={[Function]}
+ >
+ onboarding.create_organization.add_additional_info
+ <DropdownIcon
+ className="little-spacer-left"
+ turned={false}
+ />
+ </ResetButtonLink>
+ </div>
+ <div
+ className="js-additional-info"
+ hidden={true}
+ >
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationNameInput
+ initialOrgName=""
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationAvatarInput
+ initialOrgAvatar=""
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationDescriptionInput
+ initialOrgDescription=""
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationUrlInput
+ initialOrgUrl=""
+ onChange={[Function]}
+ />
+ </div>
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ continue
+ </SubmitButton>
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
+>>>>>>> 116a4ec872... SONAR-11322 Import repos from bound organizations
`;
exports[`should render result 1`] = `
<div
- className="boxed-group onboarding-step is-finished"
- onClick={[Function]}
- role="button"
- tabIndex={0}
+ className="boxed-group-actions display-flex-center"
>
- <div
- className="onboarding-step-number"
- >
- 1
- </div>
- <div
- className="boxed-group-actions display-flex-center"
- >
- <AlertSuccessIcon
- className="spacer-right"
- />
- <strong
- className="text-limited"
- >
- org
- </strong>
- </div>
- <div
- className="boxed-group-header"
- >
- <h2>
- onboarding.create_organization.enter_org_details
- </h2>
- </div>
- <div
- className="boxed-group-inner"
+ <AlertSuccessIcon
+ className="spacer-right"
/>
- <div
- className="hidden"
+ <strong
+ className="text-limited"
>
- <div
- className="boxed-group-inner"
- >
- <ValidationForm
- initialValues={
- Object {
- "avatar": "",
- "description": "",
- "key": "org",
- "name": "Organization",
- "url": "",
- }
- }
- isInitialValid={true}
- onSubmit={[MockFunction]}
- validate={[Function]}
- >
- <Component />
- </ValidationForm>
- </div>
- </div>
+ org
+ </strong>
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx
deleted file mode 100644
index 4fc1ee27506..00000000000
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
-import { createStore } from 'redux';
-import { whenLoggedIn } from '../whenLoggedIn';
-import { mockRouter } from '../../../../helpers/testUtils';
-
-class X extends React.Component {
- render() {
- return <div />;
- }
-}
-
-const UnderTest = whenLoggedIn(X);
-
-it('should render for logged in user', () => {
- const store = createStore(state => state, { users: { currentUser: { isLoggedIn: true } } });
- const wrapper = shallow(<UnderTest />, { context: { store } });
- expect(getRenderedType(wrapper)).toBe(X);
-});
-
-it('should not render for anonymous user', () => {
- const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } });
- const router = mockRouter({ replace: jest.fn() });
- const wrapper = shallow(<UnderTest />, { context: { store, router } });
- expect(getRenderedType(wrapper)).toBe(null);
- expect(router.replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' }));
-});
-
-function getRenderedType(wrapper: ShallowWrapper) {
- return wrapper
- .dive()
- .dive()
- .dive()
- .type();
-}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx
new file mode 100644
index 00000000000..7d02df3735c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { isWebUri } from 'valid-url';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+
+interface Props {
+ initialValue?: string;
+ name?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationAvatarInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const value = this.props.initialValue;
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: Boolean(error), value });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = event.currentTarget.value.trim();
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateUrl(url: string) {
+ if (url.length > 0 && !isWebUri(url)) {
+ return translate('onboarding.create_organization.url.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValidUrl = this.state.error === undefined && this.state.value !== '';
+ const isValid = this.state.touched && isValidUrl;
+ return (
+ <ValidationInput
+ description={translate('onboarding.create_organization.avatar.description')}
+ error={this.state.error}
+ id="organization-avatar"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.avatar')}>
+ <>
+ {(isValidUrl || this.props.name) && (
+ <OrganizationAvatar
+ className="display-block spacer-bottom"
+ organization={{
+ avatar: isValidUrl ? this.state.value : undefined,
+ name: this.props.name || ''
+ }}
+ />
+ )}
+ <input
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-display-name"
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </>
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx
new file mode 100644
index 00000000000..eaea25f97ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationDescriptionInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const error = this.validateDescription(this.props.initialValue);
+ this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+ const { value } = event.currentTarget;
+ const error = this.validateDescription(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateDescription(description: string) {
+ if (description.length > 256) {
+ return translate('onboarding.create_organization.description.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ error={this.state.error}
+ id="organization-display-name"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.description')}>
+ <textarea
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-description"
+ maxLength={256}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ rows={3}
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
new file mode 100644
index 00000000000..0fd3c61b35a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { debounce } from 'lodash';
+import { getOrganization } from '../../../../api/organizations';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+import { getHostUrl } from '../../../../helpers/urls';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ validating: boolean;
+ value: string;
+}
+
+export default class OrganizationKeyInput extends React.PureComponent<Props, State> {
+ mounted = false;
+ constructor(props: Props) {
+ super(props);
+ this.state = { error: undefined, editing: false, touched: false, validating: false, value: '' };
+ this.checkFreeKey = debounce(this.checkFreeKey, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.props.initialValue !== undefined) {
+ this.setState({ value: this.props.initialValue });
+ this.validateKey(this.props.initialValue);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkFreeKey = (key: string) => {
+ this.setState({ validating: true });
+ return getOrganization(key)
+ .then(organization => organization === undefined, () => true)
+ .then(
+ free => {
+ if (this.mounted) {
+ if (!free) {
+ this.setState({
+ error: translate('onboarding.create_organization.organization_name.taken'),
+ touched: true,
+ validating: false
+ });
+ this.props.onChange(undefined);
+ } else {
+ this.setState({ error: undefined, validating: false });
+ this.props.onChange(key);
+ }
+ }
+ },
+ () => {}
+ );
+ };
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ this.setState({ touched: true, value });
+ this.validateKey(value);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateKey(key: string) {
+ if (key.length > 255 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]?$/.test(key)) {
+ this.setState({
+ error: translate('onboarding.create_organization.organization_name.error'),
+ touched: true
+ });
+ this.props.onChange(undefined);
+ } else {
+ this.checkFreeKey(key);
+ }
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && !this.state.validating && this.state.error === undefined;
+ return (
+ <ValidationInput
+ error={this.state.error}
+ id="organization-key"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.organization_name')}
+ required={true}>
+ <div className="display-inline-flex-baseline">
+ <span className="little-spacer-right">
+ {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
+ </span>
+ <input
+ autoFocus={true}
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-key"
+ maxLength={255}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </div>
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx
new file mode 100644
index 00000000000..9e50b0cbb2e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationNameInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const error = this.validateName(this.props.initialValue);
+ this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ const error = this.validateName(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateName(name: string) {
+ if (name.length > 255) {
+ return translate('onboarding.create_organization.display_name.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ description={translate('onboarding.create_organization.display_name.description')}
+ error={this.state.error}
+ id="organization-display-name"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.display_name')}>
+ <input
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-display-name"
+ maxLength={255}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx
new file mode 100644
index 00000000000..a77bdc99832
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { isWebUri } from 'valid-url';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationUrlInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const value = this.props.initialValue;
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: Boolean(error), value });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = event.currentTarget.value.trim();
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateUrl(url: string) {
+ if (url.length > 0 && !isWebUri(url)) {
+ return translate('onboarding.create_organization.url.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ error={this.state.error}
+ id="organization-url"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.url')}>
+ <input
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-url"
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx
new file mode 100644
index 00000000000..c7d7c24d01e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import OrganizationAvatarInput from '../OrganizationAvatarInput';
+
+it('should render correctly', () => {
+ const wrapper = shallow(
+ <OrganizationAvatarInput initialValue="https://my.avatar" onChange={jest.fn()} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when the avatar url is not valid', () => {
+ expect(
+ shallow(<OrganizationAvatarInput initialValue="whatever" onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
+
+it('should display the fallback avatar when there is no url', () => {
+ expect(
+ shallow(<OrganizationAvatarInput initialValue="" name="Luke Skywalker" onChange={jest.fn()} />)
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx
index 117af6607dc..eab1e2ca818 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx
@@ -18,26 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { connect } from 'react-redux';
-import { CurrentUser } from '../../../app/types';
-import { Store, getCurrentUser } from '../../../store/rootReducer';
+import { shallow } from 'enzyme';
+import OrganizationDescriptionInput from '../OrganizationDescriptionInput';
-export function withCurrentUser<P>(
- WrappedComponent: React.ComponentClass<P & { currentUser: CurrentUser }>
-) {
- const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+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();
+});
- class Wrapper extends React.Component<P & { currentUser: CurrentUser }> {
- static displayName = `withCurrentUser(${wrappedDisplayName})`;
-
- render() {
- return <WrappedComponent {...this.props} />;
- }
- }
-
- function mapStateToProps(state: Store) {
- return { currentUser: getCurrentUser(state) };
- }
-
- return connect(mapStateToProps)(Wrapper);
-}
+it('should have an error when description is too long', () => {
+ expect(
+ shallow(<OrganizationDescriptionInput initialValue={'x'.repeat(260)} onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
new file mode 100644
index 00000000000..a6bcde51a7e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import OrganizationKeyInput from '../OrganizationKeyInput';
+import { getOrganization } from '../../../../../api/organizations';
+import { waitAndUpdate } from '../../../../../helpers/testUtils';
+
+jest.mock('../../../../../api/organizations', () => ({
+ getOrganization: jest.fn().mockResolvedValue(undefined)
+}));
+
+beforeEach(() => {
+ (getOrganization as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ const wrapper = shallow(<OrganizationKeyInput initialValue="key" onChange={jest.fn()} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should not display any status when the key is not defined', async () => {
+ const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(false);
+ expect(wrapper.find('ValidationInput').prop('isValid')).toBe(false);
+});
+
+it('should have an error when the key is invalid', async () => {
+ const wrapper = shallow(
+ <OrganizationKeyInput initialValue="KEy-with#speci@l_char" onChange={jest.fn()} />
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
+
+it('should have an error when the key already exists', async () => {
+ (getOrganization as jest.Mock<any>).mockResolvedValue({});
+ const wrapper = shallow(<OrganizationKeyInput initialValue="" onChange={jest.fn()} />);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx
index 142f6e94f67..ecbfdb1f190 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx
@@ -19,22 +19,19 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { createStore } from 'redux';
-import { withCurrentUser } from '../withCurrentUser';
-import { CurrentUser } from '../../../../app/types';
+import OrganizationNameInput from '../OrganizationNameInput';
-class X extends React.Component<{ currentUser: CurrentUser }> {
- render() {
- return <div />;
- }
-}
-
-const UnderTest = withCurrentUser(X);
+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 pass logged in user', () => {
- const currentUser = { isLoggedIn: false };
- const store = createStore(state => state, { users: { currentUser } });
- const wrapper = shallow(<UnderTest />, { context: { store } });
- expect(wrapper.dive().type()).toBe(X);
- expect(wrapper.dive().prop('currentUser')).toBe(currentUser);
+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/projects/create/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx
index ed3f0b178fb..357a912eda1 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/utils.ts
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx
@@ -17,36 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { memoize } from 'lodash';
-import {
- cleanQuery,
- RawQuery,
- parseAsBoolean,
- serializeOptionalBoolean,
- parseAsOptionalString,
- serializeString
-} from '../../../helpers/query';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import OrganizationUrlInput from '../OrganizationUrlInput';
-export interface Query {
- error?: string;
- manual: boolean;
- organization?: string;
-}
+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();
+});
-export const parseQuery = memoize(
- (urlQuery: RawQuery): Query => {
- return {
- error: parseAsOptionalString(urlQuery['error']),
- manual: parseAsBoolean(urlQuery['manual'], false),
- organization: parseAsOptionalString(urlQuery['organization'])
- };
- }
-);
-
-export const serializeQuery = memoize(
- (query: Query): RawQuery =>
- cleanQuery({
- manual: serializeOptionalBoolean(query.manual || undefined),
- organization: serializeString(query.organization)
- })
-);
+it('should have an error when the url is invalid', () => {
+ expect(
+ shallow(<OrganizationUrlInput initialValue="whatever" onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap
new file mode 100644
index 00000000000..292c7b24b87
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the fallback avatar when there is no url 1`] = `
+<ValidationInput
+ description="onboarding.create_organization.avatar.description"
+ id="organization-avatar"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.avatar"
+>
+ <OrganizationAvatar
+ className="display-block spacer-bottom"
+ organization={
+ Object {
+ "avatar": undefined,
+ "name": "Luke Skywalker",
+ }
+ }
+ />
+ <input
+ className="input-super-large text-middle"
+ id="organization-display-name"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value=""
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ description="onboarding.create_organization.avatar.description"
+ id="organization-avatar"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.avatar"
+>
+ <OrganizationAvatar
+ className="display-block spacer-bottom"
+ organization={
+ Object {
+ "avatar": "https://my.avatar",
+ "name": "",
+ }
+ }
+ />
+ <input
+ className="input-super-large text-middle"
+ id="organization-display-name"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="https://my.avatar"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap
new file mode 100644
index 00000000000..80e11c04f11
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ id="organization-display-name"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.description"
+>
+ <textarea
+ className="input-super-large text-middle"
+ id="organization-description"
+ maxLength={256}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ rows={3}
+ value="My description"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
new file mode 100644
index 00000000000..8cba7d969a3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ id="organization-key"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.organization_name"
+ required={true}
+>
+ <div
+ className="display-inline-flex-baseline"
+ >
+ <span
+ className="little-spacer-right"
+ >
+ localhost/organizations/
+ </span>
+ <input
+ autoFocus={true}
+ className="input-super-large text-middle"
+ id="organization-key"
+ maxLength={255}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="key"
+ />
+ </div>
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap
new file mode 100644
index 00000000000..1af9dc98684
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ description="onboarding.create_organization.display_name.description"
+ id="organization-display-name"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.display_name"
+>
+ <input
+ className="input-super-large text-middle"
+ id="organization-display-name"
+ maxLength={255}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="Org Name"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap
new file mode 100644
index 00000000000..d3f571b4db8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ id="organization-url"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.url"
+>
+ <input
+ className="input-super-large text-middle"
+ id="organization-url"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="http://my.website"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx b/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
deleted file mode 100644
index be69f2c4361..00000000000
--- a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { withRouter, WithRouterProps } from 'react-router';
-import { withCurrentUser } from './withCurrentUser';
-import { CurrentUser } from '../../../app/types';
-import { isLoggedIn } from '../../../helpers/users';
-
-export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
- const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
-
- class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> {
- static displayName = `whenLoggedIn(${wrappedDisplayName})`;
-
- componentDidMount() {
- if (!isLoggedIn(this.props.currentUser)) {
- const returnTo = window.location.pathname + window.location.search + window.location.hash;
- this.props.router.replace({
- pathname: '/sessions/new',
- query: { return_to: returnTo } // eslint-disable-line camelcase
- });
- }
- }
-
- render() {
- if (isLoggedIn(this.props.currentUser)) {
- return <WrappedComponent {...this.props} />;
- } else {
- return null;
- }
- }
- }
-
- return withCurrentUser(withRouter(Wrapper));
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
index 8fd083fb222..8fd083fb222 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
diff --git a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx
new file mode 100644
index 00000000000..4460f222b8f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import RemoteRepositories from './RemoteRepositories';
+import OrganizationSelect from './OrganizationSelect';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { AlmApplication, Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ almApplication: AlmApplication;
+ boundOrganizations: Organization[];
+ onProjectCreate: (projectKeys: string[]) => void;
+ organization?: string;
+}
+
+interface State {
+ selectedOrganization: string;
+}
+
+export default class AutoProjectCreate extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = { selectedOrganization: this.getInitialSelectedOrganization(props) };
+ }
+
+ getInitialSelectedOrganization(props: Props) {
+ const organization =
+ props.organization && props.boundOrganizations.find(o => o.key === props.organization);
+ if (organization) {
+ return organization.key;
+ }
+ if (props.boundOrganizations.length === 1) {
+ return props.boundOrganizations[0].key;
+ }
+ return '';
+ }
+
+ handleOrganizationSelect = ({ key }: Organization) => {
+ this.setState({ selectedOrganization: key });
+ };
+
+ render() {
+ const { almApplication, boundOrganizations, onProjectCreate } = this.props;
+
+ if (boundOrganizations.length === 0) {
+ return (
+ <>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={almApplication}
+ small={true}
+ url={almApplication.installationUrl}>
+ {translate(
+ 'onboarding.create_organization.choose_organization_button',
+ almApplication.key
+ )}
+ </IdentityProviderLink>
+ </>
+ );
+ }
+
+ const { selectedOrganization } = this.state;
+ return (
+ <>
+ <OrganizationSelect
+ autoImport={true}
+ onChange={this.handleOrganizationSelect}
+ organization={selectedOrganization}
+ organizations={this.props.boundOrganizations}
+ />
+ {selectedOrganization && (
+ <RemoteRepositories
+ almApplication={almApplication}
+ onProjectCreate={onProjectCreate}
+ organization={selectedOrganization}
+ />
+ )}
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index f45dccd05d7..d46f7bbaa36 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -20,70 +20,64 @@
import * as React from 'react';
import * as classNames from 'classnames';
import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
-import { Location } from 'history';
+import { WithRouterProps } from 'react-router';
import Helmet from 'react-helmet';
import AutoProjectCreate from './AutoProjectCreate';
import ManualProjectCreate from './ManualProjectCreate';
-import { serializeQuery, Query, parseQuery } from './utils';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tabs from '../../../components/controls/Tabs';
-import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
+import { fetchMyOrganizations } from '../../account/organizations/actions';
+import { getMyOrganizations, Store } from '../../../store/rootReducer';
import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
-import { CurrentUser, IdentityProvider, LoggedInUser } from '../../../app/types';
-import { skipOnboarding, getIdentityProviders } from '../../../api/users';
+import { LoggedInUser, AlmApplication, Organization } from '../../../app/types';
+import { getAlmAppInfo } from '../../../api/alm-integration';
+import { skipOnboarding } from '../../../api/users';
import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
-import { isLoggedIn } from '../../../helpers/users';
import '../../../app/styles/sonarcloud.css';
-interface OwnProps {
- location: Location;
- router: Pick<InjectedRouter, 'push' | 'replace'>;
-}
-
interface StateProps {
- currentUser: CurrentUser;
+ userOrganizations: Organization[];
}
-interface DispatchProps {
- addGlobalErrorMessage: (message: string) => void;
+interface Props {
+ currentUser: LoggedInUser;
+ fetchMyOrganizations: () => Promise<void>;
skipOnboardingAction: () => void;
}
-type Props = StateProps & DispatchProps & OwnProps;
-
interface State {
- identityProvider?: IdentityProvider;
+ almApplication?: AlmApplication;
loading: boolean;
}
-export class CreateProjectPage extends React.PureComponent<Props, State> {
+type TabKeys = 'auto' | 'manual';
+
+interface LocationState {
+ organization?: string;
+ tab?: TabKeys;
+}
+
+export class CreateProjectPage extends React.PureComponent<
+ Props & StateProps & WithRouterProps,
+ State
+> {
mounted = false;
state: State = { loading: true };
componentDidMount() {
- if (isLoggedIn(this.props.currentUser)) {
- this.mounted = true;
- const query = parseQuery(this.props.location.query);
- if (query.error) {
- this.props.addGlobalErrorMessage(query.error);
- }
- if (!hasAdvancedALMIntegration(this.props.currentUser)) {
- this.setState({ loading: false });
- this.updateQuery({ manual: true });
- } else {
- this.fetchIdentityProviders();
- }
- document.body.classList.add('white-page');
- if (document.documentElement) {
- document.documentElement.classList.add('white-page');
- }
+ this.mounted = true;
+ this.props.fetchMyOrganizations();
+ if (hasAdvancedALMIntegration(this.props.currentUser)) {
+ this.fetchAlmApplication();
} else {
- handleRequiredAuthentication();
+ this.setState({ loading: false });
+ }
+ document.body.classList.add('white-page');
+ if (document.documentElement) {
+ document.documentElement.classList.add('white-page');
}
}
@@ -105,17 +99,11 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
}
};
- fetchIdentityProviders = () => {
- getIdentityProviders().then(
- ({ identityProviders }) => {
+ fetchAlmApplication = () => {
+ return getAlmAppInfo().then(
+ ({ application }) => {
if (this.mounted) {
- this.setState({
- identityProvider: identityProviders.find(
- identityProvider =>
- identityProvider.key === (this.props.currentUser as LoggedInUser).externalProvider
- ),
- loading: false
- });
+ this.setState({ almApplication: application, loading: false });
}
},
() => {
@@ -126,28 +114,25 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
);
};
- onTabChange = (tab: 'auto' | 'manual') => {
- this.updateQuery({ manual: tab === 'manual' });
+ onTabChange = (tab: TabKeys) => {
+ this.updateUrl({ tab });
};
- updateQuery = (changes: Partial<Query>) => {
+ updateUrl = (state: Partial<LocationState> = {}) => {
this.props.router.replace({
pathname: this.props.location.pathname,
- query: serializeQuery({ ...parseQuery(this.props.location.query), ...changes })
+ query: this.props.location.query,
+ state: { ...(this.props.location.state || {}), ...state }
});
};
render() {
- const { currentUser } = this.props;
-
- if (!isLoggedIn(currentUser)) {
- return null;
- }
-
- const { identityProvider, loading } = this.state;
- const query = parseQuery(this.props.location.query);
+ const { currentUser, location, userOrganizations } = this.props;
+ const { almApplication, loading } = this.state;
+ const state: LocationState = location.state || {};
const header = translate('onboarding.create_project.header');
- const hasAutoProvisioning = hasAdvancedALMIntegration(currentUser) && identityProvider;
+ const showManualTab = state.tab === 'manual';
+
return (
<>
<Helmet title={header} titleTemplate="%s" />
@@ -159,10 +144,10 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
<DeferredSpinner />
) : (
<>
- {hasAutoProvisioning && (
- <Tabs
+ {almApplication && (
+ <Tabs<TabKeys>
onChange={this.onTabChange}
- selected={query.manual ? 'manual' : 'auto'}
+ selected={state.tab || 'auto'}
tabs={[
{
key: 'auto',
@@ -171,7 +156,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
{translate('onboarding.create_project.select_repositories')}
<span
className={classNames('beta-badge spacer-left', {
- 'is-muted': query.manual
+ 'is-muted': showManualTab
})}>
{translate('beta')}
</span>
@@ -183,16 +168,19 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
/>
)}
- {query.manual || !hasAutoProvisioning || !identityProvider ? (
+ {showManualTab || !almApplication ? (
<ManualProjectCreate
currentUser={currentUser}
onProjectCreate={this.handleProjectCreate}
- organization={query.organization}
+ organization={state.organization}
+ userOrganizations={userOrganizations}
/>
) : (
<AutoProjectCreate
- identityProvider={identityProvider}
+ almApplication={almApplication}
+ boundOrganizations={userOrganizations.filter(o => o.almId)}
onProjectCreate={this.handleProjectCreate}
+ organization={state.organization}
/>
)}
</>
@@ -203,13 +191,20 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
}
}
-const mapStateToProps = (state: Store): StateProps => ({
- currentUser: getCurrentUser(state)
-});
+const mapDispatchToProps = {
+ fetchMyOrganizations,
+ skipOnboardingAction
+};
-const mapDispatchToProps: DispatchProps = { addGlobalErrorMessage, skipOnboardingAction };
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(CreateProjectPage);
+const mapStateToProps = (state: Store) => {
+ return {
+ userOrganizations: getMyOrganizations(state)
+ };
+};
+
+export default whenLoggedIn(
+ connect<StateProps>(
+ mapStateToProps,
+ mapDispatchToProps
+ )(CreateProjectPage)
+);
diff --git a/server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
index 1101f6484ef..2820af7c8dc 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
@@ -18,34 +18,20 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { sortBy } from 'lodash';
-import { connect } from 'react-redux';
-import { Link } from 'react-router';
-import Select from '../../../components/controls/Select';
+import OrganizationSelect from './OrganizationSelect';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { SubmitButton } from '../../../components/ui/buttons';
import { LoggedInUser, Organization } from '../../../app/types';
-import { fetchMyOrganizations } from '../../account/organizations/actions';
-import { getMyOrganizations, Store } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import { createProject } from '../../../api/components';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-
-interface StateProps {
- userOrganizations: Organization[];
-}
-interface DispatchProps {
- fetchMyOrganizations: () => Promise<void>;
-}
-
-interface OwnProps {
+interface Props {
currentUser: LoggedInUser;
onProjectCreate: (projectKeys: string[]) => void;
organization?: string;
+ userOrganizations: Organization[];
}
-type Props = OwnProps & StateProps & DispatchProps;
-
interface State {
projectName: string;
projectKey: string;
@@ -53,7 +39,7 @@ interface State {
submitting: boolean;
}
-export class ManualProjectCreate extends React.PureComponent<Props, State> {
+export default class ManualProjectCreate extends React.PureComponent<Props, State> {
mounted = false;
constructor(props: Props) {
@@ -105,8 +91,8 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> {
}
};
- handleOrganizationSelect = ({ value }: { value: string }) => {
- this.setState({ selectedOrganization: value });
+ handleOrganizationSelect = ({ key }: Organization) => {
+ this.setState({ selectedOrganization: key });
};
handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -127,30 +113,11 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> {
return (
<>
<form onSubmit={this.handleFormSubmit}>
- <div className="form-field">
- <label htmlFor="select-organization">
- {translate('onboarding.create_project.organization')}
- <em className="mandatory">*</em>
- </label>
- <Select
- autoFocus={true}
- className="input-super-large"
- clearable={false}
- id="select-organization"
- onChange={this.handleOrganizationSelect}
- options={sortBy(this.props.userOrganizations, o => o.name.toLowerCase()).map(
- organization => ({
- label: organization.name,
- value: organization.key
- })
- )}
- required={true}
- value={this.state.selectedOrganization}
- />
- <Link className="big-spacer-left js-new-org" to="/create-organization">
- {translate('onboarding.create_project.create_new_org')}
- </Link>
- </div>
+ <OrganizationSelect
+ onChange={this.handleOrganizationSelect}
+ organization={this.state.selectedOrganization}
+ organizations={this.props.userOrganizations}
+ />
<div className="form-field">
<label htmlFor="project-name">
{translate('onboarding.create_project.project_name')}
@@ -192,17 +159,3 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> {
);
}
}
-
-const mapDispatchToProps = ({
- fetchMyOrganizations
-} as any) as DispatchProps;
-
-const mapStateToProps = (state: Store): StateProps => {
- return {
- userOrganizations: getMyOrganizations(state)
- };
-};
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ManualProjectCreate);
diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
new file mode 100644
index 00000000000..a1d52e0af64
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import { sortBy } from 'lodash';
+import Select from '../../../components/controls/Select';
+import { Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+ autoImport?: boolean;
+ onChange: (organization: Organization) => void;
+ organization: string;
+ organizations: Organization[];
+}
+
+export default function OrganizationSelect({
+ autoImport,
+ onChange,
+ organization,
+ organizations
+}: Props) {
+ return (
+ <div className="form-field spacer-bottom">
+ <label htmlFor="select-organization">
+ {translate('onboarding.create_project.organization')}
+ <em className="mandatory">*</em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ labelKey="name"
+ onChange={onChange}
+ optionRenderer={optionRenderer}
+ options={sortBy(organizations, o => o.name.toLowerCase())}
+ required={true}
+ value={organization}
+ valueKey="key"
+ valueRenderer={optionRenderer}
+ />
+ <Link className="big-spacer-left js-new-org" to="/create-organization">
+ {autoImport
+ ? translate('onboarding.create_project.import_new_org')
+ : translate('onboarding.create_project.create_new_org')}
+ </Link>
+ </div>
+ );
+}
+
+export function optionRenderer(organization: Organization) {
+ return (
+ <span>
+ {organization.almId && (
+ <img
+ alt={organization.almId}
+ className="spacer-right"
+ height={14}
+ src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`}
+ />
+ )}
+ {organization.name}
+ <span className="note little-spacer-left">{organization.key}</span>
+ </span>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx
index 74677605d41..66f3aef3f0e 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx
@@ -20,50 +20,55 @@
import * as React from 'react';
import AlmRepositoryItem from './AlmRepositoryItem';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
import { getRepositories, provisionProject } from '../../../api/alm-integration';
-import { IdentityProvider, AlmRepository } from '../../../app/types';
+import { AlmApplication, AlmRepository } from '../../../app/types';
import { SubmitButton } from '../../../components/ui/buttons';
-import { translateWithParameters, translate } from '../../../helpers/l10n';
-import { Alert } from '../../../components/ui/Alert';
+import { translate } from '../../../helpers/l10n';
interface Props {
- identityProvider: IdentityProvider;
+ almApplication: AlmApplication;
onProjectCreate: (projectKeys: string[]) => void;
+ organization: string;
}
+type SelectedRepositories = { [key: string]: AlmRepository | undefined };
+
interface State {
- installationUrl?: string;
- installed?: boolean;
loading: boolean;
repositories: AlmRepository[];
- selectedRepositories: { [key: string]: AlmRepository | undefined };
+ selectedRepositories: SelectedRepositories;
submitting: boolean;
}
-export default class AutoProjectCreate extends React.PureComponent<Props, State> {
+export default class RemoteRepositories extends React.PureComponent<Props, State> {
mounted = false;
- state: State = {
- loading: true,
- repositories: [],
- selectedRepositories: {},
- submitting: false
- };
+ state: State = { loading: true, repositories: [], selectedRepositories: {}, submitting: false };
componentDidMount() {
this.mounted = true;
this.fetchRepositories();
}
+ componentDidUpdate(prevProps: Props) {
+ const { organization } = this.props;
+ if (prevProps.organization !== organization) {
+ this.setState({ loading: true });
+ this.fetchRepositories();
+ }
+ }
+
componentWillUnmount() {
this.mounted = false;
}
fetchRepositories = () => {
- getRepositories().then(
- ({ almIntegration, repositories }) => {
+ const { organization } = this.props;
+ return getRepositories({
+ organization
+ }).then(
+ ({ repositories }) => {
if (this.mounted) {
- this.setState({ ...almIntegration, loading: false, repositories });
+ this.setState({ loading: false, repositories });
}
},
() => {
@@ -83,19 +88,32 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
provisionProject({
installationKeys: Object.keys(selectedRepositories).filter(key =>
Boolean(selectedRepositories[key])
- )
+ ),
+ organization: this.props.organization
}).then(
({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
- () => {
- if (this.mounted) {
- this.setState({ loading: true, submitting: false });
- this.fetchRepositories();
- }
- }
+ this.handleProvisionFail
);
}
};
+ handleProvisionFail = () => {
+ return this.fetchRepositories().then(() => {
+ if (this.mounted) {
+ this.setState(({ repositories, selectedRepositories }) => {
+ const updateSelectedRepositories: SelectedRepositories = {};
+ Object.keys(selectedRepositories).forEach(installationKey => {
+ const newRepository = repositories.find(r => r.installationKey === installationKey);
+ if (newRepository && !newRepository.linkedProjectKey) {
+ updateSelectedRepositories[newRepository.installationKey] = newRepository;
+ }
+ });
+ return { selectedRepositories: updateSelectedRepositories, submitting: false };
+ });
+ }
+ });
+ };
+
isValid = () => {
return this.state.repositories.some(repo =>
Boolean(this.state.selectedRepositories[repo.installationKey])
@@ -113,68 +131,32 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
}));
};
- renderContent = () => {
- const { identityProvider } = this.props;
- const { selectedRepositories, submitting } = this.state;
-
- if (this.state.installed) {
- return (
+ render() {
+ const { loading, selectedRepositories, submitting } = this.state;
+ const { almApplication } = this.props;
+ return (
+ <DeferredSpinner loading={loading}>
<form onSubmit={this.handleFormSubmit}>
- <ul>
- {this.state.repositories.map(repo => (
- <li className="big-spacer-bottom" key={repo.installationKey}>
- <AlmRepositoryItem
- identityProvider={identityProvider}
- repository={repo}
- selected={Boolean(selectedRepositories[repo.installationKey])}
- toggleRepository={this.toggleRepository}
- />
- </li>
- ))}
- </ul>
+ <div className="form-field">
+ <ul>
+ {this.state.repositories.map(repo => (
+ <li className="big-spacer-bottom" key={repo.installationKey}>
+ <AlmRepositoryItem
+ identityProvider={almApplication}
+ repository={repo}
+ selected={Boolean(selectedRepositories[repo.installationKey])}
+ toggleRepository={this.toggleRepository}
+ />
+ </li>
+ ))}
+ </ul>
+ </div>
<SubmitButton disabled={!this.isValid() || submitting}>
{translate('create')}
</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>
- );
- }
- return (
- <div>
- <p className="spacer-bottom">
- {translateWithParameters(
- 'onboarding.create_project.install_app_x',
- identityProvider.name
- )}
- </p>
- <IdentityProviderLink
- className="display-inline-block"
- identityProvider={identityProvider}
- small={true}
- url={this.state.installationUrl}>
- {translateWithParameters(
- 'onboarding.create_project.install_app_x.button',
- identityProvider.name
- )}
- </IdentityProviderLink>
- </div>
- );
- };
-
- render() {
- const { identityProvider } = this.props;
- const { loading } = this.state;
-
- return (
- <>
- <Alert className="width-60 big-spacer-bottom" variant="info">
- {translateWithParameters(
- 'onboarding.create_project.beta_feature_x',
- identityProvider.name
- )}
- </Alert>
- {loading ? <DeferredSpinner /> : this.renderContent()}
- </>
+ </DeferredSpinner>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/AlmRepositoryItem-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx
index 72b25cb2815..72b25cb2815 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/AlmRepositoryItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
new file mode 100644
index 00000000000..3364a73a344
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import AutoProjectCreate from '../AutoProjectCreate';
+
+const almApplication = {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.installation.url',
+ key: 'github',
+ name: 'GitHub'
+};
+
+it('should display the provider app install button', () => {
+ expect(shallowRender({ boundOrganizations: [] })).toMatchSnapshot();
+});
+
+it('should display the bounded organizations dropdown with the list of repositories', () => {
+ expect(shallowRender({ organization: 'foo' })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<AutoProjectCreate['props']> = {}) {
+ return shallow(
+ <AutoProjectCreate
+ almApplication={almApplication}
+ boundOrganizations={[
+ { almId: 'github', key: 'foo', name: 'Foo' },
+ { almId: 'github', key: 'bar', name: 'Bar' }
+ ]}
+ onProjectCreate={jest.fn()}
+ organization=""
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
index fba7c5875c7..6c9acb9d77c 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
@@ -19,22 +19,20 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { Location } from 'history';
import { CreateProjectPage } from '../CreateProjectPage';
-import { getIdentityProviders } from '../../../../api/users';
import { LoggedInUser } from '../../../../app/types';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { waitAndUpdate, mockRouter } from '../../../../helpers/testUtils';
+import { getAlmAppInfo } from '../../../../api/alm-integration';
-jest.mock('../../../../api/users', () => ({
- getIdentityProviders: jest.fn().mockResolvedValue({
- identityProviders: [
- {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'github',
- name: 'GitHub'
- }
- ]
+jest.mock('../../../../api/alm-integration', () => ({
+ getAlmAppInfo: jest.fn().mockResolvedValue({
+ application: {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.installation.url',
+ key: 'github',
+ name: 'GitHub'
+ }
})
}));
@@ -48,7 +46,7 @@ const user: LoggedInUser = {
};
beforeEach(() => {
- (getIdentityProviders as jest.Mock<any>).mockClear();
+ (getAlmAppInfo as jest.Mock<any>).mockClear();
});
it('should render correctly', async () => {
@@ -73,28 +71,25 @@ it('should switch tabs', async () => {
expect(wrapper).toMatchSnapshot();
wrapper.find('Tabs').prop<Function>('onChange')('manual');
- expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
+ expect(wrapper.find('ManualProjectCreate').exists()).toBeTruthy();
wrapper.find('Tabs').prop<Function>('onChange')('auto');
expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
});
-it('should display an error message on load', () => {
- const addGlobalErrorMessage = jest.fn();
- getWrapper({
- addGlobalErrorMessage,
- location: { pathname: 'foo', query: { error: 'Foo error' } }
- });
- expect(addGlobalErrorMessage).toHaveBeenCalledWith('Foo error');
-});
-
function getWrapper(props = {}) {
return shallow(
<CreateProjectPage
addGlobalErrorMessage={jest.fn()}
currentUser={user}
- location={{ pathname: 'foo', query: { manual: 'false' } } as Location}
- router={{ push: jest.fn(), replace: jest.fn() }}
+ fetchMyOrganizations={jest.fn()}
+ // @ts-ignore avoid passing everything from WithRouterProps
+ location={{}}
+ router={mockRouter()}
skipOnboardingAction={jest.fn()}
+ userOrganizations={[
+ { key: 'foo', name: 'Foo' },
+ { almId: 'github', key: 'bar', name: 'Bar' }
+ ]}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
index 5777d22aaaa..52d56a87e38 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { ManualProjectCreate } from '../ManualProjectCreate';
+import ManualProjectCreate from '../ManualProjectCreate';
import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
import { createProject } from '../../../../api/components';
@@ -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('Select').prop<Function>('onChange')({ value: 'foo' });
+ wrapper.find('OrganizationSelect').prop<Function>('onChange')({ key: 'foo' });
change(wrapper.find('#project-name'), 'Bar');
expect(wrapper.find('SubmitButton')).toMatchSnapshot();
@@ -56,7 +56,6 @@ function getWrapper(props = {}) {
return shallow(
<ManualProjectCreate
currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
- fetchMyOrganizations={jest.fn()}
onProjectCreate={jest.fn()}
userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
{...props}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
index 4ddeac5d913..4224b152a38 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx
@@ -19,37 +19,31 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import OrganizationDetailsInput from '../OrganizationDetailsInput';
+import OrganizationSelect, { optionRenderer } from '../OrganizationSelect';
-it('should render', () => {
- const render = jest.fn().mockReturnValue(<div />);
+const organizations = [{ key: 'foo', name: 'Foo' }, { almId: 'github', key: 'bar', name: 'Bar' }];
+
+it('should render correctly', () => {
expect(
shallow(
- <OrganizationDetailsInput
- dirty={true}
- error="This field is bad!"
- id="field"
- isSubmitting={true}
- isValidating={false}
- label="Label"
- name="field"
- onBlur={jest.fn()}
- onChange={jest.fn()}
- required={true}
- touched={true}
- value="foo">
- {render}
- </OrganizationDetailsInput>
+ <OrganizationSelect onChange={jest.fn()} organization="bar" organizations={organizations} />
)
).toMatchSnapshot();
- expect(render).toBeCalledWith(
- expect.objectContaining({
- className: 'input-super-large text-middle is-invalid',
- disabled: true,
- id: 'field',
- name: 'field',
- type: 'text',
- value: 'foo'
- })
- );
+ expect(
+ shallow(
+ <OrganizationSelect
+ autoImport={true}
+ onChange={jest.fn()}
+ organization="bar"
+ organizations={organizations}
+ />
+ )
+ .find('.js-new-org')
+ .contains('onboarding.create_project.import_new_org')
+ ).toBe(true);
+});
+
+it('should render options correctly', () => {
+ expect(shallow(optionRenderer(organizations[0]))).toMatchSnapshot();
+ expect(shallow(optionRenderer(organizations[1]))).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
new file mode 100644
index 00000000000..eefe1881c22
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import RemoteRepositories from '../RemoteRepositories';
+import { getRepositories, provisionProject } from '../../../../api/alm-integration';
+import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/alm-integration', () => ({
+ getRepositories: jest.fn().mockResolvedValue({
+ repositories: [
+ {
+ label: 'Cool Project',
+ installationKey: 'github/cool',
+ linkedProjectKey: 'proj_cool',
+ linkedProjectName: 'Proj Cool'
+ },
+ {
+ label: 'Awesome Project',
+ installationKey: 'github/awesome'
+ }
+ ]
+ }),
+ provisionProject: jest.fn().mockResolvedValue({ projects: [{ projectKey: 'awesome' }] })
+}));
+
+const almApplication = {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.installation.url',
+ key: 'github',
+ name: 'GitHub'
+};
+
+beforeEach(() => {
+ (getRepositories as jest.Mock<any>).mockClear();
+ (provisionProject as jest.Mock<any>).mockClear();
+});
+
+it('should display the list of repositories', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ await waitAndUpdate(wrapper);
+ expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should correctly create a project', async () => {
+ const onProjectCreate = jest.fn();
+ const wrapper = shallowRender({ onProjectCreate });
+ (wrapper.instance() as RemoteRepositories).toggleRepository({
+ label: 'Awesome Project',
+ installationKey: 'github/awesome'
+ });
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+ submit(wrapper.find('form'));
+ expect(provisionProject).toBeCalledWith({
+ installationKeys: ['github/awesome'],
+ organization: 'sonarsource'
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(onProjectCreate).toBeCalledWith(['awesome']);
+});
+
+function shallowRender(props: Partial<RemoteRepositories['props']> = {}) {
+ return shallow(
+ <RemoteRepositories
+ almApplication={almApplication}
+ onProjectCreate={jest.fn()}
+ organization="sonarsource"
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
index 7ed1eedfa61..7ed1eedfa61 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..147427d62a6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the bounded organizations dropdown with the list of repositories 1`] = `
+<Fragment>
+ <OrganizationSelect
+ autoImport={true}
+ onChange={[Function]}
+ organization="foo"
+ organizations={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ />
+ <RemoteRepositories
+ almApplication={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ onProjectCreate={[MockFunction]}
+ organization="foo"
+ />
+</Fragment>
+`;
+
+exports[`should display the provider app install button 1`] = `
+<Fragment>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ small={true}
+ url="https://alm.installation.url"
+ >
+ onboarding.create_organization.choose_organization_button.github
+ </IdentityProviderLink>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index 69731b0ff8e..6e1f9059e89 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -71,14 +71,24 @@ exports[`should render correctly 2`] = `
}
/>
<AutoProjectCreate
- identityProvider={
+ almApplication={
Object {
"backgroundColor": "blue",
"iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
"key": "github",
"name": "GitHub",
}
}
+ boundOrganizations={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
onProjectCreate={[Function]}
/>
</div>
@@ -105,7 +115,7 @@ exports[`should render with Manual creation only 1`] = `
onboarding.create_project.header
</h1>
</header>
- <Connect(ManualProjectCreate)
+ <ManualProjectCreate
currentUser={
Object {
"externalProvider": "microsoft",
@@ -117,6 +127,19 @@ exports[`should render with Manual creation only 1`] = `
}
}
onProjectCreate={[Function]}
+ userOrganizations={
+ Array [
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
/>
</div>
</Fragment>
@@ -166,14 +189,24 @@ exports[`should switch tabs 1`] = `
}
/>
<AutoProjectCreate
- identityProvider={
+ almApplication={
Object {
"backgroundColor": "blue",
"iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
"key": "github",
"name": "GitHub",
}
}
+ boundOrganizations={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
onProjectCreate={[Function]}
/>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
index 6993ed112b6..53fde97ce31 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
@@ -21,49 +21,22 @@ exports[`should render correctly 1`] = `
<form
onSubmit={[Function]}
>
- <div
- className="form-field"
- >
- <label
- htmlFor="select-organization"
- >
- onboarding.create_project.organization
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <Select
- autoFocus={true}
- className="input-super-large"
- clearable={false}
- id="select-organization"
- onChange={[Function]}
- options={
- Array [
- Object {
- "label": "Bar",
- "value": "bar",
- },
- Object {
- "label": "Foo",
- "value": "foo",
- },
- ]
- }
- required={true}
- value=""
- />
- <Link
- className="big-spacer-left js-new-org"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/create-organization"
- >
- onboarding.create_project.create_new_org
- </Link>
- </div>
+ <OrganizationSelect
+ onChange={[Function]}
+ organization=""
+ organizations={
+ Array [
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ />
<div
className="form-field"
>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
new file mode 100644
index 00000000000..50cd939ec7e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="form-field spacer-bottom"
+>
+ <label
+ htmlFor="select-organization"
+ >
+ onboarding.create_project.organization
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ labelKey="name"
+ onChange={[MockFunction]}
+ optionRenderer={[Function]}
+ options={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ ]
+ }
+ required={true}
+ value="bar"
+ valueKey="key"
+ valueRenderer={[Function]}
+ />
+ <Link
+ className="big-spacer-left js-new-org"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/create-organization"
+ >
+ onboarding.create_project.create_new_org
+ </Link>
+</div>
+`;
+
+exports[`should render options correctly 1`] = `
+<span>
+ Foo
+ <span
+ className="note little-spacer-left"
+ >
+ foo
+ </span>
+</span>
+`;
+
+exports[`should render options correctly 2`] = `
+<span>
+ <img
+ alt="github"
+ className="spacer-right"
+ height={14}
+ src="/images/sonarcloud/github.svg"
+ />
+ Bar
+ <span
+ className="note little-spacer-left"
+ >
+ bar
+ </span>
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
new file mode 100644
index 00000000000..01359b6a9f9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
@@ -0,0 +1,114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+ disabled={false}
+>
+ create
+</SubmitButton>
+`;
+
+exports[`should display the list of repositories 1`] = `
+<DeferredSpinner
+ loading={true}
+ timeout={100}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <ul />
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ create
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</DeferredSpinner>
+`;
+
+exports[`should display the list of repositories 2`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <ul>
+ <li
+ className="big-spacer-bottom"
+ key="github/cool"
+ >
+ <AlmRepositoryItem
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ repository={
+ Object {
+ "installationKey": "github/cool",
+ "label": "Cool Project",
+ "linkedProjectKey": "proj_cool",
+ "linkedProjectName": "Proj Cool",
+ }
+ }
+ selected={false}
+ toggleRepository={[Function]}
+ />
+ </li>
+ <li
+ className="big-spacer-bottom"
+ key="github/awesome"
+ >
+ <AlmRepositoryItem
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ repository={
+ Object {
+ "installationKey": "github/awesome",
+ "label": "Awesome Project",
+ }
+ }
+ selected={false}
+ toggleRepository={[Function]}
+ />
+ </li>
+ </ul>
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ create
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</DeferredSpinner>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx
deleted file mode 100644
index 91e189f4204..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import AutoProjectCreate from '../AutoProjectCreate';
-import { getRepositories } from '../../../../api/alm-integration';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-
-jest.mock('../../../../api/alm-integration', () => ({
- getRepositories: jest.fn().mockResolvedValue({
- almIntegration: {
- installationUrl: 'https://alm.foo.com/install',
- installed: false
- },
- repositories: []
- }),
- provisionProject: jest.fn().mockResolvedValue({ projects: [] })
-}));
-
-const identityProvider = {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'foo',
- name: 'Foo Provider'
-};
-
-const repositories = [
- {
- label: 'Cool Project',
- installationKey: 'github/cool',
- linkedProjectKey: 'proj_cool',
- linkedProjectName: 'Proj Cool'
- },
- {
- label: 'Awesome Project',
- installationKey: 'github/awesome'
- }
-];
-
-beforeEach(() => {
- (getRepositories as jest.Mock<any>).mockClear();
-});
-
-it('should display the provider app install button', async () => {
- const wrapper = getWrapper();
- expect(wrapper).toMatchSnapshot();
- expect(getRepositories).toHaveBeenCalled();
-
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should display the list of repositories', async () => {
- (getRepositories as jest.Mock<any>).mockResolvedValue({
- almIntegration: {
- installationUrl: 'https://alm.foo.com/install',
- installed: true
- },
- repositories
- });
- const wrapper = getWrapper();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <AutoProjectCreate identityProvider={identityProvider} onProjectCreate={jest.fn()} {...props} />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
deleted file mode 100644
index 619285b02fa..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
+++ /dev/null
@@ -1,123 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display the list of repositories 1`] = `
-<Fragment>
- <Alert
- className="width-60 big-spacer-bottom"
- variant="info"
- >
- onboarding.create_project.beta_feature_x.Foo Provider
- </Alert>
- <form
- onSubmit={[Function]}
- >
- <ul>
- <li
- className="big-spacer-bottom"
- key="github/cool"
- >
- <AlmRepositoryItem
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- }
- }
- repository={
- Object {
- "installationKey": "github/cool",
- "label": "Cool Project",
- "linkedProjectKey": "proj_cool",
- "linkedProjectName": "Proj Cool",
- }
- }
- selected={false}
- toggleRepository={[Function]}
- />
- </li>
- <li
- className="big-spacer-bottom"
- key="github/awesome"
- >
- <AlmRepositoryItem
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- }
- }
- repository={
- Object {
- "installationKey": "github/awesome",
- "label": "Awesome Project",
- }
- }
- selected={false}
- toggleRepository={[Function]}
- />
- </li>
- </ul>
- <SubmitButton
- disabled={true}
- >
- create
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- timeout={100}
- />
- </form>
-</Fragment>
-`;
-
-exports[`should display the provider app install button 1`] = `
-<Fragment>
- <Alert
- className="width-60 big-spacer-bottom"
- variant="info"
- >
- onboarding.create_project.beta_feature_x.Foo Provider
- </Alert>
- <DeferredSpinner
- timeout={100}
- />
-</Fragment>
-`;
-
-exports[`should display the provider app install button 2`] = `
-<Fragment>
- <Alert
- className="width-60 big-spacer-bottom"
- variant="info"
- >
- onboarding.create_project.beta_feature_x.Foo Provider
- </Alert>
- <div>
- <p
- className="spacer-bottom"
- >
- onboarding.create_project.install_app_x.Foo Provider
- </p>
- <IdentityProviderLink
- className="display-inline-block"
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- }
- }
- small={true}
- url="https://alm.foo.com/install"
- >
- onboarding.create_project.install_app_x.button.Foo Provider
- </IdentityProviderLink>
- </div>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/routes.ts b/server/sonar-web/src/main/js/apps/projects/routes.ts
index 061627c206a..5478d372823 100644
--- a/server/sonar-web/src/main/js/apps/projects/routes.ts
+++ b/server/sonar-web/src/main/js/apps/projects/routes.ts
@@ -37,7 +37,7 @@ const routes = [
{ path: 'favorite', component: FavoriteProjectsContainer },
isSonarCloud() && {
path: 'create',
- component: lazyLoad(() => import('./create/CreateProjectPage'))
+ component: lazyLoad(() => import('../create/project/CreateProjectPage'))
}
].filter(Boolean);