From 15dbb3d3496008e37f00d6f5bb6ad56e9b95f5ee Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 11 Mar 2026 21:07:00 +0100
Subject: [PATCH] feat: update email composer to handle trailing commas and
improve recipient input handling
---
components/email/email-composer.tsx | 326 ++++++++++++++++++-------
components/templates/template-form.tsx | 2 +-
locales/de/common.json | 6 +-
locales/en/common.json | 6 +-
locales/es/common.json | 6 +-
locales/fr/common.json | 6 +-
locales/it/common.json | 6 +-
locales/ja/common.json | 6 +-
locales/nl/common.json | 6 +-
locales/pt/common.json | 6 +-
10 files changed, 262 insertions(+), 114 deletions(-)
diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx
index 84cecb38..4cb42320 100644
--- a/components/email/email-composer.tsx
+++ b/components/email/email-composer.tsx
@@ -81,18 +81,21 @@ export function EmailComposer({
const getInitialTo = () => {
if (!replyTo) return "";
if (mode === 'reply') {
- return replyTo.from?.[0]?.email || "";
+ const email = replyTo.from?.[0]?.email || "";
+ return email ? email + ', ' : "";
} else if (mode === 'replyAll') {
const from = replyTo.from?.[0]?.email || "";
const originalTo = replyTo.to?.filter(r => r.email).map(r => r.email).join(", ") || "";
- return [from, originalTo].filter(Boolean).join(", ");
+ const combined = [from, originalTo].filter(Boolean).join(", ");
+ return combined ? combined + ', ' : "";
}
return "";
};
const getInitialCc = () => {
if (!replyTo || mode !== 'replyAll') return "";
- return replyTo.cc?.map(r => r.email).join(", ") || "";
+ const cc = replyTo.cc?.map(r => r.email).join(", ") || "";
+ return cc ? cc + ', ' : "";
};
const getInitialSubject = () => {
@@ -236,10 +239,12 @@ export function EmailComposer({
const setter = field === 'to' ? setTo : field === 'cc' ? setCc : setBcc;
const getter = field === 'to' ? to : field === 'cc' ? cc : bcc;
- const parts = getter.split(',');
- parts.pop();
- parts.push(` ${email}`);
- setter(parts.join(',').replace(/^,\s*/, ''));
+ const parts = getter.split(',').map(s => s.trim()).filter(Boolean);
+ if (!getter.trimEnd().endsWith(',') && parts.length > 0) {
+ parts.pop();
+ }
+ parts.push(email);
+ setter(parts.join(', ') + ', ');
setAutocompleteResults([]);
setActiveAutoField(null);
setAutoSelectedIndex(-1);
@@ -291,14 +296,14 @@ export function EmailComposer({
setSubject(filledSubject);
setBody(filledBody);
if (template.defaultRecipients?.to?.length) {
- setTo(template.defaultRecipients.to.join(', '));
+ setTo(template.defaultRecipients.to.join(', ') + ', ');
}
if (template.defaultRecipients?.cc?.length) {
- setCc(template.defaultRecipients.cc.join(', '));
+ setCc(template.defaultRecipients.cc.join(', ') + ', ');
setShowCc(true);
}
if (template.defaultRecipients?.bcc?.length) {
- setBcc(template.defaultRecipients.bcc.join(', '));
+ setBcc(template.defaultRecipients.bcc.join(', ') + ', ');
setShowBcc(true);
}
} else {
@@ -715,37 +720,26 @@ export function EmailComposer({
{/* To field */}
{t('to')}:
-
-
{
- setTo(e.target.value);
- if (validationErrors.to) setValidationErrors(prev => ({ ...prev, to: false }));
- handleAutocomplete(e.target.value, 'to');
- }}
- onKeyDown={(e) => handleAutoKeyDown(e, 'to')}
- onBlur={(e) => handleAutoBlur(e, 'to')}
- className={cn(
- "border-0 focus-visible:ring-0 h-8 px-0 text-sm",
- validationErrors.to && "ring-2 ring-red-500 dark:ring-red-400"
- )}
- role="combobox"
- aria-expanded={activeAutoField === 'to' && autocompleteResults.length > 0}
- aria-autocomplete="list"
- aria-controls={activeAutoField === 'to' ? 'autocomplete-to' : undefined}
- aria-activedescendant={activeAutoField === 'to' && autoSelectedIndex >= 0 ? `autocomplete-option-${autoSelectedIndex}` : undefined}
- aria-invalid={validationErrors.to || undefined}
- />
- {validationErrors.to && (
-
{t('validation.recipient_required')}
- )}
- {activeAutoField === 'to' && autocompleteResults.length > 0 && (
-
insertAutocomplete(email, 'to')} />
- )}
-
+
{
+ setTo(v);
+ if (validationErrors.to) setValidationErrors(prev => ({ ...prev, to: false }));
+ }}
+ inputRef={toInputRef}
+ placeholder={t('to_placeholder')}
+ field="to"
+ onAutocomplete={handleAutocomplete}
+ onAutoKeyDown={handleAutoKeyDown}
+ onAutoBlur={handleAutoBlur}
+ activeAutoField={activeAutoField}
+ autocompleteResults={autocompleteResults}
+ autoSelectedIndex={autoSelectedIndex}
+ dropdownRef={toDropdownRef}
+ onInsertAutocomplete={insertAutocomplete}
+ validationError={validationErrors.to}
+ validationMessage={t('validation.recipient_required')}
+ />
)}
@@ -800,29 +786,21 @@ export function EmailComposer({
{showBcc && (
)}
@@ -1047,7 +1025,7 @@ const AutocompleteDropdown = React.forwardRef void;
}>(function AutocompleteDropdown({ id, results, selectedIndex, onSelect }, ref) {
return (
-
+
{results.map((r, i) => (