From 44c5b3847bad51028efe0d71d1cc1078eb00f6b6 Mon Sep 17 00:00:00 2001 From: Robin Meis Date: Tue, 15 Sep 2026 13:41:54 +0200 Subject: [PATCH 1/3] Add a plain-text field to the template editor console UI EmailTemplate.Text already existed on the API and TS type (and, after the multipart fix, is now actually sent), but nothing in the console ever read or wrote it - there was no way to set a template's text/plain alternative without calling templates.update by hand. Adds an optional "Plain text" field to the main template editor's Settings tab (reusing the existing Liquid-tag validator used by subject/subject_preview), the same field per language in the Translations tab (including reset-to-default), and a read-only "Plain Text" tab in the template preview drawer, shown only when the field is set. --- .../templates/CreateTemplateDrawer.tsx | 37 +++ .../templates/TemplatePreviewDrawer.tsx | 12 + .../TemplateTranslationsTab.test.tsx | 1 + .../templates/TemplateTranslationsTab.tsx | 18 +- console/src/i18n/locales/ca.js | 2 +- console/src/i18n/locales/ca.po | 246 ++++++++++-------- console/src/i18n/locales/de.js | 2 +- console/src/i18n/locales/de.po | 246 ++++++++++-------- console/src/i18n/locales/en.js | 2 +- console/src/i18n/locales/en.po | 246 ++++++++++-------- console/src/i18n/locales/es.js | 2 +- console/src/i18n/locales/es.po | 246 ++++++++++-------- console/src/i18n/locales/fr.js | 2 +- console/src/i18n/locales/fr.po | 246 ++++++++++-------- console/src/i18n/locales/it.js | 2 +- console/src/i18n/locales/it.po | 246 ++++++++++-------- console/src/i18n/locales/ja.js | 2 +- console/src/i18n/locales/ja.po | 246 ++++++++++-------- console/src/i18n/locales/pt-BR.js | 2 +- console/src/i18n/locales/pt-BR.po | 246 ++++++++++-------- 20 files changed, 1179 insertions(+), 873 deletions(-) diff --git a/console/src/components/templates/CreateTemplateDrawer.tsx b/console/src/components/templates/CreateTemplateDrawer.tsx index 55cd7197..16224af4 100644 --- a/console/src/components/templates/CreateTemplateDrawer.tsx +++ b/console/src/components/templates/CreateTemplateDrawer.tsx @@ -260,6 +260,7 @@ export function CreateTemplateDrawer({ const senderID = Form.useWatch(['email', 'sender_id'], form) const emailSubject = Form.useWatch(['email', 'subject'], form) const emailPreview = Form.useWatch(['email', 'subject_preview'], form) + const emailText = Form.useWatch(['email', 'text'], form) const watchedCategory = Form.useWatch(['category'], form) const categoryValue = forceCategory || watchedCategory @@ -371,6 +372,7 @@ export function CreateTemplateDrawer({ enabled: true, subject: trans.email?.subject || '', subjectPreview: trans.email?.subject_preview || '', + text: trans.email?.text || undefined, visualEditorTree: trans.email?.visual_editor_tree ? (typeof trans.email.visual_editor_tree === 'object' ? (JSON.parse(JSON.stringify(trans.email.visual_editor_tree)) as EmailBlock) @@ -397,6 +399,7 @@ export function CreateTemplateDrawer({ reply_to: latest.email?.reply_to || undefined, subject: latest.email?.subject || '', subject_preview: latest.email?.subject_preview || '', + text: latest.email?.text || '', content: latest.email?.visual_editor_tree || '', visual_editor_tree: latest.email?.visual_editor_tree || createDefaultBlocks() }, @@ -462,6 +465,7 @@ export function CreateTemplateDrawer({ reply_to: template.email?.reply_to || undefined, subject: template.email?.subject || '', subject_preview: template.email?.subject_preview || '', + text: template.email?.text || '', content: template.email?.visual_editor_tree || '', visual_editor_tree: template.email?.visual_editor_tree || createDefaultBlocks() }, @@ -489,6 +493,7 @@ export function CreateTemplateDrawer({ reply_to: fromTemplate.email?.reply_to || undefined, subject: fromTemplate.email?.subject || '', subject_preview: fromTemplate.email?.subject_preview || '', + text: fromTemplate.email?.text || '', content: fromTemplate.email?.visual_editor_tree || '', visual_editor_tree: fromTemplate.email?.visual_editor_tree || createDefaultBlocks() }, @@ -714,6 +719,9 @@ export function CreateTemplateDrawer({ subject: state.subject, subject_preview: state.subjectPreview || '' } + if (state.text) { + emailTranslation.text = state.text + } if (editorMode === 'code') { emailTranslation.editor_mode = 'code' emailTranslation.mjml_source = state.mjmlSource || '' @@ -982,6 +990,34 @@ export function CreateTemplateDrawer({ + +
{t`Plain text alternative`}
+ { + const validation = validateLiquidTags(value) + if (!validation.isValid) { + return Promise.reject(new Error(validation.error)) + } + return Promise.resolve() + } + } + ]} + > + + @@ -1158,6 +1194,7 @@ export function CreateTemplateDrawer({ }} defaultSubject={emailSubject} defaultSubjectPreview={emailPreview} + defaultText={emailText} defaultVisualEditorTree={visualEditorTree} defaultMjmlSource={mjmlSource} testData={form.getFieldValue('test_data')} diff --git a/console/src/components/templates/TemplatePreviewDrawer.tsx b/console/src/components/templates/TemplatePreviewDrawer.tsx index 62a7f826..166bd599 100644 --- a/console/src/components/templates/TemplatePreviewDrawer.tsx +++ b/console/src/components/templates/TemplatePreviewDrawer.tsx @@ -232,6 +232,18 @@ const TemplatePreviewDrawer: React.FC = ({ children: }) + if (record.email?.text) { + items.push({ + key: '5', + label: t`Plain Text`, + children: ( +
+          {record.email.text}
+        
+ ) + }) + } + // Only transactional sends carry metadata, so this tab is pushed only when there is // something in it — an always-present tab reading {} on every broadcast preview is // worse than no tab. The table column, not this, is what guarantees metadata is diff --git a/console/src/components/templates/TemplateTranslationsTab.test.tsx b/console/src/components/templates/TemplateTranslationsTab.test.tsx index 64b90801..0a785bc5 100644 --- a/console/src/components/templates/TemplateTranslationsTab.test.tsx +++ b/console/src/components/templates/TemplateTranslationsTab.test.tsx @@ -42,6 +42,7 @@ const renderTab = ( onTranslationsStateChange={vi.fn()} defaultSubject="Hello" defaultSubjectPreview="" + defaultText="" defaultVisualEditorTree={tree} defaultMjmlSource="" onTestDataChange={vi.fn()} diff --git a/console/src/components/templates/TemplateTranslationsTab.tsx b/console/src/components/templates/TemplateTranslationsTab.tsx index 6dcc9ff6..a207c544 100644 --- a/console/src/components/templates/TemplateTranslationsTab.tsx +++ b/console/src/components/templates/TemplateTranslationsTab.tsx @@ -18,6 +18,7 @@ export interface TranslationEditorState { enabled: boolean subject: string subjectPreview: string + text?: string visualEditorTree?: EmailBlock mjmlSource?: string } @@ -29,6 +30,7 @@ interface TemplateTranslationsTabProps { onTranslationsStateChange: (state: Record) => void defaultSubject: string defaultSubjectPreview: string + defaultText: string defaultVisualEditorTree: EmailBlock defaultMjmlSource: string testData?: Record @@ -44,6 +46,7 @@ const TemplateTranslationsTab: React.FC = ({ onTranslationsStateChange, defaultSubject, defaultSubjectPreview, + defaultText, defaultVisualEditorTree, defaultMjmlSource, testData, @@ -85,7 +88,7 @@ const TemplateTranslationsTab: React.FC = ({ (lang: string, checked: boolean) => { if (checked) { const existing = translationsState[lang] - if (existing && (existing.subject || existing.subjectPreview || existing.visualEditorTree || existing.mjmlSource)) { + if (existing && (existing.subject || existing.subjectPreview || existing.text || existing.visualEditorTree || existing.mjmlSource)) { // Re-enable with existing data updateLangState(lang, { enabled: true }) } else { @@ -94,6 +97,7 @@ const TemplateTranslationsTab: React.FC = ({ enabled: true, subject: defaultSubject || '', subjectPreview: defaultSubjectPreview || '', + text: defaultText || undefined, visualEditorTree: editorMode === 'visual' ? (JSON.parse(JSON.stringify(defaultVisualEditorTree)) as EmailBlock) @@ -110,6 +114,7 @@ const TemplateTranslationsTab: React.FC = ({ updateLangState, defaultSubject, defaultSubjectPreview, + defaultText, defaultVisualEditorTree, defaultMjmlSource, editorMode @@ -242,6 +247,16 @@ const TemplateTranslationsTab: React.FC = ({ placeholder={t`Preview text`} /> +
+ + updateLangState(lang, { text: e.target.value })} + placeholder={t`Optional — plain-text version for this language`} + autoSize={{ minRows: 4, maxRows: 10 }} + /> +
{/* display:flex overrides the compact default inline-flex so the block button can fill the row */}