diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx index 1989af5c2..622ea3bba 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx @@ -1,8 +1,9 @@ -"use client";; +"use client"; import { useAdminApp } from "../use-admin-app"; import { PageLayout } from "../page-layout"; import { SettingCard, SettingSwitch } from "@/components/settings"; import Typography from "@/components/ui/typography"; +import { Button } from "@/components/ui/button"; export default function PageClient() { const stackAdminApp = useAdminApp(); diff --git a/packages/stack-server/src/components/ui/button.tsx b/packages/stack-server/src/components/ui/button.tsx index 50780a263..afcecab97 100644 --- a/packages/stack-server/src/components/ui/button.tsx +++ b/packages/stack-server/src/components/ui/button.tsx @@ -1,10 +1,11 @@ import * as React from "react"; -import { ReloadIcon } from "@radix-ui/react-icons"; 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 { Spinner } from "./spinner"; +import { useAsyncCallback, useAsyncCallbackWithLoggedError } from "@stackframe/stack-shared/dist/hooks/use-async-callback"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", @@ -63,28 +64,20 @@ interface ButtonProps extends OriginalButtonProps { const Button = React.forwardRef( ({ onClick, loading: loadingProp, children, ...props }, ref) => { - const [isLoading, setLoading] = React.useState(false); - const handleClick = async (e: React.MouseEvent) => { - if (onClick) { - setLoading(true); - try { - await onClick(e); - } finally { - setLoading(false); - } - } - }; + const [handleClick, isLoading] = useAsyncCallback(async (e: React.MouseEvent) => { + await onClick?.(e); + }, [onClick]); - const loading = loadingProp ?? isLoading; + const loading = loadingProp || isLoading; return ( runAsynchronously(handleClick(e))} - disabled={loading} {...props} + ref={ref} + disabled={props.disabled || loading} + onClick={(e) => runAsynchronously(handleClick(e))} > - {loading && } + {loading && } {children} ); diff --git a/packages/stack-server/src/components/ui/spinner.tsx b/packages/stack-server/src/components/ui/spinner.tsx new file mode 100644 index 000000000..7fc8230f9 --- /dev/null +++ b/packages/stack-server/src/components/ui/spinner.tsx @@ -0,0 +1,14 @@ +import { ReloadIcon } from "@radix-ui/react-icons"; +import React from "react"; + +export const Spinner = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef<'span'> +>((props, ref) => { + return ( + + + + ); +}); +Spinner.displayName = "Spinner"; diff --git a/packages/stack-server/src/components/ui/switch.tsx b/packages/stack-server/src/components/ui/switch.tsx index 9b3cb4c40..8be63e5a5 100644 --- a/packages/stack-server/src/components/ui/switch.tsx +++ b/packages/stack-server/src/components/ui/switch.tsx @@ -5,6 +5,8 @@ import * as SwitchPrimitives from "@radix-ui/react-switch"; import { cn } from "@/lib/utils"; import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback"; +import { Spinner } from "./spinner"; interface OriginalSwitchProps extends React.ComponentProps {} @@ -37,40 +39,34 @@ interface AsyncSwitchProps extends OriginalSwitchProps { const Switch = React.forwardRef< React.ElementRef, AsyncSwitchProps ->(({ loading: loadingProp, onClick, ...props }, ref) => { - const [isLoading, setLoading] = React.useState(false); - const handleClick = async (e: React.MouseEvent) => { - if (onClick) { - setLoading(true); - try { - await onClick(e); - } finally { - setLoading(false); - } - } - }; +>(({ loading: loadingProp, onClick, onCheckedChange, ...props }, ref) => { + const [handleClick, isLoadingClick] = useAsyncCallback(async (e: React.MouseEvent) => { + await onClick?.(e); + }, [onClick]); - const handleCheckedChange = async (checked: boolean) => { - if (props.onCheckedChange) { - setLoading(true); - try { - await props.onCheckedChange(checked); - } finally { - setLoading(false); - } - } - }; + const [handleCheckedChange, isLoadingCheckedChange] = useAsyncCallback(async (checked: boolean) => { + await onCheckedChange?.(checked); + }, [onCheckedChange]); - const loading = loadingProp ?? isLoading; + const loading = loadingProp || isLoadingClick || isLoadingCheckedChange; return ( - runAsynchronously(handleClick(e))} - onCheckedChange={(checked) => runAsynchronously(handleCheckedChange(checked))} - disabled={loading} - ref={ref} - {...props} - /> + + runAsynchronously(handleClick(e))} + onCheckedChange={(checked) => runAsynchronously(handleCheckedChange(checked))} + disabled={props.disabled || loading} + style={{ + visibility: loading ? "hidden" : "visible", + ...props.style, + }} + /> + + + + ); }); Switch.displayName = "Switch";