diff --git a/client/src/common/registerWizard.js b/client/src/common/registerWizard.js index b7dedb9f3..634f2271a 100644 --- a/client/src/common/registerWizard.js +++ b/client/src/common/registerWizard.js @@ -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', }, ]; \ No newline at end of file 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..d24e32079 100644 --- a/client/src/components/Dashboard/EnsureOrganizationIsReady.js +++ b/client/src/components/Dashboard/EnsureOrganizationIsReady.js @@ -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 : ( ); -} \ No newline at end of file +} + +export default compose( + withAuthentication(), + connect((state, props) => ({ + organizationId: props.currentOrganizationId, + })), + withOrganization(({ isOrganizationBuilt }) => ({ isOrganizationBuilt })), +)(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..499898113 --- /dev/null +++ b/client/src/components/Dashboard/PrivatePages.js @@ -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 ( + + + + + + + + + + + + ); +} + +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..94d846f28 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?.organizationId, }; 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/withOrganization.js b/client/src/containers/Organization/withOrganization.js new file mode 100644 index 000000000..af467dffa --- /dev/null +++ b/client/src/containers/Organization/withOrganization.js @@ -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); +}; \ No newline at end of file diff --git a/client/src/containers/Organization/withOrganizationActions.js b/client/src/containers/Organization/withOrganizationActions.js index 2a23bb6bb..e44284b3b 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()), +const mapDispatchToProps = (dispatch) => ({ + requestOrganizationBuild: () => dispatch(buildTenant()), + requestOrganizationSeed: () => dispatch(seedTenant()), + requestAllOrganizations: () => dispatch(fetchOrganizations()), }); 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 deleted file mode 100644 index b07c89167..000000000 --- a/client/src/containers/Organization/withOrganizationByOrgId.js +++ /dev/null @@ -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); -}; \ No newline at end of file diff --git a/client/src/containers/Organization/withOrganizationByTenId.js b/client/src/containers/Organization/withOrganizationByTenId.js deleted file mode 100644 index 998308914..000000000 --- a/client/src/containers/Organization/withOrganizationByTenId.js +++ /dev/null @@ -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); -}; \ No newline at end of file diff --git a/client/src/containers/Setup/EnsureOrganizationIsNotReady.js b/client/src/containers/Setup/EnsureOrganizationIsNotReady.js new file mode 100644 index 000000000..a559b0f07 --- /dev/null +++ b/client/src/containers/Setup/EnsureOrganizationIsNotReady.js @@ -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) ? ( + + ) : children; +} + +export default compose( + withAuthentication(({ currentOrganizationId }) => ({ + currentOrganizationId, + })), + connect((state, props) => ({ + organizationId: props.currentOrganizationId, + })), + withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })), +)(EnsureOrganizationIsNotReady); \ No newline at end of file diff --git a/client/src/containers/Setup/SetupInitializingForm.js b/client/src/containers/Setup/SetupInitializingForm.js new file mode 100644 index 000000000..bbae58a09 --- /dev/null +++ b/client/src/containers/Setup/SetupInitializingForm.js @@ -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 ( +
+

You organization is initializin...

+
+ ); +} + +export default compose( + withOrganizationActions +)(SetupInitializingForm); \ 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 66% rename from client/src/containers/Authentication/Register/RegisterLeftSection.js rename to client/src/containers/Setup/SetupLeftSection.js index aeb8ad0a0..732bf0f66 100644 --- a/client/src/containers/Authentication/Register/RegisterLeftSection.js +++ b/client/src/containers/Setup/SetupLeftSection.js @@ -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 ( -
+

- - -
- - - {org}, - - - - -
-
+
+ + + {org}, + + + + +
@@ -69,6 +67,5 @@ 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..23ba2d122 100644 --- a/client/src/containers/Authentication/Register/RegisterOrganizationForm.js +++ b/client/src/containers/Setup/SetupOrganizationForm.js @@ -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); diff --git a/client/src/containers/Setup/SetupRightSection.js b/client/src/containers/Setup/SetupRightSection.js new file mode 100644 index 000000000..645ef849a --- /dev/null +++ b/client/src/containers/Setup/SetupRightSection.js @@ -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 ( +
+ ( +
+ + + + +
+ + + + + + + + + + + + + + +

Ice King

+
+
+
+
+
+
+ )} /> +
+ ) +} + +export default compose( + withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })), + connect((state, props) => ({ + organizationId: props.currentOrganizationId, + })), + withOrganization(({ + organization, + isOrganizationInitialized, + isOrganizationSubscribed, + isOrganizationSeeded, + }) => ({ + organization, + isOrganizationInitialized, + isOrganizationSubscribed, + isOrganizationSeeded, + })), +)(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 86% rename from client/src/containers/Authentication/Register/RegisterSubscriptionForm.js rename to client/src/containers/Setup/SetupSubscriptionForm.js index 6274ac3aa..5ae1e9684 100644 --- a/client/src/containers/Authentication/Register/RegisterSubscriptionForm.js +++ b/client/src/containers/Setup/SetupSubscriptionForm.js @@ -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({}) {
); } - -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..ba775531b --- /dev/null +++ b/client/src/containers/Setup/WizardSetupPage.js @@ -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 ( + +
+ + +
+
+ ); +}; \ 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 b97255815..d29cb4abe 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/authentication/authentication.reducer.js b/client/src/store/authentication/authentication.reducer.js index 4cd2f0adc..12db81283 100644 --- a/client/src/store/authentication/authentication.reducer.js +++ b/client/src/store/authentication/authentication.reducer.js @@ -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; }, 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..3cbb39f22 100644 --- a/client/src/store/organizations/organizations.actions.js +++ b/client/src/store/organizations/organizations.actions.js @@ -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 } }); -}; \ No newline at end of file + 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 || []); + }); +}); \ No newline at end of file diff --git a/client/src/store/organizations/organizations.reducers.js b/client/src/store/organizations/organizations.reducers.js index b8407108d..5f1d1f848 100644 --- a/client/src/store/organizations/organizations.reducers.js +++ b/client/src/store/organizations/organizations.reducers.js @@ -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; \ 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..a8b751a8e 100644 --- a/client/src/store/organizations/organizations.selectors.js +++ b/client/src/store/organizations/organizations.selectors.js @@ -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; } ) \ No newline at end of file diff --git a/client/src/store/organizations/organizations.types.js b/client/src/store/organizations/organizations.types.js index 1b9d47692..924ea7238 100644 --- a/client/src/store/organizations/organizations.types.js +++ b/client/src/store/organizations/organizations.types.js @@ -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', }; \ 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 2fc911bcb..67ebe31fa 100644 --- a/client/src/style/pages/register-wizard-page.scss +++ b/client/src/style/pages/register-wizard-page.scss @@ -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; diff --git a/server/src/database/seeds/core/20200810121807_seed_views.js b/server/src/database/seeds/core/20200810121807_seed_views.js index 5e945978c..19da37d7b 100644 --- a/server/src/database/seeds/core/20200810121807_seed_views.js +++ b/server/src/database/seeds/core/20200810121807_seed_views.js @@ -1,3 +1,5 @@ +import Container from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService' exports.up = (knex) => { const tenancyService = Container.get(TenancyService); diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index 12ccab4ec..5a97bc254 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -87,9 +87,6 @@ export default class AuthenticationService implements IAuthenticationService { // Remove password property from user object. Reflect.deleteProperty(user, 'password'); - // Remove id property from tenant object. - Reflect.deleteProperty(tenant, 'id'); - return { user, token, tenant }; } 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