Add error boundaries and a default one with notifications.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful

This commit is contained in:
Madeorsk 2024-09-25 17:27:52 +02:00
parent a1b0cc816c
commit cbedfa9d52
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
6 changed files with 111 additions and 3 deletions

View file

@ -36,6 +36,8 @@ import {useNotify} from "../src/Components/Notifications/Notifications";
import {Notification, NotificationType} from "../src/Components/Notifications/Notification"; import {Notification, NotificationType} from "../src/Components/Notifications/Notification";
import {Box} from "../src/Components/Box"; import {Box} from "../src/Components/Box";
import {Await, useAsync} from "../src/Async"; import {Await, useAsync} from "../src/Async";
import {NotifyErrorsBoundary} from "../src/Components/Errors/NotifyErrorsBoundary";
import {DemoFailingComponent, DemoResetComponent} from "./DemoFailingComponent";
export function DemoApp() export function DemoApp()
{ {
@ -68,6 +70,8 @@ export function DemoApp()
}, 2000); }, 2000);
}), [asyncChange]); }), [asyncChange]);
const [failingComponentsCount, setFailingComponentsCount] = useState(0);
return ( return (
<Application> <Application>
<MainMenu> <MainMenu>
@ -584,8 +588,20 @@ export function DemoApp()
<p>Data: {data}</p> <p>Data: {data}</p>
)} )}
</Await> </Await>
<button type={"button"} onClick={() => setAsyncChange(asyncChange + 1)}>Change</button> <button type={"button"} onClick={() => setAsyncChange(asyncChange + 1)}>Change async deps</button>
<button type={"button"} onClick={() => setAnotherChange(anotherChange + 1)}>Another change</button> <button type={"button"} onClick={() => setAnotherChange(anotherChange + 1)}>Change something else</button>
</Card>
<h2>Error boundaries</h2>
<Card>
<button type={"button"} className={"error"}
onClick={() => setFailingComponentsCount(failingComponentsCount + 1)}>Do something dangerous</button>
<NotifyErrorsBoundary fallback={<DemoResetComponent />}>
{
[...Array(failingComponentsCount)].map(() => <DemoFailingComponent />)
}
</NotifyErrorsBoundary>
</Card> </Card>
</Application> </Application>

View file

@ -0,0 +1,26 @@
import React from "react";
import {useErrorBoundary} from "react-error-boundary";
/**
* A simple demo failing component.
*/
export function DemoFailingComponent()
{
throw new Error("Proudly thrown error.");
return (<p>I will never be shown...</p>);
}
/**
* A simple demo reset component.
*/
export function DemoResetComponent()
{
// Get error boundary.
const errorBoundary = useErrorBoundary();
return (
<button type={"button"} onClick={() => {
errorBoundary.resetBoundary();
}}>Reset to try again!</button>
);
}

View file

@ -12,6 +12,7 @@ export {useCurtain} from "./src/Components/Curtains/CurtainInstance";
export type {CurtainContextState} from "./src/Components/Curtains/CurtainInstance"; export type {CurtainContextState} from "./src/Components/Curtains/CurtainInstance";
export * from "./src/Components/Dates/Calendar"; export * from "./src/Components/Dates/Calendar";
export * from "./src/Components/Dates/Datepicker"; export * from "./src/Components/Dates/Datepicker";
export * from "./src/Components/Errors/NotifyErrorsBoundary";
export * from "./src/Components/Floating/Float"; export * from "./src/Components/Floating/Float";
export * from "./src/Components/Floating/Tooltip"; export * from "./src/Components/Floating/Tooltip";
export * from "./src/Components/Forms/Checkbox"; export * from "./src/Components/Forms/Checkbox";

View file

