Show alert when there's an error in UI components

This commit is contained in:
Stan Wohlwend 2024-06-04 19:45:13 +02:00
parent ba8de5c8f8
commit 035ba57d66
13 changed files with 49 additions and 33 deletions

View File

@ -5,7 +5,7 @@ import { Separator } from "@/components/ui/separator";
import { yupResolver } from "@hookform/resolvers/yup";
import { Form } from "@/components/ui/form";
import { InputField, SwitchListField } from "@/components/form-fields";
import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronously, runAsynchronouslyWithAlert, wait } from "@stackframe/stack-shared/dist/utils/promises";
import { useRouter } from "@/components/router";
import { useState } from "react";
import { Button } from "@/components/ui/button";
@ -79,7 +79,7 @@ export default function PageClient () {
</div>
<Form {...form}>
<form onSubmit={e => runAsynchronously(form.handleSubmit(onSubmit)(e))} className="space-y-4">
<form onSubmit={e => runAsynchronouslyWithAlert(form.handleSubmit(onSubmit)(e))} className="space-y-4">
<InputField required control={form.control} name="displayName" label="Project Name" placeholder="My Project" />
@ -127,4 +127,4 @@ export default function PageClient () {
</div>
</div>
);
}
}

View File

@ -2,7 +2,7 @@
import * as yup from "yup";
import { useEffect, useId, useState } from "react";
import { ActionDialog, ActionDialogProps } from "@/components/action-dialog";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { yupResolver } from "@hookform/resolvers/yup";
import { FieldValues, useForm } from "react-hook-form";
import { Form } from "@/components/ui/form";
@ -90,7 +90,7 @@ export function FormDialog<F extends FieldValues>(
{...props}
open={props.open ?? openState}
onOpenChange={(open) => { if(open) setOpenState(true); props.onOpenChange?.(open); }}
onClose={() => { form.reset(); setOpenState(false); runAsynchronously(props.onClose?.()); }}
onClose={() => { form.reset(); setOpenState(false); runAsynchronouslyWithAlert(props.onClose?.()); }}
okButton={{
onClick: async () => "prevent-close",
...(typeof props.okButton == "boolean" ? {} : props.okButton),
@ -103,7 +103,7 @@ export function FormDialog<F extends FieldValues>(
}}
>
<Form {...form}>
<form onSubmit={e => runAsynchronously(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}>
<form onSubmit={e => runAsynchronouslyWithAlert(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}>
{props.render(form)}
</form>
</Form>

View File

@ -12,7 +12,7 @@ import { Button } from "./ui/button";
import React, { useEffect, useId, useRef, useState } from "react";
import { Label } from "./ui/label";
import { DelayedInput, Input } from "./ui/input";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronously, runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { Accordion } from "@radix-ui/react-accordion";
import { AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion";
import { FieldValues, useForm } from "react-hook-form";
@ -114,7 +114,7 @@ export function SettingInput(props: {
<DelayedInput
className="max-w-lg"
defaultValue={props.defaultValue}
onChange={(e) => runAsynchronously(props.onChange?.(e.target.value))}
onChange={(e) => runAsynchronouslyWithAlert(props.onChange?.(e.target.value))}
/>
{props.actions}
</div>
@ -177,10 +177,10 @@ export function FormSettingCard<F extends FieldValues>(
</>
}>
<Form {...form}>
<form onSubmit={e => runAsynchronously(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}>
<form onSubmit={e => runAsynchronouslyWithAlert(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}>
{props.render(form)}
</form>
</Form>
</SettingCard>
);
}
}

View File

@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { Spinner } from "./spinner";
import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback";
@ -75,7 +75,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
ref={ref}
disabled={props.disabled || loading}
onClick={(e) => runAsynchronously(handleClick(e))}
onClick={(e) => runAsynchronouslyWithAlert(handleClick(e))}
>
{loading && <Spinner className="mr-2" />}
{children}

View File

@ -28,7 +28,7 @@ export default function ErrorPage(props: {
{props.description}
</Typography>
<Button onClick={() => runAsynchronously(router.push(props.redirectUrl))}>
<Button onClick={() => router.push(props.redirectUrl)}>
{props.redirectText}
</Button>

View File

@ -4,7 +4,7 @@ import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback";
import { Spinner } from "./spinner";
@ -55,8 +55,8 @@ const Switch = React.forwardRef<
<OriginalSwitch
{...props}
ref={ref}
onClick={(e) => runAsynchronously(handleClick(e))}
onCheckedChange={(checked) => runAsynchronously(handleCheckedChange(checked))}
onClick={(e) => runAsynchronouslyWithAlert(handleClick(e))}
onCheckedChange={(checked) => runAsynchronouslyWithAlert(handleCheckedChange(checked))}
disabled={props.disabled || loading}
style={{
visibility: loading ? "hidden" : "visible",

View File

@ -96,10 +96,25 @@ class ErrorDuringRunAsynchronously extends Error {
}
}
export function runAsynchronouslyWithAlert(...args: Parameters<typeof runAsynchronously>) {
return runAsynchronously(
args[0],
{
...args[1],
onError: error => {
alert(`An unhandled error occurred. Please ${process.env.NODE_ENV === "development" ? `check the browser console for the full error. ${error}` : "report this to the developer."}`);
args[1]?.onError?.(error);
},
},
...args.slice(2) as [],
);
}
export function runAsynchronously(
promiseOrFunc: void | Promise<unknown> | (() => void | Promise<unknown>) | undefined,
options: {
ignoreErrors?: boolean,
noErrorLogging?: boolean,
onError?: (error: Error) => void,
} = {},
): void {
if (typeof promiseOrFunc === "function") {
@ -116,7 +131,8 @@ export function runAsynchronously(
cause: error,
}
);
if (!options.ignoreErrors) {
options.onError?.(newError);
if (!options.noErrorLogging) {
captureError("runAsynchronously", newError);
}
});

View File

@ -7,7 +7,7 @@ import FormWarningText from "./form-warning";
import PasswordField from "./password-field";
import { useStackApp } from "..";
import { Button, Input, Label, Link } from "../components-core";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
const schema = yup.object().shape({
email: yup.string().email('Please enter a valid email').required('Please enter your email'),
@ -30,7 +30,7 @@ export default function CredentialSignIn() {
return (
<form
style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
onSubmit={e => runAsynchronously(handleSubmit(onSubmit)(e))}
onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))}
noValidate
>
<Label htmlFor="email">Email</Label>

View File

@ -7,7 +7,7 @@ import PasswordField from "./password-field";
import FormWarningText from "./form-warning";
import { useStackApp } from "..";
import { Label, Input, Button } from "../components-core";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import { getPasswordError } from "@stackframe/stack-shared/dist/helpers/password";
const schema = yup.object().shape({
@ -41,7 +41,7 @@ export default function CredentialSignUp() {
return (
<form
style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
onSubmit={e => runAsynchronously(handleSubmit(onSubmit)(e))}
onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))}
noValidate
>
<Label htmlFor="email">Email</Label>

View File

@ -6,7 +6,7 @@ import * as yup from "yup";
import FormWarningText from "./form-warning";
import { useStackApp } from "..";
import { Button, Input, Label } from "../components-core";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
const schema = yup.object().shape({
email: yup.string().email('Please enter a valid email').required('Please enter your email')
@ -27,7 +27,7 @@ export default function ForgotPassword({ onSent }: { onSent?: () => void }) {
return (
<form
style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
onSubmit={e => runAsynchronously(handleSubmit(onSubmit)(e))}
onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))}
noValidate
>
<Label htmlFor="email">Your Email</Label>

View File

@ -7,7 +7,7 @@ import * as yup from "yup";
import FormWarningText from "./form-warning";
import { useStackApp } from "..";
import { Button, Input, Label } from "../components-core";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
const schema = yup.object().shape({
email: yup.string().email('Please enter a valid email').required('Please enter your email')
@ -34,7 +34,7 @@ export default function MagicLinkSignIn() {
return (
<form
style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
onSubmit={e => runAsynchronously(handleSubmit(onSubmit)(e))}
onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))}
noValidate
>
<Label htmlFor="email">Email</Label>

View File

@ -12,7 +12,7 @@ import RedirectMessageCard from "./redirect-message-card";
import MessageCard from "./message-card";
import MaybeFullPage from "./maybe-full-page";
import { Button, Label, Text } from "../components-core";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
const schema = yup.object().shape({
password: yup.string().required('Please enter your password').test({
@ -71,7 +71,7 @@ export default function PasswordResetInner(
<form
style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
onSubmit={e => runAsynchronously(handleSubmit(onSubmit)(e))}
onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))}
noValidate
>
<Label htmlFor="password">New Password</Label>

View File

@ -13,7 +13,7 @@ import {
Skeleton,
CurrentUser,
} from "..";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
import UserAvatar from "./user-avatar";
import { useRouter } from "next/navigation";
import { typedEntries, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects";
@ -40,7 +40,7 @@ const icons = typedFromEntries(typedEntries({
function Item(props: { text: string, icon: React.ReactNode, onClick: () => void | Promise<void> }) {
return (
<DropdownMenuItem
onClick={() => runAsynchronously(props.onClick)}
onClick={() => runAsynchronouslyWithAlert(props.onClick)}
style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}
>
{props.icon}
@ -116,17 +116,17 @@ function UserButtonInnerInner(props: UserButtonProps & { user: CurrentUser | nul
<DropdownMenuSeparator />
{user && <Item
text="Account settings"
onClick={() => runAsynchronously(router.push(app.urls.accountSettings))}
onClick={() => router.push(app.urls.accountSettings)}
icon={icons.CircleUser}
/>}
{!user && <Item
text="Sign in"
onClick={() => runAsynchronously(router.push(app.urls.signIn))}
onClick={() => router.push(app.urls.signIn)}
icon={icons.LogIn}
/>}
{!user && <Item
text="Sign up"
onClick={() => runAsynchronously(router.push(app.urls.signUp))}
onClick={() => router.push(app.urls.signUp)}
icon={icons.UserPlus}
/>}
{user && props.extraItems && props.extraItems.map((item, index) => (