mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
fix: swap the signature in an open composer when the organisation policy or the user's details change
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import { EmailComposer } from '../email-composer';
|
||||
@@ -413,6 +413,56 @@ describe('organisation signature in the composer', () => {
|
||||
expectOrgSignatureOnce(await send(onSend), 'Team');
|
||||
});
|
||||
});
|
||||
|
||||
describe('a signature that changes while the composer is open', () => {
|
||||
const PROPS = { prefill: { to: ['bob@example.com'] } };
|
||||
|
||||
it('swaps in a locked signature whose policy arrives after the composer opened', async () => {
|
||||
setOrgPolicy({ signature: null });
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer {...PROPS} onSend={onSend} />);
|
||||
await waitFor(() => expect(editorState.html).toContain('<b>Alice</b>'));
|
||||
act(() => setOrgPolicy({ locked: true }));
|
||||
await waitFor(() => expect(editorState.html).toContain('ACME Ltd'));
|
||||
expectOrgSignatureOnce(await send(onSend), 'Me');
|
||||
});
|
||||
|
||||
it('brings in a default that arrives late for an identity without a signature', async () => {
|
||||
setOrgPolicy({ signature: null });
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer {...PROPS} onSend={onSend} />);
|
||||
fireEvent.change(fromSelect(), { target: { value: 'id-team' } });
|
||||
await waitFor(() => expect(editorState.html).not.toContain('Alice'));
|
||||
act(() => setOrgPolicy());
|
||||
await waitFor(() => expect(editorState.html).toContain('ACME Ltd'));
|
||||
expectOrgSignatureOnce(await send(onSend), 'Team');
|
||||
});
|
||||
|
||||
it('fills in the details again when they change', async () => {
|
||||
setOrgPolicy({ locked: true });
|
||||
const onSend = vi.fn();
|
||||
const { rerender } = render(<EmailComposer {...PROPS} onSend={onSend} />);
|
||||
await waitFor(() => expect(editorState.html).toContain('CEO'));
|
||||
useSettingsStore.setState({ signatureDetails: { title: 'CTO', department: '', phone: '+49 30 1', mobile: '' } } as never);
|
||||
rerender(<EmailComposer {...PROPS} onSend={onSend} />);
|
||||
await waitFor(() => expect(editorState.html).toContain('CTO'));
|
||||
const sent = await send(onSend);
|
||||
expect(countOf(sent.htmlBody!, 'ACME Ltd')).toBe(1);
|
||||
expect(sent.htmlBody).not.toContain('CEO');
|
||||
});
|
||||
|
||||
it('swaps a plain-text signature whose policy arrives late', async () => {
|
||||
useSettingsStore.setState({ plainTextMode: true });
|
||||
setOrgPolicy({ signature: null });
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer {...PROPS} onSend={onSend} />);
|
||||
const textarea = () => document.querySelector('textarea') as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(textarea().value).toContain('Alice'));
|
||||
act(() => setOrgPolicy({ locked: true }));
|
||||
await waitFor(() => expect(textarea().value).toContain('ACME Ltd'));
|
||||
expectOrgSignatureOnce(await send(onSend), 'Me');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('organisation disclaimer in the composer', () => {
|
||||
|
||||
@@ -1171,18 +1171,29 @@ export function EmailComposer({
|
||||
const prevSignatureSeparatorRef = useRef<boolean>(signatureSeparatorEnabled);
|
||||
const prevPluginSignatureRevisionRef = useRef(pluginSignatureRevision);
|
||||
const prevSignatureIdentityRef = useRef(signatureIdentity);
|
||||
// The signature's text, for a change under the same identity: the
|
||||
// organisation policy arriving after the composer opened, or the user's
|
||||
// details changing.
|
||||
const signatureContent = JSON.stringify([signatureIdentity?.htmlSignature ?? "", signatureIdentity?.textSignature ?? ""]);
|
||||
const prevSignatureContentRef = useRef(signatureContent);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
const identityChanged = prevSignatureIdentityIdRef.current !== signatureIdentity?.id;
|
||||
const separatorChanged = prevSignatureSeparatorRef.current !== signatureSeparatorEnabled;
|
||||
const pluginSignatureChanged = prevPluginSignatureRevisionRef.current !== pluginSignatureRevision;
|
||||
const contentChanged = prevSignatureContentRef.current !== signatureContent;
|
||||
const previousIdentity = prevSignatureIdentityRef.current;
|
||||
prevSignatureIdentityIdRef.current = signatureIdentity?.id;
|
||||
prevSignatureSeparatorRef.current = signatureSeparatorEnabled;
|
||||
prevPluginSignatureRevisionRef.current = pluginSignatureRevision;
|
||||
prevSignatureIdentityRef.current = signatureIdentity;
|
||||
if (!identityChanged && !separatorChanged && !pluginSignatureChanged) return;
|
||||
prevSignatureContentRef.current = signatureContent;
|
||||
if (!identityChanged && !separatorChanged && !pluginSignatureChanged && !contentChanged) return;
|
||||
// A signature that arrives where there was none comes into the body like
|
||||
// a new identity's; one the user removed stays removed.
|
||||
const hadSignature = !!(previousIdentity?.htmlSignature || previousIdentity?.textSignature);
|
||||
const bringsSignature = identityChanged || (contentChanged && !hadSignature);
|
||||
// Replies/forwards only embed when configured for "above quote". Compose
|
||||
// always embeds (see getInitialBody), so swap on identity change there too.
|
||||
const isReplyLike = mode === 'reply' || mode === 'replyAll' || mode === 'forward';
|
||||
@@ -1192,7 +1203,7 @@ export function EmailComposer({
|
||||
if (plainTextMode) {
|
||||
// A new plain-text message carries the signature at the end of the
|
||||
// body, a reply or forward above its quote header.
|
||||
if (!identityChanged) return;
|
||||
if (!identityChanged && !contentChanged) return;
|
||||
const options = { separator: signatureSeparatorEnabled };
|
||||
if (mode === 'compose') {
|
||||
setBody((prev) => replacePlainTextSignature(prev, previousIdentity, signatureIdentity, options));
|
||||
@@ -1215,8 +1226,8 @@ export function EmailComposer({
|
||||
// quoted body - see removeSignatureRange), or removed when the new
|
||||
// identity has no signature.
|
||||
const currentHtml = serializeEditorContent(editor);
|
||||
// Only a new identity brings a signature back into a body without one.
|
||||
if (!identityChanged && !containsEmbeddedSignature(currentHtml)) return;
|
||||
// Only a new identity (or a first signature) brings one into a body without one.
|
||||
if (!bringsSignature && !containsEmbeddedSignature(currentHtml)) return;
|
||||
const nextHtml = replaceEmbeddedSignature(currentHtml, buildEmbeddedSignatureHtml(signatureIdentity, {
|
||||
embed: true,
|
||||
separator: signatureSeparatorEnabled,
|
||||
@@ -1229,7 +1240,7 @@ export function EmailComposer({
|
||||
// would re-run on unrelated identity-field changes and re-splice the
|
||||
// signature into the live editor.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [signatureIdentity?.id, signatureIdentity?.htmlSignature, signatureIdentity?.textSignature, signatureSeparatorEnabled, signaturePosition, mode, plainTextMode, pluginSignatureRevision]);
|
||||
}, [signatureIdentity?.id, signatureContent, signatureSeparatorEnabled, signaturePosition, mode, plainTextMode, pluginSignatureRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutsideSendMenu = (event: MouseEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user