Fix: keep signature when inserting a template #621
This commit is contained in:
@@ -30,7 +30,7 @@ import { useContactStore, getContactDisplayName, getContactPrimaryEmail } from "
|
|||||||
import { useTemplateStore } from "@/stores/template-store";
|
import { useTemplateStore } from "@/stores/template-store";
|
||||||
import { SubAddressHelper } from "@/components/identity/sub-address-helper";
|
import { SubAddressHelper } from "@/components/identity/sub-address-helper";
|
||||||
import { generateSubAddress } from "@/lib/sub-addressing";
|
import { generateSubAddress } from "@/lib/sub-addressing";
|
||||||
import { substitutePlaceholders } from "@/lib/template-utils";
|
import { substitutePlaceholders, spliceTemplateAboveSignature } from "@/lib/template-utils";
|
||||||
import { TemplatePicker } from "@/components/templates/template-picker";
|
import { TemplatePicker } from "@/components/templates/template-picker";
|
||||||
import { TemplateForm } from "@/components/templates/template-form";
|
import { TemplateForm } from "@/components/templates/template-form";
|
||||||
import type { EmailTemplate } from "@/lib/template-types";
|
import type { EmailTemplate } from "@/lib/template-types";
|
||||||
@@ -1094,7 +1094,16 @@ export function EmailComposer({
|
|||||||
|
|
||||||
if (mode === 'compose') {
|
if (mode === 'compose') {
|
||||||
setSubject(filledSubject);
|
setSubject(filledSubject);
|
||||||
setBody(bodyContent);
|
// Compose bodies carry the embedded signature (see
|
||||||
|
// shouldEmbedSignatureInNewMail) and the send path assumes it stays
|
||||||
|
// there, so replace only the message content, not the signature block.
|
||||||
|
if (plainTextMode) {
|
||||||
|
setBody(shouldEmbedSignatureInNewMail
|
||||||
|
? appendPlainTextSignature(bodyContent, signatureIdentity, { separator: signatureSeparatorEnabled })
|
||||||
|
: bodyContent);
|
||||||
|
} else {
|
||||||
|
setBody((prev) => spliceTemplateAboveSignature(prev, bodyContent));
|
||||||
|
}
|
||||||
if (template.defaultRecipients?.to?.length) {
|
if (template.defaultRecipients?.to?.length) {
|
||||||
setTo(template.defaultRecipients.to.map(parseRecipient));
|
setTo(template.defaultRecipients.to.map(parseRecipient));
|
||||||
}
|
}
|
||||||
@@ -1115,7 +1124,7 @@ export function EmailComposer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setShowTemplatePicker(false);
|
setShowTemplatePicker(false);
|
||||||
}, [mode, plainTextMode]);
|
}, [mode, plainTextMode, shouldEmbedSignatureInNewMail, signatureIdentity, signatureSeparatorEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTemplateKey = (e: KeyboardEvent) => {
|
const handleTemplateKey = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
filterTemplates,
|
filterTemplates,
|
||||||
exportTemplates,
|
exportTemplates,
|
||||||
importTemplates,
|
importTemplates,
|
||||||
|
spliceTemplateAboveSignature,
|
||||||
} from '../template-utils';
|
} from '../template-utils';
|
||||||
import type { EmailTemplate } from '../template-types';
|
import type { EmailTemplate } from '../template-types';
|
||||||
|
|
||||||
@@ -333,3 +334,39 @@ describe('filterTemplates', () => {
|
|||||||
expect(filterTemplates(templates, 'xyz')).toHaveLength(0);
|
expect(filterTemplates(templates, 'xyz')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('spliceTemplateAboveSignature', () => {
|
||||||
|
const template = '<p>Template body</p>';
|
||||||
|
|
||||||
|
it('keeps the signature block below the template (separator marker)', () => {
|
||||||
|
const prev = '<p></p><p data-signature-block="separator">-- </p><div>My signature</div><p data-signature-block="end"></p>';
|
||||||
|
expect(spliceTemplateAboveSignature(prev, template)).toBe(
|
||||||
|
'<p>Template body</p><p data-signature-block="separator">-- </p><div>My signature</div><p data-signature-block="end"></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the signature block below the template (start marker, no separator)', () => {
|
||||||
|
const prev = '<p>old draft text</p><p data-signature-block="start"></p><div>My signature</div><p data-signature-block="end"></p>';
|
||||||
|
expect(spliceTemplateAboveSignature(prev, template)).toBe(
|
||||||
|
'<p>Template body</p><p data-signature-block="start"></p><div>My signature</div><p data-signature-block="end"></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces the whole body when there is no signature block', () => {
|
||||||
|
expect(spliceTemplateAboveSignature('<p>old draft text</p>', template)).toBe(template);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps everything from the start marker onward when the end marker is missing', () => {
|
||||||
|
const prev = '<p>old</p><p data-signature-block="separator">-- </p><div>My signature</div>';
|
||||||
|
expect(spliceTemplateAboveSignature(prev, template)).toBe(
|
||||||
|
'<p>Template body</p><p data-signature-block="separator">-- </p><div>My signature</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards user edits above the signature', () => {
|
||||||
|
const prev = '<p>half-written draft</p><p data-signature-block="separator">-- </p><div>Sig</div><p data-signature-block="end"></p>';
|
||||||
|
const result = spliceTemplateAboveSignature(prev, template);
|
||||||
|
expect(result).not.toContain('half-written draft');
|
||||||
|
expect(result).toContain('Sig');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -80,6 +80,26 @@ export function filterTemplates(templates: EmailTemplate[], query: string): Emai
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compose bodies carry the embedded signature bracketed by
|
||||||
|
// data-signature-block markers (see email-composer's
|
||||||
|
// buildEmbeddedSignatureHtml). Applying a template must replace only the
|
||||||
|
// message content, so splice the template above the signature range instead
|
||||||
|
// of overwriting the whole body.
|
||||||
|
export function spliceTemplateAboveSignature(prevHtml: string, templateHtml: string): string {
|
||||||
|
const doc = new DOMParser().parseFromString(prevHtml, 'text/html');
|
||||||
|
const startEl = doc.querySelector('[data-signature-block="separator"], [data-signature-block="start"]');
|
||||||
|
if (!startEl) return templateHtml;
|
||||||
|
const endEl = doc.querySelector('[data-signature-block="end"]');
|
||||||
|
const host = doc.createElement('div');
|
||||||
|
let cursor: Node | null = startEl;
|
||||||
|
while (cursor) {
|
||||||
|
host.appendChild(cursor.cloneNode(true));
|
||||||
|
if (cursor === endEl) break;
|
||||||
|
cursor = cursor.nextSibling;
|
||||||
|
}
|
||||||
|
return templateHtml + host.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeText(value: unknown): string {
|
function sanitizeText(value: unknown): string {
|
||||||
return DOMPurify.sanitize(String(value || ''), STRIP_HTML_CONFIG);
|
return DOMPurify.sanitize(String(value || ''), STRIP_HTML_CONFIG);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user