fix: Rendering on email without html content (#12561)

<img width="983" height="579" alt="image"
src="https://github.com/user-attachments/assets/2972e8d9-5145-4958-8f66-9e84bd9c8c4b"
/>
This commit is contained in:
Shivam Mishra
2025-10-01 13:43:05 +05:30
committed by GitHub
parent 21366e1c3b
commit ecff66146a
4 changed files with 172 additions and 30 deletions

View File

@@ -7,6 +7,7 @@ class Messages::MessageBuilder
@private = params[:private] || false @private = params[:private] || false
@conversation = conversation @conversation = conversation
@user = user @user = user
@account = conversation.account
@message_type = params[:message_type] || 'outgoing' @message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments] @attachments = params[:attachments]
@automation_rule = content_attributes&.dig(:automation_rule_id) @automation_rule = content_attributes&.dig(:automation_rule_id)
@@ -20,7 +21,9 @@ class Messages::MessageBuilder
@message = @conversation.messages.build(message_params) @message = @conversation.messages.build(message_params)
process_attachments process_attachments
process_emails process_emails
process_email_content # When the message has no quoted content, it will just be rendered as a regular message
# The frontend is equipped to handle this case
process_email_content if @account.feature_enabled?(:quoted_email_reply)
@message.save! @message.save!
@message @message
end end

View File

