small fixes

This commit is contained in:
2026-04-27 17:15:23 -05:00
parent 85b608e3d4
commit 830e52a4fa
2 changed files with 52 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { FiPlus, FiTrash2, FiEdit2, FiUser, FiShield, FiArrowLeft } from 'react-icons/fi';
import Modal from './Modal';
import LoadingOverlay from './LoadingOverlay';
@@ -219,11 +219,47 @@ const AdminForm = ({ target, domains, currentUserEmail, onCancel, onSaved, onToa
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
// Tracks whether the user has manually typed in the email field.
// Once they have, we stop auto-filling to avoid clobbering their input.
const [emailTouched, setEmailTouched] = useState(isEdit);
const emailRef = useRef(null);
const sortedDomainNames = useMemo(
() => [...domains].map((d) => d.domain).sort(),
[domains]
);
// Auto-fill email with "@<domain>" when creating a NEW domain_admin
// and exactly one domain is selected, as long as the user hasn't
// manually edited the email field yet.
useEffect(() => {
if (isEdit) return;
if (role !== 'domain_admin') return;
if (emailTouched) return;
if (allowedDomains.size === 1) {
const [onlyDomain] = [...allowedDomains];
setEmail(`@${onlyDomain}`);
// Place caret before the @ so the user can type the local part directly.
requestAnimationFrame(() => {
const el = emailRef.current;
if (el && document.activeElement === el) {
try { el.setSelectionRange(0, 0); } catch { /* ignore */ }
}
});
} else if (allowedDomains.size === 0) {
setEmail('');
}
}, [allowedDomains, role, isEdit, emailTouched]);
// When the user focuses the email field for the first time after
// an auto-fill, place the caret at position 0 so they can just type.
const handleEmailFocus = () => {
const el = emailRef.current;
if (el && !emailTouched && email.startsWith('@')) {
try { el.setSelectionRange(0, 0); } catch { /* ignore */ }
}
};
const toggleDomain = (d) => {
const next = new Set(allowedDomains);
if (next.has(d)) next.delete(d); else next.add(d);
@@ -297,9 +333,11 @@ const AdminForm = ({ target, domains, currentUserEmail, onCancel, onSaved, onToa
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Email</label>
<input
ref={emailRef}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => { setEmail(e.target.value); setEmailTouched(true); }}
onFocus={handleEmailFocus}
className="input-field"
disabled={isEdit}
placeholder="admin@example.com"
@@ -395,6 +433,11 @@ const AdminForm = ({ target, domains, currentUserEmail, onCancel, onSaved, onToa
))}
</div>
)}
{!isEdit && allowedDomains.size === 1 && !emailTouched && (
<p className="mt-2 text-xs text-gray-500">
Email pre-filled with the selected domain. You can still edit it.
</p>
)}
</div>
)}

View File

@@ -21,11 +21,11 @@ const Modal = ({ open, onClose, title, subtitle, children, size = 'md' }) => {
lg: 'max-w-4xl',
};
// Note: clicking the backdrop does NOT close the modal anymore.
// Use the X button or the Escape key instead. This avoids accidental
// dismissal when the user is in the middle of editing settings.
return (
<div
className="fixed inset-0 z-30 bg-gray-900/40 backdrop-blur-sm flex items-start justify-center p-4 sm:p-8 overflow-y-auto"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<div className="fixed inset-0 z-30 bg-gray-900/40 backdrop-blur-sm flex items-start justify-center p-4 sm:p-8 overflow-y-auto">
<div className={`bg-white rounded-xl shadow-xl border border-gray-200 w-full ${widths[size]} my-8`}>
<div className="flex items-start justify-between px-6 py-4 border-b border-gray-100">
<div>
@@ -36,6 +36,7 @@ const Modal = ({ open, onClose, title, subtitle, children, size = 'md' }) => {
onClick={onClose}
className="text-gray-400 hover:text-gray-700 p-1 rounded-lg hover:bg-gray-100 transition-colors"
aria-label="Close"
title="Close (Esc)"
>
<FiX className="w-5 h-5" />
</button>