From 50f74fbeda3be3d1b09ced672e3a6364a2e18aa2 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Wed, 18 Jan 2023 17:04:31 -0500 Subject: [PATCH] chore: refines shimmer, adds to list view and CodeEditor element --- .../elements/CodeEditor/CodeEditor.tsx | 45 +++++++ .../components/elements/CodeEditor/index.tsx | 50 ++------ .../components/elements/Loading/index.scss | 4 +- .../components/elements/Loading/index.tsx | 3 +- .../elements/ShimmerEffect/index.scss | 26 ++-- .../elements/ShimmerEffect/index.tsx | 59 ++++++++- .../forms/field-types/Code/Code.tsx | 97 --------------- .../forms/field-types/Code/index.tsx | 103 ++++++++++++++-- .../forms/field-types/JSON/JSON.tsx | 110 ----------------- .../forms/field-types/JSON/index.tsx | 115 ++++++++++++++++-- .../utilities/LoadingOverlay/index.tsx | 13 +- .../utilities/LoadingOverlay/reducer.ts | 3 +- .../views/collections/List/Default.tsx | 13 ++ .../views/collections/List/index.scss | 42 +++++++ src/admin/hooks/useDelayedRender.ts | 4 +- 15 files changed, 398 insertions(+), 289 deletions(-) create mode 100644 src/admin/components/elements/CodeEditor/CodeEditor.tsx delete mode 100644 src/admin/components/forms/field-types/Code/Code.tsx delete mode 100644 src/admin/components/forms/field-types/JSON/JSON.tsx diff --git a/src/admin/components/elements/CodeEditor/CodeEditor.tsx b/src/admin/components/elements/CodeEditor/CodeEditor.tsx new file mode 100644 index 0000000000..9e69ce3cdb --- /dev/null +++ b/src/admin/components/elements/CodeEditor/CodeEditor.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Editor from '@monaco-editor/react'; +import type { Props } from './types'; +import { useTheme } from '../../utilities/Theme'; + +import './index.scss'; +import { ShimmerEffect } from '../ShimmerEffect'; + +const baseClass = 'code-editor'; + +const CodeEditor: React.FC = (props) => { + const { readOnly, className, options, ...rest } = props; + + const { theme } = useTheme(); + + const classes = [ + baseClass, + className, + rest?.defaultLanguage ? `language--${rest.defaultLanguage}` : '', + ].filter(Boolean).join(' '); + + return ( + } + options={ + { + detectIndentation: true, + minimap: { + enabled: false, + }, + readOnly: Boolean(readOnly), + scrollBeyondLastLine: false, + tabSize: 2, + wordWrap: 'on', + ...options, + } + } + {...rest} + /> + ); +}; + +export default CodeEditor; diff --git a/src/admin/components/elements/CodeEditor/index.tsx b/src/admin/components/elements/CodeEditor/index.tsx index ebef71d6f4..140317f181 100644 --- a/src/admin/components/elements/CodeEditor/index.tsx +++ b/src/admin/components/elements/CodeEditor/index.tsx @@ -1,44 +1,18 @@ -import React from 'react'; -import Editor from '@monaco-editor/react'; -import type { Props } from './types'; -import { useTheme } from '../../utilities/Theme'; +import React, { Suspense, lazy } from 'react'; +import { ShimmerEffect } from '../ShimmerEffect'; +import { Props } from './types'; -import './index.scss'; +const LazyEditor = lazy(() => import('./CodeEditor')); -const baseClass = 'code-editor'; - -const CodeEditor: React.FC = (props) => { - const { readOnly, className, options, ...rest } = props; - - const { theme } = useTheme(); - - const classes = [ - baseClass, - className, - rest?.defaultLanguage ? `language--${rest.defaultLanguage}` : '', - ].filter(Boolean).join(' '); +export const CodeEditor: React.FC = (props) => { + const { height = '35vh' } = props; return ( - + }> + + ); }; - -export default CodeEditor; diff --git a/src/admin/components/elements/Loading/index.scss b/src/admin/components/elements/Loading/index.scss index 3e73df56ce..fa609aa437 100644 --- a/src/admin/components/elements/Loading/index.scss +++ b/src/admin/components/elements/Loading/index.scss @@ -40,7 +40,7 @@ left: 0; width: 100%; height: 100%; - background-color: var(--theme-bg); + background-color: var(--theme-elevation-50); opacity: .85; z-index: -1; } @@ -54,7 +54,7 @@ &__bar { width: 2px; - background-color: white; + background-color: var(--theme-elevation-1000); height: 15px; &:nth-child(1) { diff --git a/src/admin/components/elements/Loading/index.tsx b/src/admin/components/elements/Loading/index.tsx index 1d25c98890..99f72be26c 100644 --- a/src/admin/components/elements/Loading/index.tsx +++ b/src/admin/components/elements/Loading/index.tsx @@ -19,7 +19,6 @@ export const Loading: React.FC = () => { ); }; -const baseClass = 'fullscreen-loader'; type Props = { show?: boolean; @@ -27,6 +26,7 @@ type Props = { overlayType?: string } export const FullscreenLoader: React.FC = ({ loadingText, show = true, overlayType }) => { + const baseClass = 'fullscreen-loader'; const { t } = useTranslation('general'); return ( @@ -50,6 +50,7 @@ export const FullscreenLoader: React.FC = ({ loadingText, show = true, ov ); }; + type UseLoadingOverlayToggleT = { show: boolean; name: string; diff --git a/src/admin/components/elements/ShimmerEffect/index.scss b/src/admin/components/elements/ShimmerEffect/index.scss index cf46164976..fd3cf7d32d 100644 --- a/src/admin/components/elements/ShimmerEffect/index.scss +++ b/src/admin/components/elements/ShimmerEffect/index.scss @@ -1,26 +1,28 @@ .shimmer-effect { position: relative; overflow: hidden; + background-color: var(--theme-elevation-50); - &__shimmer { + &__shine { position: absolute; - height: 100%; + scale: 2; width: 100%; - top: 0; - left: 0; + height: 100%; 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; + animation: shimmer 2.5s infinite; + opacity: .75; + background: linear-gradient(100deg, + var(--theme-elevation-50) 0%, + var(--theme-elevation-50) 15%, + var(--theme-elevation-100) 45%, + var(--theme-elevation-100) 55%, + var(--theme-elevation-50) 85%, + var(--theme-elevation-50) 100%) } } @keyframes shimmer { 100% { - transform: translateX(100%); + transform: translate3d(100%, 0, 0); } } diff --git a/src/admin/components/elements/ShimmerEffect/index.tsx b/src/admin/components/elements/ShimmerEffect/index.tsx index 39aac4de25..55fda480d0 100644 --- a/src/admin/components/elements/ShimmerEffect/index.tsx +++ b/src/admin/components/elements/ShimmerEffect/index.tsx @@ -1,12 +1,63 @@ import * as React from 'react'; +import { useDelay } from '../../../hooks/useDelay'; import './index.scss'; -export const ShimmerEffect: React.FC<{ children?: React.ReactNode }> = ({ children }) => { +type ShimmerEffectT = { + shimmerDelay?: number | string; + height?: number | string; + width?: number | string; +} +export const ShimmerEffect: React.FC = ({ shimmerDelay = 0, height = '60px', width = '100%' }) => { return ( -
- {children} -
+
+
+
+ ); +}; + +type StaggeredShimmersT = { + count: number; + shimmerDelay?: number | string; + height?: number | string; + width?: number | string; + className?: string; + shimmerItemClassName?: string; + renderDelay?: number; +} +export const StaggeredShimmers: React.FC = ({ count, className, shimmerItemClassName, width, height, shimmerDelay = 25, renderDelay = 500 }) => { + const shimmerDelayToPass = typeof shimmerDelay === 'number' ? `${shimmerDelay}ms` : shimmerDelay; + const { hasDelayed } = useDelay(renderDelay, true); + + if (!hasDelayed) return null; + + return ( +
+ {[...Array(count)].map((_, i) => ( +
+ +
+ ))}
); }; diff --git a/src/admin/components/forms/field-types/Code/Code.tsx b/src/admin/components/forms/field-types/Code/Code.tsx deleted file mode 100644 index c557698be2..0000000000 --- a/src/admin/components/forms/field-types/Code/Code.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useCallback } from 'react'; - -import { code } from '../../../../../fields/validations'; -import Error from '../../Error'; -import FieldDescription from '../../FieldDescription'; -import Label from '../../Label'; -import { Props } from './types'; -import useField from '../../useField'; -import withCondition from '../../withCondition'; -import CodeEditor from '../../../elements/CodeEditor'; - -import './index.scss'; - -const prismToMonacoLanguageMap = { - js: 'javascript', - ts: 'typescript', -}; - -const baseClass = 'code-field'; - -const Code: React.FC = (props) => { - const { - path: pathFromProps, - name, - required, - validate = code, - admin: { - readOnly, - style, - className, - width, - language, - description, - condition, - editorOptions, - } = {}, - label, - } = props; - - const path = pathFromProps || name; - - const memoizedValidate = useCallback((value, options) => { - return validate(value, { ...options, required }); - }, [validate, required]); - - const { - value, - showError, - setValue, - errorMessage, - } = useField({ - path, - validate: memoizedValidate, - condition, - }); - - const classes = [ - baseClass, - 'field-type', - className, - showError && 'error', - readOnly && 'read-only', - ].filter(Boolean).join(' '); - - return ( -
- -
- ); -}; - -export default withCondition(Code); diff --git a/src/admin/components/forms/field-types/Code/index.tsx b/src/admin/components/forms/field-types/Code/index.tsx index 823fab6a79..1084cf7293 100644 --- a/src/admin/components/forms/field-types/Code/index.tsx +++ b/src/admin/components/forms/field-types/Code/index.tsx @@ -1,13 +1,98 @@ -import React, { Suspense, lazy } from 'react'; -import { Loading } from '../../../elements/Loading'; +import React, { useCallback } from 'react'; + +import { code } from '../../../../../fields/validations'; +import Error from '../../Error'; +import FieldDescription from '../../FieldDescription'; +import Label from '../../Label'; import { Props } from './types'; +import useField from '../../useField'; +import withCondition from '../../withCondition'; +import { CodeEditor } from '../../../elements/CodeEditor'; +import { ShimmerEffect } from '../../../elements/ShimmerEffect'; -const Code = lazy(() => import('./Code')); +import './index.scss'; -const CodeField: React.FC = (props) => ( - }> - - -); +const prismToMonacoLanguageMap = { + js: 'javascript', + ts: 'typescript', +}; -export default CodeField; +const baseClass = 'code-field'; + +const Code: React.FC = (props) => { + const { + path: pathFromProps, + name, + required, + validate = code, + admin: { + readOnly, + style, + className, + width, + language, + description, + condition, + editorOptions, + } = {}, + label, + } = props; + + const path = pathFromProps || name; + + const memoizedValidate = useCallback((value, options) => { + return validate(value, { ...options, required }); + }, [validate, required]); + + const { + value, + showError, + setValue, + errorMessage, + } = useField({ + path, + validate: memoizedValidate, + condition, + }); + + const classes = [ + baseClass, + 'field-type', + className, + showError && 'error', + readOnly && 'read-only', + ].filter(Boolean).join(' '); + + return ( +
+ +
+ ); +}; + +export default withCondition(Code); diff --git a/src/admin/components/forms/field-types/JSON/JSON.tsx b/src/admin/components/forms/field-types/JSON/JSON.tsx deleted file mode 100644 index 8d0aacba06..0000000000 --- a/src/admin/components/forms/field-types/JSON/JSON.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; - -import Error from '../../Error'; -import FieldDescription from '../../FieldDescription'; -import { json } from '../../../../../fields/validations'; -import Label from '../../Label'; -import { Props } from './types'; -import useField from '../../useField'; -import withCondition from '../../withCondition'; -import CodeEditor from '../../../elements/CodeEditor'; - -import './index.scss'; - -const baseClass = 'json-field'; - -const JSONField: React.FC = (props) => { - const { - path: pathFromProps, - name, - required, - validate = json, - admin: { - readOnly, - style, - className, - width, - description, - condition, - editorOptions, - } = {}, - label, - } = props; - - const path = pathFromProps || name; - const [stringValue, setStringValue] = useState(); - const [jsonError, setJsonError] = useState(); - - const memoizedValidate = useCallback((value, options) => { - return validate(value, { ...options, required, jsonError }); - }, [validate, required, jsonError]); - - const { - value, - initialValue, - showError, - setValue, - errorMessage, - } = useField({ - path, - validate: memoizedValidate, - condition, - }); - - const handleChange = useCallback((val) => { - if (readOnly) return; - setStringValue(val); - - try { - setValue(JSON.parse(val.trim() || '{}')); - setJsonError(undefined); - } catch (e) { - setJsonError(e); - } - }, [readOnly, setValue, setStringValue]); - - useEffect(() => { - setStringValue(JSON.stringify(initialValue, null, 2)); - }, [initialValue]); - - const classes = [ - baseClass, - 'field-type', - className, - showError && 'error', - readOnly && 'read-only', - ].filter(Boolean).join(' '); - - return ( -
- -
- ); -}; - -export default withCondition(JSONField); diff --git a/src/admin/components/forms/field-types/JSON/index.tsx b/src/admin/components/forms/field-types/JSON/index.tsx index 78b3f06e63..6b7ada50c7 100644 --- a/src/admin/components/forms/field-types/JSON/index.tsx +++ b/src/admin/components/forms/field-types/JSON/index.tsx @@ -1,13 +1,110 @@ -import React, { Suspense, lazy } from 'react'; -import { Loading } from '../../../elements/Loading'; +import React, { useCallback, useEffect, useState } from 'react'; + +import Error from '../../Error'; +import FieldDescription from '../../FieldDescription'; +import { json } from '../../../../../fields/validations'; +import Label from '../../Label'; import { Props } from './types'; +import useField from '../../useField'; +import withCondition from '../../withCondition'; +import { CodeEditor } from '../../../elements/CodeEditor'; -const JSON = lazy(() => import('./JSON')); +import './index.scss'; -const JSONField: React.FC = (props) => ( - }> - - -); +const baseClass = 'json-field'; -export default JSONField; +const JSONField: React.FC = (props) => { + const { + path: pathFromProps, + name, + required, + validate = json, + admin: { + readOnly, + style, + className, + width, + description, + condition, + editorOptions, + } = {}, + label, + } = props; + + const path = pathFromProps || name; + const [stringValue, setStringValue] = useState(); + const [jsonError, setJsonError] = useState(); + + const memoizedValidate = useCallback((value, options) => { + return validate(value, { ...options, required, jsonError }); + }, [validate, required, jsonError]); + + const { + value, + initialValue, + showError, + setValue, + errorMessage, + } = useField({ + path, + validate: memoizedValidate, + condition, + }); + + const handleChange = useCallback((val) => { + if (readOnly) return; + setStringValue(val); + + try { + setValue(JSON.parse(val.trim() || '{}')); + setJsonError(undefined); + } catch (e) { + setJsonError(e); + } + }, [readOnly, setValue, setStringValue]); + + useEffect(() => { + setStringValue(JSON.stringify(initialValue, null, 2)); + }, [initialValue]); + + const classes = [ + baseClass, + 'field-type', + className, + showError && 'error', + readOnly && 'read-only', + ].filter(Boolean).join(' '); + + return ( +
+ +
+ ); +}; + +export default withCondition(JSONField); diff --git a/src/admin/components/utilities/LoadingOverlay/index.tsx b/src/admin/components/utilities/LoadingOverlay/index.tsx index 1d350c20f7..62d2679fc2 100644 --- a/src/admin/components/utilities/LoadingOverlay/index.tsx +++ b/src/admin/components/utilities/LoadingOverlay/index.tsx @@ -20,11 +20,18 @@ export const LoadingOverlayProvider: React.FC<{ children?: React.ReactNode }> = const [loadingOverlayText, setLoadingOverlayText] = useState(t('loading')); const [overlays, dispatchOverlay] = React.useReducer(reducer, defaultLoadingOverlayState); - const { isMounted, isUnmounting, triggerRenderTimeout: suspendDisplay } = useDelayedRender({ show: overlays.isLoading }); + + const { + isMounted, + isUnmounting, + triggerDelayedRender, + } = useDelayedRender({ + show: overlays.isLoading, + }); const toggleLoadingOverlay = React.useCallback(({ type, key, isLoading }) => { if (isLoading) { - suspendDisplay(); + triggerDelayedRender(); dispatchOverlay({ type: 'add', payload: { @@ -41,7 +48,7 @@ export const LoadingOverlayProvider: React.FC<{ children?: React.ReactNode }> = }, }); } - }, [suspendDisplay]); + }, [triggerDelayedRender]); return ( { +export const reducer = (state: State, action: Action): State => { const loadersCopy = [...state.loaders]; const { type = 'fullscreen', key = 'user' } = action.payload; diff --git a/src/admin/components/views/collections/List/Default.tsx b/src/admin/components/views/collections/List/Default.tsx index e288a78cb6..246e893071 100644 --- a/src/admin/components/views/collections/List/Default.tsx +++ b/src/admin/components/views/collections/List/Default.tsx @@ -16,6 +16,7 @@ import PerPage from '../../../elements/PerPage'; import { Gutter } from '../../../elements/Gutter'; import { RelationshipProvider } from './RelationshipProvider'; import { getTranslation } from '../../../../../utilities/getTranslation'; +import { StaggeredShimmers } from '../../../elements/ShimmerEffect'; import './index.scss'; @@ -96,6 +97,18 @@ const DefaultList: React.FC = (props) => { handleSortChange={handleSortChange} handleWhereChange={handleWhereChange} /> + + {!data.docs && ( + + )} + {(data.docs && data.docs.length > 0) && ( {!upload && ( diff --git a/src/admin/components/views/collections/List/index.scss b/src/admin/components/views/collections/List/index.scss index 5fb89f7f99..93a4ce97f9 100644 --- a/src/admin/components/views/collections/List/index.scss +++ b/src/admin/components/views/collections/List/index.scss @@ -55,6 +55,34 @@ margin-left: auto; } + &__shimmer { + margin-top: base(1.75); + } + + &__shimmer--rows { + >div { + margin-top: 8px; + } + } + + &__shimmer--uploads { + // match upload cards + margin: base(2) -#{base(.5)}; + width: calc(100% + #{$baseline}); + display: flex; + flex-wrap: wrap; + + >div { + min-width: 0; + width: calc(16.66%); + + >div { + margin: base(.5); + padding-bottom: 110%; + } + } + } + @include mid-break { &__wrap { padding-top: 0; @@ -83,5 +111,19 @@ width: 100%; margin-bottom: $baseline; } + + &__shimmer--uploads { + >div { + width: 33.33%; + } + } + } + + @include small-break { + &__shimmer--uploads { + >div { + width: 50%; + } + } } } diff --git a/src/admin/hooks/useDelayedRender.ts b/src/admin/hooks/useDelayedRender.ts index d2517e42e6..059a23c780 100644 --- a/src/admin/hooks/useDelayedRender.ts +++ b/src/admin/hooks/useDelayedRender.ts @@ -21,7 +21,7 @@ type useDelayedRenderT = (props: DelayedRenderProps) => { /** `true` if the component is unmounting. */ isUnmounting: boolean; /** Call this function to trigger the timeout delay before rendering. */ - triggerRenderTimeout: () => void; + triggerDelayedRender: () => void; }; export const useDelayedRender: useDelayedRenderT = ({ show, delayBeforeShow = 1000, inTimeout = 500, minShowTime = 500, outTimeout = 500 }) => { const totalMountTime = inTimeout + minShowTime + outTimeout; @@ -57,6 +57,6 @@ export const useDelayedRender: useDelayedRenderT = ({ show, delayBeforeShow = 10 return { isMounted, isUnmounting, - triggerRenderTimeout: triggerDelay, + triggerDelayedRender: triggerDelay, }; };