Wrap dialogs with ControlledDialog for consistent modal state management
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user