fix(web): improve ConfirmDialog reliability

- Fixed potential undefined username toast in MemberSection by storing username locally before clearing target state
- Standardized data-loading attribute on confirm button to use boolean value directly
pull/5111/head
Nic Luckie 3 weeks ago
parent 9beb6ca5c1
commit 72fcbf7e10

@ -1,69 +0,0 @@
import DOMPurify from "dompurify";
import { marked } from "marked";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
export interface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: React.ReactNode;
description?: React.ReactNode;
descriptionMarkdown?: string;
confirmLabel: string;
cancelLabel: string;
onConfirm: () => void | Promise<void>;
confirmVariant?: "default" | "destructive";
}
export default function ConfirmDialog({
open,
onOpenChange,
title,
description,
descriptionMarkdown,
confirmLabel,
cancelLabel,
onConfirm,
confirmVariant = "default",
}: ConfirmDialogProps) {
const [loading, setLoading] = React.useState(false);
const handleConfirm = async () => {
try {
setLoading(true);
await onConfirm();
onOpenChange(false);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
// prepare sanitized HTML if Markdown was provided
const descriptionHtml = typeof descriptionMarkdown === "string" ? DOMPurify.sanitize(String(marked.parse(descriptionMarkdown))) : null;
return (
<Dialog open={open} onOpenChange={(o: boolean) => !loading && onOpenChange(o)}>
<DialogContent size="sm">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{descriptionHtml ? (
<DialogDescription dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : description ? (
<DialogDescription>{description}</DialogDescription>
) : null}
</DialogHeader>
<DialogFooter>
<Button variant="ghost" disabled={loading} onClick={() => onOpenChange(false)}>
{cancelLabel}
</Button>
<Button variant={confirmVariant} disabled={loading} onClick={handleConfirm}>
{confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

@ -3,7 +3,6 @@ import { marked } from "marked";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import styles from "./ConfirmDialog.module.scss";
export interface ConfirmDialogProps {
/** Whether the dialog is open */
@ -67,7 +66,7 @@ export default function ConfirmDialog({
return (
<Dialog open={open} onOpenChange={(o: boolean) => !loading && onOpenChange(o)}>
<DialogContent size="sm" className={styles.container}>
<DialogContent size="sm">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{/*
@ -85,7 +84,7 @@ export default function ConfirmDialog({
<Button variant="ghost" disabled={loading} onClick={() => onOpenChange(false)}>
{cancelLabel}
</Button>
<Button variant={confirmVariant} disabled={loading} onClick={handleConfirm} data-loading={loading ? true : undefined}>
<Button variant={confirmVariant} disabled={loading} onClick={handleConfirm} data-loading={loading}>
{confirmLabel}
</Button>
</DialogFooter>

@ -61,6 +61,7 @@ const MemberSection = observer(() => {
const confirmArchiveUser = async () => {
if (!archiveTarget) return;
const username = archiveTarget.username;
await userServiceClient.updateUser({
user: {
name: archiveTarget.name,
@ -69,8 +70,8 @@ const MemberSection = observer(() => {
updateMask: ["state"],
});
setArchiveTarget(undefined);
toast.success(t("setting.member-section.archive-success", { username: archiveTarget.username }));
fetchUsers();
toast.success(t("setting.member-section.archive-success", { username }));
await fetchUsers();
};
const handleRestoreUserClick = async (user: User) => {
@ -83,7 +84,7 @@ const MemberSection = observer(() => {
updateMask: ["state"],
});
toast.success(t("setting.member-section.restore-success", { username }));
fetchUsers();
await fetchUsers();
};
const handleDeleteUserClick = async (user: User) => {
@ -96,7 +97,7 @@ const MemberSection = observer(() => {
await userStore.deleteUser(name);
setDeleteTarget(undefined);
toast.success(t("setting.member-section.delete-success", { username }));
fetchUsers();
await fetchUsers();
};
return (

Loading…
Cancel
Save