Wrap dialogs with ControlledDialog for consistent modal state management

This commit is contained in:
Mike Cao committed 2026-07-30 22:57:49 -07:00
1 parent 6ac2b9377c
commit 0941ea06f9
16 files changed
+396 -329

No files matched your search

@@ -0,0 +1,12 @@
'use client';
import { DialogTrigger } from '@umami/react-zen';
import type { ReactElement } from 'react';
export function ControlledDialog({ children }: { children: ReactElement }) {
return (
<DialogTrigger>
<span hidden />
{children}
</DialogTrigger>
);
}
+15 -12
View File
@@ -1,5 +1,6 @@
import { Dialog, ListItem, ListSeparator, Modal, Select, type SelectProps } from '@umami/react-zen';
import { Fragment, type Key, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { DateDisplay } from '@/components/common/DateDisplay';
import { useMessages, useMobile } from '@/components/hooks';
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
@@ -128,18 +129,20 @@ export function DateFilter({
})}
</Select>
{showPicker && (
<Modal isOpen={true}>
<Dialog>
<DatePickerForm
startDate={startDate}
endDate={endDate}
minDate={new Date(2000, 0, 1)}
maxDate={getMaxSelectableDate()}
onChange={handlePickerChange}
onClose={() => setShowPicker(false)}
/>
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={true}>
<Dialog>
<DatePickerForm
startDate={startDate}
endDate={endDate}
minDate={new Date(2000, 0, 1)}
maxDate={getMaxSelectableDate()}
onChange={handlePickerChange}
onClose={() => setShowPicker(false)}
/>
</Dialog>
</Modal>
</ControlledDialog>
)}
</>
);
+10 -7
View File
@@ -7,6 +7,7 @@ import {
Modal,
} from '@umami/react-zen';
import type { CSSProperties, ReactNode } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { IconLabel } from '@/components/common/IconLabel';
import { useMobile } from '@/components/hooks';
@@ -64,13 +65,15 @@ export function DialogButton({
if (isOpen !== undefined) {
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
placement={isMobile ? 'fullscreen' : 'center'}
>
{dialog}
</Modal>
<ControlledDialog>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
placement={isMobile ? 'fullscreen' : 'center'}
>
{dialog}
</Modal>
</ControlledDialog>
);
}
+13 -10
View File
@@ -11,6 +11,7 @@ import {
Text,
} from '@umami/react-zen';
import { type ReactNode, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useMessages } from '@/components/hooks';
import { useDeleteQuery } from '@/components/hooks/queries/useDeleteQuery';
import { Edit, MoreHorizontal, Trash } from '@/components/icons';
@@ -103,16 +104,18 @@ export function ReportEditButton({
>
{children}
</DialogButton>
<Modal isOpen={showDelete} onOpenChange={open => !open && handleClose()}>
<AlertDialog
title={t(labels.delete)}
onConfirm={handleDelete}
onCancel={handleClose}
isDanger
>
<Row gap="1">{t(messages.confirmDelete, { target: name })}</Row>
</AlertDialog>
</Modal>
<ControlledDialog>
<Modal isOpen={showDelete} onOpenChange={open => !open && handleClose()}>
<AlertDialog
title={t(labels.delete)}
onConfirm={handleDelete}
onCancel={handleClose}
isDanger
>
<Row gap="1">{t(messages.confirmDelete, { target: name })}</Row>
</AlertDialog>
</Modal>
</ControlledDialog>
</>
);
}
+51 -38
View File
@@ -1,7 +1,18 @@
'use client';
import { Button, Column, Dialog, Modal, Row, Text } from '@umami/react-zen';
import { Form, FormButtons, FormField, FormSubmitButton, PasswordField } from '@umami/react-zen';
import {
Button,
Column,
Dialog,
Form,
FormButtons,
FormField,
FormSubmitButton,
Modal,
PasswordField,
Text,
} from '@umami/react-zen';
import { useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { OtpInput } from '@/components/common/OtpInput';
import { useMessages, useUpdateQuery } from '@/components/hooks';
@@ -24,41 +35,43 @@ export function TwoFactorDisableModal({ onClose, onSuccess }: TwoFactorDisableMo
};
return (
<Modal isOpen={true} onOpenChange={onClose}>
<Dialog title={t(labels.twoFactorDisableTitle)} style={{ width: 420 }}>
{({ close }) => (
<Column gap="4">
<Text>{t(messages.twoFactorDisableDescription)}</Text>
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
<FormField label={t(labels.password)} name="password">
<PasswordField autoComplete="current-password" onChange={resetError} />
</FormField>
<Column gap="1">
<Text size="sm">{t(labels.twoFactorEnterCode)}</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
resetError();
}}
disabled={isPending}
/>
</Column>
<FormButtons>
<Button variant="outline" onPress={close} isDisabled={isPending}>
{t(labels.cancel)}
</Button>
<FormSubmitButton
variant="danger"
isDisabled={isPending || otpValue.length !== 6 || !!error}
>
{t(labels.twoFactorDisable)}
</FormSubmitButton>
</FormButtons>
</Form>
</Column>
)}
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={true} onOpenChange={onClose}>
<Dialog title={t(labels.twoFactorDisableTitle)} style={{ width: 420 }}>
{({ close }) => (
<Column gap="4">
<Text>{t(messages.twoFactorDisableDescription)}</Text>
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
<FormField label={t(labels.password)} name="password">
<PasswordField autoComplete="current-password" onChange={resetError} />
</FormField>
<Column gap="1">
<Text size="sm">{t(labels.twoFactorEnterCode)}</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
resetError();
}}
disabled={isPending}
/>
</Column>
<FormButtons>
<Button variant="outline" onPress={close} isDisabled={isPending}>
{t(labels.cancel)}
</Button>
<FormSubmitButton
variant="danger"
isDisabled={isPending || otpValue.length !== 6 || !!error}
>
{t(labels.twoFactorDisable)}
</FormSubmitButton>
</FormButtons>
</Form>
</Column>
)}
</Dialog>
</Modal>
</ControlledDialog>
);
}
+92 -89
View File
@@ -19,6 +19,7 @@ import {
} from '@umami/react-zen';
import { LucideCopy } from 'lucide-react';
import { type ReactNode, useEffect, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { OtpInput } from '@/components/common/OtpInput';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import styles from './TwoFactorSetupModal.module.css';
@@ -123,96 +124,98 @@ export function TwoFactorSetupModal({ required, onClose }: TwoFactorSetupModalPr
const preventDismiss = required ? () => {} : undefined;
return (
<Modal isOpen={true} onOpenChange={preventDismiss}>
<Dialog title={t(labels.twoFactorSetupTitle)} className={styles.twoFactorSetupModal}>
{({ close }) => (
<Column gap="9">
<Text>
{required && t(messages.twoFactorSetupRequiredDescription) + ' '}
{t(messages.twoFactorSetupDescription)}
</Text>
<ControlledDialog>
<Modal isOpen={true} onOpenChange={preventDismiss}>
<Dialog title={t(labels.twoFactorSetupTitle)} className={styles.twoFactorSetupModal}>
{() => (
<Column gap="9">
<Text>
{required && `${t(messages.twoFactorSetupRequiredDescription)} `}
{t(messages.twoFactorSetupDescription)}
</Text>
{/* Step 1 - Scan QR Code */}
<Step
tag={t(labels.twoFactorStep1)}
title={t(labels.twoFactorScanQr)}
details={t(messages.twoFactorStep1Description)}
>
<Box padding="2" shadow="lg" borderRadius="lg" border width="fit">
<Row alignItems="center" gap="2">
{qrCodeDataUrl && (
<Image
src={qrCodeDataUrl}
alt="QR code"
className={styles.qrCodeImage}
borderRadius="lg"
/>
)}
<Column>
<Text weight="bold">{t(labels.twoFactorCantScan)}</Text>
<Column gap="2">
<Text size="sm">{t(labels.twoFactorManualEntry)}</Text>
<Code>{manualKey}</Code>
<div>
<Button variant="outline" onPress={handleCopy}>
<Icon size="sm">
<LucideCopy />
</Icon>
{copied ? t(labels.twoFactorCodeCopied) : t(labels.twoFactorCopyCode)}
</Button>
</div>
</Column>
</Column>
</Row>
</Box>
</Step>
{/* Step 2 - Enter Verification Code */}
<Step
tag={t(labels.twoFactorStep2)}
title={t(labels.twoFactorGetCode)}
details={t(messages.twoFactorStep2Description)}
>
<Column gap="3.5">
<Text size="sm" weight="bold">
{t(labels.twoFactorEnterCode)}
</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
if (error) setError(null);
}}
onComplete={handleConfirm}
disabled={isConfirming}
/>
</Column>
</Step>
{error && (
<Alert variant="danger">
<AlertTitle>{t(messages.error)}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Row gap="2" justifyContent="flex-end">
{!required && (
<Button variant="outline" onPress={handleCancel} isDisabled={isConfirming}>
{t(labels.cancel)}
</Button>
)}
<Button
variant="primary"
onPress={() => handleConfirm()}
isDisabled={otpValue.length !== 6 || isConfirming || !!error}
{/* Step 1 - Scan QR Code */}
<Step
tag={t(labels.twoFactorStep1)}
title={t(labels.twoFactorScanQr)}
details={t(messages.twoFactorStep1Description)}
>
{t(labels.confirm)}
</Button>
</Row>
</Column>
)}
</Dialog>
</Modal>
<Box padding="2" shadow="lg" borderRadius="lg" border width="fit">
<Row alignItems="center" gap="2">
{qrCodeDataUrl && (
<Image
src={qrCodeDataUrl}
alt="QR code"
className={styles.qrCodeImage}
borderRadius="lg"
/>
)}
<Column>
<Text weight="bold">{t(labels.twoFactorCantScan)}</Text>
<Column gap="2">
<Text size="sm">{t(labels.twoFactorManualEntry)}</Text>
<Code>{manualKey}</Code>
<div>
<Button variant="outline" onPress={handleCopy}>
<Icon size="sm">
<LucideCopy />
</Icon>
{copied ? t(labels.twoFactorCodeCopied) : t(labels.twoFactorCopyCode)}
</Button>
</div>
</Column>
</Column>
</Row>
</Box>
</Step>
{/* Step 2 - Enter Verification Code */}
<Step
tag={t(labels.twoFactorStep2)}
title={t(labels.twoFactorGetCode)}
details={t(messages.twoFactorStep2Description)}
>
<Column gap="3.5">
<Text size="sm" weight="bold">
{t(labels.twoFactorEnterCode)}
</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
if (error) setError(null);
}}
onComplete={handleConfirm}
disabled={isConfirming}
/>
</Column>
</Step>
{error && (
<Alert variant="danger">
<AlertTitle>{t(messages.error)}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Row gap="2" justifyContent="flex-end">
{!required && (
<Button variant="outline" onPress={handleCancel} isDisabled={isConfirming}>
{t(labels.cancel)}
</Button>
)}
<Button
variant="primary"
onPress={() => handleConfirm()}
isDisabled={otpValue.length !== 6 || isConfirming || !!error}
>
{t(labels.confirm)}
</Button>
</Row>
</Column>
)}
</Dialog>
</Modal>
</ControlledDialog>
);
}
+43 -40
View File
@@ -1,6 +1,7 @@
'use client';
import { Button, Column, Dialog, Modal, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useMessages } from '@/components/hooks';
import styles from './TwoFactorSuccessModal.module.css';
@@ -30,51 +31,53 @@ export function TwoFactorSuccessModal({ backupCodes, onClose }: TwoFactorSuccess
};
return (
<Modal isOpen={true}>
<Dialog title={t(labels.twoFactorSuccessTitle)} className={styles.dialog}>
{() => (
<Column gap="5">
<Text>{t(messages.twoFactorEnabledDescription)}</Text>
<ControlledDialog>
<Modal isOpen={true}>
<Dialog title={t(labels.twoFactorSuccessTitle)} className={styles.dialog}>
{() => (
<Column gap="5">
<Text>{t(messages.twoFactorEnabledDescription)}</Text>
<Column gap="2">
<Text weight="bold">{t(labels.twoFactorSaveBackupCodes)}</Text>
<Text>{t(messages.twoFactorBackupDescription)}</Text>
<Column gap="2">
<Text weight="bold">{t(labels.twoFactorSaveBackupCodes)}</Text>
<Text>{t(messages.twoFactorBackupDescription)}</Text>
<div className={styles.codesGrid}>
{backupCodes.map((code, i) => (
<code key={i} className={styles.code}>
{code}
</code>
))}
</div>
<div className={styles.codesGrid}>
{backupCodes.map((code, i) => (
<code key={i} className={styles.code}>
{code}
</code>
))}
</div>
<Row>
<Button variant="outline" onPress={handleDownload}>
{t(labels.twoFactorDownloadCodes)}
<Row>
<Button variant="outline" onPress={handleDownload}>
{t(labels.twoFactorDownloadCodes)}
</Button>
</Row>
</Column>
<Row gap="2" alignItems="center">
<input
type="checkbox"
id="saved-codes"
checked={saved}
onChange={e => setSaved(e.target.checked)}
/>
<label htmlFor="saved-codes">
<Text>{t(labels.twoFactorSavedCodes)}</Text>
</label>
</Row>
<Row justifyContent="flex-end">
<Button variant="primary" onPress={onClose} isDisabled={!saved}>
{t(labels.close)}
</Button>
</Row>
</Column>
<Row gap="2" alignItems="center">
<input
type="checkbox"
id="saved-codes"
checked={saved}
onChange={e => setSaved(e.target.checked)}
/>
<label htmlFor="saved-codes">
<Text>{t(labels.twoFactorSavedCodes)}</Text>
</label>
</Row>
<Row justifyContent="flex-end">
<Button variant="primary" onPress={onClose} isDisabled={!saved}>
{t(labels.close)}
</Button>
</Row>
</Column>
)}
</Dialog>
</Modal>
)}
</Dialog>
</Modal>
</ControlledDialog>
);
}