From e15a48dcdddee27a2e3bc59c667e524e00356844 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 11 Oct 2020 20:35:01 +0200 Subject: [PATCH] refactoring: setup wizard pages with simple architecture. --- client/src/components/App.js | 9 +- client/src/components/Dashboard/Dashboard.js | 18 +- .../Dashboard/DashboardLoadingIndicator.js | 5 +- .../Dashboard/EnsureOrganizationIsReady.js | 29 +- .../src/components/Dashboard/PrivatePages.js | 41 +++ client/src/containers/Authentication/Login.js | 6 +- .../src/containers/Authentication/Register.js | 284 ++++++++++++++++++ .../Authentication/Register/RegisterPage.js | 14 - .../Register/RegisterRightSection.js | 55 ---- .../Register/RegisterUserForm.js | 281 ----------------- .../Authentication/withAuthentication.js | 1 + .../withRegisterOrganizationActions.js | 12 - .../Organization/withOrganizationActions.js | 4 + .../Organization/withOrganizationByOrgId.js | 2 +- .../Organization/withOrganizationByTenId.js | 2 +- .../Setup/EnsureOrganizationIsNotReady.js | 25 ++ .../SetupLeftSection.js} | 11 +- .../SetupOrganizationForm.js} | 10 +- .../src/containers/Setup/SetupRightSection.js | 70 +++++ .../SetupSubscriptionForm.js} | 8 +- .../src/containers/Setup/WizardSetupPage.js | 19 ++ .../WizardSetupSteps.js} | 14 +- client/src/routes/authentication.js | 6 + .../organization/organization.actions.js | 28 -- .../organizations/organizations.actions.js | 40 ++- .../organizations/organizations.selectors.js | 14 +- .../src/style/pages/register-wizard-page.scss | 93 +++--- server/src/system/models/Tenant.js | 14 + .../system/repositories/TenantRepository.ts | 6 +- 29 files changed, 608 insertions(+), 513 deletions(-) create mode 100644 client/src/components/Dashboard/PrivatePages.js create mode 100644 client/src/containers/Authentication/Register.js delete mode 100644 client/src/containers/Authentication/Register/RegisterPage.js delete mode 100644 client/src/containers/Authentication/Register/RegisterRightSection.js delete mode 100644 client/src/containers/Authentication/Register/RegisterUserForm.js delete mode 100644 client/src/containers/Authentication/withRegisterOrganizationActions.js create mode 100644 client/src/containers/Setup/EnsureOrganizationIsNotReady.js rename client/src/containers/{Authentication/Register/RegisterLeftSection.js => Setup/SetupLeftSection.js} (92%) rename client/src/containers/{Authentication/Register/RegisterOrganizationForm.js => Setup/SetupOrganizationForm.js} (97%) create mode 100644 client/src/containers/Setup/SetupRightSection.js rename client/src/containers/{Authentication/Register/RegisterSubscriptionForm.js => Setup/SetupSubscriptionForm.js} (87%) create mode 100644 client/src/containers/Setup/WizardSetupPage.js rename client/src/containers/{Authentication/Register/RegisterWizardSteps.js => Setup/WizardSetupSteps.js} (69%) delete mode 100644 client/src/store/organization/organization.actions.js diff --git a/client/src/components/App.js b/client/src/components/App.js index aa7695535..bab88a549 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -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 }) { - - - - - + diff --git a/client/src/components/Dashboard/Dashboard.js b/client/src/components/Dashboard/Dashboard.js index 4b8dda00a..32d3ff487 100644 --- a/client/src/components/Dashboard/Dashboard.js +++ b/client/src/components/Dashboard/Dashboard.js @@ -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 ( - + @@ -62,5 +55,4 @@ function Dashboard({ export default compose( withSettingsActions, - withOrganizationsActions, )(Dashboard); \ No newline at end of file diff --git a/client/src/components/Dashboard/DashboardLoadingIndicator.js b/client/src/components/Dashboard/DashboardLoadingIndicator.js index b07f92dd5..bdb9ac09a 100644 --- a/client/src/components/Dashboard/DashboardLoadingIndicator.js +++ b/client/src/components/Dashboard/DashboardLoadingIndicator.js @@ -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 ( -
+
diff --git a/client/src/components/Dashboard/EnsureOrganizationIsReady.js b/client/src/components/Dashboard/EnsureOrganizationIsReady.js index a0b7e8929..a36d48896 100644 --- a/client/src/components/Dashboard/EnsureOrganizationIsReady.js +++ b/client/src/components/Dashboard/EnsureOrganizationIsReady.js @@ -1,16 +1,29 @@ 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'; -export default function EnsureOrganizationIsReady({ +function EnsureOrganizationIsReady({ + // #ownProps children, -}) { - const isOrganizationReady = false; + redirectTo = '/setup', - return (isOrganizationReady) ? children : ( + // #withOrganizationByOrgId + organization, +}) { + return (organization.is_ready) ? children : ( ); -} \ No newline at end of file +} + +export default compose( + withAuthentication(), + connect((state, props) => ({ + organizationId: props.currentOrganizationId, + })), + withOrganizationByOrgId(), +)(EnsureOrganizationIsReady); \ No newline at end of file diff --git a/client/src/components/Dashboard/PrivatePages.js b/client/src/components/Dashboard/PrivatePages.js new file mode 100644 index 000000000..54791a355 --- /dev/null +++ b/client/src/components/Dashboard/PrivatePages.js @@ -0,0 +1,41 @@ +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({ + requestOrganizationsList, +}) { + const fetchOrganizations = useQuery( + ['organizations'], + () => requestOrganizationsList(), + ); + + return ( + + + + + + + + + + + + ); +} + +export default compose( + withOrganizationActions, +)(DashboardPrivatePages); \ No newline at end of file diff --git a/client/src/containers/Authentication/Login.js b/client/src/containers/Authentication/Login.js index fa73a5aab..6dabb1606 100644 --- a/client/src/containers/Authentication/Login.js +++ b/client/src/containers/Authentication/Login.js @@ -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({

- +
@@ -170,5 +167,4 @@ function Login({ export default compose( withAuthenticationActions, - withOrganizationsActions, )(Login); \ No newline at end of file diff --git a/client/src/containers/Authentication/Register.js b/client/src/containers/Authentication/Register.js new file mode 100644 index 000000000..d88ec2a6e --- /dev/null +++ b/client/src/containers/Authentication/Register.js @@ -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( + () => ( + passwordRevealer()}> + + <> + {' '} + + + + + + + <> + {' '} + + + + + + + ), + [shown, passwordRevealer], + ); + + return ( + +
+
+

+ +

+ + + {' '} + + +
+ + + + + + } + intent={ + errors.first_name && touched.first_name && Intent.DANGER + } + helperText={ + + } + className={'form-group--first-name'} + > + + + + + + } + intent={errors.last_name && touched.last_name && Intent.DANGER} + helperText={ + + } + className={'form-group--last-name'} + > + + + + + + } + intent={ + errors.phone_number && touched.phone_number && Intent.DANGER + } + helperText={ + + } + className={'form-group--phone-number'} + > + + + + } + intent={errors.email && touched.email && Intent.DANGER} + helperText={ + + } + className={'form-group--email'} + > + + + + } + labelInfo={passwordRevealerTmp} + intent={errors.password && touched.password && Intent.DANGER} + helperText={ + + } + className={'form-group--password has-password-revealer'} + > + + + +
+

+
+ + + {' '} + + + {' '} + + +

+
+ +
+ +
+ + + +
+ +
+
+
+
+ ); +} + +export default compose( + withAuthenticationActions, +)(RegisterUserForm); diff --git a/client/src/containers/Authentication/Register/RegisterPage.js b/client/src/containers/Authentication/Register/RegisterPage.js deleted file mode 100644 index f8a3f350c..000000000 --- a/client/src/containers/Authentication/Register/RegisterPage.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import RegisterRightSection from './RegisterRightSection'; -import RegisterLeftSection from './RegisterLeftSection'; - -function RegisterWizardPage() { - return ( -
- - -
- ); -} - -export default RegisterWizardPage; diff --git a/client/src/containers/Authentication/Register/RegisterRightSection.js b/client/src/containers/Authentication/Register/RegisterRightSection.js deleted file mode 100644 index ac628ac0b..000000000 --- a/client/src/containers/Authentication/Register/RegisterRightSection.js +++ /dev/null @@ -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 ( -
- ( -
- - - - -
- - - - - - - - - - - - - - -

Ice King

-
-
-
-
-
-
- )} /> -
- ) -} \ No newline at end of file diff --git a/client/src/containers/Authentication/Register/RegisterUserForm.js b/client/src/containers/Authentication/Register/RegisterUserForm.js deleted file mode 100644 index 7dd592543..000000000 --- a/client/src/containers/Authentication/Register/RegisterUserForm.js +++ /dev/null @@ -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( - () => ( - passwordRevealer()}> - - <> - {' '} - - - - - - - <> - {' '} - - - - - - - ), - [shown, passwordRevealer], - ); - - return ( -
-
-

- -

- - - {' '} - - -
- -
- - - - } - intent={ - errors.first_name && touched.first_name && Intent.DANGER - } - helperText={ - - } - className={'form-group--first-name'} - > - - - - - - } - intent={errors.last_name && touched.last_name && Intent.DANGER} - helperText={ - - } - className={'form-group--last-name'} - > - - - - - - } - intent={ - errors.phone_number && touched.phone_number && Intent.DANGER - } - helperText={ - - } - className={'form-group--phone-number'} - > - - - - } - intent={errors.email && touched.email && Intent.DANGER} - helperText={ - - } - className={'form-group--email'} - > - - - - } - labelInfo={passwordRevealerTmp} - intent={errors.password && touched.password && Intent.DANGER} - helperText={ - - } - className={'form-group--password has-password-revealer'} - > - - - -
-

