[improvement] Adds error boundary (#690)
* Add error boundary * Update useStyle.tsx * Update ErrorFallback.tsx
This commit is contained in:
parent
e33edb9cab
commit
e2a6badaef
12 changed files with 280 additions and 124 deletions
|
@ -48,11 +48,11 @@
|
|||
"react-dom": "^16.8 || ^17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tldraw/intersect": "*",
|
||||
"@tldraw/vec": "*",
|
||||
"@swc-node/jest": "^1.4.3",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@tldraw/intersect": "*",
|
||||
"@tldraw/vec": "*",
|
||||
"@types/node": "^17.0.14",
|
||||
"@types/react": "^17.0.38",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
usePreventNavigationCss,
|
||||
useZoomEvents,
|
||||
|
@ -136,5 +136,3 @@ export const Canvas = observer(function _Canvas<
|
|||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const stopPropagation: React.ClipboardEventHandler<HTMLDivElement> = (e) => e.stopPropagation()
|
||||
|
|
|
@ -29,14 +29,24 @@ export function useSelection<T extends TLShape>(
|
|||
if (selectedIds.length === 1) {
|
||||
const id = selectedIds[0]
|
||||
const shape = page.shapes[id]
|
||||
|
||||
if (!shape) {
|
||||
throw Error(`selectedIds is set to the id of a shape that doesn't exist: ${id}`)
|
||||
}
|
||||
|
||||
rotation = shape.rotation || 0
|
||||
isLocked = shape.isLocked || false
|
||||
|
||||
const utils = getShapeUtils(shapeUtils, shape)
|
||||
|
||||
bounds = utils.hideBounds ? undefined : utils.getBounds(shape)
|
||||
} else if (selectedIds.length > 1) {
|
||||
const selectedShapes = selectedIds.map((id) => page.shapes[id])
|
||||
|
||||
rotation = 0
|
||||
|
||||
isLocked = selectedShapes.every((shape) => shape.isLocked)
|
||||
|
||||
bounds = selectedShapes.reduce((acc, shape, i) => {
|
||||
if (i === 0) {
|
||||
return getShapeUtils(shapeUtils, shape).getRotatedBounds(shape)
|
||||
|
@ -48,9 +58,11 @@ export function useSelection<T extends TLShape>(
|
|||
if (bounds) {
|
||||
const [minX, minY] = canvasToScreen([bounds.minX, bounds.minY], pageState.camera)
|
||||
const [maxX, maxY] = canvasToScreen([bounds.maxX, bounds.maxY], pageState.camera)
|
||||
|
||||
isLinked = !!Object.values(page.bindings).find(
|
||||
(binding) => selectedIds.includes(binding.toId) || selectedIds.includes(binding.fromId)
|
||||
)
|
||||
|
||||
rSelectionBounds.current = {
|
||||
minX,
|
||||
minY,
|
||||
|
|
|
@ -138,8 +138,14 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
|||
// If the shape's parent is a different shape (e.g. a group),
|
||||
// add the parent to the sets of shapes to render. The parent's
|
||||
// children will all be rendered.
|
||||
shapesIdsToRender.add(shape.parentId)
|
||||
shapesToRender.add(page.shapes[shape.parentId])
|
||||
const parent = page.shapes[shape.parentId]
|
||||
|
||||
if (parent === undefined) {
|
||||
throw Error(`A shape (${shape.id}) has a parent (${shape.parentId}) that does not exist!`)
|
||||
} else {
|
||||
shapesIdsToRender.add(parent.id)
|
||||
shapesToRender.add(parent)
|
||||
}
|
||||
})
|
||||
|
||||
// Call onRenderCountChange callback when number of rendering shapes changes
|
||||
|
@ -163,7 +169,11 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
|||
|
||||
const tree: IShapeTreeNode<T, M>[] = []
|
||||
|
||||
shapesToRender.forEach((shape) =>
|
||||
shapesToRender.forEach((shape) => {
|
||||
if (shape === undefined) {
|
||||
throw Error('Rendered shapes included a missing shape')
|
||||
}
|
||||
|
||||
addToShapeTree(
|
||||
shape,
|
||||
tree,
|
||||
|
@ -174,7 +184,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
|||
false,
|
||||
meta
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
tree.sort((a, b) => a.shape.childIndex - b.shape.childIndex)
|
||||
|
||||
|
|
|
@ -79,36 +79,6 @@ const defaultTheme: TLTheme = {
|
|||
}
|
||||
|
||||
export const TLCSS = css`
|
||||
@font-face {
|
||||
font-family: 'Recursive';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
|
||||
format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Recursive';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
|
||||
format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Recursive Mono';
|
||||
font-style: normal;
|
||||
font-weight: 420;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
|
||||
format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
.tl-container {
|
||||
--tl-zoom: 1;
|
||||
--tl-scale: calc(1 / var(--tl-zoom));
|
||||
|
@ -351,11 +321,6 @@ export const TLCSS = css`
|
|||
.tl-brush.dashed {
|
||||
stroke: none;
|
||||
}
|
||||
.tl-dot {
|
||||
fill: var(--tl-background);
|
||||
stroke: var(--tl-foreground);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
.tl-handle {
|
||||
pointer-events: all;
|
||||
cursor: grab;
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"idb-keyval": "^6.1.0",
|
||||
"lz-string": "^1.4.4",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hotkey-hook": "^1.0.2",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"tslib": "^2.3.1",
|
||||
|
|
|
@ -12,6 +12,8 @@ import { FocusButton } from '~components/FocusButton'
|
|||
import { TLDR } from '~state/TLDR'
|
||||
import { GRID_SIZE } from '~constants'
|
||||
import { Loading } from '~components/Loading'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { ErrorFallback } from '~components/ErrorFallback'
|
||||
|
||||
export interface TldrawProps extends TDCallbacks {
|
||||
/**
|
||||
|
@ -415,6 +417,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
<Loading />
|
||||
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
|
||||
<ContextMenu>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<Renderer
|
||||
id={id}
|
||||
containerRef={rWrapper}
|
||||
|
@ -490,6 +493,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
onDragOver={app.onDragOver}
|
||||
onDrop={app.onDrop}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</ContextMenu>
|
||||
{showUI && (
|
||||
<StyledUI>
|
||||
|
|
122
packages/tldraw/src/components/ErrorFallback/ErrorFallback.tsx
Normal file
122
packages/tldraw/src/components/ErrorFallback/ErrorFallback.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import * as React from 'react'
|
||||
import { FallbackProps } from 'react-error-boundary'
|
||||
import { Divider } from '~components/Primitives/Divider'
|
||||
import { RowButton } from '~components/Primitives/RowButton'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { styled } from '~styles'
|
||||
|
||||
export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const refreshPage = () => {
|
||||
window.location.reload()
|
||||
resetErrorBoundary()
|
||||
}
|
||||
|
||||
const copyError = () => {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = error.message
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
textarea.remove()
|
||||
}
|
||||
|
||||
const downloadBackup = () => {
|
||||
app.saveProjectAs()
|
||||
}
|
||||
|
||||
const resetDocument = () => {
|
||||
app.resetDocument()
|
||||
resetErrorBoundary()
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<InnerContainer>
|
||||
<div>We've encountered an error!</div>
|
||||
<pre>
|
||||
<code>{error.message}</code>
|
||||
</pre>
|
||||
<Buttons>
|
||||
<RowButton onClick={copyError}>Copy Error</RowButton>
|
||||
<RowButton onClick={refreshPage}>Refresh Page</RowButton>
|
||||
</Buttons>
|
||||
<Divider />
|
||||
<p>
|
||||
Keep getting this error?{' '}
|
||||
<a onClick={downloadBackup} title="Download your project">
|
||||
Download your project
|
||||
</a>{' '}
|
||||
as a backup and then{' '}
|
||||
<a onClick={resetDocument} title="Reset the document">
|
||||
reset the document
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</InnerContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled('div', {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '$canvas',
|
||||
})
|
||||
|
||||
const InnerContainer = styled('div', {
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panelContrast',
|
||||
padding: '$5',
|
||||
borderRadius: 8,
|
||||
boxShadow: '$panel',
|
||||
maxWidth: 320,
|
||||
color: '$text',
|
||||
fontFamily: '$ui',
|
||||
fontSize: '$2',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '$3',
|
||||
'& > pre': {
|
||||
marginTop: '$3',
|
||||
marginBottom: '$3',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'pre-wrap',
|
||||
backgroundColor: '$hover',
|
||||
padding: '$4',
|
||||
borderRadius: '$2',
|
||||
fontFamily: '"Menlo", "Monaco", monospace',
|
||||
fontWeight: 500,
|
||||
},
|
||||
'& p': {
|
||||
fontFamily: '$body',
|
||||
lineHeight: 1.7,
|
||||
padding: '$5',
|
||||
margin: 0,
|
||||
},
|
||||
'& a': {
|
||||
color: '$text',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
'& hr': {
|
||||
marginLeft: '-$5',
|
||||
marginRight: '-$5',
|
||||
},
|
||||
})
|
||||
|
||||
const Buttons = styled('div', {
|
||||
display: 'flex',
|
||||
'& > button > div': {
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
1
packages/tldraw/src/components/ErrorFallback/index.ts
Normal file
1
packages/tldraw/src/components/ErrorFallback/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ErrorFallback'
|
|
@ -3,9 +3,43 @@ import * as React from 'react'
|
|||
const styles = new Map<string, HTMLStyleElement>()
|
||||
|
||||
const UID = `tldraw-fonts`
|
||||
const WEBFONT_URL =
|
||||
'https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block'
|
||||
const CSS = `@import url('${WEBFONT_URL}');`
|
||||
|
||||
const CSS = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block');
|
||||
|
||||
@font-face {
|
||||
font-family: 'Recursive';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
|
||||
format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Recursive';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
|
||||
format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Recursive Mono';
|
||||
font-style: normal;
|
||||
font-weight: 420;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
|
||||
format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
`
|
||||
|
||||
export function useStylesheet() {
|
||||
React.useLayoutEffect(() => {
|
||||
|
|
|
@ -21,6 +21,7 @@ const { styled, createTheme } = createStitches({
|
|||
tooltip: '#1d1d1d',
|
||||
tooltipContrast: '#ffffff',
|
||||
warn: 'rgba(255, 100, 100, 1)',
|
||||
canvas: 'rgb(248, 249, 250)',
|
||||
},
|
||||
shadows: {
|
||||
2: '0px 1px 1px rgba(0, 0, 0, 0.14)',
|
||||
|
@ -110,6 +111,7 @@ export const dark = createTheme({
|
|||
text: '#f8f9fa',
|
||||
tooltip: '#1d1d1d',
|
||||
tooltipContrast: '#ffffff',
|
||||
canvas: '#212529',
|
||||
},
|
||||
shadows: {
|
||||
2: '0px 1px 1px rgba(0, 0, 0, 0.24)',
|
||||
|
|
|
@ -9385,6 +9385,13 @@ rc@^1.2.7, rc@^1.2.8:
|
|||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-error-boundary@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
|
||||
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-feather@^2.0.9:
|
||||
version "2.0.9"
|
||||
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"
|
||||
|
|
Loading…
Reference in a new issue