diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-10-23 08:17:14 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-16 20:21:04 +0100 |
commit | f0e05b42339d0736bf117c631bde4530cc1f356a (patch) | |
tree | c25d4714869dfcf3d61b6382e7b5221e52b2dc41 /server/sonar-web/src/main/js | |
parent | 9a64997f8446afe86429895b3795b2d61a448221 (diff) | |
download | sonarqube-f0e05b42339d0736bf117c631bde4530cc1f356a.tar.gz sonarqube-f0e05b42339d0736bf117c631bde4530cc1f356a.zip |
SONAR-11324 Bind remote orgs to existing SonarCloud orgs
* Split personal org binding and public org binding
* Remove BETA flag on tabs
Diffstat (limited to 'server/sonar-web/src/main/js')
54 files changed, 1458 insertions, 897 deletions
diff --git a/server/sonar-web/src/main/js/app/styles/sonarcloud.css b/server/sonar-web/src/main/js/app/styles/sonarcloud.css index 748afde887a..3ad81d1225a 100644 --- a/server/sonar-web/src/main/js/app/styles/sonarcloud.css +++ b/server/sonar-web/src/main/js/app/styles/sonarcloud.css @@ -32,19 +32,3 @@ font-size: var(--hugeFontSize); font-weight: bold; } - -.beta-badge { - display: inline-block; - padding: 2px 4px; - border: 1px solid var(--alertBorderInfo); - border-radius: 2px; - background-color: var(--alertBackgroundInfo); - color: var(--alertTextInfo); - font-size: 10px; -} - -.beta-badge.is-muted { - border-color: var(--disableGrayBorder); - background-color: var(--disableGrayBg); - color: var(--disableGrayText); -} diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx index 7d02df3735c..c0bcd44df9b 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationAvatarInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx @@ -20,9 +20,9 @@ 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'; +import ValidationInput from '../../../components/controls/ValidationInput'; +import { translate } from '../../../helpers/l10n'; +import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; interface Props { initialValue?: string; diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationDescriptionInput.tsx index eaea25f97ad..567da282995 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationDescriptionInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationDescriptionInput.tsx @@ -19,8 +19,8 @@ */ import * as React from 'react'; import * as classNames from 'classnames'; -import ValidationInput from '../../../../components/controls/ValidationInput'; -import { translate } from '../../../../helpers/l10n'; +import ValidationInput from '../../../components/controls/ValidationInput'; +import { translate } from '../../../helpers/l10n'; interface Props { initialValue?: string; diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx index a4fcc91d979..b88d6380411 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx @@ -20,10 +20,10 @@ 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'; +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; @@ -64,25 +64,27 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta 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); - } + .then(organization => { + if (this.mounted) { + if (organization === undefined) { + this.setState({ error: undefined, validating: false }); + this.props.onChange(key); + } else { + this.setState({ + error: translate('onboarding.create_organization.organization_name.taken'), + touched: true, + validating: false + }); + this.props.onChange(undefined); } - }, - () => {} - ); + } + }) + .catch(() => { + if (this.mounted) { + this.setState({ error: undefined, validating: false }); + this.props.onChange(key); + } + }); }; handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { @@ -130,7 +132,7 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta {!this.props.readOnly && ( <input autoFocus={true} - className={classNames('input-super-large', 'text-middle', { + className={classNames('input-super-large', { 'is-invalid': isInvalid, 'is-valid': isValid })} diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationNameInput.tsx index 9e50b0cbb2e..3194befe9ac 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationNameInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationNameInput.tsx @@ -19,8 +19,8 @@ */ import * as React from 'react'; import * as classNames from 'classnames'; -import ValidationInput from '../../../../components/controls/ValidationInput'; -import { translate } from '../../../../helpers/l10n'; +import ValidationInput from '../../../components/controls/ValidationInput'; +import { translate } from '../../../helpers/l10n'; interface Props { initialValue?: string; diff --git a/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx new file mode 100644 index 00000000000..8f7defd6730 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { sortBy } from 'lodash'; +import Select from '../../../components/controls/Select'; +import { Organization } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; +import { sanitizeAlmId } from '../../../helpers/almIntegrations'; +import { getBaseUrl } from '../../../helpers/urls'; + +interface Props { + onChange: (organization: Organization) => void; + organization: string; + organizations: Organization[]; +} + +export default function OrganizationSelect({ onChange, organization, organizations }: Props) { + return ( + <Select + autoFocus={true} + className="input-super-large" + clearable={false} + id="select-organization" + labelKey="name" + onChange={onChange} + optionRenderer={optionRenderer} + options={sortBy(organizations, o => o.name.toLowerCase())} + placeholder={translate('onboarding.import_organization.choose_organization')} + required={true} + value={organization} + valueKey="key" + valueRenderer={optionRenderer} + /> + ); +} + +export function optionRenderer(organization: Organization) { + const icon = organization.alm + ? `sonarcloud/${sanitizeAlmId(organization.alm.key)}` + : 'sonarcloud-square-logo'; + return ( + <span> + <img + alt={organization.alm ? organization.alm.key : 'SonarCloud'} + className="spacer-right" + height={14} + src={`${getBaseUrl()}/images/${icon}.svg`} + /> + {organization.name} + <span className="note little-spacer-left">{organization.key}</span> + </span> + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationUrlInput.tsx index a77bdc99832..098224f7638 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationUrlInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationUrlInput.tsx @@ -20,8 +20,8 @@ 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 ValidationInput from '../../../components/controls/ValidationInput'; +import { translate } from '../../../helpers/l10n'; interface Props { initialValue?: string; 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/components/__tests__/OrganizationAvatarInput-test.tsx index c7d7c24d01e..c7d7c24d01e 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationAvatarInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationAvatarInput-test.tsx diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationDescriptionInput-test.tsx index eab1e2ca818..eab1e2ca818 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationDescriptionInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationDescriptionInput-test.tsx 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/components/__tests__/OrganizationKeyInput-test.tsx index d559b30e4d0..ef1b459fbdc 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx @@ -20,10 +20,10 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import OrganizationKeyInput from '../OrganizationKeyInput'; -import { getOrganization } from '../../../../../api/organizations'; -import { waitAndUpdate } from '../../../../../helpers/testUtils'; +import { getOrganization } from '../../../../api/organizations'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; -jest.mock('../../../../../api/organizations', () => ({ +jest.mock('../../../../api/organizations', () => ({ getOrganization: jest.fn().mockResolvedValue(undefined) })); diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx index ecbfdb1f190..ecbfdb1f190 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationNameInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationNameInput-test.tsx diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx index cc7e426bbc4..c78fede288a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx @@ -32,18 +32,6 @@ it('should render correctly', () => { <OrganizationSelect onChange={jest.fn()} organization="bar" organizations={organizations} /> ) ).toMatchSnapshot(); - expect( - shallow( - <OrganizationSelect - autoImport={true} - onChange={jest.fn()} - organization="bar" - organizations={organizations} - /> - ) - .find('.js-new-org') - .contains('onboarding.create_project.import_new_org') - ).toBe(true); }); it('should render options correctly', () => { diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationUrlInput-test.tsx index 357a912eda1..357a912eda1 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationUrlInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationUrlInput-test.tsx 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/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap index 292c7b24b87..292c7b24b87 100644 --- 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/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap 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/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap index 80e11c04f11..80e11c04f11 100644 --- 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/components/__tests__/__snapshots__/OrganizationDescriptionInput-test.tsx.snap 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/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap index 05d2e74dd68..c4441f50319 100644 --- 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/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap @@ -18,7 +18,7 @@ exports[`should render correctly 1`] = ` </span> <input autoFocus={true} - className="input-super-large text-middle" + className="input-super-large" id="organization-key" maxLength={255} onBlur={[Function]} 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/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap index 1af9dc98684..1af9dc98684 100644 --- 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/components/__tests__/__snapshots__/OrganizationNameInput-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap new file mode 100644 index 00000000000..0208b7589ba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Select + autoFocus={true} + className="input-super-large" + clearable={false} + id="select-organization" + labelKey="name" + onChange={[MockFunction]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "alm": Object { + "key": "github", + "url": "", + }, + "key": "bar", + "name": "Bar", + }, + Object { + "key": "foo", + "name": "Foo", + }, + ] + } + placeholder="onboarding.import_organization.choose_organization" + required={true} + value="bar" + valueKey="key" + valueRenderer={[Function]} +/> +`; + +exports[`should render options correctly 1`] = ` +<span> + <img + alt="SonarCloud" + className="spacer-right" + height={14} + src="/images/sonarcloud-square-logo.svg" + /> + Foo + <span + className="note little-spacer-left" + > + foo + </span> +</span> +`; + +exports[`should render options correctly 2`] = ` +<span> + <img + alt="github" + className="spacer-right" + height={14} + src="/images/sonarcloud/github.svg" + /> + Bar + <span + className="note little-spacer-left" + > + bar + </span> +</span> +`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap index d3f571b4db8..d3f571b4db8 100644 --- 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/components/__tests__/__snapshots__/OrganizationUrlInput-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx b/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx index 72c6dc0197d..86b8d778b5f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx @@ -19,18 +19,23 @@ */ import * as React from 'react'; import { translate } from '../../../helpers/l10n'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; export default function AlmApplicationInstalling({ almKey }: { almKey?: string }) { return ( - <div className="sonarcloud page page-limited"> - <div className="huge-spacer-top text-center"> - <i className="spinner" /> - <p className="big-spacer-top"> - {almKey - ? translate('onboarding.import_organization.installing', almKey) - : translate('onboarding.import_organization.installing')} - </p> - </div> - </div> + <DeferredSpinner + customSpinner={ + <div className="sonarcloud page page-limited"> + <div className="huge-spacer-top text-center"> + <i className="spinner" /> + <p className="big-spacer-top"> + {almKey + ? translate('onboarding.import_organization.installing', almKey) + : translate('onboarding.import_organization.installing')} + </p> + </div> + </div> + } + /> ); } diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx new file mode 100644 index 00000000000..9511f639648 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.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 { Organization } from '../../../app/types'; +import OrganizationSelect from '../components/OrganizationSelect'; +import { SubmitButton } from '../../../components/ui/buttons'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + onBindOrganization: (organization: string) => Promise<void>; + unboundOrganizations: Organization[]; +} + +interface State { + organization: string; + submitting: boolean; +} + +export default class AutoOrganizationBind extends React.PureComponent<Props, State> { + mounted = false; + + constructor(props: Props) { + super(props); + this.state = { organization: this.getInitialSelectedOrganization(props), submitting: false }; + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + getInitialSelectedOrganization(props: Props) { + if (props.unboundOrganizations.length === 1) { + return props.unboundOrganizations[0].key; + } + return ''; + } + + handleChange = ({ key }: Organization) => { + this.setState({ organization: key }); + }; + + handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { + event.preventDefault(); + const { organization } = this.state; + if (organization) { + this.setState({ submitting: true }); + this.props.onBindOrganization(organization).then(this.stopSubmitting, this.stopSubmitting); + } + }; + + stopSubmitting = () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }; + + render() { + const { organization, submitting } = this.state; + return ( + <form id="bind-organization-form" onSubmit={this.handleSubmit}> + <OrganizationSelect + onChange={this.handleChange} + organization={organization} + organizations={this.props.unboundOrganizations} + /> + <div className="big-spacer-top"> + <SubmitButton disabled={submitting || !organization}> + {translate('onboarding.import_organization.bind')} + </SubmitButton> + </div> + </form> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx index 14fc63bdbf8..fc2b246d428 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx @@ -19,7 +19,9 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import AutoOrganizationBind from './AutoOrganizationBind'; import ChooseRemoteOrganizationStep from './ChooseRemoteOrganizationStep'; +import OrganizationDetailsForm from './OrganizationDetailsForm'; import OrganizationDetailsStep from './OrganizationDetailsStep'; import { AlmApplication, @@ -27,10 +29,17 @@ import { OrganizationBase, Organization } from '../../../app/types'; +import { bindAlmOrganization } from '../../../api/alm-integration'; +import { sanitizeAlmId } from '../../../helpers/almIntegrations'; import { getBaseUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; -import { sanitizeAlmId } from '../../../helpers/almIntegrations'; -import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; +import RadioToggle from '../../../components/controls/RadioToggle'; + +export enum Filters { + Bind = 'bind', + Create = 'create', + None = 'none' +} interface Props { almApplication: AlmApplication; @@ -39,59 +48,68 @@ interface Props { createOrganization: ( organization: OrganizationBase & { installationId?: string } ) => Promise<Organization>; - importPersonalOrg?: Organization; - onOrgCreated: (organization: string) => void; - updateOrganization: ( - organization: OrganizationBase & { installationId?: string } - ) => Promise<Organization>; + onOrgCreated: (organization: string, justCreated?: boolean) => void; + unboundOrganizations: Organization[]; +} + +interface State { + filter: Filters; } -export default class AutoOrganizationCreate extends React.PureComponent<Props> { +export default class AutoOrganizationCreate extends React.PureComponent<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + filter: props.unboundOrganizations.length === 0 ? Filters.Create : Filters.None + }; + } + + handleOptionChange = (filter: Filters) => { + this.setState({ filter }); + }; + handleCreateOrganization = (organization: Required<OrganizationBase>) => { if (organization) { - const { importPersonalOrg } = this.props; - let promise: Promise<Organization>; - if (importPersonalOrg) { - promise = this.props.updateOrganization({ - avatar: organization.avatar, - description: organization.description, - installationId: this.props.almInstallId, - key: importPersonalOrg.key, - name: organization.name || organization.key, - url: organization.url - }); - } else { - promise = this.props.createOrganization({ + return this.props + .createOrganization({ avatar: organization.avatar, description: organization.description, installationId: this.props.almInstallId, key: organization.key, name: organization.name || organization.key, url: organization.url - }); - } - return promise.then(({ key }) => this.props.onOrgCreated(key)); - } else { - return Promise.reject(); + }) + .then(({ key }) => this.props.onOrgCreated(key)); + } + return Promise.reject(); + }; + + handleBindOrganization = (organization: string) => { + if (this.props.almInstallId) { + return bindAlmOrganization({ + organization, + installationId: this.props.almInstallId + }).then(() => this.props.onOrgCreated(organization, false)); } + return Promise.reject(); }; render() { - const { almApplication, almInstallId, almOrganization, importPersonalOrg } = this.props; + const { almApplication, almInstallId, almOrganization, unboundOrganizations } = this.props; if (almInstallId && almOrganization) { - const description = importPersonalOrg - ? translate('onboarding.import_personal_organization_x') - : translate('onboarding.import_organization_x'); - const submitText = importPersonalOrg - ? translate('onboarding.import_organization.bind') - : translate('my_account.create_organization'); + const { filter } = this.state; + const hasUnboundOrgs = unboundOrganizations.length > 0; return ( <OrganizationDetailsStep - description={ - <p className="huge-spacer-bottom"> + finished={false} + onOpen={() => {}} + open={true} + organization={almOrganization}> + <div className="huge-spacer-bottom"> + <p className="big-spacer-bottom"> <FormattedMessage - defaultMessage={description} - id={description} + defaultMessage={translate('onboarding.import_organization_x')} + id="onboarding.import_organization_x" values={{ avatar: ( <img @@ -103,25 +121,47 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props> { width={16} /> ), - name: <strong>{almOrganization.name}</strong>, - personalAvatar: importPersonalOrg && ( - <OrganizationAvatar organization={importPersonalOrg} small={true} /> - ), - personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong> + name: <strong>{almOrganization.name}</strong> }} /> </p> - } - finished={false} - keyReadOnly={Boolean(importPersonalOrg)} - onContinue={this.handleCreateOrganization} - onOpen={() => {}} - open={true} - organization={importPersonalOrg || almOrganization} - submitText={submitText} - /> + + {hasUnboundOrgs && ( + <RadioToggle + name="filter" + onCheck={this.handleOptionChange} + options={[ + { + label: translate('onboarding.import_organization.create_new'), + value: Filters.Create + }, + { + label: translate('onboarding.import_organization.bind_existing'), + value: Filters.Bind + } + ]} + value={filter} + /> + )} + </div> + + {filter === Filters.Create && ( + <OrganizationDetailsForm + onContinue={this.handleCreateOrganization} + organization={almOrganization} + submitText={translate('onboarding.import_organization.import')} + /> + )} + {filter === Filters.Bind && ( + <AutoOrganizationBind + onBindOrganization={this.handleBindOrganization} + unboundOrganizations={unboundOrganizations} + /> + )} + </OrganizationDetailsStep> ); } + return ( <ChooseRemoteOrganizationStep almApplication={this.props.almApplication} diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx new file mode 100644 index 00000000000..47a65c594d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import OrganizationDetailsForm from './OrganizationDetailsForm'; +import OrganizationDetailsStep from './OrganizationDetailsStep'; +import { + AlmApplication, + AlmOrganization, + OrganizationBase, + Organization +} from '../../../app/types'; +import { getBaseUrl } from '../../../helpers/urls'; +import { translate } from '../../../helpers/l10n'; +import { sanitizeAlmId } from '../../../helpers/almIntegrations'; +import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; + +interface Props { + almApplication: AlmApplication; + almInstallId?: string; + almOrganization: AlmOrganization; + importPersonalOrg: Organization; + onOrgCreated: (organization: string) => void; + updateOrganization: ( + organization: OrganizationBase & { installationId?: string } + ) => Promise<Organization>; +} + +export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> { + handleCreateOrganization = (organization: Required<OrganizationBase>) => { + if (organization) { + return this.props + .updateOrganization({ + avatar: organization.avatar, + description: organization.description, + installationId: this.props.almInstallId, + key: this.props.importPersonalOrg.key, + name: organization.name || organization.key, + url: organization.url + }) + .then(({ key }) => this.props.onOrgCreated(key)); + } else { + return Promise.reject(); + } + }; + + render() { + const { almApplication, importPersonalOrg } = this.props; + return ( + <OrganizationDetailsStep + finished={false} + onOpen={() => {}} + open={true} + organization={importPersonalOrg}> + <p className="huge-spacer-bottom"> + <FormattedMessage + defaultMessage={translate('onboarding.import_personal_organization_x')} + id="onboarding.import_personal_organization_x" + values={{ + avatar: ( + <img + alt={almApplication.name} + className="little-spacer-left" + src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(almApplication.key)}.svg`} + width={16} + /> + ), + name: <strong>{this.props.almOrganization.name}</strong>, + personalAvatar: importPersonalOrg && ( + <OrganizationAvatar organization={importPersonalOrg} small={true} /> + ), + personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong> + }} + /> + </p> + <OrganizationDetailsForm + keyReadOnly={true} + onContinue={this.handleCreateOrganization} + organization={importPersonalOrg} + submitText={translate('onboarding.import_organization.bind')} + /> + </OrganizationDetailsStep> + ); + } +} 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 96318218442..80e80a5f193 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 @@ -36,10 +36,10 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr <div className="boxed-group-inner"> {almInstallId && ( <Alert className="markdown big-spacer-bottom width-60" variant="error"> - {translate('onboarding.create_organization.import_org_not_found')} + {translate('onboarding.import_organization.org_not_found')} <ul> - <li>{translate('onboarding.create_organization.import_org_not_found.tips_1')}</li> - <li>{translate('onboarding.create_organization.import_org_not_found.tips_2')}</li> + <li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li> + <li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li> </ul> </Alert> )} @@ -49,7 +49,7 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr small={true} url={almApplication.installationUrl}> {translate( - 'onboarding.create_organization.choose_organization_button', + 'onboarding.import_organization.choose_organization_button', almApplication.key )} </IdentityProviderLink> @@ -70,7 +70,7 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr renderForm={this.renderForm} renderResult={this.renderResult} stepNumber={1} - stepTitle={translate('onboarding.create_organization.import_org_details')} + stepTitle={translate('onboarding.import_organization.import_org_details')} /> ); } 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 f3301cf3e6b..81b219784cd 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 @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { Helmet } from 'react-helmet'; @@ -27,6 +26,7 @@ import { Link, withRouter, WithRouterProps } from 'react-router'; import { formatPrice, parseQuery } from './utils'; import AlmApplicationInstalling from './AlmApplicationInstalling'; import AutoOrganizationCreate from './AutoOrganizationCreate'; +import AutoPersonalOrganizationBind from './AutoPersonalOrganizationBind'; import ManualOrganizationCreate from './ManualOrganizationCreate'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Tabs from '../../../components/controls/Tabs'; @@ -141,10 +141,10 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr }); }; - handleOrgCreated = (organization: string) => { + handleOrgCreated = (organization: string, justCreated = true) => { this.props.router.push({ pathname: getOrganizationUrl(organization), - state: { justCreated: true } + state: { justCreated } }); }; @@ -166,16 +166,78 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr }); }; - render() { + renderContent = (almInstallId?: string, importPersonalOrg?: Organization) => { const { currentUser, location } = this.props; + const { almApplication, almOrganization } = this.state; const state = (location.state || {}) as LocationState; + + if (importPersonalOrg && almOrganization && almApplication) { + return ( + <AutoPersonalOrganizationBind + almApplication={almApplication} + almInstallId={almInstallId} + almOrganization={almOrganization} + importPersonalOrg={importPersonalOrg} + onOrgCreated={this.handleOrgCreated} + updateOrganization={this.props.updateOrganization} + /> + ); + } + + const showManualTab = state.tab === 'manual' && !almOrganization; + return ( + <> + {almApplication && ( + <Tabs<TabKeys> + onChange={this.onTabChange} + selected={showManualTab ? 'manual' : 'auto'} + tabs={[ + { + key: 'auto', + node: translate('onboarding.import_organization', almApplication.key) + }, + { + disabled: Boolean(almOrganization), + key: 'manual', + node: translate('onboarding.create_organization.create_manually') + } + ]} + /> + )} + + {showManualTab || !almApplication ? ( + <ManualOrganizationCreate + createOrganization={this.props.createOrganization} + deleteOrganization={this.props.deleteOrganization} + onOrgCreated={this.handleOrgCreated} + onlyPaid={state.paid} + subscriptionPlans={this.state.subscriptionPlans} + /> + ) : ( + <AutoOrganizationCreate + almApplication={almApplication} + almInstallId={almInstallId} + almOrganization={almOrganization} + createOrganization={this.props.createOrganization} + onOrgCreated={this.handleOrgCreated} + unboundOrganizations={this.props.userOrganizations.filter( + o => !o.alm && o.key !== currentUser.personalOrganization + )} + /> + )} + </> + ); + }; + + render() { + const { currentUser, location } = this.props; const query = parseQuery(location.query); if (this.state.almOrgLoading) { return <AlmApplicationInstalling almKey={query.almKey} />; } - const { almApplication, almOrganization, subscriptionPlans } = this.state; + const { almOrganization, subscriptionPlans } = this.state; const importPersonalOrg = isPersonal(almOrganization) ? this.props.userOrganizations.find(o => o.key === currentUser.personalOrganization) : undefined; @@ -187,7 +249,6 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr : translate('onboarding.create_organization.page.description'); const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; const formattedPrice = formatPrice(startedPrice); - const showManualTab = state.tab === 'manual' && !almOrganization; return ( <> @@ -216,56 +277,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr {this.state.loading ? ( <DeferredSpinner /> ) : ( - <> - {almApplication && - !importPersonalOrg && ( - <Tabs<TabKeys> - onChange={this.onTabChange} - selected={showManualTab ? 'manual' : 'auto'} - tabs={[ - { - key: 'auto', - node: ( - <> - {translate('onboarding.import_organization', almApplication.key)} - <span - className={classNames('beta-badge spacer-left', { - 'is-muted': showManualTab - })}> - {translate('beta')} - </span> - </> - ) - }, - { - disabled: Boolean(almOrganization), - key: 'manual', - node: translate('onboarding.create_organization.create_manually') - } - ]} - /> - )} - - {showManualTab || !almApplication ? ( - <ManualOrganizationCreate - createOrganization={this.props.createOrganization} - deleteOrganization={this.props.deleteOrganization} - onOrgCreated={this.handleOrgCreated} - onlyPaid={state.paid} - subscriptionPlans={this.state.subscriptionPlans} - /> - ) : ( - <AutoOrganizationCreate - almApplication={almApplication} - almInstallId={query.almInstallId} - almOrganization={almOrganization} - createOrganization={this.props.createOrganization} - importPersonalOrg={importPersonalOrg} - onOrgCreated={this.handleOrgCreated} - updateOrganization={this.props.updateOrganization} - /> - )} - </> + this.renderContent(query.almInstallId, importPersonalOrg) )} </div> </> diff --git a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx index 6bf245e9088..cda7e48af9e 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import OrganizationDetailsForm from './OrganizationDetailsForm'; import OrganizationDetailsStep from './OrganizationDetailsStep'; import PlanStep from './PlanStep'; import { formatPrice } from './utils'; @@ -108,12 +109,15 @@ export default class ManualOrganizationCreate extends React.PureComponent<Props, <> <OrganizationDetailsStep finished={this.state.organization !== undefined} - onContinue={this.handleOrganizationDetailsFinish} onOpen={this.handleOrganizationDetailsStepOpen} open={this.state.step === Step.OrganizationDetails} - organization={this.state.organization} - submitText={translate('continue')} - /> + organization={this.state.organization}> + <OrganizationDetailsForm + onContinue={this.handleOrganizationDetailsFinish} + organization={this.state.organization} + submitText={translate('continue')} + /> + </OrganizationDetailsStep> {subscriptionPlans !== undefined && ( <PlanStep diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx new file mode 100644 index 00000000000..f6b4b5ac91b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx @@ -0,0 +1,184 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import OrganizationAvatarInput from '../components/OrganizationAvatarInput'; +import OrganizationDescriptionInput from '../components/OrganizationDescriptionInput'; +import OrganizationKeyInput from '../components/OrganizationKeyInput'; +import OrganizationNameInput from '../components/OrganizationNameInput'; +import OrganizationUrlInput from '../components/OrganizationUrlInput'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import { OrganizationBase } from '../../../app/types'; +import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons'; +import { translate } from '../../../helpers/l10n'; + +type RequiredOrganization = Required<OrganizationBase>; + +interface Props { + keyReadOnly?: boolean; + onContinue: (organization: RequiredOrganization) => Promise<void>; + organization?: OrganizationBase & { key: string }; + submitText: string; +} + +interface State { + additional: boolean; + avatar?: string; + description?: string; + key?: string; + name?: string; + submitting: boolean; + url?: string; +} + +type ValidState = Pick<State, Exclude<keyof State, RequiredOrganization>> & RequiredOrganization; + +export default class OrganizationDetailsForm extends React.PureComponent<Props, State> { + mounted = false; + + constructor(props: Props) { + super(props); + const { organization } = props; + this.state = { + additional: false, + avatar: (organization && organization.avatar) || '', + description: (organization && organization.description) || '', + key: (organization && organization.key) || undefined, + name: (organization && organization.name) || '', + submitting: false, + url: (organization && organization.url) || '' + }; + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + canSubmit(state: State): state is ValidState { + return Boolean( + state.key !== undefined && + state.name !== undefined && + state.description !== undefined && + state.avatar !== undefined && + state.url !== undefined + ); + } + + handleAdditionalClick = () => { + this.setState(state => ({ additional: !state.additional })); + }; + + handleKeyUpdate = (key: string | undefined) => { + this.setState({ key }); + }; + + handleNameUpdate = (name: string | undefined) => { + this.setState({ name }); + }; + + handleDescriptionUpdate = (description: string | undefined) => { + this.setState({ description }); + }; + + handleAvatarUpdate = (avatar: string | undefined) => { + this.setState({ avatar }); + }; + + handleUrlUpdate = (url: string | undefined) => { + this.setState({ url }); + }; + + handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { + event.preventDefault(); + const { state } = this; + if (this.canSubmit(state)) { + this.setState({ submitting: true }); + this.props + .onContinue({ + avatar: state.avatar, + description: state.description, + key: state.key, + name: state.name, + url: state.url + }) + .then(this.stopSubmitting, this.stopSubmitting); + } + }; + + stopSubmitting = () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }; + + render() { + return ( + <form id="organization-form" onSubmit={this.handleSubmit}> + <OrganizationKeyInput + initialValue={this.state.key} + onChange={this.handleKeyUpdate} + readOnly={this.props.keyReadOnly} + /> + <div className="big-spacer-top"> + <ResetButtonLink onClick={this.handleAdditionalClick}> + {translate( + this.state.additional + ? 'onboarding.create_organization.hide_additional_info' + : 'onboarding.create_organization.add_additional_info' + )} + <DropdownIcon className="little-spacer-left" turned={this.state.additional} /> + </ResetButtonLink> + </div> + <div className="js-additional-info" hidden={!this.state.additional}> + <div className="big-spacer-top"> + <OrganizationNameInput + initialValue={this.state.name} + onChange={this.handleNameUpdate} + /> + </div> + <div className="big-spacer-top"> + <OrganizationAvatarInput + initialValue={this.state.avatar} + name={this.state.name} + onChange={this.handleDescriptionUpdate} + /> + </div> + <div className="big-spacer-top"> + <OrganizationDescriptionInput + initialValue={this.state.description} + onChange={this.handleAvatarUpdate} + /> + </div> + <div className="big-spacer-top"> + <OrganizationUrlInput initialValue={this.state.url} onChange={this.handleUrlUpdate} /> + </div> + </div> + <div className="big-spacer-top"> + <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}> + {this.props.submitText} + </SubmitButton> + </div> + </form> + ); + } +} 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 d6e5f0696a1..b10abe71323 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,177 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import OrganizationAvatarInput from './components/OrganizationAvatarInput'; -import OrganizationDescriptionInput from './components/OrganizationDescriptionInput'; -import OrganizationKeyInput from './components/OrganizationKeyInput'; -import OrganizationNameInput from './components/OrganizationNameInput'; -import OrganizationUrlInput from './components/OrganizationUrlInput'; import Step from '../../tutorials/components/Step'; import { translate } from '../../../helpers/l10n'; -import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons'; import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; -import DropdownIcon from '../../../components/icons-components/DropdownIcon'; import { OrganizationBase } from '../../../app/types'; -type RequiredOrganization = Required<OrganizationBase>; - interface Props { - description?: React.ReactNode; + children: React.ReactNode; finished: boolean; - keyReadOnly?: boolean; - onContinue: (organization: RequiredOrganization) => Promise<void>; onOpen: () => void; open: boolean; organization?: OrganizationBase & { key: string }; - submitText: string; -} - -interface State { - additional: boolean; - avatar?: string; - description?: string; - key?: string; - name?: string; - submitting: boolean; - url?: string; } - -type ValidState = Pick<State, Exclude<keyof State, RequiredOrganization>> & RequiredOrganization; - -export default class OrganizationDetailsStep extends React.PureComponent<Props, State> { - mounted = false; - - constructor(props: Props) { - super(props); - const { organization } = props; - this.state = { - additional: false, - avatar: (organization && organization.avatar) || '', - description: (organization && organization.description) || '', - key: (organization && organization.key) || undefined, - name: (organization && organization.name) || '', - submitting: false, - url: (organization && organization.url) || '' - }; - } - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - canSubmit(state: State): state is ValidState { - return Boolean( - state.key !== undefined && - state.name !== undefined && - state.description !== undefined && - state.avatar !== undefined && - state.url !== undefined - ); - } - - handleAdditionalClick = () => { - this.setState(state => ({ additional: !state.additional })); - }; - - handleKeyUpdate = (key: string | undefined) => { - this.setState({ key }); - }; - - handleNameUpdate = (name: string | undefined) => { - this.setState({ name }); - }; - - handleDescriptionUpdate = (description: string | undefined) => { - this.setState({ description }); - }; - - handleAvatarUpdate = (avatar: string | undefined) => { - this.setState({ avatar }); - }; - - handleUrlUpdate = (url: string | undefined) => { - this.setState({ url }); - }; - - handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { - event.preventDefault(); - const { state } = this; - if (this.canSubmit(state)) { - this.setState({ submitting: true }); - this.props - .onContinue({ - avatar: state.avatar, - description: state.description, - key: state.key, - name: state.name, - url: state.url - }) - .then(this.stopSubmitting, this.stopSubmitting); - } - }; - - stopSubmitting = () => { - if (this.mounted) { - this.setState({ submitting: false }); - } - }; - +export default class OrganizationDetailsStep extends React.PureComponent<Props> { renderForm = () => { - return ( - <div className="boxed-group-inner"> - <form id="organization-form" onSubmit={this.handleSubmit}> - {this.props.description} - <OrganizationKeyInput - initialValue={this.state.key} - onChange={this.handleKeyUpdate} - readOnly={this.props.keyReadOnly} - /> - <div className="big-spacer-top"> - <ResetButtonLink onClick={this.handleAdditionalClick}> - {translate( - this.state.additional - ? 'onboarding.create_organization.hide_additional_info' - : 'onboarding.create_organization.add_additional_info' - )} - <DropdownIcon className="little-spacer-left" turned={this.state.additional} /> - </ResetButtonLink> - </div> - <div className="js-additional-info" hidden={!this.state.additional}> - <div className="big-spacer-top"> - <OrganizationNameInput - initialValue={this.state.name} - onChange={this.handleNameUpdate} - /> - </div> - <div className="big-spacer-top"> - <OrganizationAvatarInput - initialValue={this.state.avatar} - name={this.state.name} - onChange={this.handleDescriptionUpdate} - /> - </div> - <div className="big-spacer-top"> - <OrganizationDescriptionInput - initialValue={this.state.description} - onChange={this.handleAvatarUpdate} - /> - </div> - <div className="big-spacer-top"> - <OrganizationUrlInput initialValue={this.state.url} onChange={this.handleUrlUpdate} /> - </div> - </div> - <div className="big-spacer-top"> - <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}> - {this.props.submitText} - </SubmitButton> - </div> - </form> - </div> - ); + return <div className="boxed-group-inner">{this.props.children}</div>; }; renderResult = () => { diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx new file mode 100644 index 00000000000..fc88575215c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import AutoOrganizationBind from '../AutoOrganizationBind'; +import { submit } from '../../../../helpers/testUtils'; + +const organization = { + avatar: 'http://example.com/avatar', + description: 'description-foo', + key: 'key-foo', + name: 'name-foo', + url: 'http://example.com/foo' +}; + +it('should render correctly', () => { + const onBindOrganization = jest.fn().mockResolvedValue({}); + const wrapper = shallowRender({ onBindOrganization }); + expect(wrapper).toMatchSnapshot(); + + submit(wrapper.find('form')); + expect(onBindOrganization).toHaveBeenCalled(); +}); + +function shallowRender(props: Partial<AutoOrganizationBind['props']> = {}) { + return shallow( + <AutoOrganizationBind + onBindOrganization={jest.fn()} + unboundOrganizations={[organization]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx index 17e57b70a94..4d6539e500c 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx @@ -21,6 +21,11 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import AutoOrganizationCreate from '../AutoOrganizationCreate'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { bindAlmOrganization } from '../../../../api/alm-integration'; + +jest.mock('../../../../api/alm-integration', () => ({ + bindAlmOrganization: jest.fn().mockResolvedValue({}) +})); const organization = { avatar: 'http://example.com/avatar', @@ -39,42 +44,55 @@ it('should render prefilled and create org', async () => { const onOrgCreated = jest.fn(); const wrapper = shallowRender({ almInstallId: 'id-foo', - almOrganization: { - ...organization, - type: 'ORGANIZATION' - }, + almOrganization: { ...organization, type: 'ORGANIZATION' }, createOrganization, onOrgCreated }); expect(wrapper).toMatchSnapshot(); - wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); + wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization); await waitAndUpdate(wrapper); expect(createOrganization).toBeCalledWith({ ...organization, installationId: 'id-foo' }); expect(onOrgCreated).toBeCalledWith('foo'); }); -it('should render for personal organizations', async () => { - const personalOrg = { key: 'personal-org', name: 'personal-org' }; - const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key }); +it('should display choice between import or creation', () => { + const wrapper = shallowRender({ + almInstallId: 'id-foo', + almOrganization: { ...organization, type: 'ORGANIZATION' }, + unboundOrganizations: [organization] + }); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('RadioToggle').prop<Function>('onCheck')('create'); + wrapper.update(); + expect(wrapper.find('OrganizationDetailsForm').exists()).toBe(true); + + wrapper.find('RadioToggle').prop<Function>('onCheck')('bind'); + wrapper.update(); + expect(wrapper.find('AutoOrganizationBind').exists()).toBe(true); +}); + +it('should bind existing organization', async () => { const onOrgCreated = jest.fn(); const wrapper = shallowRender({ almInstallId: 'id-foo', - almOrganization: { ...organization, type: 'USER' }, - importPersonalOrg: personalOrg, + almOrganization: { ...organization, type: 'ORGANIZATION' }, onOrgCreated, - updateOrganization + unboundOrganizations: [organization] }); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(personalOrg); + wrapper.find('RadioToggle').prop<Function>('onCheck')('bind'); + wrapper.update(); + wrapper.find('AutoOrganizationBind').prop<Function>('onBindOrganization')('foo'); + expect(bindAlmOrganization as jest.Mock<any>).toHaveBeenCalledWith({ + installationId: 'id-foo', + organization: 'foo' + }); await waitAndUpdate(wrapper); - - expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' }); - expect(onOrgCreated).toBeCalledWith(personalOrg.key); + expect(onOrgCreated).toHaveBeenCalledWith('foo', false); }); function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { @@ -89,7 +107,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { }} createOrganization={jest.fn()} onOrgCreated={jest.fn()} - updateOrganization={jest.fn()} + unboundOrganizations={[]} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx new file mode 100644 index 00000000000..d735122f941 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import AutoPersonalOrganizationBind from '../AutoPersonalOrganizationBind'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; + +it('should render correctly', async () => { + const personalOrg = { key: 'personalorg', name: 'Personal Org' }; + const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key }); + const onOrgCreated = jest.fn(); + const wrapper = shallowRender({ + almInstallId: 'id-foo', + importPersonalOrg: personalOrg, + onOrgCreated, + updateOrganization + }); + + expect(wrapper).toMatchSnapshot(); + + wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(personalOrg); + await waitAndUpdate(wrapper); + + expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' }); + expect(onOrgCreated).toBeCalledWith(personalOrg.key); +}); + +function shallowRender(props: Partial<AutoPersonalOrganizationBind['props']> = {}) { + return shallow( + <AutoPersonalOrganizationBind + almApplication={{ + backgroundColor: '#0052CC', + iconPath: '"/static/authbitbucket/bitbucket.svg"', + installationUrl: 'https://bitbucket.org/install/app', + key: 'bitbucket', + name: 'BitBucket' + }} + almOrganization={{ + avatar: 'http://example.com/avatar', + description: 'description-foo', + key: 'key-foo', + name: 'name-foo', + type: 'USER', + url: 'http://example.com/foo' + }} + importPersonalOrg={{ key: 'personalorg', name: 'Personal Org' }} + onOrgCreated={jest.fn()} + updateOrganization={jest.fn()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index 7db9ebf2636..8aa237fc1d0 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -23,6 +23,7 @@ import { shallow } from 'enzyme'; import { CreateOrganization } from '../CreateOrganization'; import { mockRouter, waitAndUpdate } from '../../../../helpers/testUtils'; import { LoggedInUser } from '../../../../app/types'; +import { getAlmOrganization } from '../../../../api/alm-integration'; jest.mock('../../../../api/billing', () => ({ getSubscriptionPlans: jest @@ -89,6 +90,22 @@ it('should render with auto tab selected and manual disabled', async () => { expect(wrapper).toMatchSnapshot(); }); +it('should render with auto personal organization bind page', async () => { + (getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({ + key: 'foo', + name: 'Foo', + avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', + type: 'USER' + }); + const wrapper = shallowRender({ + currentUser: { ...user, externalProvider: 'github', personalOrganization: 'foo' }, + location: { query: { installation_id: 'foo' } } as Location // eslint-disable-line camelcase + }); + expect(wrapper).toMatchSnapshot(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + it('should switch tabs', async () => { const replace = jest.fn(); const wrapper = shallowRender({ diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx index 6226f8173b7..1682a146013 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx @@ -38,7 +38,7 @@ it('should render and create organization', async () => { await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); - wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); + wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); @@ -52,7 +52,7 @@ it('should preselect paid plan', async () => { const wrapper = shallowRender({ onlyPaid: true }); await waitAndUpdate(wrapper); - wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); + wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization); await waitAndUpdate(wrapper); expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true); }); @@ -63,7 +63,7 @@ it('should roll back after upgrade failure', async () => { const wrapper = shallowRender({ createOrganization, deleteOrganization }); await waitAndUpdate(wrapper); - wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); + wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization); await waitAndUpdate(wrapper); wrapper.find('PlanStep').prop<Function>('createOrganization')(); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsForm-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsForm-test.tsx new file mode 100644 index 00000000000..bc7845d4caf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsForm-test.tsx @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import OrganizationDetailsForm from '../OrganizationDetailsForm'; +import { click, submit } from '../../../../helpers/testUtils'; + +it('should render form', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('.js-additional-info').prop('hidden')).toBe(true); + + click(wrapper.find('ResetButtonLink')); + wrapper.update(); + expect(wrapper.find('.js-additional-info').prop('hidden')).toBe(false); +}); + +it('should validate before submit', () => { + const wrapper = shallowRender(); + const instance = wrapper.instance() as OrganizationDetailsForm; + + expect( + instance.canSubmit({ + additional: false, + avatar: '', + description: '', + name: '', + key: 'foo', + submitting: false, + url: '' + }) + ).toBe(true); + + expect( + instance.canSubmit({ + additional: false, + avatar: '', + description: '', + name: '', + key: undefined, + submitting: false, + url: '' + }) + ).toBe(false); + + expect( + instance.canSubmit({ + additional: false, + avatar: undefined, + description: '', + name: '', + key: 'foo', + submitting: false, + url: '' + }) + ).toBe(false); + + instance.canSubmit = jest.fn() as any; + submit(wrapper.find('form')); + expect(instance.canSubmit).toHaveBeenCalled(); +}); + +function shallowRender(props: Partial<OrganizationDetailsForm['props']> = {}) { + return shallow( + <OrganizationDetailsForm onContinue={jest.fn()} submitText="continue" {...props} /> + ); +} 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 ac756ffdc8b..e905b769872 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 @@ -20,109 +20,31 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import OrganizationDetailsStep from '../OrganizationDetailsStep'; -import { click, submit } from '../../../../helpers/testUtils'; -import { getOrganization } from '../../../../api/organizations'; - -jest.mock('../../../../api/organizations', () => ({ - getOrganization: jest.fn() -})); - -beforeEach(() => { - (getOrganization as jest.Mock).mockResolvedValue(undefined); -}); it('should render form', () => { const wrapper = shallow( - <OrganizationDetailsStep - finished={false} - onContinue={jest.fn()} - onOpen={jest.fn()} - open={true} - submitText="continue" - /> + <OrganizationDetailsStep finished={false} onOpen={jest.fn()} open={true}> + <form /> + </OrganizationDetailsStep> ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.dive()).toMatchSnapshot(); - expect( - wrapper - .dive() - .find('.js-additional-info') - .prop('hidden') - ).toBe(true); - - click(wrapper.dive().find('ResetButtonLink')); - wrapper.update(); expect( wrapper .dive() - .find('.js-additional-info') - .prop('hidden') - ).toBe(false); -}); - -it('should validate before submit', () => { - const wrapper = shallow( - <OrganizationDetailsStep - finished={false} - onContinue={jest.fn()} - onOpen={jest.fn()} - open={true} - submitText="continue" - /> - ); - const instance = wrapper.instance() as OrganizationDetailsStep; - - expect( - instance.canSubmit({ - additional: false, - avatar: '', - description: '', - name: '', - key: 'foo', - submitting: false, - url: '' - }) + .find('form') + .exists() ).toBe(true); - - expect( - instance.canSubmit({ - additional: false, - avatar: '', - description: '', - name: '', - key: undefined, - submitting: false, - url: '' - }) - ).toBe(false); - - expect( - instance.canSubmit({ - additional: false, - avatar: undefined, - description: '', - name: '', - key: 'foo', - submitting: false, - url: '' - }) - ).toBe(false); - - instance.canSubmit = jest.fn() as any; - submit(wrapper.dive().find('form')); - expect(instance.canSubmit).toHaveBeenCalled(); }); -it.only('should render result', () => { +it('should render result', () => { const wrapper = shallow( <OrganizationDetailsStep finished={true} - onContinue={jest.fn()} onOpen={jest.fn()} open={false} - organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }} - submitText="continue" - /> + organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }}> + <div /> + </OrganizationDetailsStep> ); expect(wrapper.dive().find('.boxed-group-actions')).toMatchSnapshot(); expect( diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap new file mode 100644 index 00000000000..612312edf9d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<form + id="bind-organization-form" + onSubmit={[Function]} +> + <OrganizationSelect + onChange={[Function]} + organization="key-foo" + organizations={ + Array [ + Object { + "avatar": "http://example.com/avatar", + "description": "description-foo", + "key": "key-foo", + "name": "name-foo", + "url": "http://example.com/foo", + }, + ] + } + /> + <div + className="big-spacer-top" + > + <SubmitButton + disabled={false} + > + onboarding.import_organization.bind + </SubmitButton> + </div> +</form> +`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap index 423b5f2181b..f11e164d3a1 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap @@ -1,14 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render for personal organizations 1`] = ` +exports[`should display choice between import or creation 1`] = ` <OrganizationDetailsStep - description={ + finished={false} + onOpen={[Function]} + open={true} + organization={ + Object { + "avatar": "http://example.com/avatar", + "description": "description-foo", + "key": "key-foo", + "name": "name-foo", + "type": "ORGANIZATION", + "url": "http://example.com/foo", + } + } +> + <div + className="huge-spacer-bottom" + > <p - className="huge-spacer-bottom" + className="big-spacer-bottom" > <FormattedMessage - defaultMessage="onboarding.import_personal_organization_x" - id="onboarding.import_personal_organization_x" + defaultMessage="onboarding.import_organization_x" + id="onboarding.import_organization_x" values={ Object { "avatar": <img @@ -20,43 +36,53 @@ exports[`should render for personal organizations 1`] = ` "name": <strong> name-foo </strong>, - "personalAvatar": <OrganizationAvatar - organization={ - Object { - "key": "personal-org", - "name": "personal-org", - } - } - small={true} - />, - "personalName": <strong> - personal-org - </strong>, } } /> </p> - } + <RadioToggle + disabled={false} + name="filter" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.import_organization.create_new", + "value": "create", + }, + Object { + "label": "onboarding.import_organization.bind_existing", + "value": "bind", + }, + ] + } + value="none" + /> + </div> +</OrganizationDetailsStep> +`; + +exports[`should render prefilled and create org 1`] = ` +<OrganizationDetailsStep finished={false} - keyReadOnly={true} - onContinue={[Function]} onOpen={[Function]} open={true} organization={ Object { - "key": "personal-org", - "name": "personal-org", + "avatar": "http://example.com/avatar", + "description": "description-foo", + "key": "key-foo", + "name": "name-foo", + "type": "ORGANIZATION", + "url": "http://example.com/foo", } } - submitText="onboarding.import_organization.bind" -/> -`; - -exports[`should render prefilled and create org 1`] = ` -<OrganizationDetailsStep - description={ +> + <div + className="huge-spacer-bottom" + > <p - className="huge-spacer-bottom" + className="big-spacer-bottom" > <FormattedMessage defaultMessage="onboarding.import_organization_x" @@ -72,30 +98,26 @@ exports[`should render prefilled and create org 1`] = ` "name": <strong> name-foo </strong>, - "personalAvatar": undefined, - "personalName": undefined, } } /> </p> - } - finished={false} - keyReadOnly={false} - onContinue={[Function]} - onOpen={[Function]} - open={true} - organization={ - Object { - "avatar": "http://example.com/avatar", - "description": "description-foo", - "key": "key-foo", - "name": "name-foo", - "type": "ORGANIZATION", - "url": "http://example.com/foo", + </div> + <OrganizationDetailsForm + onContinue={[Function]} + organization={ + Object { + "avatar": "http://example.com/avatar", + "description": "description-foo", + "key": "key-foo", + "name": "name-foo", + "type": "ORGANIZATION", + "url": "http://example.com/foo", + } } - } - submitText="my_account.create_organization" -/> + submitText="onboarding.import_organization.import" + /> +</OrganizationDetailsStep> `; exports[`should render with import org button 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap new file mode 100644 index 00000000000..f29f5f9d008 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<OrganizationDetailsStep + finished={false} + onOpen={[Function]} + open={true} + organization={ + Object { + "key": "personalorg", + "name": "Personal Org", + } + } +> + <p + className="huge-spacer-bottom" + > + <FormattedMessage + defaultMessage="onboarding.import_personal_organization_x" + id="onboarding.import_personal_organization_x" + values={ + Object { + "avatar": <img + alt="BitBucket" + className="little-spacer-left" + src="/images/sonarcloud/bitbucket.svg" + width={16} + />, + "name": <strong> + name-foo + </strong>, + "personalAvatar": <OrganizationAvatar + organization={ + Object { + "key": "personalorg", + "name": "Personal Org", + } + } + small={true} + />, + "personalName": <strong> + Personal Org + </strong>, + } + } + /> + </p> + <OrganizationDetailsForm + keyReadOnly={true} + onContinue={[Function]} + organization={ + Object { + "key": "personalorg", + "name": "Personal Org", + } + } + submitText="onboarding.import_organization.bind" + /> +</OrganizationDetailsStep> +`; 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 227a1be6a19..6a1c633b1a2 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 @@ -5,13 +5,13 @@ exports[`should display an alert message 1`] = ` className="markdown big-spacer-bottom width-60" variant="error" > - onboarding.create_organization.import_org_not_found + onboarding.import_organization.org_not_found <ul> <li> - onboarding.create_organization.import_org_not_found.tips_1 + onboarding.import_organization.org_not_found.tips_1 </li> <li> - onboarding.create_organization.import_org_not_found.tips_2 + onboarding.import_organization.org_not_found.tips_2 </li> </ul> </Alert> @@ -30,7 +30,7 @@ exports[`should render 1`] = ` className="boxed-group-header" > <h2> - onboarding.create_organization.import_org_details + onboarding.import_organization.import_org_details </h2> </div> <div @@ -53,7 +53,7 @@ exports[`should render 1`] = ` small={true} url="https://alm.application.url" > - onboarding.create_organization.choose_organization_button.github + onboarding.import_organization.choose_organization_button.github </IdentityProviderLink> </div> </div> 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 9d3dcf6dbc8..d43925de3dc 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 @@ -1,5 +1,84 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should render with auto personal organization bind page 1`] = ` +<AlmApplicationInstalling + almKey="github" +/> +`; + +exports[`should render with auto personal organization bind page 2`] = ` +<Fragment> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="onboarding.import_organization.personal.page.header" + titleTemplate="%s" + /> + <div + className="sonarcloud page page-limited" + > + <header + className="page-header" + > + <h1 + className="page-title big-spacer-bottom" + > + onboarding.import_organization.personal.page.header + </h1> + <p + className="page-description" + > + <FormattedMessage + defaultMessage="onboarding.import_organization.personal.page.description" + id="onboarding.import_organization.personal.page.description" + values={ + Object { + "break": <br />, + "more": <Link + onlyActiveOnIndex={false} + style={Object {}} + target="_blank" + to="/documentation/sonarcloud-pricing/" + > + learn_more + </Link>, + "price": "billing.price_format.10", + } + } + /> + </p> + </header> + <AutoPersonalOrganizationBind + almApplication={ + Object { + "backgroundColor": "blue", + "iconPath": "icon/path", + "installationUrl": "https://alm.installation.url", + "key": "github", + "name": "GitHub", + } + } + almInstallId="foo" + almOrganization={ + Object { + "avatar": "https://avatars3.githubusercontent.com/u/37629810?v=4", + "key": "foo", + "name": "Foo", + "type": "USER", + } + } + importPersonalOrg={ + Object { + "key": "foo", + "name": "Foo", + } + } + onOrgCreated={[Function]} + /> + </div> +</Fragment> +`; + exports[`should render with auto tab displayed 1`] = ` <Fragment> <HelmetWrapper @@ -49,14 +128,7 @@ exports[`should render with auto tab displayed 1`] = ` Array [ Object { "key": "auto", - "node": <React.Fragment> - onboarding.import_organization.github - <span - className="beta-badge spacer-left" - > - beta - </span> - </React.Fragment>, + "node": "onboarding.import_organization.github", }, Object { "disabled": false, @@ -77,6 +149,14 @@ exports[`should render with auto tab displayed 1`] = ` } } onOrgCreated={[Function]} + unboundOrganizations={ + Array [ + Object { + "key": "foo", + "name": "Foo", + }, + ] + } /> </div> </Fragment> @@ -137,14 +217,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` Array [ Object { "key": "auto", - "node": <React.Fragment> - onboarding.import_organization.github - <span - className="beta-badge spacer-left" - > - beta - </span> - </React.Fragment>, + "node": "onboarding.import_organization.github", }, Object { "disabled": true, @@ -176,6 +249,14 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` } } onOrgCreated={[Function]} + unboundOrganizations={ + Array [ + Object { + "key": "foo", + "name": "Foo", + }, + ] + } /> </div> </Fragment> @@ -291,14 +372,7 @@ exports[`should switch tabs 1`] = ` Array [ Object { "key": "auto", - "node": <React.Fragment> - onboarding.import_organization.github - <span - className="beta-badge spacer-left" - > - beta - </span> - </React.Fragment>, + "node": "onboarding.import_organization.github", }, Object { "disabled": false, @@ -319,6 +393,14 @@ exports[`should switch tabs 1`] = ` } } onOrgCreated={[Function]} + unboundOrganizations={ + Array [ + Object { + "key": "foo", + "name": "Foo", + }, + ] + } /> </div> </Fragment> diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap index be548a156d6..6d36c3862b3 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap @@ -4,11 +4,14 @@ exports[`should render and create organization 1`] = ` <Fragment> <OrganizationDetailsStep finished={false} - onContinue={[Function]} onOpen={[Function]} open={true} - submitText="continue" - /> + > + <OrganizationDetailsForm + onContinue={[Function]} + submitText="continue" + /> + </OrganizationDetailsStep> <PlanStep createOrganization={[Function]} deleteOrganization={[Function]} @@ -36,7 +39,6 @@ exports[`should render and create organization 2`] = ` <Fragment> <OrganizationDetailsStep finished={true} - onContinue={[Function]} onOpen={[Function]} open={false} organization={ @@ -48,8 +50,21 @@ exports[`should render and create organization 2`] = ` "url": "http://example.com/foo", } } - submitText="continue" - /> + > + <OrganizationDetailsForm + onContinue={[Function]} + organization={ + Object { + "avatar": "http://example.com/avatar", + "description": "description-foo", + "key": "key-foo", + "name": "name-foo", + "url": "http://example.com/foo", + } + } + submitText="continue" + /> + </OrganizationDetailsStep> <PlanStep createOrganization={[Function]} deleteOrganization={[Function]} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap new file mode 100644 index 00000000000..3acf5cb5e3a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render form 1`] = ` +<form + id="organization-form" + onSubmit={[Function]} +> + <OrganizationKeyInput + onChange={[Function]} + /> + <div + className="big-spacer-top" + > + <ResetButtonLink + onClick={[Function]} + > + onboarding.create_organization.add_additional_info + <DropdownIcon + className="little-spacer-left" + turned={false} + /> + </ResetButtonLink> + </div> + <div + className="js-additional-info" + hidden={true} + > + <div + className="big-spacer-top" + > + <OrganizationNameInput + initialValue="" + onChange={[Function]} + /> + </div> + <div + className="big-spacer-top" + > + <OrganizationAvatarInput + initialValue="" + name="" + onChange={[Function]} + /> + </div> + <div + className="big-spacer-top" + > + <OrganizationDescriptionInput + initialValue="" + onChange={[Function]} + /> + </div> + <div + className="big-spacer-top" + > + <OrganizationUrlInput + initialValue="" + onChange={[Function]} + /> + </div> + </div> + <div + className="big-spacer-top" + > + <SubmitButton + disabled={true} + > + continue + </SubmitButton> + </div> +</form> +`; 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 169967f9e4f..3b15b770b40 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 @@ -12,241 +12,6 @@ exports[`should render form 1`] = ` /> `; -exports[`should render form 2`] = ` -<div - className="boxed-group onboarding-step is-open" -> - <div - className="onboarding-step-number" - > - 1 - </div> - <div - className="boxed-group-header" - > - <h2> - onboarding.create_organization.enter_org_details - </h2> - </div> - <div - className="" - > - <div - className="boxed-group-inner" - > -<<<<<<< HEAD - <ValidationForm - initialValues={ - Object { - "avatar": "", - "description": "", - "key": "", - "name": "", - "url": "", - } - } - isInitialValid={false} - onSubmit={[MockFunction]} - validate={[Function]} - > - <Component /> - </ValidationForm> - </div> - </div> -</div> -`; - -exports[`should render form 3`] = ` -<form - onSubmit={[Function]} -> - <OrganizationDetailsInput - description="onboarding.create_organization.organization_name.description" - dirty={false} - id="organization-key" - isSubmitting={false} - isValidating={false} - label="onboarding.create_organization.organization_name" - name="key" - onBlur={[Function]} - onChange={[Function]} - required={true} - value="" - > - <Component /> - </OrganizationDetailsInput> - <div - className="big-spacer-top" - > - <ResetButtonLink - onClick={[Function]} - > - onboarding.create_organization.add_additional_info - <DropdownIcon - className="little-spacer-left" - turned={false} - /> - </ResetButtonLink> - </div> - <div - className="js-additional-info" - hidden={true} - > - <div - className="big-spacer-top" - > - <OrganizationDetailsInput - description="onboarding.create_organization.display_name.description" - dirty={false} - id="organization-display-name" - isSubmitting={false} - isValidating={false} - label="onboarding.create_organization.display_name" - name="name" - onBlur={[Function]} - onChange={[Function]} - value="" - > - <Component /> - </OrganizationDetailsInput> - </div> - <div - className="big-spacer-top" - > - <OrganizationDetailsInput - description="onboarding.create_organization.avatar.description" - dirty={false} - id="organization-avatar" - isSubmitting={false} - isValidating={false} - label="onboarding.create_organization.avatar" - name="avatar" - onBlur={[Function]} - onChange={[Function]} - value="" - > - <Component /> - </OrganizationDetailsInput> - </div> - <div - className="big-spacer-top" - > - <OrganizationDetailsInput - dirty={false} - id="organization-description" - isSubmitting={false} - isValidating={false} - label="description" - name="description" - onBlur={[Function]} - onChange={[Function]} - value="" - > - <Component /> - </OrganizationDetailsInput> - </div> - <div - className="big-spacer-top" - > - <OrganizationDetailsInput - dirty={false} - id="organization-url" - isSubmitting={false} - isValidating={false} - label="onboarding.create_organization.url" - name="url" - onBlur={[Function]} - onChange={[Function]} - value="" - > - <Component /> - </OrganizationDetailsInput> - </div> - </div> - <div - className="big-spacer-top" - > - <SubmitButton - disabled={true} - > - continue - </SubmitButton> - </div> -</form> -======= - <form - id="organization-form" - onSubmit={[Function]} - > - <OrganizationKeyInput - onChange={[Function]} - /> - <div - className="big-spacer-top" - > - <ResetButtonLink - onClick={[Function]} - > - onboarding.create_organization.add_additional_info - <DropdownIcon - className="little-spacer-left" - turned={false} - /> - </ResetButtonLink> - </div> - <div - className="js-additional-info" - hidden={true} - > - <div - className="big-spacer-top" - > - <OrganizationNameInput - initialOrgName="" - onChange={[Function]} - /> - </div> - <div - className="big-spacer-top" - > - <OrganizationAvatarInput - initialOrgAvatar="" - onChange={[Function]} - /> - </div> - <div - className="big-spacer-top" - > - <OrganizationDescriptionInput - initialOrgDescription="" - onChange={[Function]} - /> - </div> - <div - className="big-spacer-top" - > - <OrganizationUrlInput - initialOrgUrl="" - onChange={[Function]} - /> - </div> - </div> - <div - className="big-spacer-top" - > - <SubmitButton - disabled={true} - > - continue - </SubmitButton> - </div> - </form> - </div> - </div> -</div> ->>>>>>> 116a4ec872... SONAR-11322 Import repos from bound organizations -`; - exports[`should render result 1`] = ` <div className="boxed-group-actions display-flex-center" diff --git a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx index 4460f222b8f..ad6f002914f 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import RemoteRepositories from './RemoteRepositories'; -import OrganizationSelect from './OrganizationSelect'; +import OrganizationInput from './OrganizationInput'; import IdentityProviderLink from '../../../components/ui/IdentityProviderLink'; import { AlmApplication, Organization } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; @@ -69,7 +69,7 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State> small={true} url={almApplication.installationUrl}> {translate( - 'onboarding.create_organization.choose_organization_button', + 'onboarding.import_organization.choose_organization_button', almApplication.key )} </IdentityProviderLink> @@ -80,7 +80,7 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State> const { selectedOrganization } = this.state; return ( <> - <OrganizationSelect + <OrganizationInput autoImport={true} onChange={this.handleOrganizationSelect} organization={selectedOrganization} diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx index 53382bfc34e..7afb0d1ca10 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as classNames from 'classnames'; import { connect } from 'react-redux'; import { WithRouterProps } from 'react-router'; import Helmet from 'react-helmet'; @@ -142,17 +141,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro tabs={[ { key: 'auto', - node: ( - <> - {translate('onboarding.create_project.select_repositories')} - <span - className={classNames('beta-badge spacer-left', { - 'is-muted': showManualTab - })}> - {translate('beta')} - </span> - </> - ) + node: translate('onboarding.create_project.select_repositories') }, { key: 'manual', node: translate('onboarding.create_project.create_manually') } ]} diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx index 2820af7c8dc..83b0a6ab269 100644 --- a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import OrganizationSelect from './OrganizationSelect'; +import OrganizationInput from './OrganizationInput'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import { SubmitButton } from '../../../components/ui/buttons'; import { LoggedInUser, Organization } from '../../../app/types'; @@ -113,7 +113,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat return ( <> <form onSubmit={this.handleFormSubmit}> - <OrganizationSelect + <OrganizationInput onChange={this.handleOrganizationSelect} organization={this.state.selectedOrganization} organizations={this.props.userOrganizations} diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx index ed7cf5d30ee..b917efd6125 100644 --- a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx @@ -19,12 +19,9 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import { sortBy } from 'lodash'; -import Select from '../../../components/controls/Select'; +import OrganizationSelect from '../components/OrganizationSelect'; import { Organization } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; -import { sanitizeAlmId } from '../../../helpers/almIntegrations'; -import { getBaseUrl } from '../../../helpers/urls'; interface Props { autoImport?: boolean; @@ -33,7 +30,7 @@ interface Props { organizations: Organization[]; } -export default function OrganizationSelect({ +export default function OrganizationInput({ autoImport, onChange, organization, @@ -45,19 +42,10 @@ export default function OrganizationSelect({ {translate('onboarding.create_project.organization')} <em className="mandatory">*</em> </label> - <Select - autoFocus={true} - className="input-super-large" - clearable={false} - id="select-organization" - labelKey="name" + <OrganizationSelect onChange={onChange} - optionRenderer={optionRenderer} - options={sortBy(organizations, o => o.name.toLowerCase())} - required={true} - value={organization} - valueKey="key" - valueRenderer={optionRenderer} + organization={organization} + organizations={organizations} /> <Link className="big-spacer-left js-new-org" to="/create-organization"> {autoImport @@ -67,20 +55,3 @@ export default function OrganizationSelect({ </div> ); } - -export function optionRenderer(organization: Organization) { - return ( - <span> - {organization.alm && ( - <img - alt={organization.alm.key} - className="spacer-right" - height={14} - src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`} - /> - )} - {organization.name} - <span className="note little-spacer-left">{organization.key}</span> - </span> - ); -} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx index 52d56a87e38..e8e33a81ac1 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx @@ -38,7 +38,7 @@ it('should render correctly', () => { it('should correctly create a project', async () => { const onProjectCreate = jest.fn(); const wrapper = getWrapper({ onProjectCreate }); - wrapper.find('OrganizationSelect').prop<Function>('onChange')({ key: 'foo' }); + wrapper.find('OrganizationInput').prop<Function>('onChange')({ key: 'foo' }); change(wrapper.find('#project-name'), 'Bar'); expect(wrapper.find('SubmitButton')).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx new file mode 100644 index 00000000000..f6fcd45a248 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import OrganizationInput from '../OrganizationInput'; + +const organizations = [ + { key: 'foo', name: 'Foo' }, + { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } +]; + +it('should render correctly', () => { + expect( + shallow( + <OrganizationInput onChange={jest.fn()} organization="bar" organizations={organizations} /> + ) + ).toMatchSnapshot(); + expect( + shallow( + <OrganizationInput + autoImport={true} + onChange={jest.fn()} + organization="bar" + organizations={organizations} + /> + ) + .find('.js-new-org') + .contains('onboarding.create_project.import_new_org') + ).toBe(true); +}); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap index a96a37b53b5..6623016dd4f 100644 --- 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 @@ -2,7 +2,7 @@ exports[`should display the bounded organizations dropdown with the list of repositories 1`] = ` <Fragment> - <OrganizationSelect + <OrganizationInput autoImport={true} onChange={[Function]} organization="foo" @@ -59,7 +59,7 @@ exports[`should display the provider app install button 1`] = ` small={true} url="https://alm.installation.url" > - onboarding.create_organization.choose_organization_button.github + onboarding.import_organization.choose_organization_button.github </IdentityProviderLink> </Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap index 5e2c2e1a150..c0ff4ce7535 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap @@ -54,14 +54,7 @@ exports[`should render correctly 2`] = ` Array [ Object { "key": "auto", - "node": <React.Fragment> - onboarding.create_project.select_repositories - <span - className="beta-badge spacer-left" - > - beta - </span> - </React.Fragment>, + "node": "onboarding.create_project.select_repositories", }, Object { "key": "manual", @@ -178,14 +171,7 @@ exports[`should switch tabs 1`] = ` Array [ Object { "key": "auto", - "node": <React.Fragment> - onboarding.create_project.select_repositories - <span - className="beta-badge spacer-left" - > - beta - </span> - </React.Fragment>, + "node": "onboarding.create_project.select_repositories", }, Object { "key": "manual", diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap index 53fde97ce31..4f38672a053 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap @@ -21,7 +21,7 @@ exports[`should render correctly 1`] = ` <form onSubmit={[Function]} > - <OrganizationSelect + <OrganizationInput onChange={[Function]} organization="" organizations={ diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap index 367f0265e72..fe6c8d4e3f6 100644 --- 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__/OrganizationInput-test.tsx.snap @@ -14,17 +14,16 @@ exports[`should render correctly 1`] = ` * </em> </label> - <Select - autoFocus={true} - className="input-super-large" - clearable={false} - id="select-organization" - labelKey="name" + <OrganizationSelect onChange={[MockFunction]} - optionRenderer={[Function]} - options={ + organization="bar" + organizations={ Array [ Object { + "key": "foo", + "name": "Foo", + }, + Object { "alm": Object { "key": "github", "url": "", @@ -32,16 +31,8 @@ exports[`should render correctly 1`] = ` "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" @@ -53,31 +44,3 @@ exports[`should render correctly 1`] = ` </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/tutorials/onboarding/OnboardingPage.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx index 039574a03fb..ae23029734e 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx @@ -22,13 +22,12 @@ import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { InjectedRouter } from 'react-router'; import OnboardingModal from './OnboardingModal'; -import { skipOnboarding } from '../../../api/users'; -import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; +import { skipOnboarding } from '../../../store/users'; import TeamOnboardingModal from '../teamOnboarding/TeamOnboardingModal'; import { Organization } from '../../../app/types'; interface DispatchProps { - skipOnboardingAction: () => void; + skipOnboarding: () => void; } interface OwnProps { @@ -52,8 +51,7 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps state: State = { modal: ModalKey.onboarding }; closeOnboarding = () => { - skipOnboarding(); - this.props.skipOnboardingAction(); + this.props.skipOnboarding(); this.props.router.replace('/'); }; @@ -90,7 +88,7 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps } } -const mapDispatchToProps: DispatchProps = { skipOnboardingAction }; +const mapDispatchToProps: DispatchProps = { skipOnboarding }; export default connect( null, |