fix(Setup): fix organization setup.

This commit is contained in:
a.bouhuolia
2021-03-20 18:59:40 +02:00
parent e801d5d618
commit 671af0daae
46 changed files with 517 additions and 445 deletions

View File

@@ -4,18 +4,17 @@ import { Formik } from 'formik';
import { useIntl } from 'react-intl';
import * as Yup from 'yup';
import { useHistory } from 'react-router-dom';
import Toaster from 'components/AppToaster';
import 'style/pages/Setup/PaymentViaVoucherDialog.scss';
import { usePaymentByVoucher } from 'hooks/query';
import { DialogContent } from 'components';
import PaymentViaLicenseForm from './PaymentViaVoucherForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withBillingActions from 'containers/Subscriptions/withBillingActions';
import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
import { compose } from 'utils';
import { Intent } from '@blueprintjs/core';
/**
* Payment via license dialog content.
@@ -26,30 +25,43 @@ function PaymentViaLicenseDialogContent({
// #withDialog
closeDialog,
// #withBillingActions
requestSubmitBilling,
// #withSubscriptionsActions
requestFetchSubscriptions,
}) {
const { formatMessage } = useIntl();
const history = useHistory();
// Payment via voucher
const {
mutateAsync: paymentViaVoucherMutate,
} = usePaymentByVoucher();
// Handle submit.
const handleSubmit = (values, { setSubmitting }) => {
const handleSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
requestSubmitBilling({ ...values, ...subscriptionForm })
.then(() => {
return requestFetchSubscriptions();
})
paymentViaVoucherMutate({ ...values })
.then(() => {
Toaster.show({
message: 'Payment has been done successfully.',
intent: Intent.SUCCESS,
});
return closeDialog('payment-via-voucher');
})
.then(() => {
history.push('initializing');
})
.catch(
({
response: {
data: { errors },
},
}) => {
if (errors.find((e) => e.type === 'LICENSE.CODE.IS.INVALID')) {
setErrors({
license_code: 'The license code is not valid, please try agin.',
});
}
},
)
.finally((errors) => {
setSubmitting(false);
});
@@ -57,17 +69,18 @@ function PaymentViaLicenseDialogContent({
// Initial values.
const initialValues = {
license_number: '',
license_code: '',
plan_slug: '',
period: '',
...subscriptionForm,
};
// Validation schema.
const validationSchema = Yup.object().shape({
license_number: Yup.string()
license_code: Yup.string()
.required()
.min(10)
.max(10)
.label(formatMessage({ id: 'license_number' })),
.label(formatMessage({ id: 'license_code' })),
});
return (
@@ -82,8 +95,4 @@ function PaymentViaLicenseDialogContent({
);
}
export default compose(
withDialogActions,
withBillingActions,
withSubscriptionsActions,
)(PaymentViaLicenseDialogContent);
export default compose(withDialogActions)(PaymentViaLicenseDialogContent);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, FormGroup, InputGroup, Intent } from '@blueprintjs/core';
import { Form, FastField, ErrorMessage } from 'formik';
import { Form, FastField, ErrorMessage, useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { compose } from 'redux';
@@ -15,12 +15,12 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
* Payment via license form.
*/
function PaymentViaLicenseForm({
// #ownProps
isSubmitting,
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
const licenseNumberRef = useAutofocus();
// Handle close button click.
@@ -33,15 +33,17 @@ function PaymentViaLicenseForm({
<div className={CLASSES.DIALOG_BODY}>
<p>Please enter your preferred payment method below.</p>
<FastField name="license_number">
<FastField name="license_code">
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'voucher_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="voucher_number" />}
helperText={<ErrorMessage name="license_code" />}
className={'form-group--voucher_number'}
>
<InputGroup
large={true}
intent={inputIntent({ error, touched })}
{...field}
inputRef={(ref) => (licenseNumberRef.current = ref)}
/>

View File

@@ -7,16 +7,14 @@ import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
// Lazy loading the content.
const PaymentViaLicenseDialogContent = lazy(() => import('./PaymentViaVoucherDialogContent'));
const PaymentViaLicenseDialogContent = lazy(() =>
import('./PaymentViaVoucherDialogContent'),
);
/**
* Payment via license dialog.
*/
function PaymentViaLicenseDialog({
dialogName,
payload,
isOpen
}) {
function PaymentViaLicenseDialog({ dialogName, payload, isOpen }) {
return (
<Dialog
name={dialogName}
@@ -33,9 +31,7 @@ function PaymentViaLicenseDialog({
/>
</DialogSuspense>
</Dialog>
)
);
}
export default compose(
withDialogRedux(),
)(PaymentViaLicenseDialog);
export default compose(withDialogRedux())(PaymentViaLicenseDialog);

View File

@@ -1,17 +1,11 @@
import { connect } from 'react-redux';
import {
fetchOrganizations,
buildTenant,
seedTenant,
setOrganizationSetupCompleted,
} from 'store/organizations/organizations.actions';
const mapDispatchToProps = (dispatch) => ({
requestOrganizationBuild: () => dispatch(buildTenant()),
requestOrganizationSeed: () => dispatch(seedTenant()),
requestAllOrganizations: () => dispatch(fetchOrganizations()),
setOrganizationSetupCompleted: (congrats) => dispatch(setOrganizationSetupCompleted(congrats)),
setOrganizationSetupCompleted: (congrats) =>
dispatch(setOrganizationSetupCompleted(congrats)),
});
export default connect(null, mapDispatchToProps);
export default connect(null, mapDispatchToProps);

View File

@@ -1,53 +1,49 @@
import React, { useEffect } from 'react';
import { useQuery } from 'react-query';
import { withWizard } from 'react-albus'
import { ProgressBar, Intent } from '@blueprintjs/core';
import { useBuildTenant } from 'hooks/query';
import 'style/pages/Setup/Initializing.scss';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
/**
* Setup initializing step form.
*/
function SetupInitializingForm({
// #withOrganizationActions
requestOrganizationBuild,
wizard: { next },
}) {
const { isSuccess } = useQuery(
['build-tenant'], () => requestOrganizationBuild(),
);
export default function SetupInitializingForm() {
const {
mutateAsync: buildTenantMutate,
isLoading,
isError,
} = useBuildTenant();
useEffect(() => {
if (isSuccess) {
next();
}
}, [isSuccess, next]);
buildTenantMutate();
}, [buildTenantMutate]);
return (
<div class="setup-initializing-form">
<ProgressBar intent={Intent.PRIMARY} value={null} />
{isLoading && <ProgressBar intent={Intent.PRIMARY} value={null} />}
<div className={'setup-initializing-form__title'}>
<h1>
{/* You organization is initializin... */}
It's time to make your accounting really simple!
</h1>
<p className={'paragraph'}>
while we set up your account, please remember to verify your account by
clicking on the link we sent to yout registered email address
</p>
{isLoading ? (
<>
<h1>It's time to make your accounting really simple!</h1>
<p className={'paragraph'}>
while we set up your account, please remember to verify your
account by clicking on the link we sent to yout registered email
address
</p>
</>
) : isError ? (
<>
<h1>Something went wrong!</h1>
<p class="paragraph">Please refresh the page</p>
</>
) : (
<>
<h1>Waiting to redirect</h1>
<p class="paragraph">Refresh the page if redirect not worked.</p>
</>
)}
</div>
</div>
);
}
export default compose(
withOrganizationActions,
withWizard,
)(SetupInitializingForm);

View File

@@ -14,7 +14,7 @@ import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { FormattedMessage as T } from 'react-intl';
import { Col, Row, ListSelect } from 'components';
import { FieldRequiredHint, Col, Row, ListSelect } from 'components';
import {
momentFormatter,
tansformDateValue,
@@ -38,9 +38,10 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</h3>
{/* ---------- Organization name ---------- */}
<FastField name={'name'}>
<FastField name={'organization_name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'legal_organization_name'} />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
@@ -55,6 +56,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FastField name={'financialDateStart'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'financial_starting_date'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="financialDateStart" />}
@@ -82,6 +84,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'base_currency'} />}
className={classNames(
'form-group--base-currency',
@@ -137,6 +140,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
selectedItemProp={'value'}
defaultText={<T id={'select_language'} />}
popoverProps={{ minimal: true }}
filterable={false}
/>
</FormGroup>
)}
@@ -147,6 +151,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FastField name={'fiscalYear'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'fiscal_year'} />}
className={classNames(
'form-group--fiscal_year',
@@ -167,6 +172,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
onItemSelect={(item) => {
setFieldValue('fiscalYear', item.value)
}}
filterable={false}
/>
</FormGroup>
)}
@@ -180,6 +186,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'time_zone'} />}
className={classNames(
'form-group--time-zone',

View File

@@ -3,49 +3,33 @@ import * as Yup from 'yup';
import { Formik } from 'formik';
import moment from 'moment';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { snakeCase } from 'lodash';
import { withWizard } from 'react-albus';
import { useQuery } from 'react-query';
import 'style/pages/Setup/Organization.scss';
import SetupOrganizationForm from './SetupOrganizationForm';
import { useOrganizationSetup } from 'hooks/query';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withSettings from 'containers/Settings/withSettings';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import {
compose,
transformToForm,
optionsMapToArray,
transfromToSnakeCase,
} from 'utils';
/**
* Setup organization form.
*/
function SetupOrganizationPage({
// #withSettingsActions
requestSubmitOptions,
requestFetchOptions,
// #withOrganizationActions
requestOrganizationSeed,
// #withSettings
organizationSettings,
wizard,
setOrganizationSetupCompleted,
}) {
const { formatMessage } = useIntl();
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
const { mutateAsync: organizationSetupMutate } = useOrganizationSetup();
// Validation schema.
const validationSchema = Yup.object().shape({
name: Yup.string()
organization_name: Yup.string()
.required()
.label(formatMessage({ id: 'organization_name_' })),
financialDateStart: Yup.date()
@@ -67,35 +51,21 @@ function SetupOrganizationPage({
// Initial values.
const defaultValues = {
name: '',
organization_name: '',
financialDateStart: moment(new Date()).format('YYYY-MM-DD'),
baseCurrency: '',
language: '',
language: 'en',
fiscalYear: '',
timeZone: '',
...organizationSettings,
};
const initialValues = {
...defaultValues,
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(organizationSettings, defaultValues),
};
// Handle the form submit.
const handleSubmit = (values, { setSubmitting, setErrors }) => {
const options = optionsMapToArray(values).map((option) => {
return { ...option, key: snakeCase(option.key), group: 'organization' };
});
requestSubmitOptions({ options })
.then(() => {
return requestOrganizationSeed();
})
organizationSetupMutate({ ...transfromToSnakeCase(values) })
.then(() => {
return setOrganizationSetupCompleted(true);
})
@@ -132,8 +102,4 @@ function SetupOrganizationPage({
export default compose(
withSettingsActions,
withOrganizationActions,
withWizard,
withSettings(({ organizationSettings }) => ({
organizationSettings,
})),
)(SetupOrganizationPage);

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { Wizard } from 'react-albus';
import { useHistory } from 'react-router-dom';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import SetupDialogs from './SetupDialogs';
import SetupWizardContent from './SetupWizardContent';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import withOrganization from 'containers/Organization/withOrganization';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import withSetupWizard from '../../store/organizations/withSetupWizard';
@@ -24,37 +20,17 @@ function SetupRightSection({
isOrganizationSetupCompleted,
// #withSetupWizard
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
setupStepId,
setupStepIndex,
// #withSubscriptions
isSubscriptionActive,
}) {
const history = useHistory();
const handleSkip = ({ step, push }) => {
const scenarios = [
{ condition: isCongratsStep, redirectTo: 'congrats' },
{ condition: isSubscriptionStep, redirectTo: 'subscription' },
{ condition: isInitializingStep, redirectTo: 'initializing' },
{ condition: isOrganizationStep, redirectTo: 'organization' },
];
const scenario = scenarios.find((scenario) => scenario.condition);
if (scenario) {
push(scenario.redirectTo);
}
};
return (
<section className={'setup-page__right-section'}>
<Wizard
onNext={handleSkip}
basename={'/setup'}
history={history}
render={SetupWizardContent}
<SetupWizardContent
setupStepId={setupStepId}
setupStepIndex={setupStepIndex}
/>
<SetupDialogs />
</section>
@@ -84,17 +60,8 @@ export default compose(
}),
'main',
),
withSetupWizard(
({
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
}) => ({
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
}),
),
withSetupWizard(({ setupStepId, setupStepIndex }) => ({
setupStepId,
setupStepIndex,
})),
)(SetupRightSection);

View File

@@ -0,0 +1,8 @@
import React from 'react';
export default function SetupSteps({ step, children }) {
const activeStep = React.Children.toArray(children).filter(
(child) => child.props.id === step.id,
);
return activeStep;
}

View File

@@ -1,21 +1,15 @@
import React from 'react';
import { Formik } from 'formik';
import { withWizard } from 'react-albus';
import 'style/pages/Setup/Subscription.scss';
import SetupSubscriptionForm from './SetupSubscriptionForm';
import { SubscriptionFormSchema } from './SubscriptionForm.schema';
import { compose } from 'utils';
/**
* Subscription step of wizard setup.
*/
function SetupSubscription({
// #withWizard
wizard,
}) {
export default function SetupSubscription() {
// Initial values.
const initialValues = {
plan_slug: 'free',
@@ -23,6 +17,7 @@ function SetupSubscription({
license_code: '',
};
// Handle form submit.
const handleSubmit = () => {};
return (
@@ -35,8 +30,4 @@ function SetupSubscription({
/>
</div>
);
}
export default compose(
withWizard,
)(SetupSubscription);
}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Steps, Step } from 'react-albus';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import SetupSteps from './SetupSteps';
import WizardSetupSteps from './WizardSetupSteps';
import SetupSubscription from './SetupSubscription';
@@ -12,37 +11,19 @@ import SetupCongratsPage from './SetupCongratsPage';
/**
* Setup wizard content.
*/
export default function SetupWizardContent({
step,
steps
}) {
export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
return (
<div class="setup-page__content">
<WizardSetupSteps currentStep={steps.indexOf(step) + 1} />
<WizardSetupSteps currentStep={setupStepIndex} />
<TransitionGroup>
<CSSTransition key={step.id} timeout={{ enter: 500, exit: 500 }}>
<div class="setup-page-form">
<Steps key={step.id} step={step}>
<Step id="subscription">
<SetupSubscription />
</Step>
<Step id={'initializing'}>
<SetupInitializingForm />
</Step>
<Step id="organization">
<SetupOrganizationPage />
</Step>
<Step id="congrats">
<SetupCongratsPage />
</Step>
</Steps>
</div>
</CSSTransition>
</TransitionGroup>
<div class="setup-page-form">
<SetupSteps step={{ id: setupStepId }}>
<SetupSubscription id="subscription" />
<SetupInitializingForm id={'initializing'} />
<SetupOrganizationPage id="organization" />
<SetupCongratsPage id="congrats" />
</SetupSteps>
</div>
</div>
);
}

View File

@@ -1,22 +1,19 @@
import React from 'react';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { registerWizardSteps } from 'common/registerWizard'
import { registerWizardSteps } from 'common/registerWizard';
function WizardSetupStep({
label,
isActive = false
}) {
function WizardSetupStep({ label, isActive = false }) {
return (
<li className={classNames({ 'is-active': isActive })}>
<p className={'wizard-info'}><T id={label} /></p>
<p className={'wizard-info'}>
<T id={label} />
</p>
</li>
);
}
function WizardSetupSteps({
currentStep = 1,
}) {
export default function WizardSetupSteps({ currentStep = 1 }) {
return (
<div className={'setup-page-steps-container'}>
<div className={'setup-page-steps'}>
@@ -24,7 +21,7 @@ function WizardSetupSteps({
{registerWizardSteps.map((step, index) => (
<WizardSetupStep
label={step.label}
isActive={(index + 1) == currentStep}
isActive={index + 1 === currentStep}
/>
))}
</ul>
@@ -32,5 +29,3 @@ function WizardSetupSteps({
</div>
);
}
export default WizardSetupSteps;