diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 7cea6a357..10660e884 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -1,4 +1,4 @@ -import React, { lazy, Suspense, useEffect, useState } from "react"; +import React, { Suspense, useEffect, useState } from "react"; import { Route, Switch, useRouteMatch } from "react-router-dom"; import { IntlProvider, CustomFormats } from "react-intl"; import { Helmet } from "react-helmet"; @@ -31,25 +31,32 @@ import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; import { IUIConfig } from "./core/config"; import { releaseNotes } from "./docs/en/ReleaseNotes"; import { getPlatformURL, getBaseURL } from "./core/createClient"; +import { lazyComponent } from "./utils/lazyComponent"; -const Performers = lazy(() => import("./components/Performers/Performers")); -const FrontPage = lazy(() => import("./components/FrontPage/FrontPage")); -const Scenes = lazy(() => import("./components/Scenes/Scenes")); -const Settings = lazy(() => import("./components/Settings/Settings")); -const Stats = lazy(() => import("./components/Stats")); -const Studios = lazy(() => import("./components/Studios/Studios")); -const Galleries = lazy(() => import("./components/Galleries/Galleries")); +const Performers = lazyComponent( + () => import("./components/Performers/Performers") +); +const FrontPage = lazyComponent( + () => import("./components/FrontPage/FrontPage") +); +const Scenes = lazyComponent(() => import("./components/Scenes/Scenes")); +const Settings = lazyComponent(() => import("./components/Settings/Settings")); +const Stats = lazyComponent(() => import("./components/Stats")); +const Studios = lazyComponent(() => import("./components/Studios/Studios")); +const Galleries = lazyComponent( + () => import("./components/Galleries/Galleries") +); -const Movies = lazy(() => import("./components/Movies/Movies")); -const Tags = lazy(() => import("./components/Tags/Tags")); -const Images = lazy(() => import("./components/Images/Images")); -const Setup = lazy(() => import("./components/Setup/Setup")); -const Migrate = lazy(() => import("./components/Setup/Migrate")); +const Movies = lazyComponent(() => import("./components/Movies/Movies")); +const Tags = lazyComponent(() => import("./components/Tags/Tags")); +const Images = lazyComponent(() => import("./components/Images/Images")); +const Setup = lazyComponent(() => import("./components/Setup/Setup")); +const Migrate = lazyComponent(() => import("./components/Setup/Migrate")); -const SceneFilenameParser = lazy( +const SceneFilenameParser = lazyComponent( () => import("./components/SceneFilenameParser/SceneFilenameParser") ); -const SceneDuplicateChecker = lazy( +const SceneDuplicateChecker = lazyComponent( () => import("./components/SceneDuplicateChecker/SceneDuplicateChecker") ); diff --git a/ui/v2.5/src/components/ErrorBoundary.tsx b/ui/v2.5/src/components/ErrorBoundary.tsx index 302abb2cc..a6e466efb 100644 --- a/ui/v2.5/src/components/ErrorBoundary.tsx +++ b/ui/v2.5/src/components/ErrorBoundary.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { FormattedMessage } from "react-intl"; +import { isLazyComponentError } from "src/utils/lazyComponent"; interface IErrorBoundaryProps { children?: React.ReactNode; @@ -10,6 +12,7 @@ type ErrorInfo = { interface IErrorBoundaryState { error?: Error; + errorHelpId?: string; errorInfo?: ErrorInfo; } @@ -23,22 +26,35 @@ export class ErrorBoundary extends React.Component< } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + let errorHelpId: string | undefined; + if (isLazyComponentError(error)) { + errorHelpId = "errors.lazy_component_error_help"; + } this.setState({ error, + errorHelpId, errorInfo, }); } public render() { - if (this.state.errorInfo) { + const { error, errorHelpId, errorInfo } = this.state; + if (errorInfo) { // Error path return (
-

Something went wrong.

+

+ +

+ {errorHelpId && ( +
+ +
+ )}
- {this.state.error && this.state.error.toString()} + {error?.toString()}
- {this.state.errorInfo.componentStack} + {errorInfo.componentStack.trim().replaceAll(/^\s*/gm, " ")}
); diff --git a/ui/v2.5/src/components/Help/context.tsx b/ui/v2.5/src/components/Help/context.tsx index 0268b28e3..4034e3fcb 100644 --- a/ui/v2.5/src/components/Help/context.tsx +++ b/ui/v2.5/src/components/Help/context.tsx @@ -1,6 +1,7 @@ -import React, { lazy, Suspense, useState } from "react"; +import React, { Suspense, useState } from "react"; +import { lazyComponent } from "src/utils/lazyComponent"; -const Manual = lazy(() => import("./Manual")); +const Manual = lazyComponent(() => import("./Manual")); interface IManualContextState { openManual: (tab?: string) => void; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 7e244580a..21ba9d5c4 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -1,12 +1,5 @@ import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; -import React, { - useEffect, - useState, - useMemo, - useContext, - lazy, - useRef, -} from "react"; +import React, { useEffect, useState, useMemo, useContext, useRef } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -36,29 +29,38 @@ import { OrganizedButton } from "./OrganizedButton"; import { ConfigurationContext } from "src/hooks/Config"; import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; +import { lazyComponent } from "src/utils/lazyComponent"; -const SubmitStashBoxDraft = lazy( +const SubmitStashBoxDraft = lazyComponent( () => import("src/components/Dialogs/SubmitDraft") ); -const ScenePlayer = lazy( +const ScenePlayer = lazyComponent( () => import("src/components/ScenePlayer/ScenePlayer") ); -const GalleryViewer = lazy( +const GalleryViewer = lazyComponent( () => import("src/components/Galleries/GalleryViewer") ); -const ExternalPlayerButton = lazy(() => import("./ExternalPlayerButton")); +const ExternalPlayerButton = lazyComponent( + () => import("./ExternalPlayerButton") +); -const QueueViewer = lazy(() => import("./QueueViewer")); -const SceneMarkersPanel = lazy(() => import("./SceneMarkersPanel")); -const SceneFileInfoPanel = lazy(() => import("./SceneFileInfoPanel")); -const SceneEditPanel = lazy(() => import("./SceneEditPanel")); -const SceneDetailPanel = lazy(() => import("./SceneDetailPanel")); -const SceneMoviePanel = lazy(() => import("./SceneMoviePanel")); -const SceneGalleriesPanel = lazy(() => import("./SceneGalleriesPanel")); -const DeleteScenesDialog = lazy(() => import("../DeleteScenesDialog")); -const GenerateDialog = lazy(() => import("../../Dialogs/GenerateDialog")); -const SceneVideoFilterPanel = lazy(() => import("./SceneVideoFilterPanel")); +const QueueViewer = lazyComponent(() => import("./QueueViewer")); +const SceneMarkersPanel = lazyComponent(() => import("./SceneMarkersPanel")); +const SceneFileInfoPanel = lazyComponent(() => import("./SceneFileInfoPanel")); +const SceneEditPanel = lazyComponent(() => import("./SceneEditPanel")); +const SceneDetailPanel = lazyComponent(() => import("./SceneDetailPanel")); +const SceneMoviePanel = lazyComponent(() => import("./SceneMoviePanel")); +const SceneGalleriesPanel = lazyComponent( + () => import("./SceneGalleriesPanel") +); +const DeleteScenesDialog = lazyComponent(() => import("../DeleteScenesDialog")); +const GenerateDialog = lazyComponent( + () => import("../../Dialogs/GenerateDialog") +); +const SceneVideoFilterPanel = lazyComponent( + () => import("./SceneVideoFilterPanel") +); import { objectPath, objectTitle } from "src/core/files"; interface IProps { diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 701d3c1a4..c6437568b 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo, lazy } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Button, @@ -50,9 +50,10 @@ import { import { objectTitle } from "src/core/files"; import { galleryTitle } from "src/core/galleries"; import { useRatingKeybinds } from "src/hooks/keybinds"; +import { lazyComponent } from "src/utils/lazyComponent"; -const SceneScrapeDialog = lazy(() => import("./SceneScrapeDialog")); -const SceneQueryModal = lazy(() => import("./SceneQueryModal")); +const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog")); +const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal")); interface IProps { scene: Partial; diff --git a/ui/v2.5/src/components/Scenes/Scenes.tsx b/ui/v2.5/src/components/Scenes/Scenes.tsx index 6bbbd1c68..b6f465857 100644 --- a/ui/v2.5/src/components/Scenes/Scenes.tsx +++ b/ui/v2.5/src/components/Scenes/Scenes.tsx @@ -1,14 +1,15 @@ -import React, { lazy } from "react"; +import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; import { TITLE_SUFFIX } from "src/components/Shared/constants"; import { PersistanceLevel } from "src/hooks/ListHook"; +import { lazyComponent } from "src/utils/lazyComponent"; -const SceneList = lazy(() => import("./SceneList")); -const SceneMarkerList = lazy(() => import("./SceneMarkerList")); -const Scene = lazy(() => import("./SceneDetails/Scene")); -const SceneCreate = lazy(() => import("./SceneDetails/SceneCreate")); +const SceneList = lazyComponent(() => import("./SceneList")); +const SceneMarkerList = lazyComponent(() => import("./SceneMarkerList")); +const Scene = lazyComponent(() => import("./SceneDetails/Scene")); +const SceneCreate = lazyComponent(() => import("./SceneDetails/SceneCreate")); const Scenes: React.FC = () => { const intl = useIntl(); diff --git a/ui/v2.5/src/hooks/Lightbox/context.tsx b/ui/v2.5/src/hooks/Lightbox/context.tsx index 6c9240542..55df6559c 100644 --- a/ui/v2.5/src/hooks/Lightbox/context.tsx +++ b/ui/v2.5/src/hooks/Lightbox/context.tsx @@ -1,7 +1,8 @@ -import React, { lazy, Suspense, useCallback, useState } from "react"; +import React, { Suspense, useCallback, useState } from "react"; +import { lazyComponent } from "src/utils/lazyComponent"; import { ILightboxImage } from "./types"; -const LightboxComponent = lazy(() => import("./Lightbox")); +const LightboxComponent = lazyComponent(() => import("./Lightbox")); export interface IState { images: ILightboxImage[]; diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 15f87ffa8..26946ecf1 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -739,7 +739,7 @@ div.dropdown-menu { } .error-message { - white-space: "pre-wrap"; + white-space: pre-wrap; } .btn-toolbar .form-control { diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index c4d766e74..7fcc59a62 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -840,6 +840,10 @@ "warmth": "Warmth" }, "empty_server": "Add some scenes to your server to view recommendations on this page.", + "errors": { + "something_went_wrong": "Something went wrong.", + "lazy_component_error_help": "If you recently upgraded Stash, please reload the page or clear your browser cache." + }, "ethnicity": "Ethnicity", "existing_value": "existing value", "eye_color": "Eye Colour", diff --git a/ui/v2.5/src/utils/lazyComponent.ts b/ui/v2.5/src/utils/lazyComponent.ts new file mode 100644 index 000000000..38bacceed --- /dev/null +++ b/ui/v2.5/src/utils/lazyComponent.ts @@ -0,0 +1,24 @@ +import { ComponentType, lazy } from "react"; + +interface ILazyComponentError { + __lazyComponentError?: true; +} + +export const isLazyComponentError = (e: unknown) => { + return !!(e as ILazyComponentError).__lazyComponentError; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const lazyComponent = >( + factory: Parameters>[0] +) => { + return lazy(async () => { + try { + return await factory(); + } catch (e) { + // set flag to identify lazy component loading errors + (e as ILazyComponentError).__lazyComponentError = true; + throw e; + } + }); +};