Skip to content

Commit

Permalink
fix(vision): provide error boundary with cache clear on error
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Feb 6, 2024
1 parent cb38c64 commit 3a8a987
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 2 deletions.
7 changes: 6 additions & 1 deletion packages/@sanity/vision/src/SanityVision.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<VisionConfig>
Expand All @@ -15,7 +16,11 @@ function SanityVision(props: SanityVisionProps) {
...props.tool.options,
}

return <VisionContainer client={client} config={config} />
return (
<VisionErrorBoundary>
<VisionContainer client={client} config={config} />
</VisionErrorBoundary>
)
}

export default SanityVision
87 changes: 87 additions & 0 deletions packages/@sanity/vision/src/containers/VisionErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card
height="fill"
overflow="auto"
paddingY={[4, 5, 6, 7]}
paddingX={4}
sizing="border"
tone="critical"
>
<Container width={3}>
<Stack space={4}>
<div>
<Button
onClick={withCacheClear ? this.handleRetryWithCacheClear : this.handleRetryRender}
text={withCacheClear ? 'Clear cache and retry' : 'Retry'}
tone="default"
/>
</div>

<Heading>An error occured</Heading>

<Card border radius={2} overflow="auto" padding={4} tone="inherit">
<Stack space={4}>
{message && (
<Code size={1}>
<strong>Error: {message}</strong>
</Code>
)}
</Stack>
</Card>
</Stack>
</Container>
</Card>
)
}
}
16 changes: 15 additions & 1 deletion packages/@sanity/vision/src/util/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import {isPlainObject} from './isPlainObject'

const hasLocalStorage = supportsLocalStorage()
const keyPrefix = 'sanityVision:'

export interface LocalStorageish {
get: <T>(key: string, defaultVal: T) => T
set: <T>(key: string, value: T) => T
merge: <T>(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<string, unknown> | null = null

return {get, set, merge}
Expand Down

0 comments on commit 3a8a987

Please sign in to comment.