diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-10-25 15:37:19 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-16 20:21:05 +0100 |
commit | 427bc6b8124d501d4d759f5e0a58c11458c79be3 (patch) | |
tree | 78a52cc3fc4605476a1c44ab9230f407427bfce5 /server | |
parent | 5abfbcd37980abad5ba8d127582476d1d58b7358 (diff) | |
download | sonarqube-427bc6b8124d501d4d759f5e0a58c11458c79be3.tar.gz sonarqube-427bc6b8124d501d4d759f5e0a58c11458c79be3.zip |
SONAR-11327 Redirect user after organization creation depending on context
* Correctly handle OnboardingModal for create organization page
Diffstat (limited to 'server')
12 files changed, 120 insertions, 18 deletions
diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx index 76d0083725f..528d66ec5f7 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -25,12 +25,11 @@ import { CurrentUser, Organization } from '../types'; import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates'; import { EditionKey } from '../../apps/marketplace/utils'; import { getCurrentUser, getAppState, Store } from '../../store/rootReducer'; -import { skipOnboarding as skipOnboardingAction } from '../../store/users'; +import { skipOnboarding } from '../../store/users'; import { showLicense } from '../../api/marketplace'; import { hasMessage } from '../../helpers/l10n'; import { save, get } from '../../helpers/storage'; import { isSonarCloud } from '../../helpers/system'; -import { skipOnboarding } from '../../api/users'; import { lazyLoad } from '../../components/lazyLoad'; import { isLoggedIn } from '../../helpers/users'; @@ -54,7 +53,7 @@ interface StateProps { } interface DispatchProps { - skipOnboardingAction: () => void; + skipOnboarding: () => void; } interface OwnProps { @@ -95,8 +94,7 @@ export class StartupModal extends React.PureComponent<Props, State> { closeOnboarding = () => { this.setState(state => { if (state.modal !== ModalKey.license) { - skipOnboarding(); - this.props.skipOnboardingAction(); + this.props.skipOnboarding(); return { automatic: false, modal: undefined }; } return null; @@ -165,8 +163,8 @@ export class StartupModal extends React.PureComponent<Props, State> { const { currentUser, location } = this.props; if ( currentUser.showOnboardingTutorial && - !['about', 'documentation', 'onboarding', 'projects/create'].some(path => - location.pathname.startsWith(path) + !['about', 'documentation', 'onboarding', 'projects/create', 'create-organization'].some( + path => location.pathname.startsWith(path) ) ) { this.setState({ automatic: true }); @@ -209,7 +207,7 @@ const mapStateToProps = (state: Store): StateProps => ({ currentUser: getCurrentUser(state) }); -const mapDispatchToProps: DispatchProps = { skipOnboardingAction }; +const mapDispatchToProps: DispatchProps = { skipOnboarding }; export default connect( mapStateToProps, diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx index 62feff5e458..e04e23bbdac 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx @@ -145,7 +145,7 @@ function getWrapper(props = {}) { currentUser={LOGGED_IN_USER} location={{ pathname: 'foo/bar' } as Location} router={mockRouter() as InjectedRouter} - skipOnboardingAction={jest.fn()} + skipOnboarding={jest.fn()} {...props}> <div /> </StartupModal> 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 763ebef5e17..5357076557b 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx @@ -18,13 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { differenceInMinutes } from 'date-fns'; import { times } from 'lodash'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { Helmet } from 'react-helmet'; import { FormattedMessage } from 'react-intl'; import { Link, withRouter, WithRouterProps } from 'react-router'; -import { formatPrice, parseQuery } from './utils'; +import { + formatPrice, + parseQuery, + ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP +} from './utils'; import AlmApplicationInstalling from './AlmApplicationInstalling'; import AutoOrganizationCreate from './AutoOrganizationCreate'; import AutoPersonalOrganizationBind from './AutoPersonalOrganizationBind'; @@ -51,8 +56,11 @@ import { } from '../../../app/types'; import { hasAdvancedALMIntegration, isPersonal } from '../../../helpers/almIntegrations'; import { translate } from '../../../helpers/l10n'; +import { get, remove } from '../../../helpers/storage'; import { slugify } from '../../../helpers/strings'; import { getOrganizationUrl } from '../../../helpers/urls'; +import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; +import { skipOnboarding } from '../../../api/users'; import * as api from '../../../api/organizations'; import * as actions from '../../../store/organizations'; import '../../../app/styles/sonarcloud.css'; @@ -68,6 +76,7 @@ interface Props { organization: OrganizationBase & { installationId?: string } ) => Promise<Organization>; userOrganizations: Organization[]; + skipOnboardingAction: () => void; } interface State { @@ -187,10 +196,24 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr }; handleOrgCreated = (organization: string, justCreated = true) => { - this.props.router.push({ - pathname: getOrganizationUrl(organization), - state: { justCreated } - }); + skipOnboarding().catch(() => {}); + this.props.skipOnboardingAction(); + const redirectProjectTimestamp = get(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP); + remove(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP); + if ( + redirectProjectTimestamp && + differenceInMinutes(Date.now(), Number(redirectProjectTimestamp)) < 10 + ) { + this.props.router.push({ + pathname: '/projects/create', + state: { organization, tab: this.state.almOrganization ? 'auto' : 'manual' } + }); + } else { + this.props.router.push({ + pathname: getOrganizationUrl(organization), + state: { justCreated } + }); + } }; onTabChange = (tab: TabKeys) => { @@ -367,7 +390,8 @@ function deleteOrganization(key: string) { const mapDispatchToProps = { createOrganization: createOrganization as any, deleteOrganization: deleteOrganization as any, - updateOrganization: updateOrganization as any + updateOrganization: updateOrganization as any, + skipOnboardingAction: skipOnboardingAction as any }; export default whenLoggedIn( 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 b73259b9076..09d55dcf6b1 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 @@ -31,6 +31,7 @@ import { } from '../../../../api/alm-integration'; import { getSubscriptionPlans } from '../../../../api/billing'; import { getOrganizations } from '../../../../api/organizations'; +import { get, remove } from '../../../../helpers/storage'; jest.mock('../../../../api/billing', () => ({ getSubscriptionPlans: jest @@ -63,6 +64,11 @@ jest.mock('../../../../api/organizations', () => ({ getOrganizations: jest.fn().mockResolvedValue({ organizations: [] }) })); +jest.mock('../../../../helpers/storage', () => ({ + get: jest.fn().mockReturnValue(undefined), + remove: jest.fn() +})); + const user: LoggedInUser = { groups: [], isLoggedIn: true, @@ -78,6 +84,8 @@ beforeEach(() => { (listUnboundApplications as jest.Mock<any>).mockClear(); (getSubscriptionPlans as jest.Mock<any>).mockClear(); (getOrganizations as jest.Mock<any>).mockClear(); + (get as jest.Mock<any>).mockClear(); + (remove as jest.Mock<any>).mockClear(); }); it('should render with manual tab displayed', async () => { @@ -187,14 +195,59 @@ it('should reload the alm organization when the url query changes', async () => expect(listUnboundApplications).toHaveBeenCalledTimes(2); }); +it('should redirect to organization page after creation', async () => { + const push = jest.fn(); + const wrapper = shallowRender({ router: mockRouter({ push }) }); + await waitAndUpdate(wrapper); + + wrapper.find('ManualOrganizationCreate').prop<Function>('onOrgCreated')('foo'); + expect(push).toHaveBeenCalledWith({ + pathname: '/organizations/foo', + state: { justCreated: true } + }); + + (get as jest.Mock<any>).mockReturnValueOnce('0'); + wrapper.find('ManualOrganizationCreate').prop<Function>('onOrgCreated')('foo', false); + expect(push).toHaveBeenCalledWith({ + pathname: '/organizations/foo', + state: { justCreated: false } + }); +}); + +it('should redirect to projects creation page after creation', async () => { + const push = jest.fn(); + const wrapper = shallowRender({ router: mockRouter({ push }) }); + await waitAndUpdate(wrapper); + + (get as jest.Mock<any>).mockReturnValueOnce(Date.now().toString()); + wrapper.find('ManualOrganizationCreate').prop<Function>('onOrgCreated')('foo'); + expect(get).toHaveBeenCalled(); + expect(remove).toHaveBeenCalled(); + expect(push).toHaveBeenCalledWith({ + pathname: '/projects/create', + state: { organization: 'foo', tab: 'manual' } + }); + + wrapper.setState({ almOrganization: { key: 'foo', name: 'Foo', avatar: 'my-avatar' } }); + (get as jest.Mock<any>).mockReturnValueOnce(Date.now().toString()); + wrapper.find('ManualOrganizationCreate').prop<Function>('onOrgCreated')('foo'); + expect(push).toHaveBeenCalledWith({ + pathname: '/projects/create', + state: { organization: 'foo', tab: 'auto' } + }); +}); + function shallowRender(props: Partial<CreateOrganization['props']> = {}) { return shallow( <CreateOrganization + createOrganization={jest.fn()} currentUser={user} - {...props} + deleteOrganization={jest.fn()} // @ts-ignore avoid passing everything from WithRouterProps location={{}} router={mockRouter()} + skipOnboardingAction={jest.fn()} + updateOrganization={jest.fn()} userOrganizations={[ { key: 'foo', name: 'Foo' }, { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' } 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 741ed46784e..e7b7edca442 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 @@ -74,6 +74,7 @@ exports[`should render with auto personal organization bind page 2`] = ` } } onOrgCreated={[Function]} + updateOrganization={[MockFunction]} /> </div> </Fragment> @@ -149,6 +150,7 @@ exports[`should render with auto tab displayed 1`] = ` } } almUnboundApplications={Array []} + createOrganization={[MockFunction]} onOrgCreated={[Function]} unboundOrganizations={ Array [ @@ -250,6 +252,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` } } almUnboundApplications={Array []} + createOrganization={[MockFunction]} onOrgCreated={[Function]} unboundOrganizations={ Array [ @@ -307,6 +310,8 @@ exports[`should render with manual tab displayed 1`] = ` </p> </header> <ManualOrganizationCreate + createOrganization={[MockFunction]} + deleteOrganization={[MockFunction]} onOrgCreated={[Function]} subscriptionPlans={ Array [ @@ -395,6 +400,7 @@ exports[`should switch tabs 1`] = ` } } almUnboundApplications={Array []} + createOrganization={[MockFunction]} onOrgCreated={[Function]} unboundOrganizations={ Array [ diff --git a/server/sonar-web/src/main/js/apps/create/organization/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/utils.ts index e5c92d28164..1020a9288fa 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/utils.ts +++ b/server/sonar-web/src/main/js/apps/create/organization/utils.ts @@ -28,6 +28,9 @@ import { } from '../../../helpers/query'; import { isBitbucket, isGithub } from '../../../helpers/almIntegrations'; +export const ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP = + 'sonarcloud.import_org.redirect_to_projects'; + export function formatPrice(price?: number, noSign?: boolean) { const priceFormatted = formatMeasure(price, 'FLOAT') .replace(/[.|,]0$/, '') diff --git a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx index ad6f002914f..58795c101bf 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx @@ -22,7 +22,9 @@ import RemoteRepositories from './RemoteRepositories'; import OrganizationInput from './OrganizationInput'; import IdentityProviderLink from '../../../components/ui/IdentityProviderLink'; import { AlmApplication, Organization } from '../../../app/types'; +import { ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP } from '../organization/utils'; import { translate } from '../../../helpers/l10n'; +import { save } from '../../../helpers/storage'; interface Props { almApplication: AlmApplication; @@ -53,6 +55,10 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State> return ''; } + handleInstallAppClick = () => { + save(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP, Date.now().toString(10)); + }; + handleOrganizationSelect = ({ key }: Organization) => { this.setState({ selectedOrganization: key }); }; @@ -66,6 +72,7 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State> <IdentityProviderLink className="display-inline-block" identityProvider={almApplication} + onClick={this.handleInstallAppClick} small={true} url={almApplication.installationUrl}> {translate( diff --git a/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx index e7fea91bbac..81f5d8930f2 100644 --- a/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx @@ -20,8 +20,10 @@ import * as React from 'react'; import { WithRouterProps, withRouter } from 'react-router'; import OrganizationSelect from '../components/OrganizationSelect'; +import { ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP } from '../organization/utils'; import { Organization } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; +import { save } from '../../../helpers/storage'; interface Props { autoImport?: boolean; @@ -34,6 +36,7 @@ export class OrganizationInput extends React.PureComponent<Props & WithRouterPro handleLinkClick = (event: React.MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); event.stopPropagation(); + save(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP, Date.now().toString(10)); this.props.router.push({ pathname: '/create-organization', state: { tab: this.props.autoImport ? 'auto' : 'manual' } 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 cd98e6da4c2..4e804d8e2d2 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 @@ -56,6 +56,7 @@ exports[`should display the provider app install button 1`] = ` "name": "GitHub", } } + onClick={[Function]} small={true} url="https://alm.installation.url" > diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx index 60a134428aa..d7a8aa94acd 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx @@ -51,6 +51,10 @@ export class OnboardingModal extends React.PureComponent<Props> { } } + handleOpenProjectOnboarding = () => { + this.props.onOpenProjectOnboarding(); + }; + render() { if (!isLoggedIn(this.props.currentUser)) { return null; @@ -68,7 +72,7 @@ export class OnboardingModal extends React.PureComponent<Props> { <p className="spacer-top">{translate('onboarding.header.description')}</p> </div> <div className="modal-simple-body text-center onboarding-choices"> - <Button className="onboarding-choice" onClick={this.props.onOpenProjectOnboarding}> + <Button className="onboarding-choice" onClick={this.handleOpenProjectOnboarding}> <OnboardingProjectIcon className="big-spacer-bottom" /> <h6 className="onboarding-choice-name"> {translate('onboarding.analyze_public_code')} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap index 1b46fdaca66..5888278c63d 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap @@ -24,7 +24,7 @@ exports[`renders correctly 1`] = ` > <Button className="onboarding-choice" - onClick={[MockFunction]} + onClick={[Function]} > <OnboardingProjectIcon className="big-spacer-bottom" diff --git a/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx b/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx index 9e4f87c5ed3..75354424c63 100644 --- a/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx +++ b/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx @@ -28,6 +28,7 @@ interface Props { children: React.ReactNode; className?: string; identityProvider: IdentityProvider; + onClick?: () => void; small?: boolean; url: string | undefined; } @@ -36,6 +37,7 @@ export default function IdentityProviderLink({ children, className, identityProvider, + onClick, small, url }: Props) { @@ -49,6 +51,7 @@ export default function IdentityProviderLink({ className )} href={url} + onClick={onClick} style={{ backgroundColor: identityProvider.backgroundColor }}> <img alt={identityProvider.name} |