aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-10-25 15:37:19 +0200
committerSonarTech <sonartech@sonarsource.com>2018-11-16 20:21:05 +0100
commit427bc6b8124d501d4d759f5e0a58c11458c79be3 (patch)
tree78a52cc3fc4605476a1c44ab9230f407427bfce5 /server
parent5abfbcd37980abad5ba8d127582476d1d58b7358 (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx55
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/utils.ts3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx3
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}