diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-11-05 13:49:31 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-16 20:21:06 +0100 |
commit | 4e72416a414f4651cf9e0347b161c9be74b9782a (patch) | |
tree | 07b0e3fd321ff56edef82489f47f18231c1cab47 /server/sonar-web/src/main/js/apps/create | |
parent | 3112801fbe5c8c36e15f52051162d2b6e0828812 (diff) | |
download | sonarqube-4e72416a414f4651cf9e0347b161c9be74b9782a.tar.gz sonarqube-4e72416a414f4651cf9e0347b161c9be74b9782a.zip |
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
Diffstat (limited to 'server/sonar-web/src/main/js/apps/create')
41 files changed, 834 insertions, 750 deletions
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<Props, onBlur={this.handleBlur} onChange={this.handleChange} onFocus={this.handleFocus} + placeholder={translate('onboarding.create_organization.avatar.placeholder')} type="text" value={this.state.value} /> 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<Props, Sta this.mounted = true; if (this.props.initialValue !== undefined) { this.setState({ value: this.props.initialValue }); - if (!this.props.readOnly) { - this.validateKey(this.props.initialValue); - } + this.validateKey(this.props.initialValue); } } @@ -123,28 +120,25 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta isInvalid={isInvalid} isValid={isValid} label={translate('onboarding.create_organization.organization_name')} - required={!this.props.readOnly}> + required={true}> <div className="display-inline-flex-baseline"> <span className="little-spacer-right"> {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'} - {this.props.readOnly && this.state.value} </span> - {!this.props.readOnly && ( - <input - autoFocus={true} - className={classNames('input-super-large', { - 'is-invalid': isInvalid, - 'is-valid': isValid - })} - id="organization-key" - maxLength={255} - onBlur={this.handleBlur} - onChange={this.handleChange} - onFocus={this.handleFocus} - type="text" - value={this.state.value} - /> - )} + <input + autoFocus={true} + className={classNames('input-super-large', { + 'is-invalid': isInvalid, + 'is-valid': isValid + })} + id="organization-key" + maxLength={255} + onBlur={this.handleBlur} + onChange={this.handleChange} + onFocus={this.handleFocus} + type="text" + value={this.state.value} + /> </div> </ValidationInput> ); diff --git a/server/sonar-web/src/main/js/apps/create/components/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 ( <Select - autoFocus={true} + autoFocus={!organization} className="input-super-large" clearable={false} id="select-organization" @@ -51,20 +58,24 @@ export default function OrganizationSelect({ onChange, organization, organizatio ); } -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> - ); +export function getOptionRenderer(hideIcons?: boolean) { + return function optionRenderer(organization: Organization) { + const icon = organization.alm + ? `sonarcloud/${sanitizeAlmId(organization.alm.key)}` + : 'sonarcloud-square-logo'; + return ( + <span> + {!hideIcons && ( + <img + alt={organization.alm ? organization.alm.key : 'SonarCloud'} + className="spacer-right" + height={14} + src={`${getBaseUrl()}/images/${icon}.svg`} + /> + )} + {organization.name} + <span className="note little-spacer-left">{organization.key}</span> + </span> + ); + }; } diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx index ef1b459fbdc..cf7d752b54a 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx @@ -38,13 +38,6 @@ it('should render correctly', () => { expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot(); }); -it('should render correctly with readonly mode', () => { - const wrapper = shallow( - <OrganizationKeyInput initialValue="key" onChange={jest.fn()} readOnly={true} /> - ); - expect(wrapper).toMatchSnapshot(); -}); - it('should not display any status when the key is not defined', async () => { const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />); await waitAndUpdate(wrapper); diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx index c78fede288a..31e3def095e 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import OrganizationSelect, { optionRenderer } from '../OrganizationSelect'; +import OrganizationSelect, { getOptionRenderer } from '../OrganizationSelect'; const organizations = [ { key: 'foo', name: 'Foo' }, @@ -35,6 +35,7 @@ it('should render correctly', () => { }); it('should render options correctly', () => { - expect(shallow(optionRenderer(organizations[0]))).toMatchSnapshot(); - expect(shallow(optionRenderer(organizations[1]))).toMatchSnapshot(); + expect(shallow(getOptionRenderer()(organizations[0]))).toMatchSnapshot(); + expect(shallow(getOptionRenderer()(organizations[1]))).toMatchSnapshot(); + expect(shallow(getOptionRenderer(true)(organizations[0]))).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap index 292c7b24b87..0750e70fdd4 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap @@ -23,6 +23,7 @@ exports[`should display the fallback avatar when there is no url 1`] = ` onBlur={[Function]} onChange={[Function]} onFocus={[Function]} + placeholder="onboarding.create_organization.avatar.placeholder" type="text" value="" /> @@ -52,6 +53,7 @@ exports[`should render correctly 1`] = ` onBlur={[Function]} onChange={[Function]} onFocus={[Function]} + placeholder="onboarding.create_organization.avatar.placeholder" type="text" value="https://my.avatar" /> diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap index c4441f50319..33fdc70ad11 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap @@ -32,24 +32,3 @@ exports[`should render correctly 1`] = ` `; exports[`should render correctly 2`] = `true`; - -exports[`should render correctly with readonly mode 1`] = ` -<ValidationInput - id="organization-key" - isInvalid={false} - isValid={false} - label="onboarding.create_organization.organization_name" - required={false} -> - <div - className="display-inline-flex-baseline" - > - <span - className="little-spacer-right" - > - localhost/organizations/ - key - </span> - </div> -</ValidationInput> -`; diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap index 0208b7589ba..4b5035cd307 100644 --- 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 @@ -2,7 +2,7 @@ exports[`should render correctly 1`] = ` <Select - autoFocus={true} + autoFocus={false} className="input-super-large" clearable={false} id="select-organization" @@ -66,3 +66,14 @@ exports[`should render options correctly 2`] = ` </span> </span> `; + +exports[`should render options correctly 3`] = ` +<span> + Foo + <span + className="note little-spacer-left" + > + foo + </span> +</span> +`; 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<Props, Sta organization={organization} organizations={this.props.unboundOrganizations} /> - <div className="big-spacer-top"> + <div className="display-flex-center big-spacer-top"> <SubmitButton disabled={submitting || !organization}> {translate('onboarding.import_organization.bind')} </SubmitButton> + {submitting && <DeferredSpinner className="spacer-left" />} </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 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<Organization>; onOrgCreated: (organization: string, justCreated?: boolean) => void; unboundOrganizations: Organization[]; + updateUrlQuery: (query: Partial<Query>) => void; } interface State { @@ -66,8 +70,18 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S }; } - handleOptionChange = (filter: Filters) => { - 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<OrganizationBase>) => { @@ -83,98 +97,96 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S .then(({ key }) => 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 ( - <OrganizationDetailsStep - finished={false} - onOpen={() => {}} - open={true} - organization={almOrganization}> - <div className="huge-spacer-bottom"> - <p className="big-spacer-bottom"> - <FormattedMessage - defaultMessage={translate('onboarding.import_organization_x')} - id="onboarding.import_organization_x" - values={{ - avatar: ( - <img - alt={almApplication.name} - className="little-spacer-left" - src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId( - almApplication.key - )}.svg`} - width={16} - /> - ), - name: <strong>{almOrganization.name}</strong> - }} - /> - </p> - - {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> + renderContent = (almOrganization: AlmOrganization) => { + const { almApplication, unboundOrganizations } = this.props; - {filter === Filters.Create && ( - <OrganizationDetailsForm - onContinue={this.handleCreateOrganization} - organization={almOrganization} - submitText={translate('onboarding.import_organization.import')} + const { filter } = this.state; + const hasUnboundOrgs = unboundOrganizations.length > 0; + return ( + <div className="boxed-group-inner"> + <div className="huge-spacer-bottom"> + <p className="display-flex-center big-spacer-bottom"> + <FormattedMessage + defaultMessage={translate('onboarding.import_organization_x')} + id="onboarding.import_organization_x" + values={{ + avatar: ( + <img + alt={almApplication.name} + className="little-spacer-left" + src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId( + almApplication.key + )}.svg`} + width={16} + /> + ), + name: <strong>{almOrganization.name}</strong> + }} /> - )} - {filter === Filters.Bind && ( - <AutoOrganizationBind - onBindOrganization={this.handleBindOrganization} - unboundOrganizations={unboundOrganizations} + <DeleteButton className="little-spacer-left" onClick={this.handleCancelImport} /> + </p> + + {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} /> )} - </OrganizationDetailsStep> - ); - } + </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} + /> + )} + </div> + ); + }; + + render() { + const { almInstallId, almOrganization, boundOrganization, className } = this.props; return ( - <ChooseRemoteOrganizationStep - almApplication={this.props.almApplication} - almInstallId={almInstallId} - almOrganization={almOrganization} - almUnboundApplications={this.props.almUnboundApplications} - boundOrganization={boundOrganization} - /> + <div className={classNames('boxed-group', className)}> + <div className="boxed-group-header"> + <h2>{translate('onboarding.import_organization.import_org_details')}</h2> + </div> + + {almInstallId && almOrganization && !boundOrganization ? ( + this.renderContent(almOrganization) + ) : ( + <RemoteOrganizationChoose + almApplication={this.props.almApplication} + almInstallId={almInstallId} + almOrganization={almOrganization} + almUnboundApplications={this.props.almUnboundApplications} + boundOrganization={boundOrganization} + /> + )} + </div> ); } } 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<Organization>; + updateUrlQuery: (query: Partial<Query>) => void; } export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> { + handleCancelImport = () => { + this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined }); + }; + handleCreateOrganization = (organization: Required<OrganizationBase>) => { return this.props .updateOrganization({ @@ -60,39 +66,40 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr render() { const { almApplication, importPersonalOrg } = this.props; return ( - <OrganizationDetailsStep - finished={false} - onOpen={() => {}} - open={true} - organization={importPersonalOrg}> - <div 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> - }} + <div className="boxed-group"> + <div className="boxed-group-inner"> + <div className="display-flex-center big-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> + }} + /> + <DeleteButton className="little-spacer-left" onClick={this.handleCancelImport} /> + </div> + <OrganizationDetailsForm + keyReadOnly={true} + onContinue={this.handleCreateOrganization} + organization={importPersonalOrg} + submitText={translate('onboarding.import_organization.bind')} /> </div> - <OrganizationDetailsForm - keyReadOnly={true} - onContinue={this.handleCreateOrganization} - organization={importPersonalOrg} - submitText={translate('onboarding.import_organization.bind')} - /> - </OrganizationDetailsStep> + </div> ); } } 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 c12db4930b9..9e54bf6295b 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,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import { differenceInMinutes } from 'date-fns'; import { times } from 'lodash'; import { connect } from 'react-redux'; @@ -27,8 +28,10 @@ import { FormattedMessage } from 'react-intl'; import { Link, withRouter, WithRouterProps } from 'react-router'; import { formatPrice, + ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP, parseQuery, - ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP + serializeQuery, + Query } from './utils'; import AlmApplicationInstalling from './AlmApplicationInstalling'; import AutoOrganizationCreate from './AutoOrganizationCreate'; @@ -90,6 +93,8 @@ interface State { subscriptionPlans?: SubscriptionPlan[]; } +type StateWithAutoImport = State & Required<Pick<State, 'almApplication'>>; + type TabKeys = 'auto' | 'manual'; interface LocationState { @@ -158,6 +163,10 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr }); }; + hasAutoImport(state: State, paid?: boolean): state is StateWithAutoImport { + return Boolean(state.almApplication && !paid); + } + setValidOrgKey = (almOrganization: AlmOrganization) => { const key = slugify(almOrganization.key); const keys = [key, ...times(9, i => `${key}-${i + 1}`)]; @@ -227,7 +236,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr }; onTabChange = (tab: TabKeys) => { - this.updateUrl({ tab }); + this.updateUrlState({ tab }); }; stopLoading = () => { @@ -236,7 +245,15 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr } }; - updateUrl = (state: Partial<LocationState> = {}) => { + updateUrlQuery = (query: Partial<Query> = {}) => { + this.props.router.push({ + pathname: this.props.location.pathname, + query: serializeQuery({ ...parseQuery(this.props.location.query), ...query }), + state: this.props.location.state + }); + }; + + updateUrlState = (state: Partial<LocationState> = {}) => { this.props.router.replace({ pathname: this.props.location.pathname, query: this.props.location.query, @@ -246,36 +263,36 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr renderContent = (almInstallId?: string, importPersonalOrg?: Organization) => { const { currentUser, location } = this.props; - const { almApplication, almOrganization } = this.state; - const state: LocationState = location.state || {}; + const { state } = this; + const { almOrganization } = state; + const { paid, tab = 'auto' } = (location.state || {}) as LocationState; - if (importPersonalOrg && almOrganization && almApplication) { + if (importPersonalOrg && almOrganization && state.almApplication) { return ( <AutoPersonalOrganizationBind - almApplication={almApplication} + almApplication={state.almApplication} almInstallId={almInstallId} almOrganization={almOrganization} importPersonalOrg={importPersonalOrg} onOrgCreated={this.handleOrgCreated} updateOrganization={this.props.updateOrganization} + updateUrlQuery={this.updateUrlQuery} /> ); } - const showManualTab = state.tab === 'manual' && !almOrganization; return ( <> - {almApplication && ( + {this.hasAutoImport(state, paid) && ( <Tabs<TabKeys> onChange={this.onTabChange} - selected={showManualTab ? 'manual' : 'auto'} + selected={tab || 'auto'} tabs={[ { key: 'auto', - node: translate('onboarding.import_organization', almApplication.key) + node: translate('onboarding.import_organization', state.almApplication.key) }, { - disabled: Boolean(almOrganization), key: 'manual', node: translate('onboarding.create_organization.create_manually') } @@ -283,27 +300,30 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr /> )} - {showManualTab || !almApplication ? ( - <ManualOrganizationCreate - createOrganization={this.props.createOrganization} - deleteOrganization={this.props.deleteOrganization} - onOrgCreated={this.handleOrgCreated} - onlyPaid={state.paid} - subscriptionPlans={this.state.subscriptionPlans} - /> - ) : ( + <ManualOrganizationCreate + className={classNames({ hidden: tab !== 'manual' && this.hasAutoImport(state, paid) })} + createOrganization={this.props.createOrganization} + deleteOrganization={this.props.deleteOrganization} + onOrgCreated={this.handleOrgCreated} + onlyPaid={paid} + subscriptionPlans={this.state.subscriptionPlans} + /> + + {this.hasAutoImport(state, paid) && ( <AutoOrganizationCreate - almApplication={almApplication} + almApplication={state.almApplication} almInstallId={almInstallId} almOrganization={almOrganization} almUnboundApplications={this.state.almUnboundApplications} boundOrganization={this.state.boundOrganization} + className={classNames({ hidden: tab !== 'auto' })} createOrganization={this.props.createOrganization} onOrgCreated={this.handleOrgCreated} unboundOrganizations={this.props.userOrganizations.filter( ({ actions = {}, alm, key }) => !alm && key !== currentUser.personalOrganization && actions.admin )} + updateUrlQuery={this.updateUrlQuery} /> )} </> @@ -325,9 +345,6 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr const header = importPersonalOrg ? translate('onboarding.import_organization.personal.page.header') : translate('onboarding.create_organization.page.header'); - const description = importPersonalOrg - ? translate('onboarding.import_organization.personal.page.description') - : translate('onboarding.create_organization.page.description'); const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; const formattedPrice = formatPrice(startedPrice); @@ -337,23 +354,24 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr <div className="sonarcloud page page-limited"> <header className="page-header"> <h1 className="page-title big-spacer-bottom">{header}</h1> - {startedPrice !== undefined && ( - <p className="page-description"> - <FormattedMessage - defaultMessage={description} - id={description} - values={{ - break: <br />, - price: formattedPrice, - more: ( - <Link target="_blank" to="/documentation/sonarcloud-pricing/"> - {translate('learn_more')} - </Link> - ) - }} - /> - </p> - )} + {!importPersonalOrg && + startedPrice !== undefined && ( + <p className="page-description"> + <FormattedMessage + defaultMessage={translate('onboarding.create_organization.page.description')} + id="onboarding.create_organization.page.description" + values={{ + break: <br />, + price: formattedPrice, + more: ( + <Link target="_blank" to="/documentation/sonarcloud-pricing/"> + {translate('learn_more')} + </Link> + ) + }} + /> + </p> + )} </header> {this.state.loading ? ( <DeferredSpinner /> 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 cda7e48af9e..4f67564ddbf 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 @@ -27,6 +27,7 @@ import { translate } from '../../../helpers/l10n'; interface Props { createOrganization: (organization: OrganizationBase) => Promise<Organization>; + className?: string; deleteOrganization: (key: string) => Promise<void>; onOrgCreated: (organization: string) => void; onlyPaid?: boolean; @@ -101,12 +102,12 @@ export default class ManualOrganizationCreate extends React.PureComponent<Props, }; render() { - const { subscriptionPlans } = this.props; + const { className, subscriptionPlans } = this.props; const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; const formattedPrice = formatPrice(startedPrice); return ( - <> + <div className={className}> <OrganizationDetailsStep finished={this.state.organization !== undefined} onOpen={this.handleOrganizationDetailsStepOpen} @@ -131,7 +132,7 @@ export default class ManualOrganizationCreate extends React.PureComponent<Props, subscriptionPlans={subscriptionPlans} /> )} - </> + </div> ); } } 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 index f6b4b5ac91b..de7330f912f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx @@ -18,12 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; 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'; @@ -88,20 +89,20 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props, this.setState(state => ({ additional: !state.additional })); }; - handleKeyUpdate = (key: string | undefined) => { - this.setState({ key }); - }; - - handleNameUpdate = (name: string | undefined) => { - this.setState({ name }); + handleAvatarUpdate = (avatar: string | undefined) => { + this.setState({ avatar }); }; handleDescriptionUpdate = (description: string | undefined) => { this.setState({ description }); }; - handleAvatarUpdate = (avatar: string | undefined) => { - this.setState({ avatar }); + handleKeyUpdate = (key: string | undefined) => { + this.setState({ key }); + }; + + handleNameUpdate = (name: string | undefined) => { + this.setState({ name }); }; handleUrlUpdate = (url: string | undefined) => { @@ -132,13 +133,13 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props, }; render() { + const { submitting } = this.state; + const { keyReadOnly } = this.props; return ( <form id="organization-form" onSubmit={this.handleSubmit}> - <OrganizationKeyInput - initialValue={this.state.key} - onChange={this.handleKeyUpdate} - readOnly={this.props.keyReadOnly} - /> + {!keyReadOnly && ( + <OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} /> + )} <div className="big-spacer-top"> <ResetButtonLink onClick={this.handleAdditionalClick}> {translate( @@ -160,23 +161,25 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props, <OrganizationAvatarInput initialValue={this.state.avatar} name={this.state.name} - onChange={this.handleDescriptionUpdate} + onChange={this.handleAvatarUpdate} /> </div> <div className="big-spacer-top"> <OrganizationDescriptionInput initialValue={this.state.description} - onChange={this.handleAvatarUpdate} + onChange={this.handleDescriptionUpdate} /> </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)}> + + <div className="display-flex-center big-spacer-top"> + <SubmitButton disabled={submitting || !this.canSubmit(this.state)}> {this.props.submitText} </SubmitButton> + {submitting && <DeferredSpinner className="spacer-left" />} </div> </form> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx index f7b7a0f6a42..8a5517068c8 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx @@ -91,6 +91,7 @@ export default class PlanStep extends React.PureComponent<Props, State> { }; renderForm = () => { + const { submitting } = this.state; return ( <div className="boxed-group-inner"> {this.state.ready && ( @@ -122,10 +123,10 @@ export default class PlanStep extends React.PureComponent<Props, State> { </BillingForm> ) : ( <div className="display-flex-center big-spacer-top"> - <SubmitButton disabled={this.state.submitting} onClick={this.handleFreePlanSubmit}> + <SubmitButton disabled={submitting} onClick={this.handleFreePlanSubmit}> {translate('my_account.create_organization')} </SubmitButton> - {this.state.submitting && <DeferredSpinner className="spacer-left" />} + {submitting && <DeferredSpinner className="spacer-left" />} </div> )} </> diff --git a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx index 02447552553..149586bde0d 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx @@ -25,7 +25,6 @@ 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 { @@ -50,10 +49,7 @@ interface State { unboundInstallationId: string; } -export class ChooseRemoteOrganizationStep extends React.PureComponent< - Props & WithRouterProps, - State -> { +export class RemoteOrganizationChoose extends React.PureComponent<Props & WithRouterProps, State> { state: State = { unboundInstallationId: '' }; handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { @@ -90,7 +86,7 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent< ); }; - renderForm = () => { + render() { const { almApplication, almInstallId, @@ -144,7 +140,7 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent< </Alert> )} <div className="display-flex-center"> - <div className="display-inline-block abs-width-400"> + <div className="display-inline-block"> <IdentityProviderLink className="display-inline-block" identityProvider={almApplication} @@ -194,25 +190,7 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent< </div> </div> ); - }; - - renderResult = () => { - return null; - }; - - render() { - return ( - <Step - finished={false} - onOpen={() => {}} - open={true} - renderForm={this.renderForm} - renderResult={this.renderResult} - stepNumber={1} - stepTitle={translate('onboarding.import_organization.import_org_details')} - /> - ); } } -export default withRouter(ChooseRemoteOrganizationStep); +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<AutoOrganizationCreate['props']> = {}) { 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<AutoPersonalOrganizationBind['props']> = {}) { return shallow( <AutoPersonalOrganizationBind @@ -63,6 +76,7 @@ function shallowRender(props: Partial<AutoPersonalOrganizationBind['props']> = { 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__/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__/ChooseRemoteOrganizationStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx index 5c08a1d8072..86b68fd9370 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { ChooseRemoteOrganizationStep } from '../ChooseRemoteOrganizationStep'; +import { RemoteOrganizationChoose } from '../RemoteOrganizationChoose'; import { mockRouter, submit } from '../../../../helpers/testUtils'; it('should render', () => { @@ -57,10 +57,10 @@ it('should display already bound alert message', () => { ).toMatchSnapshot(); }); -function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) { +function shallowRender(props: Partial<RemoteOrganizationChoose['props']> = {}) { return shallow( // @ts-ignore avoid passing everything from WithRouterProps - <ChooseRemoteOrganizationStep + <RemoteOrganizationChoose almApplication={{ backgroundColor: 'blue', iconPath: 'icon/path', @@ -72,5 +72,5 @@ function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = { router={mockRouter()} {...props} /> - ).dive(); + ); } 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`] = ` } /> <div - className="big-spacer-top" + className="display-flex-center big-spacer-top" > <SubmitButton disabled={false} 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 b0aa2e02a1a..985f767d6c7 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,136 +1,153 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should display choice between import or creation 1`] = ` -<OrganizationDetailsStep - finished={false} - onOpen={[Function]} - open={true} - organization={ - Object { - "avatar": "http://example.com/avatar", - "description": "description-foo", - "key": "key-foo", - "name": "name-foo", - "personal": false, - "url": "http://example.com/foo", - } - } +<div + className="boxed-group" > <div - className="huge-spacer-bottom" + className="boxed-group-header" + > + <h2> + onboarding.import_organization.import_org_details + </h2> + </div> + <div + className="boxed-group-inner" > - <p - className="big-spacer-bottom" + <div + className="huge-spacer-bottom" > - <FormattedMessage - defaultMessage="onboarding.import_organization_x" - id="onboarding.import_organization_x" - values={ - Object { - "avatar": <img - alt="BitBucket" - className="little-spacer-left" - src="/images/sonarcloud/bitbucket.svg" - width={16} - />, - "name": <strong> - name-foo - </strong>, + <p + className="display-flex-center big-spacer-bottom" + > + <FormattedMessage + defaultMessage="onboarding.import_organization_x" + id="onboarding.import_organization_x" + values={ + Object { + "avatar": <img + alt="BitBucket" + className="little-spacer-left" + src="/images/sonarcloud/bitbucket.svg" + width={16} + />, + "name": <strong> + name-foo + </strong>, + } } + /> + <DeleteButton + className="little-spacer-left" + onClick={[Function]} + /> + </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={null} /> - </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={null} - /> + </div> </div> -</OrganizationDetailsStep> +</div> `; exports[`should render prefilled and create org 1`] = ` -<OrganizationDetailsStep - finished={false} - onOpen={[Function]} - open={true} - organization={ - Object { - "avatar": "http://example.com/avatar", - "description": "description-foo", - "key": "key-foo", - "name": "name-foo", - "personal": false, - "url": "http://example.com/foo", - } - } +<div + className="boxed-group" > <div - className="huge-spacer-bottom" + className="boxed-group-header" + > + <h2> + onboarding.import_organization.import_org_details + </h2> + </div> + <div + className="boxed-group-inner" > - <p - className="big-spacer-bottom" + <div + className="huge-spacer-bottom" > - <FormattedMessage - defaultMessage="onboarding.import_organization_x" - id="onboarding.import_organization_x" - values={ - Object { - "avatar": <img - alt="BitBucket" - className="little-spacer-left" - src="/images/sonarcloud/bitbucket.svg" - width={16} - />, - "name": <strong> - name-foo - </strong>, + <p + className="display-flex-center big-spacer-bottom" + > + <FormattedMessage + defaultMessage="onboarding.import_organization_x" + id="onboarding.import_organization_x" + values={ + Object { + "avatar": <img + alt="BitBucket" + className="little-spacer-left" + src="/images/sonarcloud/bitbucket.svg" + width={16} + />, + "name": <strong> + name-foo + </strong>, + } } + /> + <DeleteButton + className="little-spacer-left" + onClick={[Function]} + /> + </p> + </div> + <OrganizationDetailsForm + onContinue={[Function]} + organization={ + Object { + "avatar": "http://example.com/avatar", + "description": "description-foo", + "key": "key-foo", + "name": "name-foo", + "personal": false, + "url": "http://example.com/foo", } - /> - </p> - </div> - <OrganizationDetailsForm - onContinue={[Function]} - organization={ - Object { - "avatar": "http://example.com/avatar", - "description": "description-foo", - "key": "key-foo", - "name": "name-foo", - "personal": false, - "url": "http://example.com/foo", } - } - submitText="onboarding.import_organization.import" - /> -</OrganizationDetailsStep> + submitText="onboarding.import_organization.import" + /> + </div> +</div> `; exports[`should render with import org button 1`] = ` -<withRouter(ChooseRemoteOrganizationStep) - almApplication={ - Object { - "backgroundColor": "#0052CC", - "iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"", - "installationUrl": "https://bitbucket.org/install/app", - "key": "bitbucket", - "name": "BitBucket", +<div + className="boxed-group" +> + <div + className="boxed-group-header" + > + <h2> + onboarding.import_organization.import_org_details + </h2> + </div> + <withRouter(RemoteOrganizationChoose) + almApplication={ + Object { + "backgroundColor": "#0052CC", + "iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"", + "installationUrl": "https://bitbucket.org/install/app", + "key": "bitbucket", + "name": "BitBucket", + } } - } - almUnboundApplications={Array []} -/> + almUnboundApplications={Array []} + /> +</div> `; 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`] = ` -<OrganizationDetailsStep - finished={false} - onOpen={[Function]} - open={true} - organization={ - Object { - "key": "personalorg", - "name": "Personal Org", - } - } +<div + className="boxed-group" > <div - className="huge-spacer-bottom" + className="boxed-group-inner" > - <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", + <div + className="display-flex-center big-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>, + small={true} + />, + "personalName": <strong> + Personal Org + </strong>, + } + } + /> + <DeleteButton + className="little-spacer-left" + onClick={[Function]} + /> + </div> + <OrganizationDetailsForm + keyReadOnly={true} + onContinue={[Function]} + organization={ + Object { + "key": "personalorg", + "name": "Personal Org", } } + submitText="onboarding.import_organization.bind" /> </div> - <OrganizationDetailsForm - keyReadOnly={true} - onContinue={[Function]} - organization={ - Object { - "key": "personalorg", - "name": "Personal Org", - } - } - submitText="onboarding.import_organization.bind" - /> -</OrganizationDetailsStep> +</div> `; 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`] = ` -<Alert - className="big-spacer-bottom width-60" - variant="error" -> - <FormattedMessage - defaultMessage="onboarding.import_organization.already_bound_x" - id="onboarding.import_organization.already_bound_x" - values={ - Object { - "avatar": <img - alt="GitHub" - className="little-spacer-left" - src="/images/sonarcloud/github.svg" - width={16} - />, - "boundAvatar": <OrganizationAvatar - className="little-spacer-left" - organization={ - Object { - "avatar": "bound-avatar", - "key": "bound", - "name": "Bound", - } - } - small={true} - />, - "boundName": <strong> - Bound - </strong>, - "name": <strong> - Foo - </strong>, - } - } - /> -</Alert> -`; - -exports[`should display an alert message 1`] = ` -<Alert - className="big-spacer-bottom width-60" - variant="error" -> - <div - className="markdown" - > - onboarding.import_organization.org_not_found - <ul> - <li> - onboarding.import_organization.org_not_found.tips_1 - </li> - <li> - onboarding.import_organization.org_not_found.tips_2 - </li> - </ul> - </div> -</Alert> -`; - -exports[`should display unbound installations 1`] = ` -<div - className="boxed-group onboarding-step is-open" -> - <div - className="onboarding-step-number" - > - 1 - </div> - <div - className="boxed-group-header" - > - <h2> - onboarding.import_organization.import_org_details - </h2> - </div> - <div - className="" - > - <div - className="boxed-group-inner" - > - <div - className="display-flex-center" - > - <div - className="display-inline-block abs-width-400" - > - <IdentityProviderLink - className="display-inline-block" - identityProvider={ - Object { - "backgroundColor": "blue", - "iconPath": "icon/path", - "installationUrl": "https://alm.application.url", - "key": "github", - "name": "GitHub", - } - } - small={true} - url="https://alm.application.url" - > - onboarding.import_organization.choose_organization_button.github - </IdentityProviderLink> - </div> - <div - className="display-flex-stretch" - > - <div - className="vertical-pipe-separator" - > - <div - className="vertical-separator " - /> - <span - className="note" - > - or - </span> - <div - className="vertical-separator" - /> - </div> - <form - className="big-spacer-top big-spacer-bottom" - onSubmit={[Function]} - > - <div - className="form-field abs-width-400" - > - <label - htmlFor="select-unbound-installation" - > - onboarding.import_organization.choose_unbound_installation.github - </label> - <Select - className="input-super-large" - clearable={false} - id="select-unbound-installation" - labelKey="name" - onChange={[Function]} - optionRenderer={[Function]} - options={ - Array [ - Object { - "installationId": "12345", - "key": "foo", - "name": "Foo", - }, - ] - } - placeholder="onboarding.import_organization.choose_organization" - value="" - valueKey="installationId" - valueRenderer={[Function]} - /> - </div> - <SubmitButton - disabled={true} - > - continue - </SubmitButton> - </form> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`should render 1`] = ` -<div - className="boxed-group onboarding-step is-open" -> - <div - className="onboarding-step-number" - > - 1 - </div> - <div - className="boxed-group-header" - > - <h2> - onboarding.import_organization.import_org_details - </h2> - </div> - <div - className="" - > - <div - className="boxed-group-inner" - > - <div - className="display-flex-center" - > - <div - className="display-inline-block abs-width-400" - > - <IdentityProviderLink - className="display-inline-block" - identityProvider={ - Object { - "backgroundColor": "blue", - "iconPath": "icon/path", - "installationUrl": "https://alm.application.url", - "key": "github", - "name": "GitHub", - } - } - small={true} - url="https://alm.application.url" - > - onboarding.import_organization.choose_organization_button.github - </IdentityProviderLink> - </div> - </div> - </div> - </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 748f5e88a96..86a76be9310 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 @@ -25,28 +25,6 @@ exports[`should render with auto personal organization bind page 2`] = ` > 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={ @@ -78,6 +56,7 @@ exports[`should render with auto personal organization bind page 2`] = ` } onOrgCreated={[Function]} updateOrganization={[MockFunction]} + updateUrlQuery={[Function]} /> </div> </Fragment> @@ -135,13 +114,30 @@ exports[`should render with auto tab displayed 1`] = ` "node": "onboarding.import_organization.github", }, Object { - "disabled": false, "key": "manual", "node": "onboarding.create_organization.create_manually", }, ] } /> + <ManualOrganizationCreate + className="hidden" + createOrganization={[MockFunction]} + deleteOrganization={[MockFunction]} + onOrgCreated={[Function]} + subscriptionPlans={ + Array [ + Object { + "maxNcloc": 100000, + "price": 10, + }, + Object { + "maxNcloc": 250000, + "price": 75, + }, + ] + } + /> <AutoOrganizationCreate almApplication={ Object { @@ -153,6 +149,7 @@ exports[`should render with auto tab displayed 1`] = ` } } almUnboundApplications={Array []} + className="" createOrganization={[MockFunction]} onOrgCreated={[Function]} unboundOrganizations={ @@ -166,6 +163,7 @@ exports[`should render with auto tab displayed 1`] = ` }, ] } + updateUrlQuery={[Function]} /> </div> </Fragment> @@ -229,13 +227,30 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` "node": "onboarding.import_organization.github", }, Object { - "disabled": true, "key": "manual", "node": "onboarding.create_organization.create_manually", }, ] } /> + <ManualOrganizationCreate + className="hidden" + createOrganization={[MockFunction]} + deleteOrganization={[MockFunction]} + onOrgCreated={[Function]} + subscriptionPlans={ + Array [ + Object { + "maxNcloc": 100000, + "price": 10, + }, + Object { + "maxNcloc": 250000, + "price": 75, + }, + ] + } + /> <AutoOrganizationCreate almApplication={ Object { @@ -258,6 +273,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` } } almUnboundApplications={Array []} + className="" createOrganization={[MockFunction]} onOrgCreated={[Function]} unboundOrganizations={ @@ -271,6 +287,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` }, ] } + updateUrlQuery={[Function]} /> </div> </Fragment> @@ -319,6 +336,7 @@ exports[`should render with manual tab displayed 1`] = ` </p> </header> <ManualOrganizationCreate + className="" createOrganization={[MockFunction]} deleteOrganization={[MockFunction]} onOrgCreated={[Function]} @@ -391,13 +409,30 @@ exports[`should switch tabs 1`] = ` "node": "onboarding.import_organization.github", }, Object { - "disabled": false, "key": "manual", "node": "onboarding.create_organization.create_manually", }, ] } /> + <ManualOrganizationCreate + className="hidden" + createOrganization={[MockFunction]} + deleteOrganization={[MockFunction]} + onOrgCreated={[Function]} + subscriptionPlans={ + Array [ + Object { + "maxNcloc": 100000, + "price": 10, + }, + Object { + "maxNcloc": 250000, + "price": 75, + }, + ] + } + /> <AutoOrganizationCreate almApplication={ Object { @@ -409,6 +444,7 @@ exports[`should switch tabs 1`] = ` } } almUnboundApplications={Array []} + className="" createOrganization={[MockFunction]} onOrgCreated={[Function]} unboundOrganizations={ @@ -422,6 +458,7 @@ exports[`should switch tabs 1`] = ` }, ] } + updateUrlQuery={[Function]} /> </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 6d36c3862b3..d538064e72c 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 @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render and create organization 1`] = ` -<Fragment> +<div> <OrganizationDetailsStep finished={false} onOpen={[Function]} @@ -32,11 +32,11 @@ exports[`should render and create organization 1`] = ` ] } /> -</Fragment> +</div> `; exports[`should render and create organization 2`] = ` -<Fragment> +<div> <OrganizationDetailsStep finished={true} onOpen={[Function]} @@ -85,5 +85,5 @@ exports[`should render and create organization 2`] = ` ] } /> -</Fragment> +</div> `; 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 index 3acf5cb5e3a..8843a17679c 100644 --- 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 @@ -60,7 +60,7 @@ exports[`should render form 1`] = ` </div> </div> <div - className="big-spacer-top" + className="display-flex-center big-spacer-top" > <SubmitButton disabled={true} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap new file mode 100644 index 00000000000..dfc8f97b04b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap @@ -0,0 +1,182 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display already bound alert message 1`] = ` +<Alert + className="big-spacer-bottom width-60" + variant="error" +> + <FormattedMessage + defaultMessage="onboarding.import_organization.already_bound_x" + id="onboarding.import_organization.already_bound_x" + values={ + Object { + "avatar": <img + alt="GitHub" + className="little-spacer-left" + src="/images/sonarcloud/github.svg" + width={16} + />, + "boundAvatar": <OrganizationAvatar + className="little-spacer-left" + organization={ + Object { + "avatar": "bound-avatar", + "key": "bound", + "name": "Bound", + } + } + small={true} + />, + "boundName": <strong> + Bound + </strong>, + "name": <strong> + Foo + </strong>, + } + } + /> +</Alert> +`; + +exports[`should display an alert message 1`] = ` +<Alert + className="big-spacer-bottom width-60" + variant="error" +> + <div + className="markdown" + > + onboarding.import_organization.org_not_found + <ul> + <li> + onboarding.import_organization.org_not_found.tips_1 + </li> + <li> + onboarding.import_organization.org_not_found.tips_2 + </li> + </ul> + </div> +</Alert> +`; + +exports[`should display unbound installations 1`] = ` +<div + className="boxed-group-inner" +> + <div + className="display-flex-center" + > + <div + className="display-inline-block" + > + <IdentityProviderLink + className="display-inline-block" + identityProvider={ + Object { + "backgroundColor": "blue", + "iconPath": "icon/path", + "installationUrl": "https://alm.application.url", + "key": "github", + "name": "GitHub", + } + } + small={true} + url="https://alm.application.url" + > + onboarding.import_organization.choose_organization_button.github + </IdentityProviderLink> + </div> + <div + className="display-flex-stretch" + > + <div + className="vertical-pipe-separator" + > + <div + className="vertical-separator " + /> + <span + className="note" + > + or + </span> + <div + className="vertical-separator" + /> + </div> + <form + className="big-spacer-top big-spacer-bottom" + onSubmit={[Function]} + > + <div + className="form-field abs-width-400" + > + <label + htmlFor="select-unbound-installation" + > + onboarding.import_organization.choose_unbound_installation.github + </label> + <Select + className="input-super-large" + clearable={false} + id="select-unbound-installation" + labelKey="name" + onChange={[Function]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "installationId": "12345", + "key": "foo", + "name": "Foo", + }, + ] + } + placeholder="onboarding.import_organization.choose_organization" + value="" + valueKey="installationId" + valueRenderer={[Function]} + /> + </div> + <SubmitButton + disabled={true} + > + continue + </SubmitButton> + </form> + </div> + </div> +</div> +`; + +exports[`should render 1`] = ` +<div + className="boxed-group-inner" +> + <div + className="display-flex-center" + > + <div + className="display-inline-block" + > + <IdentityProviderLink + className="display-inline-block" + identityProvider={ + Object { + "backgroundColor": "blue", + "iconPath": "icon/path", + "installationUrl": "https://alm.application.url", + "key": "github", + "name": "GitHub", + } + } + small={true} + url="https://alm.application.url" + > + onboarding.import_organization.choose_organization_button.github + </IdentityProviderLink> + </div> + </div> +</div> +`; 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<Props> { {repository.linkedProjectKey && ( <span className="big-spacer-left"> <CheckIcon className="little-spacer-right" fill={theme.green} /> - <Link to={getProjectUrl(repository.linkedProjectKey)}> - {translate('onboarding.create_project.already_imported')} - </Link> + <FormattedMessage + defaultMessage={translate('onboarding.create_project.repository_imported')} + id="onboarding.create_project.repository_imported" + values={{ + link: ( + <Link to={getProjectUrl(repository.linkedProjectKey)}> + {translate('onboarding.create_project.see_project')} + </Link> + ) + }} + /> </span> )} {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<Props, State> } 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<Props, State> if (boundOrganizations.length === 0) { return ( <> + <p className="spacer-bottom"> + {translate('onboarding.create_project.install_app_description', almApplication.key)} + </p> <IdentityProviderLink className="display-inline-block" identityProvider={almApplication} 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 2338c3414b8..d6e6cdab45e 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 @@ -32,7 +32,7 @@ import { LoggedInUser, AlmApplication, Organization } from '../../../app/types'; import { getAlmAppInfo } from '../../../api/alm-integration'; import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations'; import { translate } from '../../../helpers/l10n'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getProjectUrl, getOrganizationUrl } from '../../../helpers/urls'; import '../../../app/styles/sonarcloud.css'; interface Props { @@ -78,10 +78,12 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro } } - handleProjectCreate = (projectKeys: string[]) => { + 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<Props & WithRouterPro key: 'auto', node: translate('onboarding.create_project.select_repositories') }, - { key: 'manual', node: translate('onboarding.create_project.create_manually') } + { key: 'manual', node: translate('onboarding.create_project.setup_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 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<Props, Stat value={this.state.projectKey} /> </div> - <SubmitButton disabled={!this.isValid() || submitting}> - {translate('create')} - </SubmitButton> + <SubmitButton disabled={!this.isValid() || submitting}>{translate('setup')}</SubmitButton> <DeferredSpinner className="spacer-left" loading={submitting} /> </form> </> 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<Props & WithRouterPro <em className="mandatory">*</em> </label> <OrganizationSelect + hideIcons={!autoImport} onChange={onChange} organization={organization} organizations={organizations} diff --git a/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx index d27b37528cd..dd5c5bd2d84 100644 --- a/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx @@ -27,7 +27,7 @@ import { translate } from '../../../helpers/l10n'; interface Props { almApplication: AlmApplication; - onProjectCreate: (projectKeys: string[]) => void; + onProjectCreate: (projectKeys: string[], organization: string) => void; organization: string; } @@ -90,7 +90,11 @@ export default class RemoteRepositories extends React.PureComponent<Props, State }), organization: this.props.organization }).then( - ({ projects }) => 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<Props, State ))} </ul> </div> - <SubmitButton disabled={!this.isValid() || submitting}> - {translate('create')} - </SubmitButton> + <SubmitButton disabled={!this.isValid() || submitting}>{translate('setup')}</SubmitButton> <DeferredSpinner className="spacer-left" loading={submitting} /> </form> </DeferredSpinner> 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<RemoteRepositories['props']> = {}) { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap 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" /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ + <FormattedMessage + defaultMessage="onboarding.create_project.repository_imported" + id="onboarding.create_project.repository_imported" + values={ Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "proj_cool", - }, + "link": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "proj_cool", + }, + } + } + > + onboarding.create_project.see_project + </Link>, } } - > - onboarding.create_project.already_imported - </Link> + /> </span> </Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap 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`] = ` <Fragment> + <p + className="spacer-bottom" + > + onboarding.create_project.install_app_description.github + </p> <IdentityProviderLink className="display-inline-block" identityProvider={ 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 99e93d8f317..a191dcd385d 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 @@ -58,7 +58,7 @@ exports[`should render correctly 2`] = ` }, Object { "key": "manual", - "node": "onboarding.create_project.create_manually", + "node": "onboarding.create_project.setup_manually", }, ] } @@ -184,7 +184,7 @@ exports[`should switch tabs 1`] = ` }, Object { "key": "manual", - "node": "onboarding.create_project.create_manually", + "node": "onboarding.create_project.setup_manually", }, ] } 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 7f1e64d67e9..66d10b4b7be 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 @@ -4,7 +4,7 @@ exports[`should correctly create a project 1`] = ` <SubmitButton disabled={true} > - create + setup </SubmitButton> `; @@ -12,7 +12,7 @@ exports[`should correctly create a project 2`] = ` <SubmitButton disabled={false} > - create + setup </SubmitButton> `; @@ -88,7 +88,7 @@ exports[`should render correctly 1`] = ` <SubmitButton disabled={true} > - create + setup </SubmitButton> <DeferredSpinner className="spacer-left" diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap index 265fe1d1fe8..887b5cff207 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap @@ -15,6 +15,7 @@ exports[`should render correctly 1`] = ` </em> </label> <OrganizationSelect + hideIcons={true} onChange={[MockFunction]} organization="bar" organizations={ diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap index 01359b6a9f9..8379a51721e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap @@ -4,7 +4,7 @@ exports[`should correctly create a project 1`] = ` <SubmitButton disabled={false} > - create + setup </SubmitButton> `; @@ -24,7 +24,7 @@ exports[`should display the list of repositories 1`] = ` <SubmitButton disabled={true} > - create + setup </SubmitButton> <DeferredSpinner className="spacer-left" @@ -102,7 +102,7 @@ exports[`should display the list of repositories 2`] = ` <SubmitButton disabled={true} > - create + setup </SubmitButton> <DeferredSpinner className="spacer-left" |