Browse Source

add top error boundary & reset localStorage on error (#493)

* add top error boundary & reset localStorage on error

* add issue tracker details and link

* add pointer cursor to buttons

* Update src/bug-issue-template.js

Co-Authored-By: Lipis <lipiridis@gmail.com>

* Update src/styles.scss

Co-Authored-By: Lipis <lipiridis@gmail.com>

* Update src/bug-issue-template.js

Co-Authored-By: Lipis <lipiridis@gmail.com>

* use open-color colors

* use Cascadia font

Co-authored-by: Lipis <lipiridis@gmail.com>
David Luzar 5 years ago
parent
commit
20cf1078fc
3 changed files with 160 additions and 1 deletions
  1. 13 0
      src/bug-issue-template.js
  2. 97 1
      src/index.tsx
  3. 50 0
      src/styles.scss

+ 13 - 0
src/bug-issue-template.js

@@ -0,0 +1,13 @@
+export default `
+### Stack strace
+
+\`\`\`
+// paste stack trace here
+\`\`\`
+
+### localStorage
+
+\`\`\`
+// paste localStorage content here (if it doesn't contain sensitive data)
+\`\`\`
+`;

+ 97 - 1
src/index.tsx

@@ -1371,4 +1371,100 @@ export class App extends React.Component<any, AppState> {
 const AppWithTrans = withTranslation()(App);
 
 const rootElement = document.getElementById("root");
-ReactDOM.render(<AppWithTrans />, rootElement);
+
+class TopErrorBoundary extends React.Component {
+  state = { hasError: false, stack: "", localStorage: "" };
+
+  static getDerivedStateFromError(error: any) {
+    console.error(error);
+    return {
+      hasError: true,
+      localStorage: JSON.stringify({ ...localStorage }),
+      stack: error.stack
+    };
+  }
+
+  private selectTextArea(event: React.MouseEvent<HTMLTextAreaElement>) {
+    (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 {}
+
+    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">
+              Encountered an error. Please{" "}
+              <button onClick={() => window.location.reload()}>
+                reload the page
+              </button>
+              .
+            </div>
+            <div className="ErrorSplash-paragraph">
+              If reloading doesn't work. Try{" "}
+              <button
+                onClick={() => {
+                  localStorage.clear();
+                  window.location.reload();
+                }}
+              >
+                clearing the canvas
+              </button>
+              .<br />
+              <div className="smaller">
+                (This will unfortunately result in loss of work.)
+              </div>
+            </div>
+            <div>
+              <div className="ErrorSplash-paragraph">
+                Before doing so, we'd appreciate if you opened an issue on our{" "}
+                <button onClick={this.createGithubIssue}>bug tracker</button>.
+                Please include the following error stack trace & localStorage
+                content (provided it's not private):
+              </div>
+              <div className="ErrorSplash-paragraph">
+                <div className="ErrorSplash-details">
+                  <label>Error stack trace:</label>
+                  <textarea
+                    rows={10}
+                    onClick={this.selectTextArea}
+                    defaultValue={this.state.stack}
+                  />
+                  <label>LocalStorage content:</label>
+                  <textarea
+                    rows={5}
+                    onClick={this.selectTextArea}
+                    defaultValue={this.state.localStorage}
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      );
+    }
+
+    return this.props.children;
+  }
+}
+
+ReactDOM.render(
+  <TopErrorBoundary>
+    <AppWithTrans />
+  </TopErrorBoundary>,
+  rootElement
+);

+ 50 - 0
src/styles.scss

@@ -73,6 +73,8 @@ button {
   padding: 0.25rem;
   outline: transparent;
 
+  cursor: pointer;
+
   &:focus {
     box-shadow: 0 0 0 2px #a5d8ff;
   }
@@ -133,3 +135,51 @@ button {
 .App-right-menu {
   width: 13.75rem;
 }
+
+.ErrorSplash {
+  min-height: 100vh;
+  padding: 20px 0;
+  overflow: auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .ErrorSplash-messageContainer {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    padding: 40px;
+    background-color: #fff5f5;
+    border: 3px solid #c92a2a;
+  }
+
+  .ErrorSplash-paragraph {
+    margin: 15px 0;
+    text-align: center;
+    max-width: 600px;
+  }
+
+  .bigger,
+  .bigger button {
+    font-size: 1.1em;
+  }
+  .smaller,
+  .smaller button {
+    font-size: 0.9em;
+  }
+
+  .ErrorSplash-details {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    textarea {
+      width: 100%;
+      margin: 10px 0;
+      font-family: "Cascadia";
+      font-size: 0.8em;
+    }
+  }
+}