mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 00:13:09 +00:00
fix: reopen drafts from the pro view in the format they were written in
This commit is contained in:
@@ -111,7 +111,7 @@ import { PluginSidePanel } from "@/components/plugins/plugin-side-panels";
|
||||
import { MvListToolbar } from "@/components/email/mv-list-toolbar";
|
||||
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
||||
import { InlineAppView } from "@/components/layout/inline-app-view";
|
||||
import { getDraftTextBody } from '@/lib/email-body-selection';
|
||||
import { getDraftComposerBody } from '@/lib/email-body-selection';
|
||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||
import { useIdentitySync } from "@/hooks/use-identity-sync";
|
||||
import { useIsEmbedded } from "@/hooks/use-is-embedded";
|
||||
@@ -2468,17 +2468,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
|
||||
draft = await emailHooks.onBeforeEditDraft.transform(draft);
|
||||
|
||||
// Only the parts the server lists as the body: bodyValues also carries
|
||||
// every text/* ATTACHMENT, which used to be joined in as the message.
|
||||
const bodyText = getDraftTextBody(draft);
|
||||
// A plain-text-only draft lists its text/plain part under htmlBody
|
||||
// (RFC 8621 § 4.1.4 fallback) - only treat it as HTML when it really is.
|
||||
const draftHtmlPart = draft.htmlBody?.[0];
|
||||
const htmlBody = draftHtmlPart?.partId
|
||||
&& (!draftHtmlPart.type || draftHtmlPart.type.toLowerCase() === 'text/html')
|
||||
&& draft.bodyValues?.[draftHtmlPart.partId]
|
||||
? draft.bodyValues[draftHtmlPart.partId].value
|
||||
: undefined;
|
||||
const { body: draftBody, bodyText, plainTextMode: draftPlainTextMode } = getDraftComposerBody(draft);
|
||||
|
||||
// Restore the identity the draft was composed with. Match the saved From
|
||||
// (name + address) against the same list the composer renders — the flat
|
||||
@@ -2508,12 +2498,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
// Drafts saved before #1189 stored an empty subject as the composer's
|
||||
// "(No Subject)" placeholder; reopen those with an empty field.
|
||||
subject: draft.subject && draft.subject !== t('email_composer.no_subject') ? draft.subject : '',
|
||||
body: htmlBody || bodyText,
|
||||
// Re-open in the format the draft was written in: a text-only draft
|
||||
// used to land raw in the rich-text editor (newlines collapsed) and an
|
||||
// HTML draft raw in the plain-text textarea. Empty draft: the setting
|
||||
// decides (#1022).
|
||||
plainTextMode: htmlBody != null ? false : bodyText ? true : undefined,
|
||||
body: draftBody,
|
||||
// Re-open in the format the draft was written in (#1022).
|
||||
plainTextMode: draftPlainTextMode,
|
||||
showCc: (draft.cc?.length || 0) > 0,
|
||||
showBcc: (draft.bcc?.length || 0) > 0,
|
||||
selectedIdentityId: matchedIdentityId,
|
||||
|
||||
@@ -169,6 +169,28 @@ describe('Pro email view - editing a draft', () => {
|
||||
expect(initialData.selectedIdentityId).toBe('userb@v10::d');
|
||||
});
|
||||
|
||||
// A text-only draft went to the rich editor (newlines collapsed, "<you>"
|
||||
// gone) and an HTML draft to the textarea with its tags, whichever the
|
||||
// plain-text setting said.
|
||||
it('re-opens a plain-text draft as plain text', async () => {
|
||||
const initialData = await openAndEdit(undefined, undefined, {
|
||||
...DRAFT,
|
||||
bodyValues: { '1': { value: 'Hi <you>,\n\nsee below' } },
|
||||
});
|
||||
|
||||
expect(initialData).toMatchObject({ body: 'Hi <you>,\n\nsee below', plainTextMode: true });
|
||||
});
|
||||
|
||||
it('re-opens an HTML draft in the rich editor', async () => {
|
||||
const initialData = await openAndEdit(undefined, undefined, {
|
||||
...DRAFT,
|
||||
bodyValues: { '1': { value: 'Hi' }, '2': { value: '<p>Hi <b>there</b></p>' } },
|
||||
htmlBody: [{ partId: '2', type: 'text/html' }],
|
||||
});
|
||||
|
||||
expect(initialData).toMatchObject({ body: '<p>Hi <b>there</b></p>', plainTextMode: false });
|
||||
});
|
||||
|
||||
it('reads a message of the login it names through that login\'s client, not through the main list', async () => {
|
||||
const client = { getEmail: vi.fn().mockResolvedValue(DRAFT), getUsername: () => 'userb@example.org', getAccountId: () => 'e' };
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { getQuoteBodies } from "@/lib/email-composer-utils";
|
||||
import { buildForwardAsAttachmentPayload } from "@/lib/forward-as-attachment";
|
||||
import { KEYWORD_PREFIX, KEYWORD_PREFIX_LEGACY, emailKeyFor } from "@/lib/thread-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getDraftTextBody } from '@/lib/email-body-selection';
|
||||
import { getDraftComposerBody } from '@/lib/email-body-selection';
|
||||
import { messagePriorityFromHeaders } from '@/lib/message-priority';
|
||||
import { labelSourceHeaders, reopenedLabelChoice } from '@/lib/outgoing/classification';
|
||||
import { useArchiveActionEnabled } from '@/lib/archive-gate';
|
||||
@@ -461,17 +461,7 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId,
|
||||
const handleEditDraft = useCallback(() => {
|
||||
if (!email) return;
|
||||
|
||||
// Only the parts the server lists as the body: bodyValues also carries
|
||||
// every text/* ATTACHMENT, which used to be joined in as the message.
|
||||
const bodyText = getDraftTextBody(email);
|
||||
// A plain-text-only draft lists its text/plain part under htmlBody
|
||||
// (RFC 8621 § 4.1.4 fallback) - only treat it as HTML when it really is.
|
||||
const draftHtmlPart = email.htmlBody?.[0];
|
||||
const htmlBody = draftHtmlPart?.partId
|
||||
&& (!draftHtmlPart.type || draftHtmlPart.type.toLowerCase() === 'text/html')
|
||||
&& email.bodyValues?.[draftHtmlPart.partId]
|
||||
? email.bodyValues[draftHtmlPart.partId].value
|
||||
: undefined;
|
||||
const { body, bodyText, plainTextMode } = getDraftComposerBody(email);
|
||||
|
||||
// Preserve the identity the draft was composed with — match name + address
|
||||
// against the same list the composer renders, this login's identities
|
||||
@@ -493,7 +483,10 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId,
|
||||
// Drafts saved before #1189 stored an empty subject as the composer's
|
||||
// "(No Subject)" placeholder; reopen those with an empty field.
|
||||
subject: email.subject && email.subject !== t('email_composer.no_subject') ? email.subject : '',
|
||||
body: htmlBody || bodyText,
|
||||
body,
|
||||
// Re-open in the format the draft was written in, as the classic
|
||||
// view does: without it the composer's setting decided.
|
||||
plainTextMode,
|
||||
showCc: (email.cc?.length || 0) > 0,
|
||||
showBcc: (email.bcc?.length || 0) > 0,
|
||||
selectedIdentityId: matchedIdentityId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getRenderableHtmlBody, getDraftTextBody } from '@/lib/email-body-selection';
|
||||
import { getRenderableHtmlBody, getDraftTextBody, getDraftComposerBody } from '@/lib/email-body-selection';
|
||||
|
||||
type PartialEmail = Parameters<typeof getRenderableHtmlBody>[0];
|
||||
|
||||
@@ -147,3 +147,28 @@ describe('getDraftTextBody', () => {
|
||||
expect(getDraftTextBody(draft([{ partId: '1' }, { partId: '9' }], { '1': 'one' }))).toBe('one');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDraftComposerBody', () => {
|
||||
it('opens a text-only draft as plain text', () => {
|
||||
// Plain-text-only draft: the text/plain part is listed under htmlBody too.
|
||||
const email = makeEmail({
|
||||
html: { partId: '1', type: 'text/plain' },
|
||||
text: { partId: '1', type: 'text/plain' },
|
||||
values: { '1': 'Hi <you>,\n\nbye' },
|
||||
});
|
||||
expect(getDraftComposerBody(email)).toEqual({ body: 'Hi <you>,\n\nbye', bodyText: 'Hi <you>,\n\nbye', plainTextMode: true });
|
||||
});
|
||||
|
||||
it('opens an HTML draft in the rich editor', () => {
|
||||
const email = makeEmail({
|
||||
html: { partId: '2', type: 'text/html' },
|
||||
text: { partId: '1', type: 'text/plain' },
|
||||
values: { '1': 'Hi', '2': '<p>Hi</p>' },
|
||||
});
|
||||
expect(getDraftComposerBody(email)).toEqual({ body: '<p>Hi</p>', bodyText: 'Hi', plainTextMode: false });
|
||||
});
|
||||
|
||||
it('leaves the format of an empty draft to the setting', () => {
|
||||
expect(getDraftComposerBody(makeEmail({ values: {} })).plainTextMode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,3 +54,32 @@ export function getDraftTextBody(
|
||||
.filter((value): value is string => !!value)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* The body a re-opened draft seeds the composer with, and the format to open
|
||||
* it in. Shared by the classic and the Pro "Edit draft" so both re-open a
|
||||
* draft the way it was written: a text-only draft used to land raw in the
|
||||
* rich-text editor (newlines collapsed) and an HTML draft raw in the
|
||||
* plain-text textarea. An empty draft leaves the format to the setting
|
||||
* (#1022).
|
||||
*/
|
||||
export function getDraftComposerBody(
|
||||
email: Pick<Email, 'htmlBody' | 'textBody' | 'bodyValues'>
|
||||
): { body: string; bodyText: string; plainTextMode: boolean | undefined } {
|
||||
// Only the parts the server lists as the body: bodyValues also carries
|
||||
// every text/* ATTACHMENT, which used to be joined in as the message.
|
||||
const bodyText = getDraftTextBody(email);
|
||||
// A plain-text-only draft lists its text/plain part under htmlBody
|
||||
// (RFC 8621 § 4.1.4 fallback) - only treat it as HTML when it really is.
|
||||
const htmlPart = email.htmlBody?.[0];
|
||||
const htmlBody = htmlPart?.partId
|
||||
&& (!htmlPart.type || htmlPart.type.toLowerCase() === 'text/html')
|
||||
&& email.bodyValues?.[htmlPart.partId]
|
||||
? email.bodyValues[htmlPart.partId].value
|
||||
: undefined;
|
||||
return {
|
||||
body: htmlBody || bodyText,
|
||||
bodyText,
|
||||
plainTextMode: htmlBody != null ? false : bodyText ? true : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user