Merge branch 'RegisterWizard' of https://github.com/abouolia/Bigcapital into RegisterWizard

This commit is contained in:
elforjani3
2020-10-12 15:57:27 +02:00
37 changed files with 870 additions and 601 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

@@ -7,9 +7,8 @@ import { ReactQueryDevtools } from 'react-query-devtools';
import PrivateRoute from 'components/PrivateRoute';
import Authentication from 'components/Authentication';
import Dashboard from 'components/Dashboard/Dashboard';
import DashboardPrivatePages from 'components/Dashboard/PrivatePages';
import GlobalErrors from 'containers/GlobalErrors/GlobalErrors';
import RegisterWizardPage from 'containers/Authentication/Register/RegisterPage';
import messages from 'lang/en';
import 'style/App.scss';
@@ -32,12 +31,8 @@ function App({ locale }) {
<Authentication />
</Route>
<Route path={'/register'}>
<RegisterWizardPage />
</Route>
<Route path={'/'}>
<PrivateRoute component={Dashboard} />
<PrivateRoute component={DashboardPrivatePages} />
</Route>
</Switch>
</Router>

View File

@@ -14,7 +14,6 @@ import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
import EnsureOrganizationIsReady from './EnsureOrganizationIsReady';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withOrganizationsActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
@@ -22,20 +21,14 @@ import { compose } from 'utils';
function Dashboard({
// #withSettings
requestFetchOptions,
// #withOrganizations
requestOrganizationsList,
}) {
const fetchOrganizations = useQuery(
['organizations'],
(key) => requestOrganizationsList(),
);
// const fetchOptions = useQuery(
// ['options'], () => requestFetchOptions(),
// );
return (
<EnsureOrganizationIsReady>
<DashboardLoadingIndicator
isLoading={
fetchOrganizations.isLoading
}>
<DashboardLoadingIndicator isLoading={false}>
<Switch>
<Route path="/preferences">
<DashboardSplitPane>
@@ -62,5 +55,4 @@ function Dashboard({
export default compose(
withSettingsActions,
withOrganizationsActions,
)(Dashboard);

View File

@@ -2,12 +2,13 @@ import React from 'react';
import classNames from 'classnames';
import { Choose, Icon } from 'components';
export default function Dashboard({
export default function DashboardLoadingIndicator({
isLoading = false,
className,
children,
}) {
return (
<div className={classNames('dashboard')}>
<div className={classNames(className)}>
<Choose>
<Choose.When condition={isLoading}>
<div class="center">

View File

@@ -1,16 +1,31 @@
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { compose } from 'utils';
export default function EnsureOrganizationIsReady({
import withAuthentication from 'containers/Authentication/withAuthentication';
import withOrganization from 'containers/Organization/withOrganization';
function EnsureOrganizationIsReady({
// #ownProps
children,
}) {
const isOrganizationReady = false;
redirectTo = '/setup',
return (isOrganizationReady) ? children : (
// #withOrganizationByOrgId
isOrganizationBuilt,
}) {
return (isOrganizationBuilt) ? children : (
<Redirect
to={{
pathname: '/register'
}}
to={{ pathname: redirectTo }}
/>
);
}
}
export default compose(
withAuthentication(),
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
withOrganization(({ isOrganizationBuilt }) => ({ isOrganizationBuilt })),
)(EnsureOrganizationIsReady);

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import { useQuery } from 'react-query';
import Dashboard from 'components/Dashboard/Dashboard';
import SetupWizardPage from 'containers/Setup/WizardSetupPage';
import DashboardLoadingIndicator from 'components/Dashboard/DashboardLoadingIndicator';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
/**
* Dashboard inner private pages.
*/
function DashboardPrivatePages({
// #withOrganizationActions
requestAllOrganizations,
}) {
const fetchOrganizations = useQuery(
['organizations'], () => requestAllOrganizations(),
);
return (
<DashboardLoadingIndicator isLoading={fetchOrganizations.isFetching}>
<Switch>
<Route path={'/setup'}>
<SetupWizardPage />
</Route>
<Route path='/'>
<Dashboard />
</Route>
</Switch>
</DashboardLoadingIndicator>
);
}
export default compose(
withOrganizationActions,
)(DashboardPrivatePages);

View File

@@ -19,18 +19,15 @@ import Icon from 'components/Icon';
import { If } from 'components';
import withAuthenticationActions from './withAuthenticationActions';
import withOrganizationsActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
const ERRORS_TYPES = {
INVALID_DETAILS: 'INVALID_DETAILS',
USER_INACTIVE: 'USER_INACTIVE',
};
function Login({
requestLogin,
requestOrganizationsList,
}) {
const { formatMessage } = useIntl();
const history = useHistory();
@@ -105,7 +102,7 @@ function Login({
<div className={'authentication-page__label-section'}>
<h3><T id={'log_in'} /></h3>
<T id={'need_bigcapital_account'} />
<Link to='/register'> <T id={'create_an_account'} /></Link>
<Link to='/auth/register'> <T id={'create_an_account'} /></Link>
</div>
<form onSubmit={handleSubmit} className={'authentication-page__form'}>
@@ -170,5 +167,4 @@ function Login({
export default compose(
withAuthenticationActions,
withOrganizationsActions,
)(Login);

View File

@@ -0,0 +1,284 @@
import React, { useMemo, useState, useCallback } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Row, Col } from 'react-grid-system';
import { Link, useHistory } from 'react-router-dom';
import {
Button,
InputGroup,
Intent,
FormGroup,
Spinner,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import AppToaster from 'components/AppToaster';
import AuthInsider from 'containers/Authentication/AuthInsider';
import ErrorMessage from 'components/ErrorMessage';
import Icon from 'components/Icon';
import { If } from 'components';
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
import { compose } from 'utils';
function RegisterUserForm({ requestRegister, requestLogin }) {
const { formatMessage } = useIntl();
const history = useHistory();
const [shown, setShown] = useState(false);
const passwordRevealer = useCallback(() => {
setShown(!shown);
}, [shown]);
const ValidationSchema = Yup.object().shape({
first_name: Yup.string()
.required()
.label(formatMessage({ id: 'first_name_' })),
last_name: Yup.string()
.required()
.label(formatMessage({ id: 'last_name_' })),
email: Yup.string()
.email()
.required()
.label(formatMessage({ id: 'email' })),
phone_number: Yup.string()
.matches()
.required()
.label(formatMessage({ id: 'phone_number_' })),
password: Yup.string()
.min(4)
.required()
.label(formatMessage({ id: 'password' })),
});
const initialValues = useMemo(
() => ({
first_name: '',
last_name: '',
email: '',
phone_number: '',
password: '',
}),
[],
);
const {
errors,
touched,
handleSubmit,
getFieldProps,
isSubmitting,
} = useFormik({
enableReinitialize: true,
validationSchema: ValidationSchema,
initialValues: {
...initialValues,
country: 'libya',
},
onSubmit: (values, { setSubmitting, setErrors }) => {
requestRegister(values)
.then((response) => {
requestLogin({
crediential: values.email,
password: values.password,
})
.then(() => {
history.push('/register/subscription');
setSubmitting(false);
})
.catch((errors) => {
AppToaster.show({
message: formatMessage({ id: 'something_wentwrong' }),
intent: Intent.SUCCESS,
});
});
})
.catch((errors) => {
if (errors.some((e) => e.type === 'PHONE_NUMBER_EXISTS')) {
setErrors({
phone_number: formatMessage({
id: 'the_phone_number_already_used_in_another_account',
}),
});
}
if (errors.some((e) => e.type === 'EMAIL_EXISTS')) {
setErrors({
email: formatMessage({
id: 'the_email_already_used_in_another_account',
}),
});
}
setSubmitting(false);
});
},
});
const passwordRevealerTmp = useMemo(
() => (
<span class="password-revealer" onClick={() => passwordRevealer()}>
<If condition={shown}>
<>
<Icon icon="eye-slash" />{' '}
<span class="text">
<T id={'hide'} />
</span>
</>
</If>
<If condition={!shown}>
<>
<Icon icon="eye" />{' '}
<span class="text">
<T id={'show'} />
</span>
</>
</If>
</span>
),
[shown, passwordRevealer],
);
return (
<AuthInsider>
<div className={'register-form'}>
<div className={'authentication-page__label-section'}>
<h3>
<T id={'register_a_new_organization'} />
</h3>
<T id={'you_have_a_bigcapital_account'} />
<Link to="/auth/login">
{' '}
<T id={'login'} />
</Link>
</div>
<form onSubmit={handleSubmit} className={'authentication-page__form'}>
<Row className={'name-section'}>
<Col md={6}>
<FormGroup
label={<T id={'first_name'} />}
intent={
errors.first_name && touched.first_name && Intent.DANGER
}
helperText={
<ErrorMessage name={'first_name'} {...{ errors, touched }} />
}
className={'form-group--first-name'}
>
<InputGroup
intent={
errors.first_name && touched.first_name && Intent.DANGER
}
{...getFieldProps('first_name')}
/>
</FormGroup>
</Col>
<Col md={6}>
<FormGroup
label={<T id={'last_name'} />}
intent={errors.last_name && touched.last_name && Intent.DANGER}
helperText={
<ErrorMessage name={'last_name'} {...{ errors, touched }} />
}
className={'form-group--last-name'}
>
<InputGroup
intent={
errors.last_name && touched.last_name && Intent.DANGER
}
{...getFieldProps('last_name')}
/>
</FormGroup>
</Col>
</Row>
<FormGroup
label={<T id={'phone_number'} />}
intent={
errors.phone_number && touched.phone_number && Intent.DANGER
}
helperText={
<ErrorMessage name={'phone_number'} {...{ errors, touched }} />
}
className={'form-group--phone-number'}
>
<InputGroup
intent={
errors.phone_number && touched.phone_number && Intent.DANGER
}
{...getFieldProps('phone_number')}
/>
</FormGroup>
<FormGroup
label={<T id={'email'} />}
intent={errors.email && touched.email && Intent.DANGER}
helperText={
<ErrorMessage name={'email'} {...{ errors, touched }} />
}
className={'form-group--email'}
>
<InputGroup
intent={errors.email && touched.email && Intent.DANGER}
{...getFieldProps('email')}
/>
</FormGroup>
<FormGroup
label={<T id={'password'} />}
labelInfo={passwordRevealerTmp}
intent={errors.password && touched.password && Intent.DANGER}
helperText={
<ErrorMessage name={'password'} {...{ errors, touched }} />
}
className={'form-group--password has-password-revealer'}
>
<InputGroup
lang={true}
type={shown ? 'text' : 'password'}
intent={errors.password && touched.password && Intent.DANGER}
{...getFieldProps('password')}
/>
</FormGroup>
<div className={'register-form__agreement-section'}>
<p>
<T id={'signing_in_or_creating'} /> <br />
<Link>
<T id={'terms_conditions'} />
</Link>{' '}
<T id={'and'} />
<Link>
{' '}
<T id={'privacy_statement'} />
</Link>
</p>
</div>
<div className={'authentication-page__submit-button-wrap'}>
<Button
className={'btn-register'}
intent={Intent.PRIMARY}
type="submit"
fill={true}
loading={isSubmitting}
>
<T id={'register'} />
</Button>
</div>
</form>
<If condition={isSubmitting}>
<div class="authentication-page__loading-overlay">
<Spinner size={50} />
</div>
</If>
</div>
</AuthInsider>
);
}
export default compose(
withAuthenticationActions,
)(RegisterUserForm);

View File

@@ -1,14 +0,0 @@
import React from 'react';
import RegisterRightSection from './RegisterRightSection';
import RegisterLeftSection from './RegisterLeftSection';
function RegisterWizardPage() {
return (
<div class="register-page">
<RegisterLeftSection />
<RegisterRightSection />
</div>
);
}
export default RegisterWizardPage;

View File

@@ -1,55 +0,0 @@
import React from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { Wizard, Steps, Step } from 'react-albus';
import { useHistory } from "react-router-dom";
import RegisterWizardSteps from './RegisterWizardSteps';
import PrivateRoute from 'components/PrivateRoute';
import RegisterUserForm from 'containers/Authentication/Register/RegisterUserForm';
import RegisterSubscriptionForm from 'containers/Authentication/Register/RegisterSubscriptionForm';
import RegisterOrganizationForm from 'containers/Authentication/Register/RegisterOrganizationForm';
export default function RegisterRightSection () {
const history = useHistory();
return (
<section className={'register-page__right-section'}>
<Wizard
basename={'/register'}
history={history}
render={({ step, steps }) => (
<div>
<RegisterWizardSteps currentStep={steps.indexOf(step) + 1} />
<TransitionGroup>
<CSSTransition
key={step.id}
classNames="example"
timeout={{ enter: 500, exit: 500 }}
>
<div class="register-page-form">
<Steps key={step.id} step={step}>
<Step id="user">
<RegisterUserForm />
</Step>
<Step id="subscription">
<PrivateRoute component={RegisterSubscriptionForm} />
</Step>
<Step id="organization">
<PrivateRoute component={RegisterOrganizationForm} />
</Step>
<Step id="congratulations">
<h1 className="text-align-center">Ice King</h1>
</Step>
</Steps>
</div>
</CSSTransition>
</TransitionGroup>
</div>
)} />
</section>
)
}

View File

@@ -1,281 +0,0 @@
import React, { useMemo, useState, useCallback } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Row, Col } from 'react-grid-system';
import { Link, useHistory } from 'react-router-dom';
import {
Button,
InputGroup,
Intent,
FormGroup,
Spinner,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import AppToaster from 'components/AppToaster';
import ErrorMessage from 'components/ErrorMessage';
import Icon from 'components/Icon';
import { If } from 'components';
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
import { compose } from 'utils';
function RegisterUserForm({ requestRegister, requestLogin }) {
const { formatMessage } = useIntl();
const history = useHistory();
const [shown, setShown] = useState(false);
const passwordRevealer = useCallback(() => {
setShown(!shown);
}, [shown]);
const ValidationSchema = Yup.object().shape({
first_name: Yup.string()
.required()
.label(formatMessage({ id: 'first_name_' })),
last_name: Yup.string()
.required()
.label(formatMessage({ id: 'last_name_' })),
email: Yup.string()
.email()
.required()
.label(formatMessage({ id: 'email' })),
phone_number: Yup.string()
.matches()
.required()
.label(formatMessage({ id: 'phone_number_' })),
password: Yup.string()
.min(4)
.required()
.label(formatMessage({ id: 'password' })),
});
const initialValues = useMemo(
() => ({
first_name: '',
last_name: '',
email: '',
phone_number: '',
password: '',
}),
[],
);
const {
errors,
touched,
handleSubmit,
getFieldProps,
isSubmitting,
} = useFormik({
enableReinitialize: true,
validationSchema: ValidationSchema,
initialValues: {
...initialValues,
country: 'libya',
},
onSubmit: (values, { setSubmitting, setErrors }) => {
requestRegister(values)
.then((response) => {
requestLogin({
crediential: values.email,
password: values.password,
})
.then(() => {
history.push('/register/subscription');
setSubmitting(false);
})
.catch((errors) => {
AppToaster.show({
message: formatMessage({
id: 'something_wentwrong',
}),
intent: Intent.SUCCESS,
});
});
})
.catch((errors) => {
if (errors.some((e) => e.type === 'PHONE_NUMBER_EXISTS')) {
setErrors({
phone_number: formatMessage({
id: 'the_phone_number_already_used_in_another_account',
}),
});
}
if (errors.some((e) => e.type === 'EMAIL_EXISTS')) {
setErrors({
email: formatMessage({
id: 'the_email_already_used_in_another_account',
}),
});
}
setSubmitting(false);
});
},
});
const passwordRevealerTmp = useMemo(
() => (
<span class="password-revealer" onClick={() => passwordRevealer()}>
<If condition={shown}>
<>
<Icon icon="eye-slash" />{' '}
<span class="text">
<T id={'hide'} />
</span>
</>
</If>
<If condition={!shown}>
<>
<Icon icon="eye" />{' '}
<span class="text">
<T id={'show'} />
</span>
</>
</If>
</span>
),
[shown, passwordRevealer],
);
return (
<div className={'register-form'}>
<div className={'authentication-page__label-section'}>
<h3>
<T id={'register_a_new_organization'} />
</h3>
<T id={'you_have_a_bigcapital_account'} />
<Link to="/auth/login">
{' '}
<T id={'login'} />
</Link>
</div>
<form onSubmit={handleSubmit} className={'authentication-page__form'}>
<Row className={'name-section'}>
<Col md={6}>
<FormGroup
label={<T id={'first_name'} />}
intent={
errors.first_name && touched.first_name && Intent.DANGER
}
helperText={
<ErrorMessage name={'first_name'} {...{ errors, touched }} />
}
className={'form-group--first-name'}
>
<InputGroup
intent={
errors.first_name && touched.first_name && Intent.DANGER
}
{...getFieldProps('first_name')}
/>
</FormGroup>
</Col>
<Col md={6}>
<FormGroup
label={<T id={'last_name'} />}
intent={errors.last_name && touched.last_name && Intent.DANGER}
helperText={
<ErrorMessage name={'last_name'} {...{ errors, touched }} />
}
className={'form-group--last-name'}
>
<InputGroup
intent={
errors.last_name && touched.last_name && Intent.DANGER
}
{...getFieldProps('last_name')}
/>
</FormGroup>
</Col>
</Row>
<FormGroup
label={<T id={'phone_number'} />}
intent={
errors.phone_number && touched.phone_number && Intent.DANGER
}
helperText={
<ErrorMessage name={'phone_number'} {...{ errors, touched }} />
}
className={'form-group--phone-number'}
>
<InputGroup
intent={
errors.phone_number && touched.phone_number && Intent.DANGER
}
{...getFieldProps('phone_number')}
/>
</FormGroup>
<FormGroup
label={<T id={'email'} />}
intent={errors.email && touched.email && Intent.DANGER}
helperText={
<ErrorMessage name={'email'} {...{ errors, touched }} />
}
className={'form-group--email'}
>
<InputGroup
intent={errors.email && touched.email && Intent.DANGER}
{...getFieldProps('email')}
/>
</FormGroup>
<FormGroup
label={<T id={'password'} />}
labelInfo={passwordRevealerTmp}
intent={errors.password && touched.password && Intent.DANGER}
helperText={
<ErrorMessage name={'password'} {...{ errors, touched }} />
}
className={'form-group--password has-password-revealer'}
>
<InputGroup
lang={true}
type={shown ? 'text' : 'password'}
intent={errors.password && touched.password && Intent.DANGER}
{...getFieldProps('password')}
/>
</FormGroup>
<div className={'register-form__agreement-section'}>
<p>
<T id={'signing_in_or_creating'} /> <br />
<Link>
<T id={'terms_conditions'} />
</Link>{' '}
<T id={'and'} />
<Link>
{' '}
<T id={'privacy_statement'} />
</Link>
</p>
</div>
<div className={'authentication-page__submit-button-wrap'}>
<Button
className={'btn-register'}
intent={Intent.PRIMARY}
type="submit"
fill={true}
loading={isSubmitting}
>
<T id={'register'} />
</Button>
</div>
</form>
<If condition={isSubmitting}>
<div class="authentication-page__loading-overlay">
<Spinner size={50} />
</div>
</If>
</div>
);
}
export default compose(withAuthenticationActions)(RegisterUserForm);

View File

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

View File

@@ -1,12 +0,0 @@
import {
buildTenant,
seedTenant,
} from 'store/organization/organization.actions';
import { connect } from 'react-redux';
const mapDispatchToProps = (dispatch) => ({
requestBuildTenant: (id, token) => dispatch(buildTenant({ id, token })),
requestSeedTenant: (id, token) => dispatch(seedTenant({ id, token })),
});
export default connect(null, mapDispatchToProps);

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

@@ -1,10 +1,14 @@
import { connect } from 'react-redux';
import {
fetchOrganizations,
buildTenant,
seedTenant,
} from 'store/organizations/organizations.actions';
export const mapDispatchToProps = (dispatch) => ({
requestOrganizationsList: () => dispatch(fetchOrganizations()),
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.selector';
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.selector';
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

@@ -0,0 +1,27 @@
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 withOrganization from 'containers/Organization/withOrganization';
function EnsureOrganizationIsNotReady({
children,
// #withOrganization
isOrganizationReady,
}) {
return (isOrganizationReady) ? (
<Redirect to={{ pathname: '/' }} />
) : children;
}
export default compose(
withAuthentication(({ currentOrganizationId }) => ({
currentOrganizationId,
})),
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
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,16 +1,17 @@
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';
function RegisterLeftSection({
/**
* Wizard setup left section.
*/
function SetupLeftSection({
// #withAuthenticationActions
requestLogout,
isAuthorized
}) {
const [org] = useState('LibyanSpider');
@@ -19,7 +20,7 @@ function RegisterLeftSection({
}, [requestLogout]);
return (
<section className={'register-page__left-section'}>
<section className={'setup-page__left-section'}>
<div className={'content'}>
<div className={'content-logo'}>
<Icon
@@ -38,18 +39,15 @@ function RegisterLeftSection({
<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={'#!'}>
@@ -69,6 +67,5 @@ function RegisterLeftSection({
}
export default compose(
withAuthentication(({ isAuthorized }) => ({ isAuthorized })),
withAuthenticationActions,
)(RegisterLeftSection);
)(SetupLeftSection);

View File

@@ -20,11 +20,16 @@ import { momentFormatter, tansformDateValue } from 'utils';
import AppToaster from 'components/AppToaster';
import { ListSelect, ErrorMessage, FieldRequiredHint } from 'components';
import { useHistory } from 'react-router-dom';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withRegisterOrganizationActions from 'containers/Authentication/withRegisterOrganizationActions';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose, optionsMapToArray } from 'utils';
function RegisterOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
function SetupOrganizationForm({
requestSubmitOptions,
requestSeedTenant
}) {
const { formatMessage } = useIntl();
const [selected, setSelected] = useState();
const history = useHistory();
@@ -414,5 +419,5 @@ function RegisterOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
export default compose(
withSettingsActions,
withRegisterOrganizationActions,
)(RegisterOrganizationForm);
withOrganizationActions,
)(SetupOrganizationForm);

View File

@@ -0,0 +1,103 @@
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 ({
// #withAuthentication
currentOrganizationId,
// #withOrganization
isOrganizationInitialized,
isOrganizationSubscribed: hasSubscriptions,
isOrganizationSeeded
}) {
const history = useHistory();
const handleSkip = useCallback(({ step, push }) => {
const scenarios = [
{ condition: !hasSubscriptions, redirectTo: 'subscription' },
// { condition: , redirectTo: 'initializing' }
{ condition: !hasSubscriptions, redirectTo: 'organization' },
];
const scenario = scenarios.find((scenario) => scenario.condition);
if (scenario) {
push(scenario.redirectTo);
}
}, [
hasSubscriptions,
isOrganizationInitialized,
isOrganizationSeeded,
]);
return (
<section className={'setup-page__right-section'}>
<Wizard
onNext={handleSkip}
basename={'/setup'}
history={history}
render={({ step, steps }) => (
<div>
<WizardSetupSteps currentStep={steps.indexOf(step) + 1} />
<TransitionGroup>
<CSSTransition key={step.id} timeout={{ enter: 500, exit: 500 }}>
<div class="register-page-form">
<Steps key={step.id} step={step}>
<Step id="subscription">
<SetupSubscriptionForm />
</Step>
<Step id={'initializing'}>
<SetupInitializingForm />
</Step>
<Step id="organization">
<SetupOrganizationForm />
</Step>
<Step id="congratulations">
<h1 className="text-align-center">Ice King</h1>
</Step>
</Steps>
</div>
</CSSTransition>
</TransitionGroup>
</div>
)} />
</section>
)
}
export default compose(
withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })),
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
withOrganization(({
organization,
isOrganizationInitialized,
isOrganizationSubscribed,
isOrganizationSeeded,
}) => ({
organization,
isOrganizationInitialized,
isOrganizationSubscribed,
isOrganizationSeeded,
})),
)(SetupRightSection);

View File

@@ -1,11 +1,16 @@
import React, { useMemo } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormattedMessage as T } from 'react-intl';
import { Button, Intent } from '@blueprintjs/core';
import BillingTab from 'containers/Subscriptions/BillingTab';
function RegisterSubscriptionForm({}) {
/**
* Subscription step of wizard setup.
*/
export default function SetupSubscriptionForm({
}) {
const ValidationSchema = Yup.object().shape({});
const initialValues = useMemo(() => ({}), []);
@@ -36,5 +41,3 @@ function RegisterSubscriptionForm({}) {
</div>
);
}
export default RegisterSubscriptionForm;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import EnsureOrganizationIsNotReady from './EnsureOrganizationIsNotReady';
import SetupRightSection from './SetupRightSection';
import SetupLeftSection from './SetupLeftSection';
export default function WizardSetupPage() {
return (
<EnsureOrganizationIsNotReady>
<div class="setup-page">
<SetupLeftSection />
<SetupRightSection />
</div>
</EnsureOrganizationIsNotReady>
);
};

View File

@@ -3,7 +3,7 @@ import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { registerWizardSteps } from 'common/registerWizard'
function RegisterWizardStep({
function WizardSetupStep({
label,
isActive = false
}) {
@@ -14,15 +14,15 @@ function RegisterWizardStep({
);
}
function RegisterWizardSteps({
function WizardSetupSteps({
currentStep = 1,
}) {
return (
<div className={'register-wizard-steps'}>
<div className={'wizard-container'}>
<ul className={'wizard-wrapper'}>
<div className={'setup-page-steps-container'}>
<div className={'setup-page-steps'}>
<ul>
{registerWizardSteps.map((step, index) => (
<RegisterWizardStep
<WizardSetupStep
label={step.label}
isActive={(index + 1) == currentStep}
/>
@@ -33,4 +33,4 @@ function RegisterWizardSteps({
);
}
export default RegisterWizardSteps;
export default WizardSetupSteps;

View File

@@ -26,5 +26,11 @@ export default [
component: LazyLoader({
loader: () => import('containers/Authentication/InviteAccept'),
}),
},
{
path: `${BASE_URL}/register`,
component: LazyLoader({
loader: () => import('containers/Authentication/Register'),
}),
}
];

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

@@ -1,28 +0,0 @@
import ApiService from 'services/ApiService';
export const buildTenant = ({ id, token }) => {
return (dispatch) => {
return new Promise((resolve, reject) => {
ApiService.post(`organization/build${token}`, id)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
};
};
export const seedTenant = ({ id, token }) => {
return (dispatch) => {
return new Promise((resolve, reject) => {
ApiService.post(`organization/seed/${token}`, id)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
};
};

View File

@@ -1,16 +1,52 @@
import ApiService from 'services/ApiService';
import t from 'store/types';
export const fetchOrganizations = () => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get('organization/all').then((response) => {
dispatch({
type: t.ORGANIZATIONS_LIST_SET,
payload: {
organizations: response.data.organizations,
},
});
resolve(response)
}).catch(error => { reject(error); });
export const fetchOrganizations = () => (dispatch) => new Promise((resolve, reject) => {
ApiService.get('organization/all').then((response) => {
dispatch({
type: t.ORGANIZATIONS_LIST_SET,
payload: {
organizations: response.data.organizations,
},
});
resolve(response)
}).catch(error => { reject(error); });
});
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, 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 organizationSelector = (state, props) => state.organizations.data[props.organizationId];
export const getOrganizationByOrgIdFactory = () => createSelector(
organizationByIdSelector,
export const getOrganizationByIdFactory = () => createSelector(
organizationSelector,
(organization) => organization
);
export const isOrganizationSeededFactory = () => createSelector(
organizationSelector,
(organization) => {
return organization;
return !!organization?.seeded_at;
},
);
export const getOrganizationByTenantIdFactory = () => createSelector(
oragnizationByTenantIdSelector,
export const isOrganizationBuiltFactory = () => createSelector(
organizationSelector,
(organization) => {
return 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,4 @@
.register-page {
.bp3-input {
min-height: 40px;
@@ -88,6 +89,43 @@
}
}
&-form {
width: 800px;
margin: 0 auto;
// width: 690px;
// padding: 85px 60px;
// padding: 85px 105px;
// Register Form
.register-form {
padding: 85px 105px;
&__agreement-section {
margin-top: -10px;
p {
font-size: 13px;
margin-top: -10px;
margin-bottom: 24px;
line-height: 1.65;
}
}
&__submit-button-wrap {
margin: 25px 0px 25px 0px;
.bp3-button {
min-height: 45px;
background-color: #0052cc;
}
}
}
}
}
.setup-page {
&__right-section {
padding-left: 25%;
}
@@ -145,47 +183,18 @@
}
}
}
&-form {
width: 800px;
margin: 0 auto;
// width: 690px;
// padding: 85px 60px;
// padding: 85px 105px;
// Register Form
.register-form {
padding: 85px 105px;
&__agreement-section {
margin-top: -10px;
p {
font-size: 13px;
margin-top: -10px;
margin-bottom: 24px;
line-height: 1.65;
}
}
&__submit-button-wrap {
margin: 25px 0px 25px 0px;
.bp3-button {
min-height: 45px;
background-color: #0052cc;
}
}
}
}
}
// Register Wizard Steps
.wizard-container {
width: 80%;
margin: 60px auto;
.wizard-wrapper li {
.setup-page-steps {
&-container {
width: 80%;
margin: 60px auto;
}
li{
position: relative;
list-style-type: none;
width: 25%;
@@ -193,50 +202,53 @@
text-align: center;
color: #000;
font-size: 15px;
}
.wizard-wrapper li::before {
width: 13px;
height: 13px;
content: '';
line-height: 30px;
display: block;
text-align: center;
margin: 0 auto 10px auto;
border-radius: 50%;
background-color: #75859c;
}
.wizard-wrapper li::after {
width: 100%;
height: 2px;
content: '';
position: absolute;
background-color: #75859c;
top: 6px;
left: -50%;
z-index: -1;
}
.wizard-wrapper li:first-child::after {
display: none;
}
.wizard-wrapper > li.is-active::before {
background-color: #75859c;
}
&::before {
width: 13px;
height: 13px;
content: '';
line-height: 30px;
display: block;
text-align: center;
margin: 0 auto 10px auto;
border-radius: 50%;
background-color: #75859c;
}
.wizard-wrapper > li.is-active ~ li::before {
background: #ebebeb;
}
&::after {
width: 100%;
height: 2px;
content: '';
position: absolute;
background-color: #75859c;
top: 6px;
left: -50%;
z-index: -1;
}
.wizard-wrapper > li.is-active ~ li::after {
background: #ebebeb;
}
.wizard-wrapper > li.is-active p.wizard-info {
color: #004dd0;
&:first-child::after {
display: none;
}
&.is-active {
&::before {
background-color: #75859c;
}
~ li {
&:before,
&:after {
background: #ebebeb;
}
}
p.wizard-info {
color: #004dd0;
}
}
}
}
// @import './billing.scss';
//Register Subscription form
.register-subscription-form {
padding-top: 50px;