Fix the blank window after an export
Every successful export left the window blank and unresponsive, so the app had to be killed. The "Saved to ..." notice (and the export-failed notice) held buttons inside a Carbon InlineNotification. Carbon throws "component should have no interactive child nodes" in an effect, and an uncaught throw makes React unmount the whole tree. Found by driving the real debug binary through WebKitWebDriver: the page body was empty and the error listener showed that throw. - The buttons now sit beside the notice, and the failed-export notice uses ActionableNotification's own Retry action. - ErrorBoundary around the app (reload screen instead of a blank window) and around each export notice. - A lint test that fails if a Carbon notification wraps an interactive element. - The real-webview self-test mounts the saved and failed notices and fails on an uncaught React error, and issues and exports a second invoice after the first. Claude-Session: https://claude.ai/code/session_01PZypiWDfMkDTeEPeXjRhW5
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
/** What to show instead of the children. Without it, a full-window notice with a Reload button. */
|
||||
fallback?: (error: Error, reset: () => void) => ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* React unmounts the whole tree when a component throws and nothing catches it, which leaves a blank window that
|
||||
* looks like a hang. This keeps the failure to the part that threw (or, at the top, shows what happened and a way back).
|
||||
* Plain elements on purpose: the component library may be what threw.
|
||||
*/
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error("Voiced UI error:", error, info.componentStack);
|
||||
}
|
||||
|
||||
reset = () => this.setState({ error: null });
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
if (!error) return this.props.children;
|
||||
if (this.props.fallback) return this.props.fallback(error, this.reset);
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
style={{ padding: "2rem", maxWidth: "40rem", margin: "0 auto", color: "var(--cds-text-primary, #161616)", background: "var(--cds-background, #f4f4f4)", minHeight: "100vh" }}
|
||||
>
|
||||
<h1 style={{ fontSize: "1.5rem", marginBottom: "0.75rem" }}>Something went wrong</h1>
|
||||
<p style={{ marginBottom: "1rem" }}>
|
||||
Voiced hit an unexpected error. Your invoices and drafts are saved. Reload to carry on.
|
||||
</p>
|
||||
<pre style={{ whiteSpace: "pre-wrap", fontSize: "0.8125rem", marginBottom: "1.5rem" }}>{error.message}</pre>
|
||||
<button type="button" className="cds--btn cds--btn--primary" onClick={() => window.location.reload()}>
|
||||
Reload Voiced
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user