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 /server/sonar-web/src/main/js/apps | |
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
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
24 files changed, 333 insertions, 141 deletions
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(); }); |