WIP: register setup wizard pages.

This commit is contained in:
Ahmed Bouhuolia
2020-10-12 14:14:19 +02:00
parent a2ecb6c79d
commit 918e174f8a
22 changed files with 265 additions and 96 deletions

View File

@@ -2,15 +2,15 @@
export const registerWizardSteps = [ export const registerWizardSteps = [
{ {
label: 'organization_register', label: 'payment_or_trial',
}, },
{ {
label: 'payment_or_trial', label: 'initializing',
}, },
{ {
label: 'getting_started', label: 'getting_started',
}, },
{ {
label: 'initializing', label: 'Congratulations',
}, },
]; ];

View File

@@ -2,8 +2,10 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { compose } from 'utils'; import { compose } from 'utils';
import withAuthentication from 'containers/Authentication/withAuthentication'; import withAuthentication from 'containers/Authentication/withAuthentication';
import withOrganizationByOrgId from 'containers/Organization/withOrganizationByOrgId'; import withOrganization from 'containers/Organization/withOrganization';
function EnsureOrganizationIsReady({ function EnsureOrganizationIsReady({
// #ownProps // #ownProps
@@ -11,9 +13,9 @@ function EnsureOrganizationIsReady({
redirectTo = '/setup', redirectTo = '/setup',
// #withOrganizationByOrgId // #withOrganizationByOrgId
organization, isOrganizationBuilt,
}) { }) {
return (organization.is_ready) ? children : ( return (isOrganizationBuilt) ? children : (
<Redirect <Redirect
to={{ pathname: redirectTo }} to={{ pathname: redirectTo }}
/> />
@@ -25,5 +27,5 @@ export default compose(
connect((state, props) => ({ connect((state, props) => ({
organizationId: props.currentOrganizationId, organizationId: props.currentOrganizationId,
})), })),
withOrganizationByOrgId(), withOrganization(({ isOrganizationBuilt }) => ({ isOrganizationBuilt })),
)(EnsureOrganizationIsReady); )(EnsureOrganizationIsReady);

View File

@@ -14,11 +14,12 @@ import { compose } from 'utils';
* Dashboard inner private pages. * Dashboard inner private pages.
*/ */
function DashboardPrivatePages({ function DashboardPrivatePages({
requestOrganizationsList,
// #withOrganizationActions
requestAllOrganizations,
}) { }) {
const fetchOrganizations = useQuery( const fetchOrganizations = useQuery(
['organizations'], ['organizations'], () => requestAllOrganizations(),
() => requestOrganizationsList(),
); );
return ( return (

View File

@@ -6,7 +6,7 @@ export default (mapState) => {
const mapped = { const mapped = {
isAuthorized: isAuthenticated(state), isAuthorized: isAuthenticated(state),
user: state.authentication.user, user: state.authentication.user,
currentOrganizationId: state.authentication?.tenant?.organization_id, currentOrganizationId: state.authentication?.organizationId,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -0,0 +1,37 @@
import { connect } from 'react-redux';
import {
getOrganizationByIdFactory,
isOrganizationReadyFactory,
isOrganizationSeededFactory,
isOrganizationBuiltFactory,
isOrganizationSeedingFactory,
isOrganizationInitializingFactory,
isOrganizationSubscribedFactory,
} from 'store/organizations/organizations.selectors';
export default (mapState) => {
const getOrganizationById = getOrganizationByIdFactory();
const isOrganizationReady = isOrganizationReadyFactory();
const isOrganizationSeeded = isOrganizationSeededFactory();
const isOrganizationBuilt = isOrganizationBuiltFactory();
const isOrganizationInitializing = isOrganizationInitializingFactory();
const isOrganizationSeeding = isOrganizationSeedingFactory();
const isOrganizationSubscribed = isOrganizationSubscribedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
organization: getOrganizationById(state, props),
isOrganizationReady: isOrganizationReady(state, props),
isOrganizationSeeded: isOrganizationSeeded(state, props),
isOrganizationInitialized: isOrganizationBuilt(state, props),
isOrganizationSeeding: isOrganizationInitializing(state, props),
isOrganizationInitializing: isOrganizationSeeding(state, props),
isOrganizationSubscribed: isOrganizationSubscribed(state, props),
};
return (mapState) ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -5,10 +5,10 @@ import {
seedTenant, seedTenant,
} from 'store/organizations/organizations.actions'; } from 'store/organizations/organizations.actions';
export const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
requestOrganizationsList: () => dispatch(fetchOrganizations()), requestOrganizationBuild: () => dispatch(buildTenant()),
requestBuildTenant: () => dispatch(buildTenant()), requestOrganizationSeed: () => dispatch(seedTenant()),
requestSeedTenant: () => dispatch(seedTenant()), requestAllOrganizations: () => dispatch(fetchOrganizations()),
}); });
export default connect(null, mapDispatchToProps); export default connect(null, mapDispatchToProps);

View File

@@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import {
getOrganizationByOrgIdFactory,
} from 'store/organizations/organizations.selectors';
export default (mapState) => {
const getOrganizationByOrgId = getOrganizationByOrgIdFactory();
const mapStateToProps = (state, props) => {
const mapped = {
organization: getOrganizationByOrgId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import {
getOrganizationByTenantIdFactory,
} from 'store/organizations/organizations.selectors';
export default (mapState) => {
const getOrgByTenId = getOrganizationByTenantIdFactory();
const mapStateToProps = (state, props) => {
const mapped = {
organization: getOrgByTenId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -3,23 +3,25 @@ import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { compose } from 'utils'; import { compose } from 'utils';
import withAuthentication from 'containers/Authentication/withAuthentication'; import withAuthentication from 'containers/Authentication/withAuthentication';
import withOrganizationByOrgId from 'containers/Organization/withOrganizationByOrgId'; import withOrganization from 'containers/Organization/withOrganization';
function EnsureOrganizationIsNotReady({ function EnsureOrganizationIsNotReady({
children, children,
// #withOrganizationByOrgId // #withOrganization
organization, isOrganizationReady,
}) { }) {
return (organization.is_ready) ? ( return (isOrganizationReady) ? (
<Redirect to={{ pathname: '/' }} /> <Redirect to={{ pathname: '/' }} />
) : children; ) : children;
} }
export default compose( export default compose(
withAuthentication(), withAuthentication(({ currentOrganizationId }) => ({
currentOrganizationId,
})),
connect((state, props) => ({ connect((state, props) => ({
organizationId: props.currentOrganizationId, organizationId: props.currentOrganizationId,
})), })),
withOrganizationByOrgId(), withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })),
)(EnsureOrganizationIsNotReady); )(EnsureOrganizationIsNotReady);

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { useQuery } from 'react-query';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import withOrganization from 'containers/Organization/withOrganization'
import { compose } from 'utils';
/**
* Setup initializing step form.
*/
function SetupInitializingForm({
// #withOrganizationActions
requestOrganizationBuild,
}) {
const requestBuildOrgnization = useQuery(
['build-tenant'], () => requestOrganizationBuild(),
);
return (
<div class="setup-initializing-form">
<h1>You organization is initializin...</h1>
</div>
);
}
export default compose(
withOrganizationActions
)(SetupInitializingForm);

View File

@@ -1,8 +1,7 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { Icon, If } from 'components'; import { Icon } from 'components';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import withAuthentication from 'containers/Authentication/withAuthentication';
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions'; import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -11,8 +10,8 @@ import { compose } from 'utils';
* Wizard setup left section. * Wizard setup left section.
*/ */
function SetupLeftSection({ function SetupLeftSection({
// #withAuthenticationActions
requestLogout, requestLogout,
isAuthorized
}) { }) {
const [org] = useState('LibyanSpider'); const [org] = useState('LibyanSpider');
@@ -40,17 +39,15 @@ function SetupLeftSection({
<T id={'you_have_a_bigcapital_account'} /> <T id={'you_have_a_bigcapital_account'} />
</p> </p>
<If condition={!!isAuthorized}> <div className={'content-org'}>
<div className={'content-org'}> <span>
<span> <T id={'welcome'} />
<T id={'welcome'} /> {org},
{org}, </span>
</span> <span>
<span> <a onClick={onClickLogout} href="#"><T id={'sign_out'} /></a>
<a onClick={onClickLogout} href="#"><T id={'sign_out'} /></a> </span>
</span> </div>
</div>
</If>
<div className={'content-contact'}> <div className={'content-contact'}>
<a href={'#!'}> <a href={'#!'}>
@@ -70,6 +67,5 @@ function SetupLeftSection({
} }
export default compose( export default compose(
withAuthentication(({ isAuthorized }) => ({ isAuthorized })),
withAuthenticationActions, withAuthenticationActions,
)(SetupLeftSection); )(SetupLeftSection);

View File

@@ -26,7 +26,10 @@ import withOrganizationActions from 'containers/Organization/withOrganizationAct
import { compose, optionsMapToArray } from 'utils'; import { compose, optionsMapToArray } from 'utils';
function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) { function SetupOrganizationForm({
requestSubmitOptions,
requestSeedTenant
}) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [selected, setSelected] = useState(); const [selected, setSelected] = useState();
const history = useHistory(); const history = useHistory();

View File

@@ -2,33 +2,48 @@ import React, { useCallback } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group'; import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { Wizard, Steps, Step } from 'react-albus'; import { Wizard, Steps, Step } from 'react-albus';
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { connect } from 'react-redux';
import WizardSetupSteps from './WizardSetupSteps'; import WizardSetupSteps from './WizardSetupSteps';
import SetupSubscriptionForm from './SetupSubscriptionForm'; import SetupSubscriptionForm from './SetupSubscriptionForm';
import SetupOrganizationForm from './SetupOrganizationForm'; import SetupOrganizationForm from './SetupOrganizationForm';
import SetupInitializingForm from './SetupInitializingForm';
import withAuthentication from 'containers/Authentication/withAuthentication'; import withAuthentication from 'containers/Authentication/withAuthentication';
import withOrganization from 'containers/Organization/withOrganization'
import { compose } from 'utils'; import { compose } from 'utils';
/** /**
* Wizard setup right section. * Wizard setup right section.
*/ */
function SetupRightSection ({ function SetupRightSection ({
isTenantHasSubscriptions: hasSubscriptions = false, // #withAuthentication
currentOrganizationId,
// #withOrganization
isOrganizationInitialized,
isOrganizationSubscribed: hasSubscriptions,
isOrganizationSeeded
}) { }) {
const history = useHistory(); const history = useHistory();
const handleSkip = useCallback(({ step, push }) => { const handleSkip = useCallback(({ step, push }) => {
const scenarios = [ const scenarios = [
{ condition: hasSubscriptions, redirectTo: 'organization' },
{ condition: !hasSubscriptions, redirectTo: 'subscription' }, { condition: !hasSubscriptions, redirectTo: 'subscription' },
// { condition: , redirectTo: 'initializing' }
{ condition: !hasSubscriptions, redirectTo: 'organization' },
]; ];
const scenario = scenarios.find((scenario) => scenario.condition); const scenario = scenarios.find((scenario) => scenario.condition);
if (scenario) { if (scenario) {
push(scenario.redirectTo); push(scenario.redirectTo);
} }
}, [hasSubscriptions]); }, [
hasSubscriptions,
isOrganizationInitialized,
isOrganizationSeeded,
]);
return ( return (
<section className={'setup-page__right-section'}> <section className={'setup-page__right-section'}>
@@ -48,6 +63,10 @@ function SetupRightSection ({
<SetupSubscriptionForm /> <SetupSubscriptionForm />
</Step> </Step>
<Step id={'initializing'}>
<SetupInitializingForm />
</Step>
<Step id="organization"> <Step id="organization">
<SetupOrganizationForm /> <SetupOrganizationForm />
</Step> </Step>
@@ -66,5 +85,19 @@ function SetupRightSection ({
} }
export default compose( export default compose(
withAuthentication(({ isAuthorized }) => ({ isAuthorized })), withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })),
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
withOrganization(({
organization,
isOrganizationInitialized,
isOrganizationSubscribed,
isOrganizationSeeded,
}) => ({
organization,
isOrganizationInitialized,
isOrganizationSubscribed,
isOrganizationSeeded,
})),
)(SetupRightSection); )(SetupRightSection);

View File

@@ -5,6 +5,9 @@ import { FormattedMessage as T } from 'react-intl';
import { Button, Intent } from '@blueprintjs/core'; import { Button, Intent } from '@blueprintjs/core';
import BillingTab from 'containers/Subscriptions/BillingTab'; import BillingTab from 'containers/Subscriptions/BillingTab';
/**
* Subscription step of wizard setup.
*/
export default function SetupSubscriptionForm({ export default function SetupSubscriptionForm({
}) { }) {

View File

@@ -5,9 +5,7 @@ import SetupRightSection from './SetupRightSection';
import SetupLeftSection from './SetupLeftSection'; import SetupLeftSection from './SetupLeftSection';
export default function WizardSetupPage({ export default function WizardSetupPage() {
organizationId,
}) {
return ( return (
<EnsureOrganizationIsNotReady> <EnsureOrganizationIsNotReady>
<div class="setup-page"> <div class="setup-page">

View File

@@ -4,6 +4,7 @@ import t from 'store/types';
const initialState = { const initialState = {
token: '', token: '',
organization: '', organization: '',
organizationId: null,
user: '', user: '',
tenant: {}, tenant: {},
locale: '', locale: '',
@@ -16,6 +17,7 @@ export default createReducer(initialState, {
state.token = token; state.token = token;
state.user = user; state.user = user;
state.organization = tenant.organization_id; state.organization = tenant.organization_id;
state.organizationId = tenant.id;
state.tenant = tenant; state.tenant = tenant;
}, },

View File

@@ -13,18 +13,38 @@ export const fetchOrganizations = () => (dispatch) => new Promise((resolve, reje
}).catch(error => { reject(error); }); }).catch(error => { reject(error); });
}); });
export const buildTenant = () => (dispatch) => new Promise((resolve, reject) => { export const buildTenant = () => (dispatch, getState) => new Promise((resolve, reject) => {
const organizationId = getState().authentication.organizationId;
dispatch({
type: t.SET_ORGANIZATION_INITIALIZING,
payload: { organizationId }
});
ApiService.post(`organization/build`).then((response) => { ApiService.post(`organization/build`).then((response) => {
resolve(response); resolve(response);
dispatch({
type: t.SET_ORGANIZATION_INITIALIZED,
payload: { organizationId }
});
}) })
.catch((error) => { .catch((error) => {
reject(error.response.data.errors || []); reject(error.response.data.errors || []);
}); });
}); });
export const seedTenant = () => (dispatch) => new Promise((resolve, reject) => { export const seedTenant = () => (dispatch, getState) => new Promise((resolve, reject) => {
const organizationId = getState().authentication.organizationId;
dispatch({
type: t.SET_ORGANIZATION_INITIALIZING,
payload: { organizationId }
});
ApiService.post(`organization/seed/`).then((response) => { ApiService.post(`organization/seed/`).then((response) => {
resolve(response); resolve(response);
dispatch({
type: t.SET_ORGANIZATION_INITIALIZED,
payload: { organizationId }
});
}) })
.catch((error) => { .catch((error) => {
reject(error.response.data.errors || []); reject(error.response.data.errors || []);

View File

@@ -20,6 +20,44 @@ const reducer = createReducer(initialState, {
state.data = _data; state.data = _data;
state.byOrganizationId = _dataByOrganizationId; state.byOrganizationId = _dataByOrganizationId;
}, },
[t.SET_ORGANIZATION_SEEDING]: (state, action) => {
const { organizationId } = action.payload;
state.data[organizationId] = {
...(state.data[organizationId] || {}),
is_seeding: true,
};
},
[t.SET_ORGANIZATION_SEEDED]: (state, action) => {
const { organizationId } = action.payload;
state.data[organizationId] = {
...(state.data[organizationId] || {}),
is_seeding: false,
seeded_at: new Date().toISOString(),
};
},
[t.SET_ORGANIZATION_INITIALIZING]: (state, action) => {
const { organizationId } = action.payload;
state.data[organizationId] = {
...(state.data[organizationId] || {}),
is_initializing: true,
};
},
[t.SET_ORGANIZATION_INITIALIZED]: (state, action) => {
const { organizationId } = action.payload;
state.data[organizationId] = {
...(state.data[organizationId] || {}),
is_initializing: false,
initialized_at: new Date().toISOString(),
};
},
}) })
export default reducer; export default reducer;

View File

@@ -1,18 +1,50 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
const oragnizationByTenantIdSelector = (state, props) => state.organizations[props.tenantId]; const organizationSelector = (state, props) => state.organizations.data[props.organizationId];
const organizationByIdSelector = (state, props) => state.organizations.byOrganizationId[props.organizationId];
const organizationsDataSelector = (state, props) => state.organizations.data;
export const getOrganizationByOrgIdFactory = () => createSelector( export const getOrganizationByIdFactory = () => createSelector(
organizationByIdSelector, organizationSelector,
organizationsDataSelector, (organization) => organization
(organizationId, organizationsData) => { );
return organizationsData[organizationId];
export const isOrganizationSeededFactory = () => createSelector(
organizationSelector,
(organization) => {
return !!organization?.seeded_at;
},
);
export const isOrganizationBuiltFactory = () => createSelector(
organizationSelector,
(organization) => {
return !!organization?.initialized_at;
},
);
export const isOrganizationInitializingFactory = () => createSelector(
organizationSelector,
(organization) => {
return organization?.is_initializing;
},
);
export const isOrganizationSeedingFactory = () => createSelector(
organizationSelector,
(organization) => {
return organization?.is_seeding;
},
);
export const isOrganizationReadyFactory = () => createSelector(
organizationSelector,
(organization) => {
return organization?.is_ready;
},
);
export const isOrganizationSubscribedFactory = () => createSelector(
organizationSelector,
(organization) => {
return organization?.subscriptions?.length > 0;
} }
); )
export const getOrganizationByTenantIdFactory = () => createSelector(
oragnizationByTenantIdSelector,
(organization) => organization,
);

View File

@@ -1,5 +1,10 @@
export default { export default {
ORGANIZATIONS_LIST_SET: 'ORGANIZATIONS_LIST_SET', ORGANIZATIONS_LIST_SET: 'ORGANIZATIONS_LIST_SET',
SET_ORGANIZATION_SEEDING: 'SET_ORGANIZATION_SEEDING',
SET_ORGANIZATION_SEEDED: 'SET_ORGANIZATION_SEEDED',
SET_ORGANIZATION_INITIALIZED: 'SET_ORGANIZATION_INITIALIZED',
SET_ORGANIZATION_INITIALIZING: 'SET_ORGANIZATION_INITIALIZING',
}; };

View File

@@ -1,3 +1,5 @@
import Container from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService'
exports.up = (knex) => { exports.up = (knex) => {
const tenancyService = Container.get(TenancyService); const tenancyService = Container.get(TenancyService);

View File

@@ -87,9 +87,6 @@ export default class AuthenticationService implements IAuthenticationService {
// Remove password property from user object. // Remove password property from user object.
Reflect.deleteProperty(user, 'password'); Reflect.deleteProperty(user, 'password');
// Remove id property from tenant object.
Reflect.deleteProperty(tenant, 'id');
return { user, token, tenant }; return { user, token, tenant };
} }