diff --git a/src/components/AppIntlLoader.js b/src/components/AppIntlLoader.js index 68623edaf..801e3cd45 100644 --- a/src/components/AppIntlLoader.js +++ b/src/components/AppIntlLoader.js @@ -7,9 +7,10 @@ import rtlDetect from 'rtl-detect'; import * as R from 'ramda'; import { AppIntlProvider } from './AppIntlProvider'; +import { useSplashLoading } from '../hooks/state'; +import { useWatch } from '../hooks'; import withDashboardActions from '../containers/Dashboard/withDashboardActions'; -import withDashboard from '../containers/Dashboard/withDashboard'; const SUPPORTED_LOCALES = [ { name: 'English', value: 'en' }, @@ -63,20 +64,13 @@ function transformMomentLocale(currentLocale) { } /** - * Application Intl loader. + * Loads application locales of the given current locale. + * @param {string} currentLocale + * @returns {{ isLoading: boolean }} */ -function AppIntlLoader({ appIntlIsLoading, setAppIntlIsLoading, children }) { - const [isLocalsLoading, setIsLocalsLoading] = React.useState(true); - const [isYupLoading, setIsYupLoading] = React.useState(true); - - // Retrieve the current locale. - const currentLocale = getCurrentLocal(); - - // Detarmines the document direction based on the given locale. - const isRTL = rtlDetect.isRtlLang(currentLocale); - - // Modifies the html document direction - useDocumentDirectionModifier(currentLocale, isRTL); +function useAppLoadLocales(currentLocale) { + const [startLoading, stopLoading] = useSplashLoading(); + const [isLoading, setIsLoading] = React.useState(true); React.useEffect(() => { // Lodas the locales data file. @@ -91,33 +85,72 @@ function AppIntlLoader({ appIntlIsLoading, setAppIntlIsLoading, children }) { }) .then(() => { moment.locale(transformMomentLocale(currentLocale)); - setIsLocalsLoading(false); + setIsLoading(false); }); - }, [currentLocale, setIsLocalsLoading]); + }, [currentLocale, stopLoading]); + + // Watches the value to start/stop splash screen. + useWatch(isLoading, (value) => (value ? startLoading() : stopLoading()), { + immediate: true, + }); + + return { isLoading }; +} + +/** + * Loads application yup locales based on the given current locale. + * @param {string} currentLocale + * @returns {{ isLoading: boolean }} + */ +function useAppYupLoadLocales(currentLocale) { + const [startLoading, stopLoading] = useSplashLoading(); + const [isLoading, setIsLoading] = React.useState(true); React.useEffect(() => { loadYupLocales(currentLocale) .then(({ locale }) => { setLocale(locale); - setIsYupLoading(false); + setIsLoading(false); }) .then(() => {}); - }, [currentLocale]); + }, [currentLocale, stopLoading]); - React.useEffect(() => { - if (!isLocalsLoading && !isYupLoading) { - setAppIntlIsLoading(false); - } + // Watches the valiue to start/stop splash screen. + useWatch(isLoading, (value) => (value ? startLoading() : stopLoading()), { + immediate: true, }); + return { isLoading }; +} + +/** + * Application Intl loader. + */ +function AppIntlLoader({ children }) { + // Retrieve the current locale. + const currentLocale = getCurrentLocal(); + + // Detarmines the document direction based on the given locale. + const isRTL = rtlDetect.isRtlLang(currentLocale); + + // Modifies the html document direction + useDocumentDirectionModifier(currentLocale, isRTL); + + // Loads yup localization of the given locale. + const { isLoading: isAppYupLocalesLoading } = + useAppYupLoadLocales(currentLocale); + + // Loads application locales of the given locale. + const { isLoading: isAppLocalesLoading } = useAppLoadLocales(currentLocale); + + // Detarmines whether the app locales loading. + const isLoading = isAppYupLocalesLoading && isAppLocalesLoading; + return ( - {appIntlIsLoading ? null : children} + {isLoading ? null : children} ); } -export default R.compose( - withDashboardActions, - withDashboard(({ appIntlIsLoading }) => ({ appIntlIsLoading })), -)(AppIntlLoader); +export default R.compose(withDashboardActions)(AppIntlLoader); diff --git a/src/components/Authentication.js b/src/components/Authentication.js index a9b8575c2..a34060825 100644 --- a/src/components/Authentication.js +++ b/src/components/Authentication.js @@ -6,7 +6,6 @@ import authenticationRoutes from 'routes/authentication'; import { FormattedMessage as T } from 'components'; import Icon from 'components/Icon'; import { useIsAuthenticated } from 'hooks/state'; -import {AuthenticationBoot} from '../containers/Authentication/AuthenticationBoot'; import 'style/pages/Authentication/Auth.scss'; function PageFade(props) { @@ -26,7 +25,6 @@ export default function AuthenticationWrapper({ ...rest }) { ) : (
- { - // Once the all requests complete change the app loading state. - if ( - isAuthUserSuccess && + const [startLoading, stopLoading] = useSplashLoading(); + + // Splash loading when organization request loading and + // applicaiton still not booted. + useWatch(isOrgLoading, (value) => { + value && !isBooted.current && startLoading(); + }); + + // Splash loading when request authenticated user loading and + // application still not booted yet. + useWatch(isAuthUserLoading, (value) => { + value && !isBooted.current && startLoading(); + }); + + // Stop splash loading once organization request success. + useWatch(isCurrentOrganizationSuccess, (value) => { + value && stopLoading(); + }); + + // Stop splash loading once authenticated user request success. + useWatch(isAuthUserSuccess, (value) => { + value && stopLoading(); + }); + + // Once the all requests complete change the app loading state. + useWhen( + isAuthUserSuccess && isCurrentOrganizationSuccess && - localeCookie === organization?.metadata?.language - ) { - setAppIsLoading(false); + localeCookie === organization?.metadata?.language, + () => { isBooted.current = true; - } - }, [ - isAuthUserSuccess, - isCurrentOrganizationSuccess, - organization, - setAppIsLoading, - localeCookie, - ]); + }, + ); return null; } @@ -72,5 +95,4 @@ export const DashboardBoot = R.compose( withAuthentication(({ authenticatedUserId }) => ({ authenticatedUserId, })), - withDashboardActions, )(DashboardBootJSX); diff --git a/src/components/Dashboard/PrivatePages.js b/src/components/Dashboard/PrivatePages.js index 8c2a2d421..643752389 100644 --- a/src/components/Dashboard/PrivatePages.js +++ b/src/components/Dashboard/PrivatePages.js @@ -4,10 +4,9 @@ import { Switch, Route } from 'react-router'; import Dashboard from 'components/Dashboard/Dashboard'; import SetupWizardPage from 'containers/Setup/WizardSetupPage'; -import EnsureOrganizationIsReady from 'components/Guards/EnsureOrganizationIsReady'; -import EnsureOrganizationIsNotReady from 'components/Guards/EnsureOrganizationIsNotReady'; +import EnsureOrganizationIsReady from '../../components/Guards/EnsureOrganizationIsReady'; +import EnsureOrganizationIsNotReady from '../../components/Guards/EnsureOrganizationIsNotReady'; import { PrivatePagesProvider } from './PrivatePagesProvider'; -import { DashboardBoot } from '../../components'; import 'style/pages/Dashboard/Dashboard.scss'; @@ -17,8 +16,6 @@ import 'style/pages/Dashboard/Dashboard.scss'; export default function DashboardPrivatePages() { return ( - - diff --git a/src/components/Dashboard/PrivatePagesProvider.js b/src/components/Dashboard/PrivatePagesProvider.js index 956c6063a..1f7098817 100644 --- a/src/components/Dashboard/PrivatePagesProvider.js +++ b/src/components/Dashboard/PrivatePagesProvider.js @@ -1,9 +1,31 @@ import React from 'react'; +import * as R from 'ramda'; + import { AuthenticatedUser } from './AuthenticatedUser'; +import { DashboardBoot } from '../../components'; + +import withDashboard from '../../containers/Dashboard/withDashboard'; /** * Private pages provider. */ -export function PrivatePagesProvider({ children }) { - return {children}; +function PrivatePagesProviderComponent({ + splashScreenCompleted, + + // #ownProps + children, +}) { + return ( + + + + {splashScreenCompleted ? children : null} + + ); } + +export const PrivatePagesProvider = R.compose( + withDashboard(({ splashScreenCompleted }) => ({ + splashScreenCompleted, + })), +)(PrivatePagesProviderComponent); diff --git a/src/components/Dashboard/SplashScreen.js b/src/components/Dashboard/SplashScreen.js index fc32548e4..238cc94f0 100644 --- a/src/components/Dashboard/SplashScreen.js +++ b/src/components/Dashboard/SplashScreen.js @@ -3,13 +3,12 @@ import * as R from 'ramda'; import BigcapitalLoading from './BigcapitalLoading'; import withDashboard from '../../containers/Dashboard/withDashboard'; -function SplashScreenComponent({ appIsLoading, appIntlIsLoading }) { - return appIsLoading || appIntlIsLoading ? : null; +function SplashScreenComponent({ splashScreenLoading }) { + return splashScreenLoading ? : null; } export const SplashScreen = R.compose( - withDashboard(({ appIsLoading, appIntlIsLoading }) => ({ - appIsLoading, - appIntlIsLoading, + withDashboard(({ splashScreenLoading }) => ({ + splashScreenLoading, })), )(SplashScreenComponent); diff --git a/src/components/Guards/EnsureOrganizationIsReady.js b/src/components/Guards/EnsureOrganizationIsReady.js index ba9e8024f..097efcdd4 100644 --- a/src/components/Guards/EnsureOrganizationIsReady.js +++ b/src/components/Guards/EnsureOrganizationIsReady.js @@ -6,7 +6,6 @@ import { compose } from 'utils'; import withAuthentication from 'containers/Authentication/withAuthentication'; import withOrganization from 'containers/Organization/withOrganization'; - function EnsureOrganizationIsReady({ // #ownProps children, @@ -15,10 +14,10 @@ function EnsureOrganizationIsReady({ // #withOrganizationByOrgId isOrganizationReady, }) { - return (isOrganizationReady) ? children : ( - + return isOrganizationReady ? ( + children + ) : ( + ); } @@ -28,4 +27,4 @@ export default compose( organizationId: props.currentOrganizationId, })), withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })), -)(EnsureOrganizationIsReady); \ No newline at end of file +)(EnsureOrganizationIsReady); diff --git a/src/containers/Authentication/AuthenticationBoot.js b/src/containers/Authentication/AuthenticationBoot.js deleted file mode 100644 index 4ac6c9899..000000000 --- a/src/containers/Authentication/AuthenticationBoot.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import * as R from 'ramda'; - -import withDashboardActions from '../../containers/Dashboard/withDashboardActions'; - -function AuthenticationBootJSX({ setAppIsLoading }) { - React.useEffect(() => { - setAppIsLoading(false); - }, [setAppIsLoading]); - - return null; -} -export const AuthenticationBoot = R.compose(withDashboardActions)( - AuthenticationBootJSX, -); diff --git a/src/containers/Dashboard/withDashboard.js b/src/containers/Dashboard/withDashboard.js index d4303c6dc..d3e885adc 100644 --- a/src/containers/Dashboard/withDashboard.js +++ b/src/containers/Dashboard/withDashboard.js @@ -11,8 +11,8 @@ export default (mapState) => { sidebarExpended: state.dashboard.sidebarExpended, preferencesPageTitle: state.dashboard.preferencesPageTitle, dashboardBackLink: state.dashboard.backLink, - appIsLoading: state.dashboard.appIsLoading, - appIntlIsLoading: state.dashboard.appIntlIsLoading + splashScreenLoading: state.dashboard.splashScreenLoading > 0, + splashScreenCompleted: state.dashboard.splashScreenLoading === 0, }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/src/containers/Dashboard/withDashboardActions.js b/src/containers/Dashboard/withDashboardActions.js index 8c188539e..69b00a3d4 100644 --- a/src/containers/Dashboard/withDashboardActions.js +++ b/src/containers/Dashboard/withDashboardActions.js @@ -2,9 +2,8 @@ import { connect } from 'react-redux'; import t from 'store/types'; import { toggleExpendSidebar, - appIsLoading, - appIntlIsLoading } from 'store/dashboard/dashboard.actions'; +import { splashStartLoading, splashStopLoading } from '../../store/dashboard/dashboard.actions'; const mapActionsToProps = (dispatch) => ({ changePageTitle: (pageTitle) => @@ -50,15 +49,17 @@ const mapActionsToProps = (dispatch) => ({ dispatch({ type: 'CHANGE_PREFERENCES_PAGE_TITLE', pageTitle, - }), + }), setDashboardBackLink: (backLink) => dispatch({ type: t.SET_DASHBOARD_BACK_LINK, payload: { backLink }, }), - setAppIsLoading: (isLoading) => dispatch(appIsLoading(isLoading)), - setAppIntlIsLoading: (isLoading) => dispatch(appIntlIsLoading(isLoading)), + + // Splash screen start/stop loading. + splashStartLoading: () => splashStartLoading(), + splashStopLoading: () => splashStopLoading(), }); export default connect(null, mapActionsToProps); diff --git a/src/hooks/index.js b/src/hooks/index.js index cc615a214..dbb2944e0 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import React, { useRef, useEffect, useMemo } from 'react'; import useAsync from './async'; import useAutofocus from './useAutofocus'; @@ -97,7 +98,6 @@ export function useLocalStorage(key, initialValue) { return [storedValue, setValue]; } - export function useMemorizedColumnsWidths(tableName) { const [get, save] = useLocalStorage(`${tableName}.columns_widths`, {}); @@ -105,4 +105,43 @@ export function useMemorizedColumnsWidths(tableName) { save(columnsResizing.columnWidths); }; return [get, save, handleColumnResizing]; -} \ No newline at end of file +} + +// Hook +function usePrevious(value) { + // The ref object is a generic container whose current property is mutable ... + // ... and can hold any value, similar to an instance property on a class + const ref = useRef(); + // Store current value in ref + useEffect(() => { + ref.current = value; + }, [value]); // Only re-run if value changes + // Return previous value (happens before update in useEffect above) + return ref.current; +} + +export function useWhen(condition, callback) { + React.useEffect(() => { + if (condition) { + callback(); + } + }, [condition, callback]); +} + +export function useWhenNot(condition, callback) { + return useWhen(!condition, callback); +} + +export function useWatch(state, callback, props) { + const config = { immediate: false, ...props }; + + const previosuState = usePrevious(state); + const flag = React.useRef(false); + + React.useEffect(() => { + if (!isEqual(previosuState, state) || (config.immediate && !flag.current)) { + flag.current = true; + callback(state); + } + }, [previosuState, state, config.immediate, callback]); +} diff --git a/src/hooks/state/authentication.js b/src/hooks/state/authentication.js index 6116988b7..8a0afd385 100644 --- a/src/hooks/state/authentication.js +++ b/src/hooks/state/authentication.js @@ -3,11 +3,13 @@ import { useCallback } from 'react'; import { isAuthenticated } from 'store/authentication/authentication.reducer'; import { setLogin, - setStoreReset, } from 'store/authentication/authentication.actions'; import { useQueryClient } from 'react-query'; import { removeCookie } from '../../utils'; +/** + * Removes the authentication cookies. + */ function removeAuthenticationCookies() { removeCookie('token'); removeCookie('organization_id'); diff --git a/src/hooks/state/dashboard.js b/src/hooks/state/dashboard.js index 542fe458b..04c22e37f 100644 --- a/src/hooks/state/dashboard.js +++ b/src/hooks/state/dashboard.js @@ -1,6 +1,10 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { dashboardPageTitle } from 'store/dashboard/dashboard.actions'; +import { + splashStopLoading, + splashStartLoading, + dashboardPageTitle, +} from '../../store/dashboard/dashboard.actions'; export const useDispatchAction = (action) => { const dispatch = useDispatch(); @@ -17,11 +21,12 @@ export const useDashboardPageTitle = () => { return useDispatchAction(dashboardPageTitle); }; -export const useSetAccountsTableQuery = () => { - +/** + * Splash loading screen actions. + */ +export const useSplashLoading = () => { + return [ + useDispatchAction(splashStartLoading), + useDispatchAction(splashStopLoading), + ]; }; - -export const useAccountsTableQuery = () => { - -} -