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 = [
{
label: 'organization_register',
label: 'payment_or_trial',
},
{
label: 'payment_or_trial',
label: 'initializing',
},
{
label: 'getting_started',
},
{
label: 'initializing',
label: 'Congratulations',
},
];

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ export default (mapState) => {
const mapped = {
isAuthorized: isAuthenticated(state),
user: state.authentication.user,
currentOrganizationId: state.authentication?.tenant?.organization_id,
currentOrganizationId: state.authentication?.organizationId,
};
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,
} from 'store/organizations/organizations.actions';
export const mapDispatchToProps = (dispatch) => ({
requestOrganizationsList: () => dispatch(fetchOrganizations()),
requestBuildTenant: () => dispatch(buildTenant()),
requestSeedTenant: () => dispatch(seedTenant()),
const mapDispatchToProps = (dispatch) => ({
requestOrganizationBuild: () => dispatch(buildTenant()),
requestOrganizationSeed: () => dispatch(seedTenant()),
requestAllOrganizations: () => dispatch(fetchOrganizations()),
});
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 { compose } from 'utils';
import withAuthentication from 'containers/Authentication/withAuthentication';
import withOrganizationByOrgId from 'containers/Organization/withOrganizationByOrgId';
import withOrganization from 'containers/Organization/withOrganization';
function EnsureOrganizationIsNotReady({
children,
// #withOrganizationByOrgId
organization,
// #withOrganization
isOrganizationReady,
}) {
return (organization.is_ready) ? (
return (isOrganizationReady) ? (
<Redirect to={{ pathname: '/' }} />
) : children;
}
export default compose(
withAuthentication(),
withAuthentication(({ currentOrganizationId }) => ({
currentOrganizationId,
})),
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
withOrganizationByOrgId(),
withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })),
)(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 { Icon, If } from 'components';
import { Icon } from 'components';
import { FormattedMessage as T } from 'react-intl';
import withAuthentication from 'containers/Authentication/withAuthentication';
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
import { compose } from 'utils';
@@ -11,8 +10,8 @@ import { compose } from 'utils';
* Wizard setup left section.
*/
function SetupLeftSection({
// #withAuthenticationActions
requestLogout,
isAuthorized
}) {
const [org] = useState('LibyanSpider');
@@ -40,17 +39,15 @@ function SetupLeftSection({
<T id={'you_have_a_bigcapital_account'} />
</p>
<If condition={!!isAuthorized}>
<div className={'content-org'}>
<span>
<T id={'welcome'} />
{org},
</span>
<span>
<a onClick={onClickLogout} href="#"><T id={'sign_out'} /></a>
</span>
</div>
</If>
<div className={'content-org'}>
<span>
<T id={'welcome'} />
{org},
</span>
<span>
<a onClick={onClickLogout} href="#"><T id={'sign_out'} /></a>
</span>
</div>
<div className={'content-contact'}>
<a href={'#!'}>
@@ -70,6 +67,5 @@ function SetupLeftSection({
}
export default compose(
withAuthentication(({ isAuthorized }) => ({ isAuthorized })),
withAuthenticationActions,
)(SetupLeftSection);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,18 +13,38 @@ export const fetchOrganizations = () => (dispatch) => new Promise((resolve, reje
}).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) => {
resolve(response);
dispatch({
type: t.SET_ORGANIZATION_INITIALIZED,
payload: { organizationId }
});
})
.catch((error) => {
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) => {
resolve(response);
dispatch({
type: t.SET_ORGANIZATION_INITIALIZED,
payload: { organizationId }
});
})
.catch((error) => {
reject(error.response.data.errors || []);

View File

@@ -20,6 +20,44 @@ const reducer = createReducer(initialState, {
state.data = _data;
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;

View File

@@ -1,18 +1,50 @@
import { createSelector } from '@reduxjs/toolkit';
const oragnizationByTenantIdSelector = (state, props) => state.organizations[props.tenantId];
const organizationByIdSelector = (state, props) => state.organizations.byOrganizationId[props.organizationId];
const organizationsDataSelector = (state, props) => state.organizations.data;
const organizationSelector = (state, props) => state.organizations.data[props.organizationId];
export const getOrganizationByOrgIdFactory = () => createSelector(
organizationByIdSelector,
organizationsDataSelector,
(organizationId, organizationsData) => {
return organizationsData[organizationId];
}
export const getOrganizationByIdFactory = () => createSelector(
organizationSelector,
(organization) => organization
);
export const getOrganizationByTenantIdFactory = () => createSelector(
oragnizationByTenantIdSelector,
(organization) => organization,
);
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;
}
)

View File

@@ -1,5 +1,10 @@
export default {
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) => {
const tenancyService = Container.get(TenancyService);

View File

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