chore: revamps loading provider
This commit is contained in:
@@ -13,8 +13,8 @@ import Versions from './views/Versions';
|
||||
import Version from './views/Version';
|
||||
import { DocumentInfoProvider } from './utilities/DocumentInfo';
|
||||
import { useLocale } from './utilities/Locale';
|
||||
import { useFullscreenLoader } from './utilities/FullscreenLoaderProvider';
|
||||
import { ForceFullscreenLoader } from './elements/Loading';
|
||||
import { useLoadingOverlay } from './utilities/LoadingOverlay';
|
||||
import { FullscreenLoader } from './elements/Loading';
|
||||
|
||||
const Dashboard = lazy(() => import('./views/Dashboard'));
|
||||
const ForgotPassword = lazy(() => import('./views/ForgotPassword'));
|
||||
@@ -33,7 +33,7 @@ const Routes = () => {
|
||||
const [initialized, setInitialized] = useState(null);
|
||||
const { user, permissions, refreshCookie } = useAuth();
|
||||
const { i18n } = useTranslation();
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
const locale = useLocale();
|
||||
|
||||
const canAccessAdmin = permissions?.canAccessAdmin;
|
||||
@@ -74,12 +74,9 @@ const Routes = () => {
|
||||
}
|
||||
}, [i18n.language, routes, userCollection]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setShowLoader(isLoadingUser);
|
||||
}, [isLoadingUser, setShowLoader]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ForceFullscreenLoader />}>
|
||||
<Suspense fallback={<FullscreenLoader />}>
|
||||
<FullscreenLoader show={isLoadingUser} />
|
||||
<Route
|
||||
path={routes.admin}
|
||||
render={({ match }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import Loading from '../Loading';
|
||||
import { Loading } from '../Loading';
|
||||
import { Props } from './types';
|
||||
|
||||
const DatePicker = lazy(() => import('./DatePicker'));
|
||||
|
||||
@@ -1 +1,139 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.fullscreen-loader {
|
||||
isolation: isolate;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
pointer-events: none;
|
||||
z-index: calc(var(--z-status) + 1);
|
||||
|
||||
&.fullscreen-loader--entering {
|
||||
opacity: 1;
|
||||
animation: 500ms fade-in ease-in-out;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&.fullscreen-loader--exiting {
|
||||
opacity: 0;
|
||||
animation: 500ms fade-out ease-in-out;
|
||||
}
|
||||
|
||||
&.fullscreen-loader--withoutNav {
|
||||
left: var(--nav-width);
|
||||
width: calc(100% - var(--nav-width));
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--theme-bg);
|
||||
opacity: .85;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&__bars {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
width: 2px;
|
||||
background-color: white;
|
||||
height: 15px;
|
||||
|
||||
&:nth-child(1) {
|
||||
transform: translateY(0);
|
||||
animation: animate-bar--odd 1.25s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
transform: translateY(-2px);
|
||||
animation: animate-bar--even 1.25s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
transform: translateY(0);
|
||||
animation: animate-bar--odd 1.25s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
transform: translateY(-2px);
|
||||
animation: animate-bar--even 1.25s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
transform: translateY(0);
|
||||
animation: animate-bar--odd 1.25s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: base(.75);
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-body);
|
||||
font-size: base(.75);
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animate-bar--even {
|
||||
0% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animate-bar--odd {
|
||||
0% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFullscreenLoader } from '../../utilities/FullscreenLoaderProvider';
|
||||
import { useLoadingOverlay } from '../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
export const Loading: React.FC = () => {
|
||||
const baseClass = 'loading';
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
@@ -18,18 +18,53 @@ const Loading: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ForceFullscreenLoader: React.FC = () => {
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
const baseClass = 'fullscreen-loader';
|
||||
|
||||
type Props = {
|
||||
show?: boolean;
|
||||
loadingText?: string;
|
||||
overlayType?: string
|
||||
}
|
||||
export const FullscreenLoader: React.FC<Props> = ({ loadingText, show = true, overlayType }) => {
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
show ? `${baseClass}--entering` : `${baseClass}--exiting`,
|
||||
overlayType ? `${baseClass}--${overlayType}` : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__bars`}>
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
</div>
|
||||
|
||||
<span className={`${baseClass}__text`}>{loadingText || t('loading')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SuspenseLoader: React.FC = () => {
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
|
||||
React.useEffect(() => {
|
||||
setShowLoader(true);
|
||||
toggleLoadingOverlay({
|
||||
key: 'suspense',
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
setShowLoader(false);
|
||||
toggleLoadingOverlay({
|
||||
key: 'suspense',
|
||||
isLoading: false,
|
||||
});
|
||||
};
|
||||
}, [setShowLoader]);
|
||||
}, [toggleLoadingOverlay]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
||||
26
src/admin/components/elements/ShimmerEffect/index.scss
Normal file
26
src/admin/components/elements/ShimmerEffect/index.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.shimmer-effect {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&__shimmer {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(130deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(#1A1A1A, .5) 35%,
|
||||
rgba(#252525, 1) 50%,
|
||||
rgba(#1A1A1A, .5) 65%,
|
||||
rgba(0, 0, 0, 0) 100%);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
12
src/admin/components/elements/ShimmerEffect/index.tsx
Normal file
12
src/admin/components/elements/ShimmerEffect/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const ShimmerEffect: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="shimmer-effect">
|
||||
{children}
|
||||
<div className="shimmer-effect__shimmer" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import Loading from '../../../elements/Loading';
|
||||
import { Loading } from '../../../elements/Loading';
|
||||
import { Props } from './types';
|
||||
|
||||
const Code = lazy(() => import('./Code'));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import Loading from '../../../elements/Loading';
|
||||
import { Loading } from '../../../elements/Loading';
|
||||
import { Props } from './types';
|
||||
|
||||
const JSON = lazy(() => import('./JSON'));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import Loading from '../../../elements/Loading';
|
||||
import { Loading } from '../../../elements/Loading';
|
||||
import { Props } from './types';
|
||||
|
||||
const RichText = lazy(() => import('./RichText'));
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.fullscreenLoader {
|
||||
isolation: isolate;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
z-index: 9000;
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
&.fullscreenLoader--entering {
|
||||
opacity: 1;
|
||||
animation: fade-in ease-in-out;
|
||||
}
|
||||
|
||||
&.fullscreenLoader--exiting {
|
||||
opacity: 0;
|
||||
animation: fade-out ease-in-out;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--theme-bg);
|
||||
opacity: .85;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&__bars {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
width: 2px;
|
||||
background-color: white;
|
||||
height: 15px;
|
||||
|
||||
&:nth-child(1) {
|
||||
transform: translateY(0);
|
||||
animation: animate-bar--odd 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
transform: translateY(-2px);
|
||||
animation: animate-bar--even 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
transform: translateY(0);
|
||||
animation: animate-bar--odd 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
transform: translateY(-2px);
|
||||
animation: animate-bar--even 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
transform: translateY(0);
|
||||
animation: animate-bar--odd 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: base(.75);
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-body);
|
||||
font-size: base(.75);
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animate-bar--even {
|
||||
0% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animate-bar--odd {
|
||||
0% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import React, {
|
||||
createContext, useContext, useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDelay } from '../../../hooks/useDelay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export type FullscreenLoaderContext = {
|
||||
showLoader: boolean
|
||||
setShowLoader: (show: boolean) => void
|
||||
}
|
||||
|
||||
const initialContext: FullscreenLoaderContext = {
|
||||
showLoader: false,
|
||||
setShowLoader: undefined,
|
||||
};
|
||||
|
||||
const Context = createContext(initialContext);
|
||||
|
||||
const delayBeforeRender = 500;
|
||||
const animationDuration = 500;
|
||||
const minShowTime = (animationDuration * 2) + 750;
|
||||
|
||||
const baseClass = 'fullscreenLoader';
|
||||
|
||||
export const FullscreenLoaderProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
const [isShowing, setIsShowing] = React.useState(false);
|
||||
const [hasDelayed, setHasDelayed] = useDelay(delayBeforeRender, showLoader);
|
||||
const [isHiding, setIsHiding] = React.useState(false);
|
||||
const [displayedAt, setDisplayedAt] = React.useState<number>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasDelayed && showLoader && !isShowing) {
|
||||
setDisplayedAt(Date.now());
|
||||
setIsShowing(true);
|
||||
}
|
||||
}, [showLoader, isShowing, displayedAt, hasDelayed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let closeTimeout: NodeJS.Timeout;
|
||||
const shouldHide = isShowing && !showLoader;
|
||||
|
||||
const hide = () => {
|
||||
const timeDisplayed = Date.now() - displayedAt;
|
||||
const remainingShowTime = minShowTime - timeDisplayed;
|
||||
|
||||
if (remainingShowTime > 0 && timeDisplayed < minShowTime) {
|
||||
closeTimeout = setTimeout(hide, remainingShowTime);
|
||||
} else {
|
||||
setIsHiding(true);
|
||||
closeTimeout = setTimeout(() => {
|
||||
setIsShowing(false);
|
||||
setHasDelayed(false);
|
||||
setIsHiding(false);
|
||||
}, animationDuration);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (shouldHide) {
|
||||
hide();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (closeTimeout) clearTimeout(closeTimeout);
|
||||
};
|
||||
}, [showLoader, isShowing, displayedAt, setHasDelayed]);
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
setShowLoader,
|
||||
showLoader,
|
||||
}}
|
||||
>
|
||||
{hasDelayed && isShowing && (
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
isHiding ? `${baseClass}--exiting` : `${baseClass}--entering`,
|
||||
].filter(Boolean).join(' ')}
|
||||
style={{
|
||||
animationDuration: `${animationDuration}ms`,
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__bars`}>
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
<div className={`${baseClass}__bar`} />
|
||||
</div>
|
||||
|
||||
<span className={`${baseClass}__text`}>{t('loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFullscreenLoader = (): FullscreenLoaderContext => useContext(Context);
|
||||
|
||||
export default Context;
|
||||
72
src/admin/components/utilities/LoadingOverlay/index.tsx
Normal file
72
src/admin/components/utilities/LoadingOverlay/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, {
|
||||
createContext, useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTimeoutRender } from '../../../hooks/useSuspendedRender';
|
||||
import { reducer, defaultLoadingOverlayState } from './reducer';
|
||||
import { FullscreenLoader } from '../../elements/Loading';
|
||||
import type { LoadingOverlayContext, ToggleLoadingOverlay } from './types';
|
||||
|
||||
const initialContext: LoadingOverlayContext = {
|
||||
toggleLoadingOverlay: undefined,
|
||||
setLoadingOverlayText: undefined,
|
||||
};
|
||||
|
||||
const Context = createContext(initialContext);
|
||||
|
||||
export const LoadingOverlayProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
const [loadingOverlayText, setLoadingOverlayText] = useState<string>(t('loading'));
|
||||
const [overlays, dispatchOverlay] = React.useReducer(reducer, defaultLoadingOverlayState);
|
||||
const { isMounted, isUnmounting, triggerRenderTimeout: suspendDisplay } = useTimeoutRender({ show: overlays.isLoading });
|
||||
|
||||
const toggleLoadingOverlay = React.useCallback<ToggleLoadingOverlay>(({ type, key, isLoading }) => {
|
||||
if (isLoading) {
|
||||
suspendDisplay();
|
||||
dispatchOverlay({
|
||||
type: 'add',
|
||||
payload: {
|
||||
type,
|
||||
key,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
dispatchOverlay({
|
||||
type: 'remove',
|
||||
payload: {
|
||||
key,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [suspendDisplay]);
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
setLoadingOverlayText,
|
||||
toggleLoadingOverlay,
|
||||
}}
|
||||
>
|
||||
{isMounted && (
|
||||
<FullscreenLoader
|
||||
show={!isUnmounting}
|
||||
loadingText={loadingOverlayText}
|
||||
overlayType={overlays.overlayType}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLoadingOverlay = (): LoadingOverlayContext => {
|
||||
const contextHook = React.useContext(Context);
|
||||
if (contextHook === undefined) {
|
||||
throw new Error('useLoadingOverlay must be used within a LoadingOverlayProvider');
|
||||
}
|
||||
|
||||
return contextHook;
|
||||
};
|
||||
|
||||
export default Context;
|
||||
26
src/admin/components/utilities/LoadingOverlay/reducer.ts
Normal file
26
src/admin/components/utilities/LoadingOverlay/reducer.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Action, State } from './types';
|
||||
|
||||
export const defaultLoadingOverlayState = {
|
||||
isLoading: false,
|
||||
overlayType: null,
|
||||
loaders: [],
|
||||
};
|
||||
|
||||
// react reducer return type
|
||||
export const reducer = (state: State, action: Action) => {
|
||||
const loadersCopy = [...state.loaders];
|
||||
const { type = 'fullscreen', key = 'user' } = action.payload;
|
||||
|
||||
if (action.type === 'add') {
|
||||
loadersCopy.push({ type, key });
|
||||
} else if (action.type === 'remove') {
|
||||
const index = loadersCopy.findIndex((item) => item.key === key && item.type === type);
|
||||
loadersCopy.splice(index, 1);
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: loadersCopy.length > 0,
|
||||
overlayType: loadersCopy.length > 0 ? loadersCopy[loadersCopy.length - 1].type : state?.overlayType,
|
||||
loaders: loadersCopy,
|
||||
};
|
||||
};
|
||||
47
src/admin/components/utilities/LoadingOverlay/types.ts
Normal file
47
src/admin/components/utilities/LoadingOverlay/types.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export type LoadingOverlayTypes = 'fullscreen' | 'withoutNav'
|
||||
|
||||
type ToggleLoadingOverlayOptions = {
|
||||
type?: LoadingOverlayTypes
|
||||
key: string
|
||||
isLoading?: boolean
|
||||
}
|
||||
export type ToggleLoadingOverlay = (options: ToggleLoadingOverlayOptions) => void
|
||||
|
||||
type Add = {
|
||||
type: 'add'
|
||||
payload: {
|
||||
type: LoadingOverlayTypes
|
||||
key: string
|
||||
loadingText?: string
|
||||
}
|
||||
}
|
||||
type Remove = {
|
||||
type: 'remove'
|
||||
payload: {
|
||||
key: string
|
||||
type?: never
|
||||
loadingText?: never
|
||||
}
|
||||
}
|
||||
export type Action = Add | Remove
|
||||
export type State = {
|
||||
isLoading: boolean
|
||||
overlayType: null | string
|
||||
loaders: {
|
||||
type: LoadingOverlayTypes
|
||||
key: string
|
||||
}[]
|
||||
}
|
||||
export type ReducerState = {
|
||||
isLoading: boolean
|
||||
overlayType: null | LoadingOverlayTypes
|
||||
loaders: {
|
||||
type: LoadingOverlayTypes
|
||||
key: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export type LoadingOverlayContext = {
|
||||
toggleLoadingOverlay: ToggleLoadingOverlay
|
||||
setLoadingOverlayText?: (text: string) => void
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { Gutter } from '../../elements/Gutter';
|
||||
import ReactSelect from '../../elements/ReactSelect';
|
||||
import Label from '../../forms/Label';
|
||||
import type { Translation } from '../../../../translations/type';
|
||||
import { useLoadingOverlay } from '../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -52,11 +53,20 @@ const DefaultAccount: React.FC<Props> = (props) => {
|
||||
|
||||
const { admin: { dateFormat }, routes: { admin } } = useConfig();
|
||||
const { t, i18n } = useTranslation('authentication');
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
|
||||
const languageOptions = Object.entries(i18n.options.resources).map(([language, resource]) => (
|
||||
{ label: (resource as Translation).general.thisLanguage, value: language }
|
||||
));
|
||||
|
||||
React.useEffect(() => {
|
||||
toggleLoadingOverlay({
|
||||
key: 'account',
|
||||
type: 'withoutNav',
|
||||
isLoading,
|
||||
});
|
||||
}, [isLoading, toggleLoadingOverlay]);
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
@@ -12,7 +12,6 @@ import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
import { Fields } from '../../forms/Form/types';
|
||||
import { usePreferences } from '../../utilities/Preferences';
|
||||
import { useFullscreenLoader } from '../../utilities/FullscreenLoaderProvider';
|
||||
|
||||
const AccountView: React.FC = () => {
|
||||
const { state: locationState } = useLocation<{ data: unknown }>();
|
||||
@@ -22,7 +21,6 @@ const AccountView: React.FC = () => {
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
const { id, preferencesKey, docPermissions, getDocPermissions, slug } = useDocumentInfo();
|
||||
const { getPreference } = usePreferences();
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
|
||||
const {
|
||||
serverURL,
|
||||
@@ -40,7 +38,6 @@ const AccountView: React.FC = () => {
|
||||
} = useConfig();
|
||||
const { t } = useTranslation('authentication');
|
||||
|
||||
const isLoading = !initialState || !docPermissions;
|
||||
const collection = collections.find((coll) => coll.slug === slug);
|
||||
|
||||
const { fields } = collection;
|
||||
@@ -76,8 +73,6 @@ const AccountView: React.FC = () => {
|
||||
}, [setStepNav, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingData) return;
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema({
|
||||
fieldSchema: fields,
|
||||
@@ -93,11 +88,9 @@ const AccountView: React.FC = () => {
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [dataToRender, fields, id, user, locale, preferencesKey, getPreference, t, isLoadingData]);
|
||||
}, [dataToRender, fields, id, user, locale, preferencesKey, getPreference, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoader(isLoading);
|
||||
}, [isLoading, setShowLoader]);
|
||||
const isLoading = !initialState || !docPermissions || isLoadingData;
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
|
||||
@@ -22,6 +22,7 @@ import Autosave from '../../elements/Autosave';
|
||||
import { OperationContext } from '../../utilities/OperationProvider';
|
||||
import { Gutter } from '../../elements/Gutter';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { useLoadingOverlay } from '../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -31,6 +32,7 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
||||
const { admin: { dateFormat } } = useConfig();
|
||||
const { publishedDoc } = useDocumentInfo();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
|
||||
const {
|
||||
global, data, onSave, permissions, action, apiURL, initialState, isLoading, updatedAt,
|
||||
@@ -49,6 +51,14 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
||||
|
||||
const hasSavePermission = permissions?.update?.permission;
|
||||
|
||||
React.useEffect(() => {
|
||||
toggleLoadingOverlay({
|
||||
key: 'global-edit',
|
||||
type: 'withoutNav',
|
||||
isLoading,
|
||||
});
|
||||
}, [toggleLoadingOverlay, isLoading]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{!isLoading && (
|
||||
|
||||
@@ -13,7 +13,6 @@ import { IndexProps } from './types';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
import { Fields } from '../../forms/Form/types';
|
||||
import { usePreferences } from '../../utilities/Preferences';
|
||||
import { useFullscreenLoader } from '../../utilities/FullscreenLoaderProvider';
|
||||
|
||||
const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
const { state: locationState } = useLocation<{ data?: Record<string, unknown> }>();
|
||||
@@ -25,9 +24,6 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
const { getVersions, preferencesKey, docPermissions, getDocPermissions } = useDocumentInfo();
|
||||
const { getPreference } = usePreferences();
|
||||
const { t } = useTranslation();
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
|
||||
const isLoading = !initialState || !docPermissions;
|
||||
|
||||
const {
|
||||
serverURL,
|
||||
@@ -75,8 +71,6 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
}, [setStepNav, label]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingData) return;
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: 'update', locale, t });
|
||||
await getPreference(preferencesKey);
|
||||
@@ -84,11 +78,9 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [dataToRender, fields, user, locale, getPreference, preferencesKey, t, isLoadingData]);
|
||||
}, [dataToRender, fields, user, locale, getPreference, preferencesKey, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoader(isLoading);
|
||||
}, [isLoading, setShowLoader]);
|
||||
const isLoading = !initialState || !docPermissions || isLoadingData;
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
|
||||
@@ -11,7 +11,7 @@ import Password from '../../forms/field-types/Password';
|
||||
import FormSubmit from '../../forms/Submit';
|
||||
import Button from '../../elements/Button';
|
||||
import Meta from '../../utilities/Meta';
|
||||
import { useFullscreenLoader } from '../../utilities/FullscreenLoaderProvider';
|
||||
import { useLoadingOverlay } from '../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -21,7 +21,7 @@ const Login: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation('authentication');
|
||||
const { user, setToken } = useAuth();
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
const config = useConfig();
|
||||
const {
|
||||
admin: {
|
||||
@@ -43,8 +43,11 @@ const Login: React.FC = () => {
|
||||
const collection = collections.find(({ slug }) => slug === userSlug);
|
||||
|
||||
const onSuccess = (data) => {
|
||||
setShowLoader(false);
|
||||
if (data.token) {
|
||||
toggleLoadingOverlay({
|
||||
key: 'login',
|
||||
isLoading: false,
|
||||
});
|
||||
setToken(data.token);
|
||||
history.push(admin);
|
||||
}
|
||||
@@ -97,7 +100,12 @@ const Login: React.FC = () => {
|
||||
disableSuccessStatus
|
||||
waitForAutocomplete
|
||||
disableNativeFormSubmission={false}
|
||||
onSubmit={() => setShowLoader(true)}
|
||||
onSubmit={() => {
|
||||
toggleLoadingOverlay({
|
||||
key: 'login',
|
||||
isLoading: true,
|
||||
});
|
||||
}}
|
||||
onSuccess={onSuccess}
|
||||
method="post"
|
||||
action={`${serverURL}${api}/${userSlug}/login`}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Field, FieldAffectingData, fieldAffectsData } from '../../../../fields/
|
||||
import { FieldPermissions } from '../../../../auth';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { Gutter } from '../../elements/Gutter';
|
||||
import { useFullscreenLoader } from '../../utilities/FullscreenLoaderProvider';
|
||||
import { useLoadingOverlay } from '../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -37,7 +37,7 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
|
||||
const { permissions } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
|
||||
let originalDocFetchURL: string;
|
||||
let versionFetchURL: string;
|
||||
@@ -141,8 +141,11 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
|
||||
}, [setStepNav, collection, global, dateFormat, doc, mostRecentDoc, admin, id, locale, t, i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoader(isLoadingData);
|
||||
}, [isLoadingData, setShowLoader]);
|
||||
toggleLoadingOverlay({
|
||||
key: 'version',
|
||||
isLoading: isLoadingData,
|
||||
});
|
||||
}, [isLoadingData, toggleLoadingOverlay]);
|
||||
|
||||
let metaTitle: string;
|
||||
let metaDesc: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import Loading from '../../elements/Loading';
|
||||
import { Loading } from '../../elements/Loading';
|
||||
import { Props } from './types';
|
||||
|
||||
const VersionView = lazy(() => import('./Version'));
|
||||
|
||||
@@ -21,7 +21,7 @@ import { SanitizedGlobalConfig } from '../../../../globals/config/types';
|
||||
import { shouldIncrementVersionCount } from '../../../../versions/shouldIncrementVersionCount';
|
||||
import { Gutter } from '../../elements/Gutter';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { useFullscreenLoader } from '../../utilities/FullscreenLoaderProvider';
|
||||
import { useLoadingOverlay } from '../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -35,7 +35,7 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
const [tableColumns] = useState(() => getColumns(collection, global, t));
|
||||
const [fetchURL, setFetchURL] = useState('');
|
||||
const { page, sort, limit } = useSearchParams();
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
|
||||
let docURL: string;
|
||||
let entityLabel: string;
|
||||
@@ -148,8 +148,11 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
}, [setParams, page, sort, limit, serverURL, api, id, global, collection]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoader(isLoadingData);
|
||||
}, [isLoadingData, setShowLoader]);
|
||||
toggleLoadingOverlay({
|
||||
key: 'versions',
|
||||
isLoading: isLoadingData,
|
||||
});
|
||||
}, [isLoadingData, toggleLoadingOverlay]);
|
||||
|
||||
let useIDLabel = doc[useAsTitle] === doc?.id;
|
||||
let heading: string;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { OperationContext } from '../../../utilities/OperationProvider';
|
||||
import { Gutter } from '../../../elements/Gutter';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { SetStepNav } from './SetStepNav';
|
||||
import { useLoadingOverlay } from '../../../utilities/LoadingOverlay';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -57,6 +58,8 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
updatedAt,
|
||||
} = props;
|
||||
|
||||
const { toggleLoadingOverlay } = useLoadingOverlay();
|
||||
|
||||
const {
|
||||
slug,
|
||||
fields,
|
||||
@@ -79,6 +82,14 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
|
||||
const operation = isEditing ? 'update' : 'create';
|
||||
|
||||
React.useEffect(() => {
|
||||
toggleLoadingOverlay({
|
||||
key: 'collection-edit',
|
||||
type: 'withoutNav',
|
||||
isLoading,
|
||||
});
|
||||
}, [isLoading, toggleLoadingOverlay]);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{!isLoading && (
|
||||
@@ -95,7 +106,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
<SetStepNav
|
||||
collection={collection}
|
||||
isEditing={isEditing}
|
||||
id={data.id}
|
||||
id={data?.id}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__main`}>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Fields } from '../../../forms/Form/types';
|
||||
import { usePreferences } from '../../../utilities/Preferences';
|
||||
import { EditDepthContext } from '../../../utilities/EditDepth';
|
||||
import { CollectionPermission } from '../../../../../auth';
|
||||
import { useFullscreenLoader } from '../../../utilities/FullscreenLoaderProvider';
|
||||
|
||||
const EditView: React.FC<IndexProps> = (props) => {
|
||||
const { collection: incomingCollection, isEditing } = props;
|
||||
@@ -47,9 +46,6 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
const { getVersions, preferencesKey, getDocPermissions, docPermissions } = useDocumentInfo();
|
||||
const { getPreference } = usePreferences();
|
||||
const { t } = useTranslation('general');
|
||||
const { setShowLoader } = useFullscreenLoader();
|
||||
|
||||
const isLoading = !initialState || !docPermissions;
|
||||
|
||||
const onSave = useCallback(async (json: any) => {
|
||||
getVersions();
|
||||
@@ -71,8 +67,6 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
const dataToRender = (locationState as Record<string, unknown>)?.data || data;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingData) return;
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
setUpdatedAt(dataToRender?.updatedAt);
|
||||
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: isEditing ? 'update' : 'create', id, locale, t });
|
||||
@@ -89,10 +83,6 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
}
|
||||
}, [history, redirect]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoader(isLoading);
|
||||
}, [isLoading, setShowLoader]);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Redirect to={`${admin}/not-found`} />
|
||||
@@ -102,6 +92,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
const apiURL = `${serverURL}${api}/${slug}/${id}${collection.versions.drafts ? '?draft=true' : ''}`;
|
||||
const action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`;
|
||||
const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission);
|
||||
const isLoading = !initialState || !docPermissions || isLoadingData;
|
||||
|
||||
return (
|
||||
<EditDepthContext.Provider value={1}>
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
type Result = [boolean, React.Dispatch<React.SetStateAction<boolean>>];
|
||||
export const useDelay = (delay: number, run = true): Result => {
|
||||
type Result = [boolean, () => void];
|
||||
export const useDelay = (delay: number): Result => {
|
||||
const [hasDelayed, setHasDelayed] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
let delayTimeout: NodeJS.Timeout;
|
||||
if (run) {
|
||||
delayTimeout = setTimeout(() => {
|
||||
setHasDelayed(true);
|
||||
}, delay);
|
||||
}
|
||||
const triggerDelay = React.useCallback(() => {
|
||||
setHasDelayed(false);
|
||||
const delayTimeout = setTimeout(() => {
|
||||
setHasDelayed(true);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(delayTimeout);
|
||||
};
|
||||
}, [delay, run]);
|
||||
}, [delay]);
|
||||
|
||||
return [hasDelayed, setHasDelayed];
|
||||
return [hasDelayed, triggerDelay];
|
||||
};
|
||||
|
||||
61
src/admin/hooks/useSuspendedRender.ts
Normal file
61
src/admin/hooks/useSuspendedRender.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as React from 'react';
|
||||
import { useDelay } from './useDelay';
|
||||
|
||||
type TimeoutRenderProps = {
|
||||
/** `true` starts the mount process.
|
||||
* `false` starts the unmount process.
|
||||
* */
|
||||
show: boolean;
|
||||
/** Time in ms to wait before "mounting" the component. */
|
||||
timeout?: number;
|
||||
/** Time in ms the `appear` phase of the animation. (enter transition time + appear time) */
|
||||
appearTime?: number;
|
||||
/** Time in ms the `exit` phase of the animation. */
|
||||
exitTimeout?: number;
|
||||
/** Time in ms to wait before actually unmounting the component. */
|
||||
unmountTimeout?: number;
|
||||
};
|
||||
type useTimeoutRender = (props: TimeoutRenderProps) => {
|
||||
/** `true` if the component has mounted after the timeout. */
|
||||
isMounted: boolean;
|
||||
/** `true` if the component is unmounting. */
|
||||
isUnmounting: boolean;
|
||||
/** Call this function to trigger the timeout delay before rendering. */
|
||||
triggerRenderTimeout: () => void;
|
||||
};
|
||||
export const useTimeoutRender: useTimeoutRender = ({ show, timeout = 500, appearTime = 1250, exitTimeout = 500 }) => {
|
||||
const [hasDelayed, triggerDelay] = useDelay(timeout);
|
||||
const [isMounted, setIsMounted] = React.useState(false);
|
||||
const [isUnmounting, setIsUnmounting] = React.useState(false);
|
||||
const onMountTimestampRef = React.useRef(0);
|
||||
const unmountTimeoutRef: React.MutableRefObject<NodeJS.Timeout | undefined> = React.useRef();
|
||||
|
||||
const unmount = React.useCallback(() => {
|
||||
setIsUnmounting(true);
|
||||
unmountTimeoutRef.current = setTimeout(() => {
|
||||
setIsMounted(false);
|
||||
setIsUnmounting(false);
|
||||
}, exitTimeout);
|
||||
}, [setIsUnmounting, exitTimeout]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const shouldMount = hasDelayed && !isMounted && show;
|
||||
const shouldUnmount = isMounted && !show;
|
||||
|
||||
if (shouldMount) {
|
||||
onMountTimestampRef.current = Date.now();
|
||||
setIsMounted(true);
|
||||
} else if (shouldUnmount) {
|
||||
const totalTimeMounted = Date.now() - onMountTimestampRef.current;
|
||||
const timeoutExtension = (appearTime + exitTimeout) - totalTimeMounted;
|
||||
clearTimeout(unmountTimeoutRef.current);
|
||||
unmountTimeoutRef.current = setTimeout(unmount, Math.max(0, timeoutExtension));
|
||||
}
|
||||
}, [isMounted, show, unmount, appearTime, exitTimeout, hasDelayed]);
|
||||
|
||||
return {
|
||||
isMounted,
|
||||
isUnmounting,
|
||||
triggerRenderTimeout: triggerDelay,
|
||||
};
|
||||
};
|
||||
@@ -18,7 +18,7 @@ import Routes from './components/Routes';
|
||||
import { StepNavProvider } from './components/elements/StepNav';
|
||||
import { ThemeProvider } from './components/utilities/Theme';
|
||||
import { I18n } from './components/utilities/I18n';
|
||||
import { FullscreenLoaderProvider } from './components/utilities/FullscreenLoaderProvider';
|
||||
import { LoadingOverlayProvider } from './components/utilities/LoadingOverlay';
|
||||
|
||||
import './scss/app.scss';
|
||||
|
||||
@@ -46,11 +46,11 @@ const Index = () => (
|
||||
<SearchParamsProvider>
|
||||
<LocaleProvider>
|
||||
<StepNavProvider>
|
||||
<FullscreenLoaderProvider>
|
||||
<LoadingOverlayProvider>
|
||||
<CustomProvider>
|
||||
<Routes />
|
||||
</CustomProvider>
|
||||
</FullscreenLoaderProvider>
|
||||
</LoadingOverlayProvider>
|
||||
</StepNavProvider>
|
||||
</LocaleProvider>
|
||||
</SearchParamsProvider>
|
||||
|
||||
Reference in New Issue
Block a user