-
- - - {' '} - - - {' '} - - -

-
- -
- -
-
- - -
- -
-
-
- ); -} - -export default compose(withAuthenticationActions)(RegisterUserForm); diff --git a/client/src/containers/Authentication/withAuthentication.js b/client/src/containers/Authentication/withAuthentication.js index 9ad30b97a..34a678227 100644 --- a/client/src/containers/Authentication/withAuthentication.js +++ b/client/src/containers/Authentication/withAuthentication.js @@ -6,6 +6,7 @@ export default (mapState) => { const mapped = { isAuthorized: isAuthenticated(state), user: state.authentication.user, + currentOrganizationId: state.authentication?.tenant?.organization_id, }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/client/src/containers/Authentication/withRegisterOrganizationActions.js b/client/src/containers/Authentication/withRegisterOrganizationActions.js deleted file mode 100644 index 35537fa47..000000000 --- a/client/src/containers/Authentication/withRegisterOrganizationActions.js +++ /dev/null @@ -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); diff --git a/client/src/containers/Organization/withOrganizationActions.js b/client/src/containers/Organization/withOrganizationActions.js index 2a23bb6bb..d2eeea2a6 100644 --- a/client/src/containers/Organization/withOrganizationActions.js +++ b/client/src/containers/Organization/withOrganizationActions.js @@ -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()), + requestBuildTenant: () => dispatch(buildTenant()), + requestSeedTenant: () => dispatch(seedTenant()), }); export default connect(null, mapDispatchToProps); \ No newline at end of file diff --git a/client/src/containers/Organization/withOrganizationByOrgId.js b/client/src/containers/Organization/withOrganizationByOrgId.js index b07c89167..9d94d97f0 100644 --- a/client/src/containers/Organization/withOrganizationByOrgId.js +++ b/client/src/containers/Organization/withOrganizationByOrgId.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { getOrganizationByOrgIdFactory, -} from 'store/organizations/organizations.selector'; +} from 'store/organizations/organizations.selectors'; export default (mapState) => { const getOrganizationByOrgId = getOrganizationByOrgIdFactory(); diff --git a/client/src/containers/Organization/withOrganizationByTenId.js b/client/src/containers/Organization/withOrganizationByTenId.js index 998308914..a92fe1a8b 100644 --- a/client/src/containers/Organization/withOrganizationByTenId.js +++ b/client/src/containers/Organization/withOrganizationByTenId.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { getOrganizationByTenantIdFactory, -} from 'store/organizations/organizations.selector'; +} from 'store/organizations/organizations.selectors'; export default (mapState) => { const getOrgByTenId = getOrganizationByTenantIdFactory(); diff --git a/client/src/containers/Setup/EnsureOrganizationIsNotReady.js b/client/src/containers/Setup/EnsureOrganizationIsNotReady.js new file mode 100644 index 000000000..a31425b9b --- /dev/null +++ b/client/src/containers/Setup/EnsureOrganizationIsNotReady.js @@ -0,0 +1,25 @@ +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'; + +function EnsureOrganizationIsNotReady({ + children, + + // #withOrganizationByOrgId + organization, +}) { + return (organization.is_ready) ? ( + + ) : children; +} + +export default compose( + withAuthentication(), + connect((state, props) => ({ + organizationId: props.currentOrganizationId, + })), + withOrganizationByOrgId(), +)(EnsureOrganizationIsNotReady); \ No newline at end of file diff --git a/client/src/containers/Authentication/Register/RegisterLeftSection.js b/client/src/containers/Setup/SetupLeftSection.js similarity index 92% rename from client/src/containers/Authentication/Register/RegisterLeftSection.js rename to client/src/containers/Setup/SetupLeftSection.js index aeb8ad0a0..5dc327998 100644 --- a/client/src/containers/Authentication/Register/RegisterLeftSection.js +++ b/client/src/containers/Setup/SetupLeftSection.js @@ -7,8 +7,10 @@ import withAuthenticationActions from 'containers/Authentication/withAuthenticat import { compose } from 'utils'; - -function RegisterLeftSection({ +/** + * Wizard setup left section. + */ +function SetupLeftSection({ requestLogout, isAuthorized }) { @@ -19,7 +21,7 @@ function RegisterLeftSection({ }, [requestLogout]); return ( -
+

-
@@ -71,4 +72,4 @@ function RegisterLeftSection({ export default compose( withAuthentication(({ isAuthorized }) => ({ isAuthorized })), withAuthenticationActions, -)(RegisterLeftSection); +)(SetupLeftSection); diff --git a/client/src/containers/Authentication/Register/RegisterOrganizationForm.js b/client/src/containers/Setup/SetupOrganizationForm.js similarity index 97% rename from client/src/containers/Authentication/Register/RegisterOrganizationForm.js rename to client/src/containers/Setup/SetupOrganizationForm.js index 3f155608e..29c264bd0 100644 --- a/client/src/containers/Authentication/Register/RegisterOrganizationForm.js +++ b/client/src/containers/Setup/SetupOrganizationForm.js @@ -20,11 +20,13 @@ 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 +416,5 @@ function RegisterOrganizationForm({ requestSubmitOptions, requestSeedTenant }) { export default compose( withSettingsActions, - withRegisterOrganizationActions, -)(RegisterOrganizationForm); + withOrganizationActions, +)(SetupOrganizationForm); diff --git a/client/src/containers/Setup/SetupRightSection.js b/client/src/containers/Setup/SetupRightSection.js new file mode 100644 index 000000000..d00d38ccf --- /dev/null +++ b/client/src/containers/Setup/SetupRightSection.js @@ -0,0 +1,70 @@ +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 WizardSetupSteps from './WizardSetupSteps'; + +import SetupSubscriptionForm from './SetupSubscriptionForm'; +import SetupOrganizationForm from './SetupOrganizationForm'; + +import withAuthentication from 'containers/Authentication/withAuthentication'; + +import { compose } from 'utils'; + +/** + * Wizard setup right section. + */ +function SetupRightSection ({ + isTenantHasSubscriptions: hasSubscriptions = false, +}) { + const history = useHistory(); + const handleSkip = useCallback(({ step, push }) => { + const scenarios = [ + { condition: hasSubscriptions, redirectTo: 'organization' }, + { condition: !hasSubscriptions, redirectTo: 'subscription' }, + ]; + const scenario = scenarios.find((scenario) => scenario.condition); + + if (scenario) { + push(scenario.redirectTo); + } + }, [hasSubscriptions]); + + return ( +
+ ( +
+ + + + +
+ + + + + + + + + + +

Ice King

+
+
+
+
+
+
+ )} /> +
+ ) +} + +export default compose( + withAuthentication(({ isAuthorized }) => ({ isAuthorized })), +)(SetupRightSection); \ No newline at end of file diff --git a/client/src/containers/Authentication/Register/RegisterSubscriptionForm.js b/client/src/containers/Setup/SetupSubscriptionForm.js similarity index 87% rename from client/src/containers/Authentication/Register/RegisterSubscriptionForm.js rename to client/src/containers/Setup/SetupSubscriptionForm.js index 6274ac3aa..17fab2a76 100644 --- a/client/src/containers/Authentication/Register/RegisterSubscriptionForm.js +++ b/client/src/containers/Setup/SetupSubscriptionForm.js @@ -1,11 +1,13 @@ 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({}) { +export default function SetupSubscriptionForm({ + +}) { const ValidationSchema = Yup.object().shape({}); const initialValues = useMemo(() => ({}), []); @@ -36,5 +38,3 @@ function RegisterSubscriptionForm({}) {
); } - -export default RegisterSubscriptionForm; diff --git a/client/src/containers/Setup/WizardSetupPage.js b/client/src/containers/Setup/WizardSetupPage.js new file mode 100644 index 000000000..dcd4debb8 --- /dev/null +++ b/client/src/containers/Setup/WizardSetupPage.js @@ -0,0 +1,19 @@ +import React from 'react'; + +import EnsureOrganizationIsNotReady from './EnsureOrganizationIsNotReady'; +import SetupRightSection from './SetupRightSection'; +import SetupLeftSection from './SetupLeftSection'; + + +export default function WizardSetupPage({ + organizationId, +}) { + return ( + +
+ + +
+
+ ); +}; \ No newline at end of file diff --git a/client/src/containers/Authentication/Register/RegisterWizardSteps.js b/client/src/containers/Setup/WizardSetupSteps.js similarity index 69% rename from client/src/containers/Authentication/Register/RegisterWizardSteps.js rename to client/src/containers/Setup/WizardSetupSteps.js index b21a58a89..aaa39d6b7 100644 --- a/client/src/containers/Authentication/Register/RegisterWizardSteps.js +++ b/client/src/containers/Setup/WizardSetupSteps.js @@ -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 ( -
-
-
    +
    +
    +
      {registerWizardSteps.map((step, index) => ( - @@ -33,4 +33,4 @@ function RegisterWizardSteps({ ); } -export default RegisterWizardSteps; +export default WizardSetupSteps; diff --git a/client/src/routes/authentication.js b/client/src/routes/authentication.js index a4d1c32ac..f3fb7da68 100644 --- a/client/src/routes/authentication.js +++ b/client/src/routes/authentication.js @@ -26,5 +26,11 @@ export default [ component: LazyLoader({ loader: () => import('containers/Authentication/InviteAccept'), }), + }, + { + path: `${BASE_URL}/register`, + component: LazyLoader({ + loader: () => import('containers/Authentication/Register'), + }), } ]; diff --git a/client/src/store/organization/organization.actions.js b/client/src/store/organization/organization.actions.js deleted file mode 100644 index f6ab6cb04..000000000 --- a/client/src/store/organization/organization.actions.js +++ /dev/null @@ -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 || []); - }); - }); - }; -}; diff --git a/client/src/store/organizations/organizations.actions.js b/client/src/store/organizations/organizations.actions.js index fec7dee04..8f6b84934 100644 --- a/client/src/store/organizations/organizations.actions.js +++ b/client/src/store/organizations/organizations.actions.js @@ -1,16 +1,32 @@ 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) => new Promise((resolve, reject) => { + ApiService.post(`organization/build`).then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error.response.data.errors || []); }); -}; \ No newline at end of file +}); + +export const seedTenant = () => (dispatch) => new Promise((resolve, reject) => { + ApiService.post(`organization/seed/`).then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error.response.data.errors || []); + }); +}); \ No newline at end of file diff --git a/client/src/store/organizations/organizations.selectors.js b/client/src/store/organizations/organizations.selectors.js index 4d5368b93..e4a2ba6da 100644 --- a/client/src/store/organizations/organizations.selectors.js +++ b/client/src/store/organizations/organizations.selectors.js @@ -2,17 +2,17 @@ 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; export const getOrganizationByOrgIdFactory = () => createSelector( organizationByIdSelector, - (organization) => { - return organization; - }, + organizationsDataSelector, + (organizationId, organizationsData) => { + return organizationsData[organizationId]; + } ); export const getOrganizationByTenantIdFactory = () => createSelector( oragnizationByTenantIdSelector, - (organization) => { - return organization; - } -) \ No newline at end of file + (organization) => organization, +); \ No newline at end of file diff --git a/client/src/style/pages/register-wizard-page.scss b/client/src/style/pages/register-wizard-page.scss index 57407a948..a53e1add5 100644 --- a/client/src/style/pages/register-wizard-page.scss +++ b/client/src/style/pages/register-wizard-page.scss @@ -1,5 +1,5 @@ -.register-page { +.setup-page { &__right-section { padding-left: 25%; @@ -60,13 +60,14 @@ } } - -// Register Wizard Steps -.wizard-container { - width: 80%; - margin: 60px auto; +.setup-page-steps { - .wizard-wrapper li { + &-container { + width: 80%; + margin: 60px auto; + } + + li{ position: relative; list-style-type: none; width: 25%; @@ -74,48 +75,54 @@ 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.complete::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.complete ~ 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.complete ~ li::after { - background: #ebebeb; - } - .wizard-wrapper > li.complete 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 diff --git a/server/src/system/models/Tenant.js b/server/src/system/models/Tenant.js index 701597109..8c9b1688d 100644 --- a/server/src/system/models/Tenant.js +++ b/server/src/system/models/Tenant.js @@ -17,6 +17,20 @@ export default class Tenant extends BaseModel { return ['createdAt', 'updatedAt']; } + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['isReady']; + } + + /** + * Tenant is ready. + */ + get isReady() { + return !!(this.initializedAt && this.seededAt); + } + /** * Query modifiers. */ diff --git a/server/src/system/repositories/TenantRepository.ts b/server/src/system/repositories/TenantRepository.ts index b5392740d..9af7a37e4 100644 --- a/server/src/system/repositories/TenantRepository.ts +++ b/server/src/system/repositories/TenantRepository.ts @@ -77,9 +77,7 @@ export default class TenantRepository extends SystemRepository { * @param {number} tenantId - Tenant id. */ getByIdWithSubscriptions(tenantId: number) { - return this.cache.get(`tenant.id.${tenantId}.subscriptions`, () => { - return Tenant.query().findById(tenantId) - .withGraphFetched('subscriptions.plan'); - }); + return Tenant.query().findById(tenantId) + .withGraphFetched('subscriptions.plan'); } } \ No newline at end of file