* 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 orgtags/7.5
@@ -115,7 +115,7 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
}; | |||
openOrganizationOnboarding = () => { | |||
this.closeOnboarding(); | |||
this.setState({ automatic: false, modal: undefined }); | |||
this.props.router.push({ pathname: '/create-organization', state: { paid: true } }); | |||
}; | |||
@@ -106,7 +106,7 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps, | |||
return ( | |||
<li> | |||
<a className="js-new-project" href="#" onClick={this.handleNewProjectClick}> | |||
{translate('provisioning.create_new_project')} | |||
{translate('provisioning.analyze_new_project')} | |||
</a> | |||
</li> | |||
); |
@@ -12,7 +12,7 @@ exports[`render 1`] = ` | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
provisioning.create_new_project | |||
provisioning.analyze_new_project | |||
</a> | |||
</li> | |||
</ul> | |||
@@ -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 | |||
</a> | |||
</li> | |||
<li> | |||
@@ -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 | |||
</a> | |||
</li> | |||
<li> |
@@ -368,6 +368,7 @@ td.big-spacer-top { | |||
.vertical-pipe-separator { | |||
display: flex; | |||
flex-direction: column; | |||
margin-left: 60px; | |||
margin-right: 60px; | |||
} | |||
@@ -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} | |||
/> |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
}; | |||
} |
@@ -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); |
@@ -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(); | |||
}); |
@@ -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" | |||
/> |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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 /> |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); |
@@ -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> | |||
)} | |||
</> |
@@ -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); |
@@ -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} | |||
/> | |||
); |
@@ -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} | |||
/> | |||
); |
@@ -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 () => { |
@@ -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(); | |||
); | |||
} |
@@ -21,7 +21,7 @@ exports[`should render correctly 1`] = ` | |||
} | |||
/> | |||
<div | |||
className="big-spacer-top" | |||
className="display-flex-center big-spacer-top" | |||
> | |||
<SubmitButton | |||
disabled={false} |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> |
@@ -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> | |||
`; |
@@ -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} |
@@ -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> | |||
`; |
@@ -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 && ( |
@@ -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} |
@@ -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') } | |||
]} | |||
/> | |||
)} |
@@ -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> | |||
</> |
@@ -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} |
@@ -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> |
@@ -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']> = {}) { |
@@ -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> | |||
`; |
@@ -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={ |
@@ -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", | |||
}, | |||
] | |||
} |
@@ -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" |
@@ -15,6 +15,7 @@ exports[`should render correctly 1`] = ` | |||
</em> | |||
</label> | |||
<OrganizationSelect | |||
hideIcons={true} | |||
onChange={[MockFunction]} | |||
organization="bar" | |||
organizations={ |
@@ -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" |
@@ -21,10 +21,11 @@ import * as React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import { debounce } from 'lodash'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { updateOrganization } from '../actions'; | |||
import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; | |||
import { SubmitButton } from '../../../components/ui/buttons'; | |||
import { updateOrganization } from '../actions'; | |||
import { Organization, OrganizationBase } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface DispatchProps { | |||
updateOrganization: (organization: string, changes: OrganizationBase) => Promise<any>; | |||
@@ -138,19 +139,25 @@ export class OrganizationEdit extends React.PureComponent<Props, State> { | |||
maxLength={256} | |||
name="avatar" | |||
onChange={this.handleAvatarInputChange} | |||
placeholder={translate('onboarding.create_organization.avatar.placeholder')} | |||
type="text" | |||
value={this.state.avatar} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.avatar.description')} | |||
</div> | |||
{!!this.state.avatarImage && ( | |||
{(this.state.avatarImage || this.state.name) && ( | |||
<div className="spacer-top spacer-bottom"> | |||
<div className="little-spacer-bottom"> | |||
{translate('organization.avatar.preview')} | |||
{':'} | |||
</div> | |||
<img alt="" height={30} src={this.state.avatarImage} /> | |||
<OrganizationAvatar | |||
organization={{ | |||
avatar: this.state.avatarImage || undefined, | |||
name: this.state.name || '' | |||
}} | |||
/> | |||
</div> | |||
)} | |||
</div> |
@@ -53,7 +53,7 @@ export class OrganizationJustCreated extends React.PureComponent<Props & WithRou | |||
<Button className="onboarding-choice" onClick={this.handleNewProjectClick}> | |||
<OnboardingProjectIcon className="big-spacer-bottom" /> | |||
<h6 className="onboarding-choice-name"> | |||
{translate('provisioning.create_new_project')} | |||
{translate('provisioning.analyze_new_project')} | |||
</h6> | |||
</Button> | |||
<Button className="onboarding-choice" onClick={this.handleAddMembersClick}> |
@@ -67,6 +67,7 @@ exports[`smoke test 1`] = ` | |||
maxLength={256} | |||
name="avatar" | |||
onChange={[Function]} | |||
placeholder="onboarding.create_organization.avatar.placeholder" | |||
type="text" | |||
value="" | |||
/> | |||
@@ -75,6 +76,24 @@ exports[`smoke test 1`] = ` | |||
> | |||
organization.avatar.description | |||
</div> | |||
<div | |||
className="spacer-top spacer-bottom" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
organization.avatar.preview | |||
: | |||
</div> | |||
<OrganizationAvatar | |||
organization={ | |||
Object { | |||
"avatar": undefined, | |||
"name": "Foo", | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field" | |||
@@ -203,6 +222,7 @@ exports[`smoke test 2`] = ` | |||
maxLength={256} | |||
name="avatar" | |||
onChange={[Function]} | |||
placeholder="onboarding.create_organization.avatar.placeholder" | |||
type="text" | |||
value="foo-avatar" | |||
/> | |||
@@ -220,10 +240,13 @@ exports[`smoke test 2`] = ` | |||
organization.avatar.preview | |||
: | |||
</div> | |||
<img | |||
alt="" | |||
height={30} | |||
src="foo-avatar-image" | |||
<OrganizationAvatar | |||
organization={ | |||
Object { | |||
"avatar": "foo-avatar-image", | |||
"name": "New Foo", | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
@@ -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 | |||
: | |||
</div> | |||
<img | |||
alt="" | |||
height={30} | |||
src="foo-avatar-image" | |||
<OrganizationAvatar | |||
organization={ | |||
Object { | |||
"avatar": "foo-avatar-image", | |||
"name": "New Foo", | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> |
@@ -22,7 +22,7 @@ exports[`should render 1`] = ` | |||
<h6 | |||
className="onboarding-choice-name" | |||
> | |||
provisioning.create_new_project | |||
provisioning.analyze_new_project | |||
</h6> | |||
</Button> | |||
<Button |
@@ -54,9 +54,7 @@ export class NoFavoriteProjects extends React.PureComponent<StateProps> { | |||
<p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p> | |||
<div className="huge-spacer-top"> | |||
<Button onClick={this.onAnalyzeProjectClick}> | |||
{isSonarCloud() | |||
? translate('provisioning.create_new_project') | |||
: translate('my_account.analyze_new_project')} | |||
{translate('provisioning.analyze_new_project')} | |||
</Button> | |||
<Dropdown |
@@ -48,7 +48,7 @@ exports[`renders for SonarCloud 1`] = ` | |||
<Button | |||
onClick={[Function]} | |||
> | |||
provisioning.create_new_project | |||
provisioning.analyze_new_project | |||
</Button> | |||
<Dropdown | |||
className="display-inline-block big-spacer-left" |
@@ -112,7 +112,7 @@ | |||
.Select-value svg, | |||
.Select-value img { | |||
padding-top: 3px; | |||
padding-top: 4px; | |||
} | |||
.Select-option svg, |
@@ -152,6 +152,7 @@ see_all=See All | |||
select_verb=Select | |||
selected=Selected | |||
set=Set | |||
setup=Setup | |||
severity=Severity | |||
shared=Shared | |||
start_date=Start Date | |||
@@ -1499,13 +1500,12 @@ my_account.organizations.no_results=You are not a member of any organizations ye | |||
my_account.create_organization=Create Organization | |||
my_account.search_project=Search Project | |||
my_account.set_notifications_for=Set notifications for | |||
my_account.analyze_new_project=Analyze new project | |||
my_account.create_new_portfolio_application=Create new portfolio / application | |||
my_account.create_new.VW=Create new portfolio | |||
my_account.create_new.APP=Create new application | |||
my_account.create_new_organization=Create new organization | |||
my_account.create_new_project_or_organization=Create new project or organization | |||
my_account.create_new_project_portfolio_or_application=Create new project, portfolio or application | |||
my_account.create_new_project_or_organization=Analyze new project or create new organization | |||
my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application | |||
#------------------------------------------------------------------------------ | |||
@@ -1513,7 +1513,7 @@ my_account.create_new_project_portfolio_or_application=Create new project, portf | |||
# PROJECT PROVISIONING | |||
# | |||
#------------------------------------------------------------------------------ | |||
provisioning.create_new_project=Create new project | |||
provisioning.analyze_new_project=Analyze new project | |||
provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration. | |||
provisioning.no_analysis.delete=Either you should retry analysis or simply {link}. | |||
provisioning.no_analysis.delete_project=delete the project | |||
@@ -2717,15 +2717,17 @@ onboarding.project_analysis.simply_link=Simply {link}. | |||
onboarding.project_analysis.suggestions.bitbucket=If you are using Bitbucket Cloud Pipelines, the SonarCloud App makes it easier to run these commands with your CI process. | |||
onboarding.project_analysis.suggestions.github=If you are using Travis CI, the SonarCloud Travis Add-on makes it easier to run these commands with your CI process. | |||
onboarding.create_project.header=Create project(s) | |||
onboarding.create_project.already_imported=Repository already imported | |||
onboarding.create_project.create_manually=Create manually | |||
onboarding.create_project.header=Analyze projects | |||
onboarding.create_project.setup_manually=Setup manually | |||
onboarding.create_project.create_new_org=I want to create another organization | |||
onboarding.create_project.import_new_org=I want to import another organization | |||
onboarding.create_project.install_app_x.button=Install SonarCloud {0} application | |||
onboarding.create_project.install_app_description.bitbucket=We need you to install the SonarCloud Bitbucket application on one of your team in order to select which repositories you want to analyze. | |||
onboarding.create_project.install_app_description.github=We need you to install the SonarCloud GitHub application on one of your organization in order to select which repositories you want to analyze. | |||
onboarding.create_project.organization=Organization | |||
onboarding.create_project.project_key=Project key | |||
onboarding.create_project.project_name=Project name | |||
onboarding.create_project.repository_imported=Already imported: {link} | |||
onboarding.create_project.see_project=See the project | |||
onboarding.create_project.select_repositories=Select repositories | |||
onboarding.create_organization.page.header=Create Organization | |||
@@ -2744,6 +2746,7 @@ onboarding.create_organization.display_name.error=The provided value doesn't mat | |||
onboarding.create_organization.avatar=Avatar | |||
onboarding.create_organization.avatar.description=Url of a small image that represents the organization (preferably 30px height). | |||
onboarding.create_organization.avatar.error=The value must be a valid url. | |||
onboarding.create_organization.avatar.placeholder=Default avatar | |||
onboarding.create_organization.url=URL | |||
onboarding.create_organization.url.error=The value must be a valid url. | |||
onboarding.create_organization.description=Description | |||
@@ -2765,15 +2768,14 @@ onboarding.import_organization.org_not_found.tips_2=Try to uninstall and re-inst | |||
onboarding.import_organization.choose_organization=Choose an organization... | |||
onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket | |||
onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub | |||
onboarding.import_organization.installing=Installation of the ALM application in progress... | |||
onboarding.import_organization.installing.bitbucket=Installation of the Bitbucket application in progress.. | |||
onboarding.import_organization.installing.github=Installation of the GitHub application in progress... | |||
onboarding.import_organization.installing=Finalize installation of the ALM application... | |||
onboarding.import_organization.installing.bitbucket=Finalize installation of the Bitbucket application.. | |||
onboarding.import_organization.installing.github=Finalize installation of the GitHub application... | |||
onboarding.import_organization.personal.page.header=Bind to your personal organization | |||
onboarding.import_organization.personal.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. | |||
onboarding.import_organization.private.disabled=It is not possible to automatically import private repositories yet. | |||
onboarding.import_organization.private.disabled=Selecting private repository is not available yet and will come soon. Meanwhile, you need to create the project manually. | |||
onboarding.import_organization.bitbucket=Import from BitBucket teams | |||
onboarding.import_organization.github=Import from GitHub organizations | |||
onboarding.import_organization.bind_existing=Bind to an existing sonarcloud organization | |||
onboarding.import_organization.bind_existing=Bind to an existing SonarCloud organization | |||
onboarding.import_organization.create_new=Create new SonarCloud organization from it | |||
onboarding.import_organization.already_bound_x=Your organization {avatar} {name} is already bound to the SonarCloud organization {boundAvatar} {boundName}. Try again and choose a different organization. | |||
onboarding.import_organization_x=Import {avatar} {name} into SonarCloud organization |