diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-10-19 17:25:13 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-16 20:21:04 +0100 |
commit | 07546d5e1f4047a1030a91d0ffaa39fb96e66a41 (patch) | |
tree | 442327150154571ec1be84a47cf4c012f0ba50bc | |
parent | 3ea9808248000c145f53a4f1cdb8711d63b97da4 (diff) | |
download | sonarqube-07546d5e1f4047a1030a91d0ffaa39fb96e66a41.tar.gz sonarqube-07546d5e1f4047a1030a91d0ffaa39fb96e66a41.zip |
SONAR-11323 Ease workflow to bind personal organizations
* Create withUserOrganizations and use it in create Orgs/Projects page
* Update ALM object format in api/navigation/component and api/organizations/search
36 files changed, 488 insertions, 174 deletions
diff --git a/server/sonar-web/src/main/js/api/alm-integration.ts b/server/sonar-web/src/main/js/api/alm-integration.ts index 5805eea72b9..568eb2407ff 100644 --- a/server/sonar-web/src/main/js/api/alm-integration.ts +++ b/server/sonar-web/src/main/js/api/alm-integration.ts @@ -17,10 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getJSON, postJSON } from '../helpers/request'; +import { getJSON, postJSON, post } from '../helpers/request'; import { AlmRepository, AlmApplication, AlmOrganization } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; +export function bindAlmOrganization(data: { installationId: string; organization: string }) { + return post('/api/alm_integration/bind_organization', data).catch(throwGlobalError); +} + export function getAlmAppInfo(): Promise<{ application: AlmApplication }> { return getJSON('/api/alm_integration/show_app_info').catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts index 1b72037ce83..3ed1fe0c87c 100644 --- a/server/sonar-web/src/main/js/api/organizations.ts +++ b/server/sonar-web/src/main/js/api/organizations.ts @@ -55,7 +55,7 @@ export function getOrganizationNavigation(key: string): Promise<GetOrganizationN } export function createOrganization( - data: OrganizationBase & { installId?: string } + data: OrganizationBase & { installationId?: string } ): Promise<Organization> { return postJSON('/api/organizations/create', data).then(r => r.organization, throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx index f32e3dfa308..f3301290cd3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx @@ -73,17 +73,17 @@ export function ComponentNavHeader(props: Props) { )} {renderBreadcrumbs(component.breadcrumbs)} {isSonarCloud() && - component.almRepoUrl && ( + component.alm && ( <a className="link-no-underline" - href={component.almRepoUrl} + href={component.alm.url} rel="noopener noreferrer" target="_blank"> <img - alt={sanitizeAlmId(component.almId)} + alt={sanitizeAlmId(component.alm.key)} className="text-text-top spacer-left" height={16} - src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.almId)}.svg`} + src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.alm.key)}.svg`} width={16} /> </a> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx index 4c3eb39bf08..2d1dd8d5f65 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx @@ -77,8 +77,7 @@ it('should render alm links', () => { branchLikes={[]} component={{ ...component, - almId: 'bitbucketcloud', - almRepoUrl: 'https://bitbucket.org/foo' + alm: { key: 'bitbucketcloud', url: 'https://bitbucket.org/foo' } }} currentBranchLike={undefined} organization={organization} diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 803ba673ad2..df5915bbfd0 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -89,8 +89,7 @@ export interface Breadcrumb { } export interface Component extends LightComponent { - almId?: string; - almRepoUrl?: string; + alm?: { key: string; url: string }; analysisDate?: string; breadcrumbs: Breadcrumb[]; configuration?: ComponentConfiguration; @@ -412,6 +411,7 @@ export interface LoggedInUser extends CurrentUser { local?: boolean; login: string; name: string; + personalOrganization?: string; scmAccounts: string[]; } @@ -480,8 +480,7 @@ export interface Notification { } export interface Organization extends OrganizationBase { - almId?: string; - almRepoUrl?: string; + alm?: { key: string; url: string }; adminPages?: Extension[]; canAdmin?: boolean; canDelete?: boolean; 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 f7882de2ba9..14fc63bdbf8 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 @@ -30,45 +30,68 @@ import { import { getBaseUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; import { sanitizeAlmId } from '../../../helpers/almIntegrations'; +import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; interface Props { almApplication: AlmApplication; almInstallId?: string; almOrganization?: AlmOrganization; createOrganization: ( - organization: OrganizationBase & { installId?: string } + organization: OrganizationBase & { installationId?: string } ) => Promise<Organization>; + importPersonalOrg?: Organization; onOrgCreated: (organization: string) => void; + updateOrganization: ( + organization: OrganizationBase & { installationId?: string } + ) => Promise<Organization>; } export default class AutoOrganizationCreate extends React.PureComponent<Props> { handleCreateOrganization = (organization: Required<OrganizationBase>) => { if (organization) { - return this.props - .createOrganization({ + const { importPersonalOrg } = this.props; + let promise: Promise<Organization>; + if (importPersonalOrg) { + promise = this.props.updateOrganization({ + avatar: organization.avatar, + description: organization.description, + installationId: this.props.almInstallId, + key: importPersonalOrg.key, + name: organization.name || organization.key, + url: organization.url + }); + } else { + promise = this.props.createOrganization({ avatar: organization.avatar, description: organization.description, - installId: this.props.almInstallId, + installationId: this.props.almInstallId, key: organization.key, name: organization.name || organization.key, url: organization.url - }) - .then(({ key }) => this.props.onOrgCreated(key)); + }); + } + return promise.then(({ key }) => this.props.onOrgCreated(key)); } else { return Promise.reject(); } }; render() { - const { almApplication, almInstallId, almOrganization } = this.props; + const { almApplication, almInstallId, almOrganization, importPersonalOrg } = this.props; if (almInstallId && almOrganization) { + const description = importPersonalOrg + ? translate('onboarding.import_personal_organization_x') + : translate('onboarding.import_organization_x'); + const submitText = importPersonalOrg + ? translate('onboarding.import_organization.bind') + : translate('my_account.create_organization'); return ( <OrganizationDetailsStep description={ <p className="huge-spacer-bottom"> <FormattedMessage - defaultMessage={translate('onboarding.create_organization.import_organization_x')} - id="onboarding.create_organization.import_organization_x" + defaultMessage={description} + id={description} values={{ avatar: ( <img @@ -80,17 +103,22 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props> { width={16} /> ), - name: <strong>{almOrganization.name}</strong> + name: <strong>{almOrganization.name}</strong>, + personalAvatar: importPersonalOrg && ( + <OrganizationAvatar organization={importPersonalOrg} small={true} /> + ), + personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong> }} /> </p> } finished={false} + keyReadOnly={Boolean(importPersonalOrg)} onContinue={this.handleCreateOrganization} onOpen={() => {}} open={true} - organization={almOrganization} - submitText={translate('my_account.create_organization')} + organization={importPersonalOrg || almOrganization} + submitText={submitText} /> ); } 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 56e401bd103..17c0e3a83ee 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 @@ -30,7 +30,12 @@ import ManualOrganizationCreate from './ManualOrganizationCreate'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Tabs from '../../../components/controls/Tabs'; import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; -import { getAlmAppInfo, getAlmOrganization } from '../../../api/alm-integration'; +import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations'; +import { + getAlmAppInfo, + getAlmOrganization, + bindAlmOrganization +} from '../../../api/alm-integration'; import { getSubscriptionPlans } from '../../../api/billing'; import { LoggedInUser, @@ -40,7 +45,7 @@ import { AlmOrganization, OrganizationBase } from '../../../app/types'; -import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations'; +import { hasAdvancedALMIntegration, isPersonal } from '../../../helpers/almIntegrations'; import { translate } from '../../../helpers/l10n'; import { getOrganizationUrl } from '../../../helpers/urls'; import * as api from '../../../api/organizations'; @@ -49,9 +54,15 @@ import '../../../app/styles/sonarcloud.css'; import '../../tutorials/styles.css'; // TODO remove me interface Props { - createOrganization: (organization: OrganizationBase) => Promise<Organization>; + createOrganization: ( + organization: OrganizationBase & { installationId?: string } + ) => Promise<Organization>; currentUser: LoggedInUser; deleteOrganization: (key: string) => Promise<void>; + updateOrganization: ( + organization: OrganizationBase & { installationId?: string } + ) => Promise<Organization>; + userOrganizations: Organization[]; } interface State { @@ -146,11 +157,19 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr }; render() { - const { location } = this.props; - const { almApplication, loading, subscriptionPlans } = this.state; + const { currentUser, location } = this.props; + const { almApplication, almOrganization, loading, subscriptionPlans } = this.state; const state = (location.state || {}) as LocationState; const query = parseQuery(location.query); - const header = translate('onboarding.create_organization.page.header'); + const importPersonalOrg = isPersonal(almOrganization) + ? this.props.userOrganizations.find(o => o.key === currentUser.personalOrganization) + : undefined; + 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); const showManualTab = state.tab === 'manual' && !query.almInstallId; @@ -164,8 +183,8 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr {startedPrice !== undefined && ( <p className="page-description"> <FormattedMessage - defaultMessage={translate('onboarding.create_organization.page.description')} - id="onboarding.create_organization.page.description" + defaultMessage={description} + id={description} values={{ break: <br />, price: formattedPrice, @@ -184,36 +203,34 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr <DeferredSpinner /> ) : ( <> - {almApplication && ( - <Tabs<TabKeys> - onChange={this.onTabChange} - selected={showManualTab ? 'manual' : 'auto'} - tabs={[ - { - key: 'auto', - node: ( - <> - {translate( - 'onboarding.create_organization.import_organization', - almApplication.key - )} - <span - className={classNames('beta-badge spacer-left', { - 'is-muted': showManualTab - })}> - {translate('beta')} - </span> - </> - ) - }, - { - disabled: Boolean(query.almInstallId), - key: 'manual', - node: translate('onboarding.create_organization.create_manually') - } - ]} - /> - )} + {almApplication && + !importPersonalOrg && ( + <Tabs<TabKeys> + onChange={this.onTabChange} + selected={showManualTab ? 'manual' : 'auto'} + tabs={[ + { + key: 'auto', + node: ( + <> + {translate('onboarding.import_organization', almApplication.key)} + <span + className={classNames('beta-badge spacer-left', { + 'is-muted': showManualTab + })}> + {translate('beta')} + </span> + </> + ) + }, + { + disabled: Boolean(query.almInstallId), + key: 'manual', + node: translate('onboarding.create_organization.create_manually') + } + ]} + /> + )} {showManualTab || !almApplication ? ( <ManualOrganizationCreate @@ -227,9 +244,11 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr <AutoOrganizationCreate almApplication={almApplication} almInstallId={query.almInstallId} - almOrganization={this.state.almOrganization} + almOrganization={almOrganization} createOrganization={this.props.createOrganization} + importPersonalOrg={importPersonalOrg} onOrgCreated={this.handleOrgCreated} + updateOrganization={this.props.updateOrganization} /> )} </> @@ -240,7 +259,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr } } -function createOrganization(organization: OrganizationBase & { installId?: string }) { +function createOrganization(organization: OrganizationBase & { installationId?: string }) { return (dispatch: Dispatch) => { return api.createOrganization(organization).then((organization: Organization) => { dispatch(actions.createOrganization(organization)); @@ -249,6 +268,22 @@ function createOrganization(organization: OrganizationBase & { installId?: strin }; } +function updateOrganization( + organization: OrganizationBase & { key: string; installationId?: string } +) { + return (dispatch: Dispatch) => { + const { key, installationId, ...changes } = organization; + const promises = [api.updateOrganization(key, changes)]; + if (installationId) { + promises.push(bindAlmOrganization({ organization: key, installationId })); + } + return Promise.all(promises).then(() => { + dispatch(actions.updateOrganization(key, changes)); + return organization; + }); + }; +} + function deleteOrganization(key: string) { return (dispatch: Dispatch) => { return api.deleteOrganization(key).then(() => { @@ -259,14 +294,17 @@ function deleteOrganization(key: string) { const mapDispatchToProps = { createOrganization: createOrganization as any, - deleteOrganization: deleteOrganization as any + deleteOrganization: deleteOrganization as any, + updateOrganization: updateOrganization as any }; export default whenLoggedIn( - withRouter( - connect( - null, - mapDispatchToProps - )(CreateOrganization) + withUserOrganizations( + withRouter( + connect( + null, + mapDispatchToProps + )(CreateOrganization) + ) ) ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx index 2b31d8a2379..d6e5f0696a1 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx @@ -35,6 +35,7 @@ type RequiredOrganization = Required<OrganizationBase>; interface Props { description?: React.ReactNode; finished: boolean; + keyReadOnly?: boolean; onContinue: (organization: RequiredOrganization) => Promise<void>; onOpen: () => void; open: boolean; @@ -141,7 +142,11 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props, <div className="boxed-group-inner"> <form id="organization-form" onSubmit={this.handleSubmit}> {this.props.description} - <OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} /> + <OrganizationKeyInput + initialValue={this.state.key} + onChange={this.handleKeyUpdate} + readOnly={this.props.keyReadOnly} + /> <div className="big-spacer-top"> <ResetButtonLink onClick={this.handleAdditionalClick}> {translate( @@ -162,6 +167,7 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props, <div className="big-spacer-top"> <OrganizationAvatarInput initialValue={this.state.avatar} + name={this.state.name} onChange={this.handleDescriptionUpdate} /> </div> 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 8beb62e897c..17e57b70a94 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 @@ -52,10 +52,31 @@ it('should render prefilled and create org', async () => { wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization); await waitAndUpdate(wrapper); - expect(createOrganization).toBeCalledWith({ ...organization, installId: 'id-foo' }); + expect(createOrganization).toBeCalledWith({ ...organization, installationId: 'id-foo' }); expect(onOrgCreated).toBeCalledWith('foo'); }); +it('should render for personal organizations', async () => { + const personalOrg = { key: 'personal-org', name: 'personal-org' }; + const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key }); + const onOrgCreated = jest.fn(); + const wrapper = shallowRender({ + almInstallId: 'id-foo', + almOrganization: { ...organization, type: 'USER' }, + importPersonalOrg: personalOrg, + onOrgCreated, + updateOrganization + }); + + expect(wrapper).toMatchSnapshot(); + + wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(personalOrg); + await waitAndUpdate(wrapper); + + expect(updateOrganization).toBeCalledWith({ ...personalOrg, installationId: 'id-foo' }); + expect(onOrgCreated).toBeCalledWith(personalOrg.key); +}); + function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { return shallow( <AutoOrganizationCreate @@ -68,6 +89,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { }} createOrganization={jest.fn()} onOrgCreated={jest.fn()} + updateOrganization={jest.fn()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index 9c1e58367fb..330f8062730 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 @@ -116,6 +116,10 @@ function shallowRender(props: Partial<CreateOrganization['props']> = {}) { // @ts-ignore avoid passing everything from WithRouterProps location={{}} router={mockRouter()} + userOrganizations={[ + { key: 'foo', name: 'Foo' }, + { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } + ]} {...props} /> ); 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 a57042c6f50..423b5f2181b 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,5 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should render for personal organizations 1`] = ` +<OrganizationDetailsStep + description={ + <p + className="huge-spacer-bottom" + > + <FormattedMessage + defaultMessage="onboarding.import_personal_organization_x" + id="onboarding.import_personal_organization_x" + values={ + Object { + "avatar": <img + alt="BitBucket" + className="little-spacer-left" + src="/images/sonarcloud/bitbucket.svg" + width={16} + />, + "name": <strong> + name-foo + </strong>, + "personalAvatar": <OrganizationAvatar + organization={ + Object { + "key": "personal-org", + "name": "personal-org", + } + } + small={true} + />, + "personalName": <strong> + personal-org + </strong>, + } + } + /> + </p> + } + finished={false} + keyReadOnly={true} + onContinue={[Function]} + onOpen={[Function]} + open={true} + organization={ + Object { + "key": "personal-org", + "name": "personal-org", + } + } + submitText="onboarding.import_organization.bind" +/> +`; + exports[`should render prefilled and create org 1`] = ` <OrganizationDetailsStep description={ @@ -7,8 +59,8 @@ exports[`should render prefilled and create org 1`] = ` className="huge-spacer-bottom" > <FormattedMessage - defaultMessage="onboarding.create_organization.import_organization_x" - id="onboarding.create_organization.import_organization_x" + defaultMessage="onboarding.import_organization_x" + id="onboarding.import_organization_x" values={ Object { "avatar": <img @@ -20,12 +72,15 @@ exports[`should render prefilled and create org 1`] = ` "name": <strong> name-foo </strong>, + "personalAvatar": undefined, + "personalName": undefined, } } /> </p> } finished={false} + keyReadOnly={false} onContinue={[Function]} onOpen={[Function]} open={true} 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 c4b506bc1f3..c25c32f3e2c 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 @@ -50,7 +50,7 @@ exports[`should render with auto tab displayed 1`] = ` Object { "key": "auto", "node": <React.Fragment> - onboarding.create_organization.import_organization.github + onboarding.import_organization.github <span className="beta-badge spacer-left" > @@ -132,7 +132,7 @@ exports[`should render with auto tab selected and manual disabled 1`] = ` Object { "key": "auto", "node": <React.Fragment> - onboarding.create_organization.import_organization.github + onboarding.import_organization.github <span className="beta-badge spacer-left" > @@ -286,7 +286,7 @@ exports[`should switch tabs 1`] = ` Object { "key": "auto", "node": <React.Fragment> - onboarding.create_organization.import_organization.github + onboarding.import_organization.github <span className="beta-badge spacer-left" > diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx index 0fd3c61b35a..a4fcc91d979 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/components/OrganizationKeyInput.tsx @@ -28,6 +28,7 @@ import { getHostUrl } from '../../../../helpers/urls'; interface Props { initialValue?: string; onChange: (value: string | undefined) => void; + readOnly?: boolean; } interface State { @@ -50,7 +51,9 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta this.mounted = true; if (this.props.initialValue !== undefined) { this.setState({ value: this.props.initialValue }); - this.validateKey(this.props.initialValue); + if (!this.props.readOnly) { + this.validateKey(this.props.initialValue); + } } } @@ -118,25 +121,28 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta isInvalid={isInvalid} isValid={isValid} label={translate('onboarding.create_organization.organization_name')} - required={true}> + required={!this.props.readOnly}> <div className="display-inline-flex-baseline"> <span className="little-spacer-right"> {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'} + {this.props.readOnly && this.state.value} </span> - <input - autoFocus={true} - className={classNames('input-super-large', 'text-middle', { - 'is-invalid': isInvalid, - 'is-valid': isValid - })} - id="organization-key" - maxLength={255} - onBlur={this.handleBlur} - onChange={this.handleChange} - onFocus={this.handleFocus} - type="text" - value={this.state.value} - /> + {!this.props.readOnly && ( + <input + autoFocus={true} + className={classNames('input-super-large', 'text-middle', { + 'is-invalid': isInvalid, + 'is-valid': isValid + })} + id="organization-key" + maxLength={255} + onBlur={this.handleBlur} + onChange={this.handleChange} + onFocus={this.handleFocus} + type="text" + value={this.state.value} + /> + )} </div> </ValidationInput> ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx index a6bcde51a7e..d559b30e4d0 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/OrganizationKeyInput-test.tsx @@ -38,6 +38,13 @@ 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/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap index 8cba7d969a3..05d2e74dd68 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap @@ -32,3 +32,24 @@ 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/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx index d46f7bbaa36..53382bfc34e 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 @@ -27,8 +27,7 @@ import ManualProjectCreate from './ManualProjectCreate'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Tabs from '../../../components/controls/Tabs'; import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; -import { fetchMyOrganizations } from '../../account/organizations/actions'; -import { getMyOrganizations, Store } from '../../../store/rootReducer'; +import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations'; import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; import { LoggedInUser, AlmApplication, Organization } from '../../../app/types'; import { getAlmAppInfo } from '../../../api/alm-integration'; @@ -38,14 +37,10 @@ import { translate } from '../../../helpers/l10n'; import { getProjectUrl } from '../../../helpers/urls'; import '../../../app/styles/sonarcloud.css'; -interface StateProps { - userOrganizations: Organization[]; -} - interface Props { currentUser: LoggedInUser; - fetchMyOrganizations: () => Promise<void>; skipOnboardingAction: () => void; + userOrganizations: Organization[]; } interface State { @@ -60,16 +55,12 @@ interface LocationState { tab?: TabKeys; } -export class CreateProjectPage extends React.PureComponent< - Props & StateProps & WithRouterProps, - State -> { +export class CreateProjectPage extends React.PureComponent<Props & WithRouterProps, State> { mounted = false; state: State = { loading: true }; componentDidMount() { this.mounted = true; - this.props.fetchMyOrganizations(); if (hasAdvancedALMIntegration(this.props.currentUser)) { this.fetchAlmApplication(); } else { @@ -178,7 +169,7 @@ export class CreateProjectPage extends React.PureComponent< ) : ( <AutoProjectCreate almApplication={almApplication} - boundOrganizations={userOrganizations.filter(o => o.almId)} + boundOrganizations={userOrganizations.filter(o => o.alm)} onProjectCreate={this.handleProjectCreate} organization={state.organization} /> @@ -191,20 +182,13 @@ export class CreateProjectPage extends React.PureComponent< } } -const mapDispatchToProps = { - fetchMyOrganizations, - skipOnboardingAction -}; - -const mapStateToProps = (state: Store) => { - return { - userOrganizations: getMyOrganizations(state) - }; -}; +const mapDispatchToProps = { skipOnboardingAction }; export default whenLoggedIn( - connect<StateProps>( - mapStateToProps, - mapDispatchToProps - )(CreateProjectPage) + withUserOrganizations( + connect( + null, + mapDispatchToProps + )(CreateProjectPage) + ) ); diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx index a1d52e0af64..ed7cf5d30ee 100644 --- a/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/OrganizationSelect.tsx @@ -71,12 +71,12 @@ export default function OrganizationSelect({ export function optionRenderer(organization: Organization) { return ( <span> - {organization.almId && ( + {organization.alm && ( <img - alt={organization.almId} + alt={organization.alm.key} className="spacer-right" height={14} - src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`} + src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`} /> )} {organization.name} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx index 3364a73a344..a7664c362be 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx @@ -42,8 +42,8 @@ function shallowRender(props: Partial<AutoProjectCreate['props']> = {}) { <AutoProjectCreate almApplication={almApplication} boundOrganizations={[ - { almId: 'github', key: 'foo', name: 'Foo' }, - { almId: 'github', key: 'bar', name: 'Bar' } + { alm: { key: 'github', url: '' }, key: 'foo', name: 'Foo' }, + { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } ]} onProjectCreate={jest.fn()} organization="" diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx index 6c9acb9d77c..12af30eb2b8 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx @@ -81,14 +81,13 @@ function getWrapper(props = {}) { <CreateProjectPage addGlobalErrorMessage={jest.fn()} currentUser={user} - fetchMyOrganizations={jest.fn()} // @ts-ignore avoid passing everything from WithRouterProps location={{}} router={mockRouter()} skipOnboardingAction={jest.fn()} userOrganizations={[ { key: 'foo', name: 'Foo' }, - { almId: 'github', key: 'bar', name: 'Bar' } + { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } ]} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx index 4224b152a38..cc7e426bbc4 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationSelect-test.tsx @@ -21,7 +21,10 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import OrganizationSelect, { optionRenderer } from '../OrganizationSelect'; -const organizations = [{ key: 'foo', name: 'Foo' }, { almId: 'github', key: 'bar', name: 'Bar' }]; +const organizations = [ + { key: 'foo', name: 'Foo' }, + { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } +]; it('should render correctly', () => { expect( 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 147427d62a6..a96a37b53b5 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 @@ -9,12 +9,18 @@ exports[`should display the bounded organizations dropdown with the list of repo organizations={ Array [ Object { - "almId": "github", + "alm": Object { + "key": "github", + "url": "", + }, "key": "foo", "name": "Foo", }, Object { - "almId": "github", + "alm": Object { + "key": "github", + "url": "", + }, "key": "bar", "name": "Bar", }, 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 6e1f9059e89..5e2c2e1a150 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 @@ -83,7 +83,10 @@ exports[`should render correctly 2`] = ` boundOrganizations={ Array [ Object { - "almId": "github", + "alm": Object { + "key": "github", + "url": "", + }, "key": "bar", "name": "Bar", }, @@ -134,7 +137,10 @@ exports[`should render with Manual creation only 1`] = ` "name": "Foo", }, Object { - "almId": "github", + "alm": Object { + "key": "github", + "url": "", + }, "key": "bar", "name": "Bar", }, @@ -201,7 +207,10 @@ exports[`should switch tabs 1`] = ` boundOrganizations={ Array [ Object { - "almId": "github", + "alm": Object { + "key": "github", + "url": "", + }, "key": "bar", "name": "Bar", }, diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap index 50cd939ec7e..367f0265e72 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap @@ -25,7 +25,10 @@ exports[`should render correctly 1`] = ` options={ Array [ Object { - "almId": "github", + "alm": Object { + "key": "github", + "url": "", + }, "key": "bar", "name": "Bar", }, diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx index d376085573a..4d7bb7f71e1 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx @@ -58,17 +58,17 @@ export default function OrganizationNavigationHeader({ organization, organizatio ) : ( <span className="spacer-left">{organization.name}</span> )} - {organization.almRepoUrl && ( + {organization.alm && ( <a className="link-no-underline" - href={organization.almRepoUrl} + href={organization.alm.url} rel="noopener noreferrer" target="_blank"> <img - alt={sanitizeAlmId(organization.almId)} + alt={sanitizeAlmId(organization.alm.key)} className="text-text-top spacer-left" height={16} - src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`} + src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`} width={16} /> </a> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx index 021b80766e4..2d6853a617d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx @@ -38,8 +38,7 @@ it('renders with alm integration', () => { shallow( <OrganizationNavigationHeader organization={{ - almId: 'github', - almRepoUrl: 'https://github.com/foo', + alm: { key: 'github', url: 'https://github.com/foo' }, key: 'foo', name: 'Foo', projectVisibility: Visibility.Public diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap index cf3e383e573..ca3bd2d87cd 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap @@ -70,8 +70,10 @@ exports[`renders with alm integration 1`] = ` <OrganizationAvatar organization={ Object { - "almId": "github", - "almRepoUrl": "https://github.com/foo", + "alm": Object { + "key": "github", + "url": "https://github.com/foo", + }, "key": "foo", "name": "Foo", "projectVisibility": "public", diff --git a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx index d930fb3d064..031af9848cd 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorial.tsx @@ -56,7 +56,7 @@ export default class AnalyzeTutorial extends React.PureComponent<Props, State> { const { component, currentUser } = this.props; const { step, token } = this.state; - const almId = component.almId || currentUser.externalProvider; + const almKey = (component.alm && component.alm.key) || currentUser.externalProvider; return ( <> <div className="page-header big-spacer-bottom"> @@ -64,9 +64,9 @@ export default class AnalyzeTutorial extends React.PureComponent<Props, State> { <p className="page-description">{translate('onboarding.project_analysis.description')}</p> </div> - <AnalyzeTutorialSuggestion almId={almId} /> + <AnalyzeTutorialSuggestion almKey={almKey} /> - {!isVSTS(almId) && ( + {!isVSTS(almKey) && ( <> <TokenStep currentUser={currentUser} diff --git a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx index d433da91112..65b4f31b510 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/AnalyzeTutorialSuggestion.tsx @@ -24,8 +24,8 @@ import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/urls'; import { Alert } from '../../../components/ui/Alert'; -export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) { - if (isBitbucket(almId)) { +export default function AnalyzeTutorialSuggestion({ almKey }: { almKey?: string }) { + if (isBitbucket(almKey)) { return ( <Alert className="big-spacer-bottom" variant="info"> <p>{translate('onboarding.project_analysis.commands_for_analysis')}</p> @@ -49,7 +49,7 @@ export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) /> </Alert> ); - } else if (isGithub(almId)) { + } else if (isGithub(almKey)) { return ( <Alert className="big-spacer-bottom" variant="info"> <p>{translate('onboarding.project_analysis.commands_for_analysis')} </p> @@ -70,7 +70,7 @@ export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) /> </Alert> ); - } else if (isVSTS(almId)) { + } else if (isVSTS(almKey)) { return ( <Alert className="big-spacer-bottom" variant="info"> <FormattedMessage diff --git a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx index bc4a6d7b55b..22c182361a1 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/analyzeProject/__tests__/AnalyzeTutorialSuggestion-test.tsx @@ -22,17 +22,17 @@ import { shallow } from 'enzyme'; import AnalyzeTutorialSuggestion from '../AnalyzeTutorialSuggestion'; it('should not render', () => { - expect(shallow(<AnalyzeTutorialSuggestion almId={undefined} />).type()).toBeNull(); + expect(shallow(<AnalyzeTutorialSuggestion almKey={undefined} />).type()).toBeNull(); }); it('renders bitbucket suggestions correctly', () => { - expect(shallow(<AnalyzeTutorialSuggestion almId="bitbucket" />)).toMatchSnapshot(); + expect(shallow(<AnalyzeTutorialSuggestion almKey="bitbucket" />)).toMatchSnapshot(); }); it('renders github suggestions correctly', () => { - expect(shallow(<AnalyzeTutorialSuggestion almId="github" />)).toMatchSnapshot(); + expect(shallow(<AnalyzeTutorialSuggestion almKey="github" />)).toMatchSnapshot(); }); it('renders vsts suggestions correctly', () => { - expect(shallow(<AnalyzeTutorialSuggestion almId="microsoft" />)).toMatchSnapshot(); + expect(shallow(<AnalyzeTutorialSuggestion almKey="microsoft" />)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx index e75dad4cdd8..0bed5354f4e 100644 --- a/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/whenLoggedIn-test.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { createStore } from 'redux'; -import { mockRouter } from '../../../helpers/testUtils'; import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; import { whenLoggedIn } from '../whenLoggedIn'; @@ -44,8 +43,7 @@ it('should render for logged in user', () => { it('should not render for anonymous user', () => { const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } }); - const router = mockRouter({ replace: jest.fn() }); - const wrapper = shallow(<UnderTest />, { context: { store, router } }); + const wrapper = shallow(<UnderTest />, { context: { store } }); expect(getRenderedType(wrapper)).toBe(null); expect(handleRequiredAuthentication).toBeCalled(); }); @@ -54,6 +52,5 @@ function getRenderedType(wrapper: ShallowWrapper) { return wrapper .dive() .dive() - .dive() .type(); } diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx new file mode 100644 index 00000000000..dee2f7fec03 --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { createStore } from 'redux'; +import { Organization } from '../../../app/types'; +import { withUserOrganizations } from '../withUserOrganizations'; + +jest.mock('../../../api/organizations', () => ({ getOrganizations: jest.fn() })); + +class X extends React.Component<{ userOrganizations: Organization[] }> { + render() { + return <div />; + } +} + +const UnderTest = withUserOrganizations(X); + +// TODO Find a way to make this work, currently getting the following error : Actions must be plain objects. Use custom middleware for async actions. +it.skip('should pass user organizations and logged in user', () => { + const org = { key: 'my-org', name: 'My Organization' }; + const store = createStore(state => state, { + organizations: { byKey: { 'my-org': org }, my: ['my-org'] } + }); + const wrapper = shallow(<UnderTest />, { context: { store } }); + const wrappedComponent = wrapper + .dive() + .dive() + .dive(); + expect(wrappedComponent.type()).toBe(X); + expect(wrappedComponent.prop('userOrganizations')).toEqual([org]); +}); diff --git a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx index 00dd040670e..2ce4c8894c0 100644 --- a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx +++ b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { withRouter, WithRouterProps } from 'react-router'; import { withCurrentUser } from './withCurrentUser'; import { CurrentUser } from '../../app/types'; import { isLoggedIn } from '../../helpers/users'; @@ -27,7 +26,7 @@ import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthenti export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) { const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; - class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> { + class Wrapper extends React.Component<P & { currentUser: CurrentUser }> { static displayName = `whenLoggedIn(${wrappedDisplayName})`; componentDidMount() { @@ -45,5 +44,5 @@ export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) { } } - return withCurrentUser(withRouter(Wrapper)); + return withCurrentUser(Wrapper); } diff --git a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx new file mode 100644 index 00000000000..bedc033bc09 --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Store, getMyOrganizations } from '../../store/rootReducer'; +import { fetchMyOrganizations } from '../../apps/account/organizations/actions'; +import { Organization } from '../../app/types'; + +export function withUserOrganizations<P>( + WrappedComponent: React.ComponentClass< + P & { + personalOrganization?: Organization; + userOrganizations: Organization[]; + } + > +) { + type Props = P & { fetchMyOrganizations: () => Promise<void>; userOrganizations: Organization[] }; + const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + class Wrapper extends React.Component<Props> { + static displayName = `withUserOrganizations(${wrappedDisplayName})`; + + componentDidMount() { + this.props.fetchMyOrganizations(); + } + + render() { + // @ts-ignore Rest operator not supported yet by TS for generics + const { fetchMyOrganizations, ...other } = this.props; + return <WrappedComponent {...other} />; + } + } + + const mapDispatchToProps = { fetchMyOrganizations }; + + function mapStateToProps(state: Store) { + return { userOrganizations: getMyOrganizations(state) }; + } + + return connect( + mapStateToProps, + mapDispatchToProps + )(Wrapper); +} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts index 02353fe12e2..7a54b6b68bd 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isBitbucket, isGithub, isVSTS, sanitizeAlmId } from '../almIntegrations'; +import { isBitbucket, isGithub, isPersonal, isVSTS, sanitizeAlmId } from '../almIntegrations'; it('#isBitbucket', () => { expect(isBitbucket('bitbucket')).toBeTruthy(); @@ -35,6 +35,11 @@ it('#isVSTS', () => { expect(isVSTS('github')).toBeFalsy(); }); +it('#isPersonal', () => { + expect(isPersonal({ key: 'foo', name: 'Foo', type: 'USER' })).toBeTruthy(); + expect(isPersonal({ key: 'foo', name: 'Foo', type: 'ORGANIZATION' })).toBeFalsy(); +}); + it('#sanitizeAlmId', () => { expect(sanitizeAlmId('bitbucketcloud')).toBe('bitbucket'); expect(sanitizeAlmId('bitbucket')).toBe('bitbucket'); diff --git a/server/sonar-web/src/main/js/helpers/almIntegrations.ts b/server/sonar-web/src/main/js/helpers/almIntegrations.ts index c943f67b90e..fdfe7abd17c 100644 --- a/server/sonar-web/src/main/js/helpers/almIntegrations.ts +++ b/server/sonar-web/src/main/js/helpers/almIntegrations.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { isLoggedIn } from './users'; -import { CurrentUser } from '../app/types'; +import { CurrentUser, AlmOrganization } from '../app/types'; export function hasAdvancedALMIntegration(user: CurrentUser) { return ( @@ -26,21 +26,25 @@ export function hasAdvancedALMIntegration(user: CurrentUser) { ); } -export function isBitbucket(almId?: string) { - return almId && almId.startsWith('bitbucket'); +export function isBitbucket(almKey?: string) { + return almKey && almKey.startsWith('bitbucket'); } -export function isGithub(almId?: string) { - return almId === 'github'; +export function isGithub(almKey?: string) { + return almKey === 'github'; } -export function isVSTS(almId?: string) { - return almId === 'microsoft'; +export function isVSTS(almKey?: string) { + return almKey === 'microsoft'; } -export function sanitizeAlmId(almId?: string) { - if (isBitbucket(almId)) { +export function isPersonal(organization?: AlmOrganization) { + return Boolean(organization && organization.type === 'USER'); +} + +export function sanitizeAlmId(almKey?: string) { + if (isBitbucket(almKey)) { return 'bitbucket'; } - return almId; + return almKey; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b798554254f..1f34203174b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2748,9 +2748,6 @@ onboarding.create_organization.url.error=The value must be a valid url. onboarding.create_organization.description=Description onboarding.create_organization.enter_org_details=Enter your organization details onboarding.create_organization.create_manually=Create manually -onboarding.create_organization.import_organization.bitbucket=Import from BitBucket teams -onboarding.create_organization.import_organization.github=Import from GitHub organizations -onboarding.create_organization.import_organization_x=Import {avatar} {name} into SonarCloud organization onboarding.create_organization.import_org_details=Import organization details onboarding.create_organization.import_org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue: onboarding.create_organization.import_org_not_found.tips_1=You must be an administrator of the organization @@ -2762,6 +2759,14 @@ onboarding.create_organization.choose_plan=Choose a plan onboarding.create_organization.enter_your_coupon=Enter your coupon onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade onboarding.create_organization.ready=All set! Your organization is now ready to go +onboarding.import_organization.bind=Bind Organization +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.bitbucket=Import from BitBucket teams +onboarding.import_organization.github=Import from GitHub organizations +onboarding.import_organization_x=Import {avatar} {name} into SonarCloud organization +onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName} + onboarding.team.header=Join a team onboarding.team.first_step=Well congrats, the first step is done! |