excalidraw/src/components/TopErrorBoundary.tsx
David Luzar 3bf3d96d9f
make error message more user-friendly (#812)
* make error message more user-friendly

* tweak aria

* override user-select for error splash screen

* localize messages

* fix naming

* log error

* include spaces in i18n
2020-03-09 17:09:45 +01:00

162 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from "react";
import { resetCursor } from "../utils";
import { t } from "../i18n";
interface TopErrorBoundaryState {
unresolvedError: Error[] | null;
hasError: boolean;
stack: string;
localStorage: string;
}
export class TopErrorBoundary extends React.Component<
any,
TopErrorBoundaryState
> {
state: TopErrorBoundaryState = {
unresolvedError: null,
hasError: false,
stack: "",
localStorage: "",
};
componentDidCatch(error: Error) {
resetCursor();
const _localStorage: any = {};
for (const [key, value] of Object.entries({ ...localStorage })) {
try {
_localStorage[key] = JSON.parse(value);
} catch (error) {
_localStorage[key] = value;
}
}
this.setState(state => ({
hasError: true,
unresolvedError: state.unresolvedError
? state.unresolvedError.concat(error)
: [error],
localStorage: JSON.stringify(_localStorage),
}));
}
async componentDidUpdate() {
if (this.state.unresolvedError !== null) {
let stack = "";
for (const error of this.state.unresolvedError) {
if (stack) {
stack += `\n\n================\n\n`;
}
stack += `${error.message}:\n\n`;
try {
const StackTrace = await import("stacktrace-js");
stack += (await StackTrace.fromError(error)).join("\n");
} catch (error) {
console.error(error);
stack += error.stack || "";
}
}
this.setState(state => ({
unresolvedError: null,
stack: `${
state.stack ? `${state.stack}\n\n================\n\n${stack}` : stack
}`,
}));
}
}
private selectTextArea(event: React.MouseEvent<HTMLTextAreaElement>) {
if (event.target !== document.activeElement) {
event.preventDefault();
(event.target as HTMLTextAreaElement).select();
}
}
private async createGithubIssue() {
let body = "";
try {
const templateStr = (await import("../bug-issue-template")).default;
if (typeof templateStr === "string") {
body = encodeURIComponent(templateStr);
}
} catch (error) {
console.error(error);
}
window.open(
`https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
);
}
render() {
if (this.state.hasError) {
return (
<div className="ErrorSplash">
<div className="ErrorSplash-messageContainer">
<div className="ErrorSplash-paragraph bigger align-center">
{t("errorSplash.headingMain_pre")}
<button onClick={() => window.location.reload()}>
{t("errorSplash.headingMain_button")}
</button>
</div>
<div className="ErrorSplash-paragraph align-center">
{t("errorSplash.clearCanvasMessage")}
<button
onClick={() => {
localStorage.clear();
window.location.reload();
}}
>
{t("errorSplash.clearCanvasMessage_button")}
</button>
<br />
<div className="smaller">
<span role="img" aria-label="warning">
</span>
{t("errorSplash.clearCanvasCaveat")}
<span role="img" aria-hidden="true">
</span>
</div>
</div>
<div>
<div className="ErrorSplash-paragraph">
{t("errorSplash.openIssueMessage_pre")}
<button onClick={this.createGithubIssue}>
{t("errorSplash.openIssueMessage_button")}
</button>
{t("errorSplash.openIssueMessage_post")}
</div>
<div className="ErrorSplash-paragraph">
<div className="ErrorSplash-details">
<label>{t("errorSplash.errorStack")}</label>
<textarea
rows={10}
onPointerDown={this.selectTextArea}
readOnly={true}
value={
this.state.unresolvedError
? t("errorSplash.errorStack_loading")
: this.state.stack
}
/>
<label>{t("errorSplash.sceneContent")}</label>
<textarea
rows={5}
onPointerDown={this.selectTextArea}
readOnly={true}
value={this.state.localStorage}
/>
</div>
</div>
</div>
</div>
</div>
);
}
return this.props.children;
}
}