compose and add-account are real forms, so enter submits them
Compose's To/Cc/Bcc/Subject fields, and each of the three add-account forms, sat in a plain div — enter did nothing and the button was the only way in. Wrapped each in a <form onSubmit>, marked every button that isn't the submit action type="button" (attach, cancel, cc/bcc toggles, the recipient-suggestion picks), so none of them accidentally submit now that a form exists around them. The compose body stays a contenteditable div, which never triggers native enter-to-submit, so Cmd/Ctrl+Enter wasn't needed to keep newlines working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+121
-100
@@ -70,6 +70,7 @@ const RecipientInput = ({
|
|||||||
{suggestions.map((s) => (
|
{suggestions.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.address}
|
key={s.address}
|
||||||
|
type="button"
|
||||||
onMouseDown={(ev) => {
|
onMouseDown={(ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
pick(s.address);
|
pick(s.address);
|
||||||
@@ -215,7 +216,10 @@ export const ComposeModal = () => {
|
|||||||
setDraft(null);
|
setDraft(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const send = async () => {
|
// The body is a contenteditable div, not an <input>/<textarea>, so it never triggers a native
|
||||||
|
// Enter-to-submit — only the To/Cc/Bcc/Subject fields do, which is the point.
|
||||||
|
const send = async (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
if (!to.trim()) {
|
if (!to.trim()) {
|
||||||
toast.error('Add at least one recipient');
|
toast.error('Add at least one recipient');
|
||||||
return;
|
return;
|
||||||
@@ -286,115 +290,132 @@ export const ComposeModal = () => {
|
|||||||
Drop images to inline · other files attach
|
Drop images to inline · other files attach
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center border-b px-4 py-2.5 pr-10">
|
{/* A real <form>: Enter in To/Cc/Bcc/Subject sends, matching every other single-line field.
|
||||||
<DialogTitle className="text-sm font-semibold">{/^re:/i.test(subject) ? 'Reply' : 'New message'}</DialogTitle>
|
`contents` keeps it out of the flex layout — the fields around it don't know it's there. */}
|
||||||
</div>
|
<form onSubmit={send} className="contents">
|
||||||
|
<div className="flex items-center border-b px-4 py-2.5 pr-10">
|
||||||
|
<DialogTitle className="text-sm font-semibold">
|
||||||
|
{/^re:/i.test(subject) ? 'Reply' : 'New message'}
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center border-b">
|
|
||||||
<span className="pl-4 text-xs opacity-50">To</span>
|
|
||||||
<div className="flex-1">
|
|
||||||
<RecipientInput value={to} onChange={setTo} placeholder="Recipients" autoFocus />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 px-3">
|
|
||||||
{!showCc && (
|
|
||||||
<button onClick={() => setShowCc(true)} className="text-xs opacity-50 hover:opacity-100 cursor-pointer">
|
|
||||||
Cc
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!showBcc && (
|
|
||||||
<button onClick={() => setShowBcc(true)} className="text-xs opacity-50 hover:opacity-100 cursor-pointer">
|
|
||||||
Bcc
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showCc && (
|
|
||||||
<div className="flex items-center border-b">
|
<div className="flex items-center border-b">
|
||||||
<span className="pl-4 text-xs opacity-50">Cc</span>
|
<span className="pl-4 text-xs opacity-50">To</span>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<RecipientInput value={cc} onChange={setCc} placeholder="Cc recipients" />
|
<RecipientInput value={to} onChange={setTo} placeholder="Recipients" autoFocus />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2 px-3">
|
||||||
)}
|
{!showCc && (
|
||||||
{showBcc && (
|
|
||||||
<div className="flex items-center border-b">
|
|
||||||
<span className="pl-4 text-xs opacity-50">Bcc</span>
|
|
||||||
<div className="flex-1">
|
|
||||||
<RecipientInput value={bcc} onChange={setBcc} placeholder="Bcc recipients" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
value={subject}
|
|
||||||
onChange={(ev) => setSubject(ev.target.value)}
|
|
||||||
placeholder="Subject"
|
|
||||||
className="border-b bg-transparent px-4 py-2 text-sm outline-none placeholder:opacity-40"
|
|
||||||
/>
|
|
||||||
<div className="relative flex-1 overflow-hidden">
|
|
||||||
{bodyEmpty && (
|
|
||||||
<div className="pointer-events-none absolute left-4 top-3 text-sm opacity-40">Write your message…</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
ref={editorRef}
|
|
||||||
contentEditable
|
|
||||||
onInput={refreshEmpty}
|
|
||||||
onPaste={onEditorPaste}
|
|
||||||
className="h-full overflow-y-auto px-4 py-3 text-sm outline-none [&_img]:my-1 [&_img]:rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{files.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5 border-t px-4 py-2">
|
|
||||||
{files.map((f, i) => (
|
|
||||||
<span key={`${f.name}-${i}`} className="flex items-center gap-1.5 rounded bg-accent px-2 py-1 text-xs">
|
|
||||||
<Paperclip className="h-3 w-3 opacity-60" />
|
|
||||||
<span className="max-w-[16rem] truncate">{f.name}</span>
|
|
||||||
<span className="opacity-50">{fmtSize(f.size)}</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
type="button"
|
||||||
className="opacity-50 hover:opacity-100 cursor-pointer"
|
onClick={() => setShowCc(true)}
|
||||||
title="Remove"
|
className="text-xs opacity-50 hover:opacity-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
Cc
|
||||||
</button>
|
</button>
|
||||||
</span>
|
)}
|
||||||
))}
|
{!showBcc && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowBcc(true)}
|
||||||
|
className="text-xs opacity-50 hover:opacity-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
Bcc
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{showCc && (
|
||||||
|
<div className="flex items-center border-b">
|
||||||
<div className="flex items-center justify-end gap-2 border-t px-4 py-2.5">
|
<span className="pl-4 text-xs opacity-50">Cc</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<RecipientInput value={cc} onChange={setCc} placeholder="Cc recipients" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showBcc && (
|
||||||
|
<div className="flex items-center border-b">
|
||||||
|
<span className="pl-4 text-xs opacity-50">Bcc</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<RecipientInput value={bcc} onChange={setBcc} placeholder="Bcc recipients" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
ref={fileInput}
|
value={subject}
|
||||||
type="file"
|
onChange={(ev) => setSubject(ev.target.value)}
|
||||||
multiple
|
placeholder="Subject"
|
||||||
className="hidden"
|
className="border-b bg-transparent px-4 py-2 text-sm outline-none placeholder:opacity-40"
|
||||||
onChange={(ev) => {
|
|
||||||
if (ev.target.files?.length) addAttachments(ev.target.files);
|
|
||||||
ev.target.value = '';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<div className="relative flex-1 overflow-hidden">
|
||||||
onClick={() => fileInput.current?.click()}
|
{bodyEmpty && (
|
||||||
className="mr-auto flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
<div className="pointer-events-none absolute left-4 top-3 text-sm opacity-40">Write your message…</div>
|
||||||
title="Attach files"
|
)}
|
||||||
>
|
<div
|
||||||
<Paperclip className="h-4 w-4" />
|
ref={editorRef}
|
||||||
</button>
|
contentEditable
|
||||||
<button
|
onInput={refreshEmpty}
|
||||||
onClick={close}
|
onPaste={onEditorPaste}
|
||||||
className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
className="h-full overflow-y-auto px-4 py-3 text-sm outline-none [&_img]:my-1 [&_img]:rounded"
|
||||||
>
|
/>
|
||||||
Cancel
|
</div>
|
||||||
</button>
|
|
||||||
<button
|
{files.length > 0 && (
|
||||||
onClick={send}
|
<div className="flex flex-wrap gap-1.5 border-t px-4 py-2">
|
||||||
disabled={sending}
|
{files.map((f, i) => (
|
||||||
className="flex items-center gap-1.5 rounded-md bg-duck-teal px-4 py-1.5 text-sm font-medium text-white hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
|
<span key={`${f.name}-${i}`} className="flex items-center gap-1.5 rounded bg-accent px-2 py-1 text-xs">
|
||||||
>
|
<Paperclip className="h-3 w-3 opacity-60" />
|
||||||
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
<span className="max-w-[16rem] truncate">{f.name}</span>
|
||||||
Send
|
<span className="opacity-50">{fmtSize(f.size)}</span>
|
||||||
</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
||||||
|
className="opacity-50 hover:opacity-100 cursor-pointer"
|
||||||
|
title="Remove"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 border-t px-4 py-2.5">
|
||||||
|
<input
|
||||||
|
ref={fileInput}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={(ev) => {
|
||||||
|
if (ev.target.files?.length) addAttachments(ev.target.files);
|
||||||
|
ev.target.value = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInput.current?.click()}
|
||||||
|
className="mr-auto flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||||
|
title="Attach files"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={sending}
|
||||||
|
className="flex items-center gap-1.5 rounded-md bg-duck-teal px-4 py-1.5 text-sm font-medium text-white hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
+22
-13
@@ -85,7 +85,8 @@ export const EmailAccounts = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGmailOAuth = async () => {
|
const handleGmailOAuth = async (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const status = await client.get<{ connected: boolean; email: string | null }>('/integrations/google/status');
|
const status = await client.get<{ connected: boolean; email: string | null }>('/integrations/google/status');
|
||||||
@@ -116,7 +117,8 @@ export const EmailAccounts = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordSubmit = async () => {
|
const handlePasswordSubmit = async (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
if (!form.email.trim() || !form.password.trim()) return;
|
if (!form.email.trim() || !form.password.trim()) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
@@ -304,7 +306,10 @@ export const EmailAccounts = () => {
|
|||||||
|
|
||||||
{/* Gmail OAuth form */}
|
{/* Gmail OAuth form */}
|
||||||
{activeForm === 'gmail-oauth' && (
|
{activeForm === 'gmail-oauth' && (
|
||||||
<div className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
|
<form
|
||||||
|
onSubmit={handleGmailOAuth}
|
||||||
|
className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
|
||||||
|
>
|
||||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (OAuth)</p>
|
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (OAuth)</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -317,16 +322,19 @@ export const EmailAccounts = () => {
|
|||||||
<Button type="button" variant="ghost" onClick={resetForm} className="h-9 cursor-pointer">
|
<Button type="button" variant="ghost" onClick={resetForm} className="h-9 cursor-pointer">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" onClick={handleGmailOAuth} disabled={saving} className="h-9 flex-1 cursor-pointer">
|
<Button type="submit" disabled={saving} className="h-9 flex-1 cursor-pointer">
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Connect & Add'}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Connect & Add'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Gmail App Password form */}
|
{/* Gmail App Password form */}
|
||||||
{activeForm === 'gmail-password' && (
|
{activeForm === 'gmail-password' && (
|
||||||
<div className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
|
<form
|
||||||
|
onSubmit={handlePasswordSubmit}
|
||||||
|
className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
|
||||||
|
>
|
||||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (App Password)</p>
|
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (App Password)</p>
|
||||||
<div className="text-xs text-duck-dark/50 dark:text-foreground/50 grid gap-1.5">
|
<div className="text-xs text-duck-dark/50 dark:text-foreground/50 grid gap-1.5">
|
||||||
<p>To create an App Password:</p>
|
<p>To create an App Password:</p>
|
||||||
@@ -373,20 +381,22 @@ export const EmailAccounts = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={handlePasswordSubmit}
|
|
||||||
disabled={!form.email.trim() || !form.password.trim() || saving}
|
disabled={!form.email.trim() || !form.password.trim() || saving}
|
||||||
className="h-9 flex-1 cursor-pointer"
|
className="h-9 flex-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Generic IMAP form */}
|
{/* Generic IMAP form */}
|
||||||
{activeForm === 'imap' && (
|
{activeForm === 'imap' && (
|
||||||
<div className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
|
<form
|
||||||
|
onSubmit={handlePasswordSubmit}
|
||||||
|
className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
|
||||||
|
>
|
||||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add IMAP Account</p>
|
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add IMAP Account</p>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
@@ -439,15 +449,14 @@ export const EmailAccounts = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={handlePasswordSubmit}
|
|
||||||
disabled={!form.email.trim() || !form.password.trim() || !form.imapHost.trim() || saving}
|
disabled={!form.email.trim() || !form.password.trim() || !form.imapHost.trim() || saving}
|
||||||
className="h-9 flex-1 cursor-pointer"
|
className="h-9 flex-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add account button */}
|
{/* Add account button */}
|
||||||
|
|||||||
Reference in New Issue
Block a user