@ -1,5 +1,5 @@
{ {
"version": "1.5.0", "version": "1.6.0",
"name": "@kernelui/core", "name": "@kernelui/core",
"description": "Kernel UI Core.", "description": "Kernel UI Core.",
"scripts": { "scripts": {
@ -21,6 +21,7 @@
"@fontsource-variable/jetbrains-mono": "^5.0.21", "@fontsource-variable/jetbrains-mono": "^5.0.21",
"@fontsource-variable/manrope": "^5.0.20", "@fontsource-variable/manrope": "^5.0.20",
"@fontsource-variable/source-serif-4": "^5.0.19", "@fontsource-variable/source-serif-4": "^5.0.19",
"react-error-boundary": "^4.0.13",
"react-merge-refs": "^2.1.1", "react-merge-refs": "^2.1.1",
"uuid": "^10.0.0" "uuid": "^10.0.0"
}, },

View file

@ -0,0 +1,36 @@
import React, {useCallback, useMemo} from "react";
import {ErrorBoundary, ErrorBoundaryProps} from "react-error-boundary";
import {useNotify} from "../Notifications/Notifications";
import {Notification, NotificationType} from "../Notifications/Notification";
/**
* A React error boundary that show errors in notifications.
*/
export function NotifyErrorsBoundary({children, onError, fallback, fallbackRender, FallbackComponent, ...props}: React.PropsWithChildren<Partial<ErrorBoundaryProps>>)
{
// Get notification function.
const notify = useNotify();
/**
* Error handling function.
*/
const handleError = useCallback((error: Error, info: React.ErrorInfo) => {
// Show a notification about the error.
notify(<Notification type={NotificationType.ERROR}>Unexpected error: {error.message}</Notification>);
// Then call defined onError, if there is one.
onError?.(error, info);
}, [onError]);
// Define default fallback component.
const defaultFallback = useMemo(() => <></>, []);
if (!fallback && !fallbackRender && !FallbackComponent)
// Set default fallback if nothing is set.
fallback = defaultFallback;
return (
<ErrorBoundary onError={handleError} fallback={fallback} {...props}>
{children}
</ErrorBoundary>
);
}

View file

@ -229,6 +229,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.12.5":
version: 7.25.6
resolution: "@babel/runtime@npm:7.25.6"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10c0/d6143adf5aa1ce79ed374e33fdfd74fa975055a80bc6e479672ab1eadc4e4bfd7484444e17dd063a1d180e051f3ec62b357c7a2b817e7657687b47313158c3d2
languageName: node
linkType: hard
"@babel/template@npm:^7.24.6": "@babel/template@npm:^7.24.6":
version: 7.24.6 version: 7.24.6
resolution: "@babel/template@npm:7.24.6" resolution: "@babel/template@npm:7.24.6"
@ -576,6 +585,7 @@ __metadata:
less: "npm:^4.2.0" less: "npm:^4.2.0"
react: "npm:^18.3.1" react: "npm:^18.3.1"
react-dom: "npm:^18.3.1" react-dom: "npm:^18.3.1"
react-error-boundary: "npm:^4.0.13"
react-merge-refs: "npm:^2.1.1" react-merge-refs: "npm:^2.1.1"
react-router-dom: "npm:^6.24.1" react-router-dom: "npm:^6.24.1"
typescript: "npm:^5.4.5" typescript: "npm:^5.4.5"
@ -2331,6 +2341,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-error-boundary@npm:^4.0.13":
version: 4.0.13
resolution: "react-error-boundary@npm:4.0.13"
dependencies:
"@babel/runtime": "npm:^7.12.5"
peerDependencies:
react: ">=16.13.1"
checksum: 10c0/6f3e0e4d7669f680ccf49c08c9571519c6e31f04dcfc30a765a7136c7e6fbbbe93423dd5a9fce12107f8166e54133e9dd5c2079a00c7a38201ac811f7a28b8e7
languageName: node
linkType: hard
"react-merge-refs@npm:^2.1.1": "react-merge-refs@npm:^2.1.1":
version: 2.1.1 version: 2.1.1
resolution: "react-merge-refs@npm:2.1.1" resolution: "react-merge-refs@npm:2.1.1"
@ -2378,6 +2399,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"regenerator-runtime@npm:^0.14.0":
version: 0.14.1
resolution: "regenerator-runtime@npm:0.14.1"
checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4
languageName: node
linkType: hard
"resolve@npm:~1.19.0": "resolve@npm:~1.19.0":
version: 1.19.0 version: 1.19.0
resolution: "resolve@npm:1.19.0" resolution: "resolve@npm:1.19.0"