From 4e72416a414f4651cf9e0347b161c9be74b9782a Mon Sep 17 00:00:00 2001 From: Grégoire Aubert Date: Mon, 5 Nov 2018 13:49:31 +0100 Subject: SONAR-11321 Apply feedback * Do not autofocus when a default org is selected * Do not skip onboarding when opening the organization create page * Add button to cancel org import * Fix bug of org created with description in place of avatar * Redirect to organization projects after multiple projects import * Correctly select newly create organization when redirected to project creation page * Remove tutorial steps in auto import organization components * Update already imported repository link * Hide key value in the additional info when read only * Hide org type icons in the organization select of the page to manually create a project * Update wording to analyze projects instead of create projects * Display spinner while importing organization * Disable auto import of org for now when the user must create a paid org * Add placeholder to avatar input when there is no url specified * Add missing app installation text in create project page * Allow to switch between tabs during organization import and keep data * Remove read-only key when binding personal org --- .../src/main/js/app/components/StartupModal.tsx | 2 +- .../js/app/components/nav/global/GlobalNavPlus.tsx | 2 +- .../__snapshots__/GlobalNavPlus-test.tsx.snap | 6 +- .../sonar-web/src/main/js/app/styles/init/misc.css | 1 + .../create/components/OrganizationAvatarInput.tsx | 1 + .../create/components/OrganizationKeyInput.tsx | 38 ++-- .../apps/create/components/OrganizationSelect.tsx | 47 ++-- .../__tests__/OrganizationKeyInput-test.tsx | 7 - .../__tests__/OrganizationSelect-test.tsx | 7 +- .../OrganizationAvatarInput-test.tsx.snap | 2 + .../OrganizationKeyInput-test.tsx.snap | 21 -- .../__snapshots__/OrganizationSelect-test.tsx.snap | 13 +- .../create/organization/AutoOrganizationBind.tsx | 6 +- .../create/organization/AutoOrganizationCreate.tsx | 188 ++++++++-------- .../organization/AutoPersonalOrganizationBind.tsx | 71 +++--- .../organization/ChooseRemoteOrganizationStep.tsx | 218 ------------------- .../create/organization/CreateOrganization.tsx | 102 +++++---- .../organization/ManualOrganizationCreate.tsx | 7 +- .../organization/OrganizationDetailsForm.tsx | 39 ++-- .../main/js/apps/create/organization/PlanStep.tsx | 5 +- .../organization/RemoteOrganizationChoose.tsx | 196 +++++++++++++++++ .../__tests__/AutoOrganizationCreate-test.tsx | 15 +- .../AutoPersonalOrganizationBind-test.tsx | 18 +- .../ChooseRemoteOrganizationStep-test.tsx | 76 ------- .../__tests__/CreateOrganization-test.tsx | 6 +- .../__tests__/RemoteOrganizationChoose-test.tsx | 76 +++++++ .../AutoOrganizationBind-test.tsx.snap | 2 +- .../AutoOrganizationCreate-test.tsx.snap | 237 +++++++++++---------- .../AutoPersonalOrganizationBind-test.tsx.snap | 96 ++++----- .../ChooseRemoteOrganizationStep-test.tsx.snap | 222 ------------------- .../__snapshots__/CreateOrganization-test.tsx.snap | 87 +++++--- .../ManualOrganizationCreate-test.tsx.snap | 8 +- .../OrganizationDetailsForm-test.tsx.snap | 2 +- .../RemoteOrganizationChoose-test.tsx.snap | 182 ++++++++++++++++ .../js/apps/create/project/AlmRepositoryItem.tsx | 17 +- .../js/apps/create/project/AutoProjectCreate.tsx | 17 +- .../js/apps/create/project/CreateProjectPage.tsx | 10 +- .../js/apps/create/project/ManualProjectCreate.tsx | 4 +- .../js/apps/create/project/OrganizationInput.tsx | 1 + .../js/apps/create/project/RemoteRepositories.tsx | 12 +- .../project/__tests__/RemoteRepositories-test.tsx | 2 +- .../__snapshots__/AlmRepositoryItem-test.tsx.snap | 32 +-- .../__snapshots__/AutoProjectCreate-test.tsx.snap | 5 + .../__snapshots__/CreateProjectPage-test.tsx.snap | 4 +- .../ManualProjectCreate-test.tsx.snap | 6 +- .../__snapshots__/OrganizationInput-test.tsx.snap | 1 + .../__snapshots__/RemoteRepositories-test.tsx.snap | 6 +- .../organizations/components/OrganizationEdit.tsx | 15 +- .../components/OrganizationJustCreated.tsx | 2 +- .../__snapshots__/OrganizationEdit-test.tsx.snap | 43 +++- .../OrganizationJustCreated-test.tsx.snap | 2 +- .../projects/components/NoFavoriteProjects.tsx | 4 +- .../__snapshots__/NoFavoriteProjects-test.tsx.snap | 2 +- .../main/js/components/controls/react-select.css | 2 +- 54 files changed, 1155 insertions(+), 1038 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx delete mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap (limited to 'server/sonar-web/src/main') diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx index 528d66ec5f7..3a3b19c755a 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -115,7 +115,7 @@ export class StartupModal extends React.PureComponent { }; openOrganizationOnboarding = () => { - this.closeOnboarding(); + this.setState({ automatic: false, modal: undefined }); this.props.router.push({ pathname: '/create-organization', state: { paid: true } }); }; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index 4e3f45d648b..a0267f7197e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -106,7 +106,7 @@ export class GlobalNavPlus extends React.PureComponent - {translate('provisioning.create_new_project')} + {translate('provisioning.analyze_new_project')} ); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap index 0660c2be47c..98968ea1224 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap @@ -12,7 +12,7 @@ exports[`render 1`] = ` href="#" onClick={[Function]} > - provisioning.create_new_project + provisioning.analyze_new_project @@ -49,7 +49,7 @@ exports[`should display create new organization on SonarCloud only 1`] = ` href="#" onClick={[Function]} > - provisioning.create_new_project + provisioning.analyze_new_project
  • @@ -95,7 +95,7 @@ exports[`should display new organization and new project on SonarCloud 1`] = ` href="#" onClick={[Function]} > - provisioning.create_new_project + provisioning.analyze_new_project
  • diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index d208189636e..0e080164b07 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -368,6 +368,7 @@ td.big-spacer-top { .vertical-pipe-separator { display: flex; flex-direction: column; + margin-left: 60px; margin-right: 60px; } diff --git a/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx index c0bcd44df9b..d39cde7e67e 100644 --- a/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx @@ -101,6 +101,7 @@ export default class OrganizationAvatarInput extends React.PureComponent diff --git a/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx index b88d6380411..84ab3bbe500 100644 --- a/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx @@ -28,7 +28,6 @@ import { getHostUrl } from '../../../helpers/urls'; interface Props { initialValue?: string; onChange: (value: string | undefined) => void; - readOnly?: boolean; } interface State { @@ -51,9 +50,7 @@ export default class OrganizationKeyInput extends React.PureComponent + required={true}>
    {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'} - {this.props.readOnly && this.state.value} - {!this.props.readOnly && ( - - )} +
    ); 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 index 8f7defd6730..321548ec75d 100644 --- a/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx @@ -26,15 +26,22 @@ import { sanitizeAlmId } from '../../../helpers/almIntegrations'; import { getBaseUrl } from '../../../helpers/urls'; interface Props { + hideIcons?: boolean; onChange: (organization: Organization) => void; organization: string; organizations: Organization[]; } -export default function OrganizationSelect({ onChange, organization, organizations }: Props) { +export default function OrganizationSelect({ + hideIcons, + onChange, + organization, + organizations +}: Props) { + const optionRenderer = getOptionRenderer(hideIcons); return ( `; + +exports[`should render options correctly 3`] = ` + + Foo + + foo + + +`; 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 index 9511f639648..f8275990c8d 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx @@ -18,8 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Organization } from '../../../app/types'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; import OrganizationSelect from '../components/OrganizationSelect'; +import { Organization } from '../../../app/types'; import { SubmitButton } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; @@ -84,10 +85,11 @@ export default class AutoOrganizationBind extends React.PureComponent -
    +
    {translate('onboarding.import_organization.bind')} + {submitting && }
    ); 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 5adbae0b71c..178e1e12d41 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 @@ -18,12 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import { FormattedMessage } from 'react-intl'; import AutoOrganizationBind from './AutoOrganizationBind'; -import ChooseRemoteOrganizationStep from './ChooseRemoteOrganizationStep'; +import RemoteOrganizationChoose from './RemoteOrganizationChoose'; import OrganizationDetailsForm from './OrganizationDetailsForm'; -import OrganizationDetailsStep from './OrganizationDetailsStep'; +import { Query } from './utils'; import RadioToggle from '../../../components/controls/RadioToggle'; +import { DeleteButton } from '../../../components/ui/buttons'; import { AlmApplication, AlmOrganization, @@ -47,11 +49,13 @@ interface Props { almOrganization?: AlmOrganization; almUnboundApplications: AlmUnboundApplication[]; boundOrganization?: OrganizationBase; + className?: string; createOrganization: ( organization: OrganizationBase & { installationId?: string } ) => Promise; onOrgCreated: (organization: string, justCreated?: boolean) => void; unboundOrganizations: Organization[]; + updateUrlQuery: (query: Partial) => void; } interface State { @@ -66,8 +70,18 @@ export default class AutoOrganizationCreate extends React.PureComponent { - this.setState({ filter }); + handleBindOrganization = (organization: string) => { + if (this.props.almInstallId) { + return bindAlmOrganization({ + organization, + installationId: this.props.almInstallId + }).then(() => this.props.onOrgCreated(organization, false)); + } + return Promise.reject(); + }; + + handleCancelImport = () => { + this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined }); }; handleCreateOrganization = (organization: Required) => { @@ -83,98 +97,96 @@ export default class AutoOrganizationCreate extends React.PureComponent this.props.onOrgCreated(key)); }; - handleBindOrganization = (organization: string) => { - if (this.props.almInstallId) { - return bindAlmOrganization({ - organization, - installationId: this.props.almInstallId - }).then(() => this.props.onOrgCreated(organization, false)); - } - return Promise.reject(); + handleOptionChange = (filter: Filters) => { + this.setState({ filter }); }; - render() { - const { - almApplication, - almInstallId, - almOrganization, - boundOrganization, - unboundOrganizations - } = this.props; - if (almInstallId && almOrganization && !boundOrganization) { - const { filter } = this.state; - const hasUnboundOrgs = unboundOrganizations.length > 0; - return ( - {}} - open={true} - organization={almOrganization}> -
    -

    - - ), - name: {almOrganization.name} - }} - /> -

    - - {hasUnboundOrgs && ( - - )} -
    + renderContent = (almOrganization: AlmOrganization) => { + const { almApplication, unboundOrganizations } = this.props; - {filter === Filters.Create && ( - 0; + return ( +
    +
    +

    + + ), + name: {almOrganization.name} + }} /> - )} - {filter === Filters.Bind && ( - +

    + + {hasUnboundOrgs && ( + )} - - ); - } +
    + + {filter === Filters.Create && ( + + )} + {filter === Filters.Bind && ( + + )} +
    + ); + }; + + render() { + const { almInstallId, almOrganization, boundOrganization, className } = this.props; return ( - +
    +
    +

    {translate('onboarding.import_organization.import_org_details')}

    +
    + + {almInstallId && almOrganization && !boundOrganization ? ( + this.renderContent(almOrganization) + ) : ( + + )} +
    ); } } 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 index 5524c1fc19a..5191bbee8be 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx @@ -20,7 +20,8 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import OrganizationDetailsForm from './OrganizationDetailsForm'; -import OrganizationDetailsStep from './OrganizationDetailsStep'; +import { Query } from './utils'; +import { DeleteButton } from '../../../components/ui/buttons'; import { AlmApplication, AlmOrganization, @@ -41,9 +42,14 @@ interface Props { updateOrganization: ( organization: OrganizationBase & { installationId?: string } ) => Promise; + updateUrlQuery: (query: Partial) => void; } export default class AutoPersonalOrganizationBind extends React.PureComponent { + handleCancelImport = () => { + this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined }); + }; + handleCreateOrganization = (organization: Required) => { return this.props .updateOrganization({ @@ -60,39 +66,40 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent {}} - open={true} - organization={importPersonalOrg}> -
    - - ), - name: {this.props.almOrganization.name}, - personalAvatar: importPersonalOrg && ( - - ), - personalName: importPersonalOrg && {importPersonalOrg.name} - }} +
    +
    +
    + + ), + name: {this.props.almOrganization.name}, + personalAvatar: importPersonalOrg && ( + + ), + personalName: importPersonalOrg && {importPersonalOrg.name} + }} + /> + +
    +
    - - +
    ); } } 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 deleted file mode 100644 index 02447552553..00000000000 --- a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx +++ /dev/null @@ -1,218 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { WithRouterProps, withRouter } from 'react-router'; -import { FormattedMessage } from 'react-intl'; -import { sortBy } from 'lodash'; -import { serializeQuery } from './utils'; -import IdentityProviderLink from '../../../components/ui/IdentityProviderLink'; -import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; -import Select from '../../../components/controls/Select'; -import Step from '../../tutorials/components/Step'; -import { Alert } from '../../../components/ui/Alert'; -import { SubmitButton } from '../../../components/ui/buttons'; -import { - AlmApplication, - AlmOrganization, - AlmUnboundApplication, - OrganizationBase -} from '../../../app/types'; -import { getBaseUrl } from '../../../helpers/urls'; -import { sanitizeAlmId } from '../../../helpers/almIntegrations'; -import { translate } from '../../../helpers/l10n'; - -interface Props { - almApplication: AlmApplication; - almInstallId?: string; - almOrganization?: AlmOrganization; - almUnboundApplications: AlmUnboundApplication[]; - boundOrganization?: OrganizationBase; -} - -interface State { - unboundInstallationId: string; -} - -export class ChooseRemoteOrganizationStep extends React.PureComponent< - Props & WithRouterProps, - State -> { - state: State = { unboundInstallationId: '' }; - - handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - - const { unboundInstallationId } = this.state; - if (unboundInstallationId) { - this.props.router.push({ - pathname: '/create-organization', - query: serializeQuery({ - almInstallId: unboundInstallationId, - almKey: this.props.almApplication.key - }) - }); - } - }; - - handleInstallationChange = ({ installationId }: AlmUnboundApplication) => { - this.setState({ unboundInstallationId: installationId }); - }; - - renderOption = (organization: AlmUnboundApplication) => { - const { almApplication } = this.props; - return ( - - {almApplication.name} - {organization.name} - - ); - }; - - renderForm = () => { - const { - almApplication, - almInstallId, - almOrganization, - almUnboundApplications, - boundOrganization - } = this.props; - const { unboundInstallationId } = this.state; - return ( -
    - {almInstallId && - !almOrganization && ( - -
    - {translate('onboarding.import_organization.org_not_found')} -
      -
    • {translate('onboarding.import_organization.org_not_found.tips_1')}
    • -
    • {translate('onboarding.import_organization.org_not_found.tips_2')}
    • -
    -
    -
    - )} - {almOrganization && - boundOrganization && ( - - - ), - name: {almOrganization.name}, - boundAvatar: ( - - ), - boundName: {boundOrganization.name} - }} - /> - - )} -
    -
    - - {translate( - 'onboarding.import_organization.choose_organization_button', - almApplication.key - )} - -
    - {almUnboundApplications.length > 0 && ( -
    -
    -
    - {translate('or')} -
    -
    -
    -
    - - o.name.toLowerCase())} + placeholder={translate('onboarding.import_organization.choose_organization')} + value={unboundInstallationId} + valueKey="installationId" + valueRenderer={this.renderOption} + /> +
    + + {translate('continue')} + +
    +
    + )} +
    +
    + ); + } +} + +export default withRouter(RemoteOrganizationChoose); 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 6cd346021e2..aaed1cd3a24 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 @@ -20,7 +20,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import AutoOrganizationCreate from '../AutoOrganizationCreate'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { waitAndUpdate, click } from '../../../../helpers/testUtils'; import { bindAlmOrganization } from '../../../../api/alm-integration'; jest.mock('../../../../api/alm-integration', () => ({ @@ -58,6 +58,18 @@ it('should render prefilled and create org', async () => { expect(onOrgCreated).toBeCalledWith('foo'); }); +it('should allow to cancel org import', () => { + const updateUrlQuery = jest.fn().mockResolvedValue({ key: 'foo' }); + const wrapper = shallowRender({ + almInstallId: 'id-foo', + almOrganization: { ...organization, personal: false }, + updateUrlQuery + }); + + click(wrapper.find('DeleteButton')); + expect(updateUrlQuery).toBeCalledWith({ almInstallId: undefined, almKey: undefined }); +}); + it('should display choice between import or creation', () => { const wrapper = shallowRender({ almInstallId: 'id-foo', @@ -109,6 +121,7 @@ function shallowRender(props: Partial = {}) { createOrganization={jest.fn()} onOrgCreated={jest.fn()} unboundOrganizations={[]} + updateUrlQuery={jest.fn()} {...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 index dd1eebc4620..eeb1de2935d 100644 --- 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 @@ -20,10 +20,11 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import AutoPersonalOrganizationBind from '../AutoPersonalOrganizationBind'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { waitAndUpdate, click } from '../../../../helpers/testUtils'; + +const personalOrg = { key: 'personalorg', name: 'Personal Org' }; 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({ @@ -42,6 +43,18 @@ it('should render correctly', async () => { expect(onOrgCreated).toBeCalledWith(personalOrg.key); }); +it('should allow to cancel org import', () => { + const updateUrlQuery = jest.fn(); + const wrapper = shallowRender({ + almInstallId: 'id-foo', + importPersonalOrg: personalOrg, + updateUrlQuery + }); + + click(wrapper.find('DeleteButton')); + expect(updateUrlQuery).toBeCalledWith({ almInstallId: undefined, almKey: undefined }); +}); + function shallowRender(props: Partial = {}) { return shallow( = { importPersonalOrg={{ key: 'personalorg', name: 'Personal Org' }} onOrgCreated={jest.fn()} updateOrganization={jest.fn()} + updateUrlQuery={jest.fn()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx deleted file mode 100644 index 5c08a1d8072..00000000000 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { shallow } from 'enzyme'; -import { ChooseRemoteOrganizationStep } from '../ChooseRemoteOrganizationStep'; -import { mockRouter, submit } from '../../../../helpers/testUtils'; - -it('should render', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should display an alert message', () => { - expect(shallowRender({ almInstallId: 'foo' }).find('Alert')).toMatchSnapshot(); -}); - -it('should display unbound installations', () => { - const installation = { installationId: '12345', key: 'foo', name: 'Foo' }; - const push = jest.fn(); - const wrapper = shallowRender({ - almUnboundApplications: [installation], - router: mockRouter({ push }) - }); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('Select').prop('onChange')(installation); - submit(wrapper.find('form')); - expect(push).toHaveBeenCalledWith({ - pathname: '/create-organization', - query: { installation_id: installation.installationId } - }); -}); - -it('should display already bound alert message', () => { - expect( - shallowRender({ - almInstallId: 'foo', - almOrganization: { avatar: 'foo-avatar', key: 'foo', name: 'Foo', personal: false }, - boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' } - }).find('Alert') - ).toMatchSnapshot(); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - // @ts-ignore avoid passing everything from WithRouterProps - - ).dive(); -} 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 14434ece244..febe22899e8 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 @@ -185,9 +185,11 @@ it('should switch tabs', async () => { expect(wrapper).toMatchSnapshot(); (wrapper.find('Tabs').prop('onChange') as Function)('manual'); - expect(wrapper.find('ManualOrganizationCreate').exists()).toBeTruthy(); + expect(wrapper.find('ManualOrganizationCreate').hasClass('hidden')).toBeFalsy(); + expect(wrapper.find('AutoOrganizationCreate').hasClass('hidden')).toBeTruthy(); (wrapper.find('Tabs').prop('onChange') as Function)('auto'); - expect(wrapper.find('AutoOrganizationCreate').exists()).toBeTruthy(); + expect(wrapper.find('AutoOrganizationCreate').hasClass('hidden')).toBeFalsy(); + expect(wrapper.find('ManualOrganizationCreate').hasClass('hidden')).toBeTruthy(); }); it('should reload the alm organization when the url query changes', async () => { diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx new file mode 100644 index 00000000000..86b68fd9370 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { RemoteOrganizationChoose } from '../RemoteOrganizationChoose'; +import { mockRouter, submit } from '../../../../helpers/testUtils'; + +it('should render', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should display an alert message', () => { + expect(shallowRender({ almInstallId: 'foo' }).find('Alert')).toMatchSnapshot(); +}); + +it('should display unbound installations', () => { + const installation = { installationId: '12345', key: 'foo', name: 'Foo' }; + const push = jest.fn(); + const wrapper = shallowRender({ + almUnboundApplications: [installation], + router: mockRouter({ push }) + }); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('Select').prop('onChange')(installation); + submit(wrapper.find('form')); + expect(push).toHaveBeenCalledWith({ + pathname: '/create-organization', + query: { installation_id: installation.installationId } + }); +}); + +it('should display already bound alert message', () => { + expect( + shallowRender({ + almInstallId: 'foo', + almOrganization: { avatar: 'foo-avatar', key: 'foo', name: 'Foo', personal: false }, + boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' } + }).find('Alert') + ).toMatchSnapshot(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + // @ts-ignore avoid passing everything from WithRouterProps + + ); +} 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 index 612312edf9d..16cca64c656 100644 --- 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 @@ -21,7 +21,7 @@ exports[`should render correctly 1`] = ` } />
    +

    + onboarding.import_organization.import_org_details +

    +
    +
    -

    - , - "name": - name-foo - , +

    + , + "name": + name-foo + , + } } + /> + +

    + -

    - +
    - +
    `; exports[`should render prefilled and create org 1`] = ` -
    +

    + onboarding.import_organization.import_org_details +

    +
    +
    -

    - , - "name": - name-foo - , +

    + , + "name": + name-foo + , + } } + /> + +

    +
    + -

    -
    - - + submitText="onboarding.import_organization.import" + /> +
    +
    `; exports[`should render with import org button 1`] = ` - +
    +

    + onboarding.import_organization.import_org_details +

    +
    + + almUnboundApplications={Array []} + /> + `; 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 index 2b2026ad813..b1487a7738b 100644 --- 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 @@ -1,60 +1,60 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -
    - , - "name": - name-foo - , - "personalAvatar": + , + "name": + name-foo + , + "personalAvatar": , - "personalName": - Personal Org - , + small={true} + />, + "personalName": + Personal Org + , + } + } + /> + +
    + - -
    + `; 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 deleted file mode 100644 index 6faa756f708..00000000000 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap +++ /dev/null @@ -1,222 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display already bound alert message 1`] = ` - - , - "boundAvatar": , - "boundName": - Bound - , - "name": - Foo - , - } - } - /> - -`; - -exports[`should display an alert message 1`] = ` - -
    - onboarding.import_organization.org_not_found -
      -
    • - onboarding.import_organization.org_not_found.tips_1 -
    • -
    • - onboarding.import_organization.org_not_found.tips_2 -
    • -
    -
    -
    -`; - -exports[`should display unbound installations 1`] = ` -
    -
    - 1 -
    -
    -

    - onboarding.import_organization.import_org_details -

    -
    -
    -
    -
    -
    - - onboarding.import_organization.choose_organization_button.github - -
    -
    -
    -
    - - or - -
    -
    -
    -
    - - +
    + + continue + +
    +
    +
    +
    +`; + +exports[`should render 1`] = ` +
    +
    +
    + + onboarding.import_organization.choose_organization_button.github + +
    +
    +
    +`; diff --git a/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx index efc8e0397ba..cf360366a10 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx @@ -18,14 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import * as theme from '../../../app/theme'; import Checkbox from '../../../components/controls/Checkbox'; import CheckIcon from '../../../components/icons-components/CheckIcon'; +import Tooltip from '../../../components/controls/Tooltip'; import { AlmRepository, IdentityProvider } from '../../../app/types'; import { getBaseUrl, getProjectUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; -import Tooltip from '../../../components/controls/Tooltip'; interface Props { identityProvider: IdentityProvider; @@ -61,9 +62,17 @@ export default class AlmRepositoryItem extends React.PureComponent { {repository.linkedProjectKey && ( - - {translate('onboarding.create_project.already_imported')} - + + {translate('onboarding.create_project.see_project')} + + ) + }} + /> )} {repository.private && ( 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 58795c101bf..0737d87182f 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 @@ -29,7 +29,7 @@ import { save } from '../../../helpers/storage'; interface Props { almApplication: AlmApplication; boundOrganizations: Organization[]; - onProjectCreate: (projectKeys: string[]) => void; + onProjectCreate: (projectKeys: string[], organization: string) => void; organization?: string; } @@ -44,15 +44,13 @@ export default class AutoProjectCreate extends React.PureComponent } getInitialSelectedOrganization(props: Props) { - const organization = - props.organization && props.boundOrganizations.find(o => o.key === props.organization); - if (organization) { - return organization.key; - } - if (props.boundOrganizations.length === 1) { + if (props.organization) { + return props.organization; + } else if (props.boundOrganizations.length === 1) { return props.boundOrganizations[0].key; + } else { + return ''; } - return ''; } handleInstallAppClick = () => { @@ -69,6 +67,9 @@ export default class AutoProjectCreate extends React.PureComponent if (boundOrganizations.length === 0) { return ( <> +

    + {translate('onboarding.create_project.install_app_description', almApplication.key)} +

    { + handleProjectCreate = (projectKeys: string[], organization?: string) => { this.props.skipOnboarding(); if (projectKeys.length > 1) { - this.props.router.push({ pathname: '/projects' }); + this.props.router.push({ + pathname: (organization ? getOrganizationUrl(organization) : '') + '/projects' + }); } else if (projectKeys.length === 1) { this.props.router.push(getProjectUrl(projectKeys[0])); } @@ -141,7 +143,7 @@ export class CreateProjectPage extends React.PureComponent )} 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 83b0a6ab269..41d05c735ed 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 @@ -150,9 +150,7 @@ export default class ManualProjectCreate extends React.PureComponent
    - - {translate('create')} - + {translate('setup')} diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx index 81f5d8930f2..6a52175bfcb 100644 --- a/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx @@ -52,6 +52,7 @@ export class OrganizationInput extends React.PureComponent* void; + onProjectCreate: (projectKeys: string[], organization: string) => void; organization: string; } @@ -90,7 +90,11 @@ export default class RemoteRepositories extends React.PureComponent this.props.onProjectCreate(projects.map(project => project.projectKey)), + ({ projects }) => + this.props.onProjectCreate( + projects.map(project => project.projectKey), + this.props.organization + ), this.handleProvisionFail ); } @@ -150,9 +154,7 @@ export default class RemoteRepositories extends React.PureComponent
    - - {translate('create')} - + {translate('setup')} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx index eefe1881c22..42529ab5b5b 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx @@ -79,7 +79,7 @@ it('should correctly create a project', async () => { }); await waitAndUpdate(wrapper); - expect(onProjectCreate).toBeCalledWith(['awesome']); + expect(onProjectCreate).toBeCalledWith(['awesome'], 'sonarsource'); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap index 663adf3c8bc..e2c87e1c800 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap @@ -61,21 +61,29 @@ exports[`should render disabled 1`] = ` className="little-spacer-right" fill="#00aa00" /> - + onboarding.create_project.see_project + , } } - > - onboarding.create_project.already_imported - + /> `; 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 4e804d8e2d2..8db84dbe8df 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 @@ -45,6 +45,11 @@ exports[`should display the bounded organizations dropdown with the list of repo exports[`should display the provider app install button 1`] = ` +

    + onboarding.create_project.install_app_description.github +

    - create + setup `; @@ -12,7 +12,7 @@ exports[`should correctly create a project 2`] = ` - create + setup `; @@ -88,7 +88,7 @@ exports[`should render correctly 1`] = ` - create + setup - create + setup `; @@ -24,7 +24,7 @@ exports[`should display the list of repositories 1`] = ` - create + setup - create + setup Promise; @@ -138,19 +139,25 @@ export class OrganizationEdit extends React.PureComponent { maxLength={256} name="avatar" onChange={this.handleAvatarInputChange} + placeholder={translate('onboarding.create_organization.avatar.placeholder')} type="text" value={this.state.avatar} />
    {translate('organization.avatar.description')}
    - {!!this.state.avatarImage && ( + {(this.state.avatarImage || this.state.name) && (
    {translate('organization.avatar.preview')} {':'}
    - +
    )}
    diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx index c1ad4439f60..71a96eb4e8d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx @@ -53,7 +53,7 @@ export class OrganizationJustCreated extends React.PureComponent
    - {translate('provisioning.create_new_project')} + {translate('provisioning.analyze_new_project')}
    +
    +
    + organization.avatar.preview + : +
    + +
    @@ -220,10 +240,13 @@ exports[`smoke test 2`] = ` organization.avatar.preview :
    - @@ -354,6 +377,7 @@ exports[`smoke test 3`] = ` maxLength={256} name="avatar" onChange={[Function]} + placeholder="onboarding.create_organization.avatar.placeholder" type="text" value="foo-avatar" /> @@ -371,10 +395,13 @@ exports[`smoke test 3`] = ` organization.avatar.preview : - diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap index 4a96855b821..0ce7ab7b2fa 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap @@ -22,7 +22,7 @@ exports[`should render 1`] = `
    - provisioning.create_new_project + provisioning.analyze_new_project
    - provisioning.create_new_project + provisioning.analyze_new_project