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';
}
interface DispatchProps {
- skipOnboardingAction: () => void;
+ skipOnboarding: () => void;
}
interface OwnProps {
closeOnboarding = () => {
this.setState(state => {
if (state.modal !== ModalKey.license) {
- skipOnboarding();
- this.props.skipOnboardingAction();
+ this.props.skipOnboarding();
return { automatic: false, modal: undefined };
}
return null;
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 });
currentUser: getCurrentUser(state)
});
-const mapDispatchToProps: DispatchProps = { skipOnboardingAction };
+const mapDispatchToProps: DispatchProps = { skipOnboarding };
export default connect(
mapStateToProps,
currentUser={LOGGED_IN_USER}
location={{ pathname: 'foo/bar' } as Location}
router={mockRouter() as InjectedRouter}
- skipOnboardingAction={jest.fn()}
+ skipOnboarding={jest.fn()}
{...props}>
<div />
</StartupModal>
* 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';
} 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';
organization: OrganizationBase & { installationId?: string }
) => Promise<Organization>;
userOrganizations: Organization[];
+ skipOnboardingAction: () => void;
}
interface State {
};
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) => {
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(
} 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
getOrganizations: jest.fn().mockResolvedValue({ organizations: [] })
}));
+jest.mock('../../../../helpers/storage', () => ({
+ get: jest.fn().mockReturnValue(undefined),
+ remove: jest.fn()
+}));
+
const user: LoggedInUser = {
groups: [],
isLoggedIn: true,
(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 () => {
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' }
}
}
onOrgCreated={[Function]}
+ updateOrganization={[MockFunction]}
/>
</div>
</Fragment>
}
}
almUnboundApplications={Array []}
+ createOrganization={[MockFunction]}
onOrgCreated={[Function]}
unboundOrganizations={
Array [
}
}
almUnboundApplications={Array []}
+ createOrganization={[MockFunction]}
onOrgCreated={[Function]}
unboundOrganizations={
Array [
</p>
</header>
<ManualOrganizationCreate
+ createOrganization={[MockFunction]}
+ deleteOrganization={[MockFunction]}
onOrgCreated={[Function]}
subscriptionPlans={
Array [
}
}
almUnboundApplications={Array []}
+ createOrganization={[MockFunction]}
onOrgCreated={[Function]}
unboundOrganizations={
Array [
} 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$/, '')
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;
return '';
}
+ handleInstallAppClick = () => {
+ save(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP, Date.now().toString(10));
+ };
+
handleOrganizationSelect = ({ key }: Organization) => {
this.setState({ selectedOrganization: key });
};
<IdentityProviderLink
className="display-inline-block"
identityProvider={almApplication}
+ onClick={this.handleInstallAppClick}
small={true}
url={almApplication.installationUrl}>
{translate(
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;
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' }
"name": "GitHub",
}
}
+ onClick={[Function]}
small={true}
url="https://alm.installation.url"
>
}
}
+ handleOpenProjectOnboarding = () => {
+ this.props.onOpenProjectOnboarding();
+ };
+
render() {
if (!isLoggedIn(this.props.currentUser)) {
return null;
<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')}
>
<Button
className="onboarding-choice"
- onClick={[MockFunction]}
+ onClick={[Function]}
>
<OnboardingProjectIcon
className="big-spacer-bottom"
children: React.ReactNode;
className?: string;
identityProvider: IdentityProvider;
+ onClick?: () => void;
small?: boolean;
url: string | undefined;
}
children,
className,
identityProvider,
+ onClick,
small,
url
}: Props) {
className
)}
href={url}
+ onClick={onClick}
style={{ backgroundColor: identityProvider.backgroundColor }}>
<img
alt={identityProvider.name}