chore: revamps loading provider

This commit is contained in:
Jarrod Flesch
2023-01-16 08:26:35 -05:00
parent bb565f9450
commit 5a93683a26
27 changed files with 511 additions and 319 deletions

View File

@@ -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 }) => {

View File

@@ -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'));

View File

@@ -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;
}
}

View File

@@ -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;

View 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%);
}
}

View 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>
);
};

View File

@@ -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'));

View File

@@ -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'));

View File

@@ -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'));

View File

@@ -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;
}
}

View File

@@ -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;

View 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;

View 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,
};
};

View 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
}

View File

@@ -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(' ');

View File

@@ -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

View File

@@ -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 && (

View File

@@ -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

View File

@@ -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`}

View File

@@ -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;

View File

@@ -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'));

View File

@@ -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;

View File

@@ -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`}>

View File

@@ -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}>

View File

@@ -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];
};

View 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,
};
};

View File

@@ -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>