Feat/email composer improvements (#23188)

- Move composer to dedicated page
- Add test email option
- Auto saved as draft can be revisited from `objects/messageCampaigns`
later
- Campaign stats component



https://github.com/user-attachments/assets/9e523116-e79b-496d-9c9d-3887e0c9213f



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23188?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2026-07-28 16:43:00 +05:30
committed by GitHub
parent 30bbf4149a
commit 1e58c3073c
119 changed files with 5345 additions and 2606 deletions
@@ -35,10 +35,6 @@ import { Button, type SelectOption } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const EMAIL_EDITOR_MIN_HEIGHT = 340;
const EMAIL_EDITOR_MAX_WIDTH = 600;
type WorkflowEditActionEmailBaseProps = {
action: WorkflowEmailAction;
actionOptions:
@@ -375,8 +371,7 @@ export const WorkflowEditActionEmailBase = ({
children: t`Email Editor`,
},
]}
minHeight={EMAIL_EDITOR_MIN_HEIGHT}
maxWidth={EMAIL_EDITOR_MAX_WIDTH}
preset="workflowEmailBody"
/>
<WorkflowSendEmailAttachments
label={t`Attachments`}
@@ -0,0 +1,70 @@
import { getInitialAdvancedTextEditorContent } from '@/workflow/workflow-variables/utils/getInitialAdvancedTextEditorContent';
describe('getInitialAdvancedTextEditorContent', () => {
it('should return an empty document when the content is blank', () => {
expect(getInitialAdvancedTextEditorContent(' ')).toEqual({
type: 'doc',
content: [{ type: 'paragraph', content: [] }],
});
});
it('should return the parsed document when the content is TipTap JSON', () => {
const document = {
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
],
};
expect(
getInitialAdvancedTextEditorContent(JSON.stringify(document)),
).toEqual(document);
});
it('should wrap a BlockNote array in a document', () => {
const blocks = [{ type: 'paragraph', content: [] }];
expect(getInitialAdvancedTextEditorContent(JSON.stringify(blocks))).toEqual(
{
type: 'doc',
content: blocks,
},
);
});
it('should hand legacy HTML back untouched so TipTap parses it', () => {
const html = '<p>Hi <strong>there</strong></p><p>Bye</p>';
expect(getInitialAdvancedTextEditorContent(html)).toBe(html);
});
it('should hand legacy HTML back untouched when it is indented', () => {
const html = '\n <h1 class="title">Title</h1>';
expect(getInitialAdvancedTextEditorContent(html)).toBe(html);
});
it('should still convert plain text with variables into variable tags', () => {
expect(getInitialAdvancedTextEditorContent('Hi {{firstName}}')).toEqual({
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Hi ' },
{ type: 'variableTag', attrs: { variable: '{{firstName}}' } },
],
},
],
});
});
it('should not treat plain text that merely contains angle brackets as HTML', () => {
const text = 'Reply to a <b>bold</b> claim';
expect(getInitialAdvancedTextEditorContent(text)).toEqual({
type: 'doc',
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
});
});
});
@@ -1,12 +1,19 @@
import { getInitialEditorContent } from '@/workflow/workflow-variables/utils/getInitialEditorContent';
import type { JSONContent } from '@tiptap/react';
// Campaign bodies were stored as the editor's own HTML before they moved to
// JSON. TipTap parses an HTML string natively, so hand it through untouched
// rather than letting the plain-text fallback render the markup as literal
// text. Requiring a leading tag keeps plain-text bodies, which need the
// variable-tag conversion below, out of this branch.
const LEADING_HTML_TAG_PATTERN = /^<[a-z][a-z0-9]*(\s[^>]*)?>/i;
// Previous format of the email body was plain text,
// but from now on we will save it as JSON.
// So it will fail to parse the content, that's why we have this fallback.
export const getInitialAdvancedTextEditorContent = (
rawContent: string,
): JSONContent => {
): JSONContent | string => {
// Handle empty or null content
if (!rawContent || rawContent.trim() === '') {
return {
@@ -33,6 +40,10 @@ export const getInitialAdvancedTextEditorContent = (
return json;
} catch {
if (LEADING_HTML_TAG_PATTERN.test(rawContent.trim())) {
return rawContent;
}
return getInitialEditorContent(rawContent);
}
};