diff --git a/packages/@sanity/vision/src/SanityVision.tsx b/packages/@sanity/vision/src/SanityVision.tsx index 248daf120f5..a79a648186c 100644 --- a/packages/@sanity/vision/src/SanityVision.tsx +++ b/packages/@sanity/vision/src/SanityVision.tsx @@ -3,6 +3,7 @@ import {type Tool, useClient} from 'sanity' import {type VisionConfig} from './types' import {DEFAULT_API_VERSION} from './apiVersions' import {VisionContainer} from './containers/VisionContainer' +import {VisionErrorBoundary} from './containers/VisionErrorBoundary' interface SanityVisionProps { tool: Tool @@ -15,7 +16,11 @@ function SanityVision(props: SanityVisionProps) { ...props.tool.options, } - return + return ( + + + + ) } export default SanityVision diff --git a/packages/@sanity/vision/src/containers/VisionErrorBoundary.tsx b/packages/@sanity/vision/src/containers/VisionErrorBoundary.tsx new file mode 100644 index 00000000000..7cd29c686bb --- /dev/null +++ b/packages/@sanity/vision/src/containers/VisionErrorBoundary.tsx @@ -0,0 +1,87 @@ +/* eslint-disable @sanity/i18n/no-attribute-string-literals */ +/* eslint-disable i18next/no-literal-string */ +import {Button, Card, Code, Container, Heading, Stack} from '@sanity/ui' +import {Component, type PropsWithChildren} from 'react' +import {clearLocalStorage} from '../util/localStorage' + +/** + * @internal + */ +export type VisionErrorBoundaryProps = PropsWithChildren + +/** + * @internal + */ +interface VisionErrorBoundaryState { + error: string | null + numRetries: number +} + +/** + * @internal + */ +export class VisionErrorBoundary extends Component< + VisionErrorBoundaryProps, + VisionErrorBoundaryState +> { + constructor(props: VisionErrorBoundaryProps) { + super(props) + this.state = {error: null, numRetries: 0} + } + + static getDerivedStateFromError(error: unknown) { + return {error: error instanceof Error ? error.message : `${error}`} + } + + handleRetryRender = () => + this.setState((prev) => ({error: null, numRetries: prev.numRetries + 1})) + + handleRetryWithCacheClear = () => { + clearLocalStorage() + this.handleRetryRender() + } + + render() { + if (!this.state.error) { + return this.props.children + } + + const message = this.state.error + const withCacheClear = this.state.numRetries > 0 + + return ( + + + +
+
+ + An error occured + + + + {message && ( + + Error: {message} + + )} + + +
+
+
+ ) + } +} diff --git a/packages/@sanity/vision/src/util/localStorage.ts b/packages/@sanity/vision/src/util/localStorage.ts index 4bdc7052bd4..0a0b88923b9 100644 --- a/packages/@sanity/vision/src/util/localStorage.ts +++ b/packages/@sanity/vision/src/util/localStorage.ts @@ -1,6 +1,7 @@ import {isPlainObject} from './isPlainObject' const hasLocalStorage = supportsLocalStorage() +const keyPrefix = 'sanityVision:' export interface LocalStorageish { get: (key: string, defaultVal: T) => T @@ -8,8 +9,21 @@ export interface LocalStorageish { merge: (props: T) => T } +export function clearLocalStorage() { + if (!hasLocalStorage) { + return + } + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key?.startsWith(keyPrefix)) { + localStorage.removeItem(key) + } + } +} + export function getLocalStorage(namespace: string): LocalStorageish { - const storageKey = `sanityVision:${namespace}` + const storageKey = `${keyPrefix}${namespace}` let loadedState: Record | null = null return {get, set, merge}