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 && (
{t('bcc_label')} -
- { - setBcc(e.target.value); - handleAutocomplete(e.target.value, 'bcc'); - }} - onKeyDown={(e) => handleAutoKeyDown(e, 'bcc')} - onBlur={(e) => handleAutoBlur(e, 'bcc')} - className="border-0 focus-visible:ring-0 h-8 px-0 text-sm" - role="combobox" - aria-expanded={activeAutoField === 'bcc' && autocompleteResults.length > 0} - aria-autocomplete="list" - aria-controls={activeAutoField === 'bcc' ? 'autocomplete-bcc' : undefined} - aria-activedescendant={activeAutoField === 'bcc' && autoSelectedIndex >= 0 ? `autocomplete-option-${autoSelectedIndex}` : undefined} - /> - {activeAutoField === 'bcc' && autocompleteResults.length > 0 && ( - insertAutocomplete(email, 'bcc')} /> - )} -
+
)} @@ -1047,7 +1025,7 @@ const AutocompleteDropdown = React.forwardRef void; }>(function AutocompleteDropdown({ id, results, selectedIndex, onSelect }, ref) { return ( -
+
{results.map((r, i) => ( + + ))} + 0} + aria-autocomplete="list" + aria-controls={activeAutoField === field ? `autocomplete-${field}` : undefined} + aria-activedescendant={activeAutoField === field && autoSelectedIndex >= 0 ? `autocomplete-option-${autoSelectedIndex}` : undefined} + aria-invalid={validationError || undefined} + /> +
+ {validationError && validationMessage && ( +

{validationMessage}

+ )} + {activeAutoField === field && autocompleteResults.length > 0 && ( + onInsertAutocomplete(email, field)} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/components/templates/template-form.tsx b/components/templates/template-form.tsx index 1a01338a..355331a5 100644 --- a/components/templates/template-form.tsx +++ b/components/templates/template-form.tsx @@ -275,7 +275,7 @@ function PlaceholderDropdown({ return ( <>
-
+
{BUILT_IN_PLACEHOLDERS.map((p) => (