@@ -6,6 +6,7 @@ import { allowedCssProperties } from 'lettersanitizer';
import Icon from 'next/icon/Icon.vue'; import Icon from 'next/icon/Icon.vue';
import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js'; import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js';
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
import BaseBubble from 'next/message/bubbles/Base.vue'; import BaseBubble from 'next/message/bubbles/Base.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import EmailMeta from './EmailMeta.vue'; import EmailMeta from './EmailMeta.vue';
@@ -46,6 +47,22 @@ const originalEmailHtml = computed(
originalEmailText.value originalEmailText.value
); );
const hasEmailContent = computed(() => {
return (
contentAttributes?.value?.email?.textContent?.full ||
contentAttributes?.value?.email?.htmlContent?.full
);
});
const messageContent = computed(() => {
// If translations exist and we're showing translations (not original)
if (hasTranslations.value && !renderOriginal.value) {
return translationContent.value;
}
// Otherwise show original content
return content.value;
});
const textToShow = computed(() => { const textToShow = computed(() => {
// If translations exist and we're showing translations (not original) // If translations exist and we're showing translations (not original)
if (hasTranslations.value && !renderOriginal.value) { if (hasTranslations.value && !renderOriginal.value) {
@@ -126,30 +143,37 @@ const handleSeeOriginal = () => {
{{ $t('EMAIL_HEADER.EXPAND') }} {{ $t('EMAIL_HEADER.EXPAND') }}
</button> </button>
</div> </div>
<Letter <FormattedContent
v-if="showQuotedMessage" v-if="isOutgoing && content && !hasEmailContent"
:key="`letter-quoted-${translationKeySuffix}`" class="text-n-slate-12"
class-name="prose prose-bubble !max-w-none letter-render" :content="messageContent"
:allowed-css-properties="[
...allowedCssProperties,
'transform',
'transform-origin',
]"
:html="fullHTML"
:text="textToShow"
/>
<Letter
v-else
:key="`letter-unquoted-${translationKeySuffix}`"
class-name="prose prose-bubble !max-w-none letter-render"
:html="unquotedHTML"
:allowed-css-properties="[
...allowedCssProperties,
'transform',
'transform-origin',
]"
:text="textToShow"
/> />
<template v-else>
<Letter
v-if="showQuotedMessage"
:key="`letter-quoted-${translationKeySuffix}`"
class-name="prose prose-bubble !max-w-none letter-render"
:allowed-css-properties="[
...allowedCssProperties,
'transform',
'transform-origin',
]"
:html="fullHTML"
:text="textToShow"
/>
<Letter
v-else
:key="`letter-unquoted-${translationKeySuffix}`"
class-name="prose prose-bubble !max-w-none letter-render"
:html="unquotedHTML"
:allowed-css-properties="[
...allowedCssProperties,
'transform',
'transform-origin',
]"
:text="textToShow"
/>
</template>
<button <button
v-if="hasQuotedMessage" v-if="hasQuotedMessage"
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2" class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"

View File

@@ -63,7 +63,7 @@ const toggleExpand = () => {
</div> </div>
<div <div
v-dompurify-html="formattedQuotedEmailText" v-dompurify-html="formattedQuotedEmailText"
class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer pr-8" class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer ltr:pr-8 rtl:pl-8"
:class="{ :class="{
'line-clamp-1': !isExpanded, 'line-clamp-1': !isExpanded,
'max-h-60 overflow-y-auto': isExpanded, 'max-h-60 overflow-y-auto': isExpanded,

View File

@@ -4,7 +4,10 @@ import {
getEmailSenderEmail, getEmailSenderEmail,
getEmailDate, getEmailDate,
formatQuotedEmailDate, formatQuotedEmailDate,
getInboxEmail,
buildQuotedEmailHeader, buildQuotedEmailHeader,
buildQuotedEmailHeaderFromContact,
buildQuotedEmailHeaderFromInbox,
formatQuotedTextAsBlockquote, formatQuotedTextAsBlockquote,
extractQuotedEmailText, extractQuotedEmailText,
truncatePreviewText, truncatePreviewText,
@@ -131,7 +134,40 @@ describe('quotedEmailHelper', () => {
}); });
}); });
describe('buildQuotedEmailHeader', () => { describe('getInboxEmail', () => {
it('returns email from contentAttributes.email.to', () => {
const lastEmail = {
contentAttributes: {
email: { to: ['inbox@example.com'] },
},
};
const result = getInboxEmail(lastEmail, {});
expect(result).toBe('inbox@example.com');
});
it('returns inbox email as fallback', () => {
const lastEmail = {};
const inbox = { email: 'support@example.com' };
const result = getInboxEmail(lastEmail, inbox);
expect(result).toBe('support@example.com');
});
it('returns empty string if no email found', () => {
expect(getInboxEmail({}, {})).toBe('');
});
it('trims whitespace from emails', () => {
const lastEmail = {
contentAttributes: {
email: { to: [' inbox@example.com '] },
},
};
const result = getInboxEmail(lastEmail, {});
expect(result).toBe('inbox@example.com');
});
});
describe('buildQuotedEmailHeaderFromContact', () => {
it('builds complete header with name and email', () => { it('builds complete header with name and email', () => {
const lastEmail = { const lastEmail = {
sender: { name: 'John Doe', email: 'john@example.com' }, sender: { name: 'John Doe', email: 'john@example.com' },
@@ -139,7 +175,7 @@ describe('quotedEmailHelper', () => {
email: { date: '2024-01-15T10:30:00Z' }, email: { date: '2024-01-15T10:30:00Z' },
}, },
}; };
const result = buildQuotedEmailHeader(lastEmail, {}); const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
expect(result).toContain('John Doe'); expect(result).toContain('John Doe');
expect(result).toContain('john@example.com'); expect(result).toContain('john@example.com');
expect(result).toContain('wrote:'); expect(result).toContain('wrote:');
@@ -152,14 +188,93 @@ describe('quotedEmailHelper', () => {
email: { date: '2024-01-15T10:30:00Z' }, email: { date: '2024-01-15T10:30:00Z' },
}, },
}; };
const result = buildQuotedEmailHeader(lastEmail, {}); const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
expect(result).toContain('<john@example.com>'); expect(result).toContain('<john@example.com>');
expect(result).not.toContain('undefined'); expect(result).not.toContain('undefined');
}); });
it('returns empty string if missing required data', () => { it('returns empty string if missing required data', () => {
expect(buildQuotedEmailHeader(null, {})).toBe(''); expect(buildQuotedEmailHeaderFromContact(null, {})).toBe('');
expect(buildQuotedEmailHeader({}, {})).toBe(''); expect(buildQuotedEmailHeaderFromContact({}, {})).toBe('');
});
});
describe('buildQuotedEmailHeaderFromInbox', () => {
it('builds complete header with inbox name and email', () => {
const lastEmail = {
contentAttributes: {
email: {
date: '2024-01-15T10:30:00Z',
to: ['support@example.com'],
},
},
};
const inbox = { name: 'Support Team', email: 'support@example.com' };
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
expect(result).toContain('Support Team');
expect(result).toContain('support@example.com');
expect(result).toContain('wrote:');
});
it('builds header without name if not available', () => {
const lastEmail = {
contentAttributes: {
email: {
date: '2024-01-15T10:30:00Z',
to: ['inbox@example.com'],
},
},
};
const inbox = { email: 'inbox@example.com' };
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
expect(result).toContain('<inbox@example.com>');
expect(result).not.toContain('undefined');
});
it('returns empty string if missing required data', () => {
expect(buildQuotedEmailHeaderFromInbox(null, {})).toBe('');
expect(buildQuotedEmailHeaderFromInbox({}, {})).toBe('');
});
});
describe('buildQuotedEmailHeader', () => {
it('uses inbox email for outgoing messages (message_type: 1)', () => {
const lastEmail = {
message_type: 1,
contentAttributes: {
email: {
date: '2024-01-15T10:30:00Z',
to: ['support@example.com'],
},
},
};
const inbox = { name: 'Support', email: 'support@example.com' };
const contact = { name: 'John Doe', email: 'john@example.com' };
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
expect(result).toContain('Support');
expect(result).toContain('support@example.com');
expect(result).not.toContain('John Doe');
});
it('uses contact email for incoming messages (message_type: 0)', () => {
const lastEmail = {
message_type: 0,
sender: { name: 'Jane Smith', email: 'jane@example.com' },
contentAttributes: {
email: { date: '2024-01-15T10:30:00Z' },
},
};
const inbox = { name: 'Support', email: 'support@example.com' };
const contact = { name: 'Jane Smith', email: 'jane@example.com' };
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
expect(result).toContain('Jane Smith');
expect(result).toContain('jane@example.com');
expect(result).not.toContain('Support');
});
it('returns empty string if missing required data', () => {
expect(buildQuotedEmailHeader(null, {}, {})).toBe('');
expect(buildQuotedEmailHeader({}, {}, {})).toBe('');
}); });
}); });