diff --git a/src/routes/Boards/History/History.tsx b/src/routes/Boards/History/History.tsx index 177ce0dd76..d49f38c58f 100644 --- a/src/routes/Boards/History/History.tsx +++ b/src/routes/Boards/History/History.tsx @@ -1,4 +1,4 @@ -import {useEffect} from "react"; +import {useEffect, useState} from "react"; import {Outlet, useNavigate, useOutletContext} from "react-router"; import {useTranslation} from "react-i18next"; import classNames from "classnames"; @@ -9,6 +9,8 @@ import {useAppDispatch, useAppSelector} from "store"; import {HistoryBoard, getBoards} from "store/features"; import {HistoryCard} from "./HistoryCard/HistoryCard"; import {Button} from "components/Button"; +import {LoadingIndicator} from "components/LoadingIndicator"; +import {useDelayedLoading} from "utils/hooks/useDelayedLoading"; import "./History.scss"; export type {HistoryBoard} from "store/features"; @@ -21,6 +23,8 @@ export const History = () => { const dispatch = useAppDispatch(); const historyBoards = useAppSelector((state) => state.history); + const [isLoading, setIsLoading] = useState(false); + const showLoading = useDelayedLoading(isLoading, 200); const isAnonymous = useAppSelector((state) => state.auth.user?.isAnonymous); const allowAnonymousHistory = useAppSelector((state) => state.view.allowAnonymousHistory); @@ -29,7 +33,8 @@ export const History = () => { // init history boards useEffect(() => { if (canViewHistory) { - dispatch(getBoards()); + setIsLoading(true); + dispatch(getBoards()).finally(() => setIsLoading(false)); } }, [dispatch, canViewHistory]); @@ -65,6 +70,11 @@ export const History = () => { if (!canViewHistory) { return renderRequireRegisteredUser(); } + + if (isLoading) { + return showLoading ? : null; + } + if (historyBoards.length === 0) { return renderEmptyHistory(); } diff --git a/src/routes/Boards/Templates/Templates.tsx b/src/routes/Boards/Templates/Templates.tsx index aa43a4c0d7..ffca2ec25e 100644 --- a/src/routes/Boards/Templates/Templates.tsx +++ b/src/routes/Boards/Templates/Templates.tsx @@ -13,6 +13,8 @@ import {Portal} from "components/Portal"; import {AccessSettings} from "components/Templates/AccessSettings/AccessSettings"; import {toggleRecommendedFavourite} from "store/features/templates"; import sortBy from "lodash/sortBy"; +import {LoadingIndicator} from "components/LoadingIndicator"; +import {useDelayedLoading} from "utils/hooks/useDelayedLoading"; import "./Templates.scss"; type Side = "left" | "right"; @@ -28,6 +30,8 @@ export const Templates = () => { const [selectedTemplateWithColumns, setSelectedTemplateWithColumns] = useState(null); const [showAccessSettingsPortal, setShowAccessSettingsPortal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const showLoading = useDelayedLoading(isLoading, 200); const {searchBarInput} = useOutletContext<{searchBarInput: string}>(); @@ -44,7 +48,8 @@ export const Templates = () => { // init templates useEffect(() => { - dispatch(getTemplates()); + setIsLoading(true); + dispatch(getTemplates()).finally(() => setIsLoading(false)); }, [dispatch]); const toggleFavourite = (templateId: string, favourite: boolean, type: "RECOMMENDED" | "CUSTOM") => { @@ -140,6 +145,32 @@ export const Templates = () => { ); + const renderCustomTemplatesContent = () => { + if (isLoading) { + return showLoading ? : null; + } + + return sortBy( + templates + .filter((template) => template.type === "CUSTOM") + .filter(matchSearchInput) + .filter(excludeDefaultTemplate), + (template: Template) => !template.favourite + ).map((template: Template) => ( + toggleFavourite(id, fav, "CUSTOM")} + disabled={!canCreateBoard} + disabledReason={!canCreateBoard ? t("Templates.TemplateCard.signInToCreateBoards") : undefined} + key={template.id} + /> + )); + }; + return ( <> {/* settings */} @@ -183,25 +214,7 @@ export const Templates = () => { {renderContainerHeader("right", t("Templates.savedTemplates"))}
- {sortBy( - templates - .filter((template) => template.type === "CUSTOM") - .filter(matchSearchInput) - .filter(excludeDefaultTemplate), - (template: Template) => !template.favourite - ).map((template: Template) => ( - toggleFavourite(id, fav, "CUSTOM")} - disabled={!canCreateBoard} - disabledReason={!canCreateBoard ? t("Templates.TemplateCard.signInToCreateBoards") : undefined} - key={template.id} - /> - ))} + {renderCustomTemplatesContent()}
)} diff --git a/src/utils/hooks/useDelayedLoading.ts b/src/utils/hooks/useDelayedLoading.ts new file mode 100644 index 0000000000..8174f784d4 --- /dev/null +++ b/src/utils/hooks/useDelayedLoading.ts @@ -0,0 +1,22 @@ +import {useEffect, useState} from "react"; + +// this hook delays the loading indicator (or in fact any boolean state) by a specified amount of time to prevent render flickering when the state changes quickly +export const useDelayedLoading = (isLoading: boolean, delay = 200) => { + const [showLoading, setShowLoading] = useState(false); + + useEffect(() => { + let timeout: ReturnType; + + if (isLoading) { + timeout = setTimeout(() => { + setShowLoading(true); + }, delay); + } else { + setShowLoading(false); + } + + return () => clearTimeout(timeout); + }, [isLoading, delay]); + + return showLoading; +};