From 1d755983ffe776b737676d44d993f4fc5d95c819 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:38:06 +0200 Subject: [PATCH] Feat/advanced text editor capability presets (#23657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Email editor for Compose campaigns ## Short version Campaign bodies are currently plain rich text. This PR turns the composer into an email editor: a centered email canvas with section, column, button, divider, image and raw-HTML blocks, each editable through a settings side panel, rendered to email-safe HTML per recipient at send time. Modelled on Resend's Broadcast editor. **Product** - Email canvas with page/body styling (background, width, padding, corner radius, border, text colour, alignment) - Blocks: section, 2/3 columns, button, divider, raw HTML, images — insertable from a floating left rail or the slash menu - A **section is a container whose typography cascades to its contents**, so one part of an email can have its own look - Block settings panel focuses whatever you select and shows its effective values - Per-recipient variables (`{{firstName}}`, `{{lastName}}`, `{{fullName}}`, `{{email}}`, `{{personId}}`) usable in text, button/link/image URLs, image labels and raw HTML - Image upload by drag-drop, paste or file picker **Technical** - Presets now declare **capabilities** instead of surfaces forking the editor; the UI derives itself from loaded extensions - Editor behavior lives in `twenty-front`; the versioned email-document schema and structural traversal live in `twenty-shared`; rendering lives in `twenty-emails` — HTML is produced server-side per recipient - Section typography cascade is **resolved at render time**, not left to CSS: react-email hardcodes `fontSize`/`lineHeight` on every paragraph and Outlook ignores `inherit` - Logic vendored from Resend (MIT); all controls rebuilt on `twenty-ui` + Linaria **Also fixes:** the unsubscribe footer was being appended *after* ``, outside the document, where Gmail strips it — legally significant since unsubscribe is required. --- ## Detailed version ### Product requirements **Problem.** The Compose campaign body was a single rich-text field. Marketing email needs layout — banded sections, columns, call-to-action buttons, images with links — and it needs that layout to survive Outlook, which means table-based HTML rather than the divs a text editor produces. It also needs per-recipient personalisation. **Reference.** Resend's Broadcast editor, chosen because it solves the same problem (TipTap authoring → react-email output) and is MIT licensed. #### What a user can now do | Area | Capability | |---|---| | Canvas | Email renders as a centered page with its own background, width, padding, corner radius and border | | Blocks | Section, 2/3 columns, button, divider, raw HTML, image | | Insertion | Floating left rail (pointer-first) or the `/` slash menu (keyboard-first) | | Sections | Own text colour, font size, line height, letter spacing and alignment, cascading to everything inside | | Images | Upload by drag-drop, paste or picker; link URL, alt text, width, spacing, border | | Raw HTML | Edited as source in the panel, previewed on the canvas with scripts neutralised | | Variables | `{{firstName}}`, `{{lastName}}`, `{{fullName}}`, `{{email}}`, `{{personId}}` in text, button URLs, link hrefs and raw HTML | | Settings panel | Follows selection; shows effective values; opens automatically when a block is clicked | #### Deliberate product decisions - **Variables display as literal placeholders**, not prose labels, so the syntax is copyable into HTML blocks and button URLs by hand. - **Sections inherit until they override.** The panel shows what actually renders rather than blank fields, but writes nothing until you edit — so changing the body text colour still flows into sections. - **Headings keep their own scale** inside a styled section; only colour, family and spacing cascade, otherwise every heading would collapse to body size. - **Clicking a block opens its settings**, but only on whole-node selections, so typing inside a section does not reopen a panel you just closed. ### Technical strategy #### 1. Capability presets (the foundation) Per-surface variation previously worked by **forking**: three separate `useEditor` call sites with hardcoded extension arrays. Inside the shared tree there was no variation at all — all five surfaces received a byte-identical extension list, and presets controlled only sizing, chrome and serialization format. Adding email blocks that way meant either leaking section/column nodes into the record rich-text field and workflow email body, or writing a fourth fork. Now: - a preset declares a **capability list** (`basicMarks`, `headings`, `lists`, `links`, `images`, `campaignVariables`, `slashCommand`, `blocks`, `mentions`) - capabilities resolve to extensions through a factory registry - the UI derives itself from the loaded extensions via `hasEditorExtension` — no capability list is prop-drilled into a menu, because the `Editor` already knows what it can do The acceptance test was collapsing the AI chat fork into an `aiChat` preset with no visible change to that composer. `campaignBody` is the only preset opting into the shared `EMAIL_DOCUMENT_CAPABILITIES` today. Workflow email keeps its current field UI, but can opt into the same canvas, block settings and image uploader later without adding another schema or renderer. #### 2. Schema / renderer split The hard constraint: **our HTML is produced server-side, per recipient, at send time**, because variables substitute into nodes rather than into a serialized string. That rules out Resend's `renderToReactEmail`-on-the-extension pattern. ``` twenty-front TipTap extensions + node views + shared email settings UI twenty-shared versioned email-document schema + structural traversal twenty-emails react-email renderers (imported by twenty-server) twenty-server surface-specific variable resolution, validation, send ``` Logic was **vendored, not depended on** — Resend's TipTap is 3.17 against our 3.4, and their UI is Radix. We copied the schema/serializer approach and rebuilt every control on `twenty-ui` + Linaria. #### 3. Section typography cascade The subtle part, and the one that would have silently shipped broken. Section typography *looks* like it should cascade via CSS. It does not: ```js // react-email's Text style: { fontSize: "14px", lineHeight: "24px", ...style, ...margins } ``` Every paragraph re-declares `fontSize` and `lineHeight`, overriding any enclosing section. `inherit` is not a fix either — Outlook's Word engine ignores it. So the cascade is **resolved in the renderer**: the tree walk threads the enclosing section's typography down and writes computed values explicitly onto each text node. Nested sections refine what they inherit. Verified against real rendered output: | | rendered | |---|---| | paragraph inside section | `font-size:22px; color:rgb(255,0,0); letter-spacing:2px` | | h1 inside section | `font-size:32px` (own scale) + section colour and spacing | | paragraph outside | `font-size:14px`, no colour — untouched | #### 4. Storage `bodyTemplate` stays serialized TipTap JSON in a `TEXT` column. Block attributes are ProseMirror node attrs, so richer blocks add keys to JSON already being serialized — no migration, and it flows into the existing 500 ms debounced draft save unchanged. Since the feature has not shipped, the legacy HTML-string body path was removed rather than maintained. That is a tightening, not just a deletion: `bodyTemplate` is writable through the record API, and the old fallback would interpolate an arbitrary string and email it as markup. A body that is neither empty nor a valid TipTap document is now rejected at the send gate. #### 5. Image hosting Inline assets use an `EmailImage` file folder with `ignoreExpirationToken: true` and immutable cache headers, because recipients' mail clients never authenticate and may open an email years later. The shared uploader returns `{ fileId, url }`; the image node keeps both the durable file identity and its delivery URL so ownership/lifecycle or URL resolution can evolve later without a document migration. The server verifies the uploaded bytes and only accepts GIF, JPEG, PNG and WebP. This is intentionally separate from workflow/email **attachments**. Attachments remain private files that the server reads and embeds as MIME parts at send time; inline images need a durable recipient-facing URL. A future workflow canvas should reuse `useUploadEmailImage` for inline content while keeping its existing attachment control unchanged. Adding the folder requires three registrations — the folder config, the route guard's `SUPPORTED_FILE_FOLDERS`, and `DIRECT_UPLOAD_FILE_FOLDERS` in the upload service. ### Bugs fixed along the way - **Unsubscribe footer was appended after ``**, outside the document, where Gmail strips it. Legally significant, since an unsubscribe link is required. Now inserted before ``. - **Body text colour never reached the email.** - **`onImageUpload` was declared but never passed** by any production call site, so drag-drop and paste image upload were inert everywhere outside Storybook. - **Message lists were not user-facing**, so members could not be added from the list page. - **Image resize wrote an undeclared `width` attribute** that TipTap silently dropped. - **The text bubble menu appeared over selected atom blocks** with nothing to format. ### Review notes / known limitations **Security posture to check.** Anything in `EmailImage` is readable by anyone holding the URL, forever. The server now enforces an image-only MIME allowlist from sniffed bytes, but it cannot determine whether the image itself is confidential. This remains a deliberate trade-off for recipient-visible inline assets. **Test gap.** The section typography cascade has no regression test: react-email's `render()` hangs under Jest (tried 60s), and `twenty-emails` has no test target at all. Verified by rendering through the built package instead. Adding a test target there is worthwhile follow-up. **Sending needs configuration.** `EMAILING_DOMAIN_DRIVER` defaults to `LOG`, which fakes a messageId, reports any domain as verified, and only logs — a campaign reaches "sent" with nothing delivered. Real sending needs `AWS_SES`. **Unrelated platform bug found.** The pinned "Create new record" command throws on viewless objects like `messageListMember`, because `recordIndexId` derives from the current view. **Not done.** Panel chrome from the reference: breadcrumb (`Page style / Section`), collapsible groups, per-side spacing grid, and a variable-insert button inside link fields. All presentation over the same data. **Deferred.** Drag-to-reorder blocks. `@tiptap/extension-drag-handle-react@3.4.2` matches our pinned versions exactly, so no upgrade is needed, but its behaviour around atom node views (HTML block, image) is unverified and belongs in its own change. Review in cubic --------- Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../src/metadata/generated/schema.graphql | 2 + .../src/metadata/generated/schema.ts | 6 +- packages/twenty-emails/src/common-style.ts | 2 +- .../twenty-emails/src/components/BaseHead.tsx | 6 +- .../src/components/CallToAction.tsx | 16 +- .../src/components/HighlightedContainer.tsx | 8 +- .../src/components/HighlightedText.tsx | 12 +- .../twenty-emails/src/components/Link.tsx | 4 +- .../twenty-emails/src/components/MainText.tsx | 12 +- .../src/components/ShadowText.tsx | 10 +- .../twenty-emails/src/components/SubTitle.tsx | 10 +- .../twenty-emails/src/components/Title.tsx | 10 +- .../src/emails/send-invite-link.email.tsx | 4 +- .../validate-approved-access-domain.email.tsx | 4 +- .../utils/email-renderer/email-renderer.tsx | 44 ++- .../src/utils/email-renderer/marks/link.tsx | 4 +- .../email-renderer/nodes/bullet-list.tsx | 9 +- .../src/utils/email-renderer/nodes/button.tsx | 23 ++ .../utils/email-renderer/nodes/columns.tsx | 44 +++ .../utils/email-renderer/nodes/divider.tsx | 8 + .../utils/email-renderer/nodes/heading.tsx | 12 +- .../src/utils/email-renderer/nodes/html.tsx | 12 + .../src/utils/email-renderer/nodes/image.tsx | 43 +- .../utils/email-renderer/nodes/list-item.tsx | 9 +- .../email-renderer/nodes/ordered-list.tsx | 9 +- .../utils/email-renderer/nodes/paragraph.tsx | 12 +- .../utils/email-renderer/nodes/section.tsx | 22 ++ .../email-renderer/renderers/render-node.tsx | 33 +- .../utils/email-renderer/utils/block-style.ts | 14 + .../utils/inherited-typography.ts | 35 ++ .../utils/email-renderer/utils/safe-url.ts | 18 + packages/twenty-emails/tsconfig.json | 1 + packages/twenty-emails/tsconfig.lib.json | 3 + .../src/generated-metadata/graphql.ts | 2 + .../emails/components/CampaignBodyField.tsx | 31 ++ .../components/CampaignDetailsFields.tsx | 12 +- .../hooks/useCampaignEmailEditorVariables.ts | 22 ++ .../components/AdvancedTextEditor.tsx | 141 ++++++- .../components/BubbleMenuIconButton.tsx | 34 +- .../components/ImageBubbleMenu.tsx | 2 +- .../components/InsertRail.tsx | 373 ++++++++++++++++++ .../components/LinkBubbleMenu.tsx | 2 +- .../components/StyledBubbleMenuContainer.tsx | 14 + .../components/TextBubbleMenu.tsx | 38 +- .../AdvancedTextEditor.stories.tsx | 18 +- .../AdvancedTextEditorCapabilityExtensions.ts | 69 ++++ .../AdvancedTextEditorCoreExtensions.ts | 27 ++ .../constants/AdvancedTextEditorPresets.ts | 16 +- .../constants/EmailDocumentCapabilities.ts | 11 + .../constants/FullRichTextCapabilities.ts | 12 + .../extensions/blocks/ButtonNode.ts | 77 ++++ .../extensions/blocks/ColumnNode.ts | 39 ++ .../extensions/blocks/ColumnsNode.ts | 40 ++ .../extensions/blocks/DividerNode.ts | 48 +++ .../extensions/blocks/HtmlNode.ts | 36 ++ .../extensions/blocks/HtmlNodeView.tsx | 30 ++ .../extensions/blocks/SectionNode.ts | 45 +++ .../extensions/blocks/ThemedDocument.ts | 18 + .../blocks/readBlockStyleAttribute.ts | 11 + .../CampaignVariableChip.tsx | 13 + .../campaign-variables/CampaignVariableTag.ts | 10 + .../resizable-image/ResizableImage.ts | 15 + .../resizable-image/ResizableImageView.tsx | 11 +- .../resizable-image/UploadImageExtension.ts | 11 +- .../resizable-image/UploadImagePlugin.ts | 11 +- .../slash-command/DefaultSlashCommands.ts | 122 ++++++ .../hooks/useAdvancedTextEditor.ts | 100 ++--- .../hooks/useTurnIntoBlockOptions.ts | 71 ++-- .../hooks/useUploadEmailImage.ts | 55 +++ .../states/activeEmailEditorState.ts | 7 + .../types/AdvancedTextEditorCapability.ts | 11 + .../types/AdvancedTextEditorContentType.ts | 1 + .../types/AdvancedTextEditorPreset.ts | 7 +- .../types/UploadedImage.ts | 4 + .../buildAdvancedTextEditorExtensions.test.ts | 110 ++++++ .../__tests__/getBlockSelectionTarget.test.ts | 103 +++++ .../__tests__/hasEditorExtension.test.ts | 26 ++ .../utils/__tests__/parseCssSizeValue.test.ts | 15 + .../__tests__/sanitizeHtmlPreview.test.ts | 66 ++++ ...serializeAdvancedTextEditorContent.test.ts | 45 +++ .../buildAdvancedTextEditorExtensions.ts | 37 ++ .../utils/getBlockSelectionTarget.ts | 50 +++ .../utils/getBlockStyle.ts | 11 + .../utils/hasEditorExtension.ts | 6 + .../utils/inlineStyleToCss.ts | 13 + .../utils/parseCssSizeValue.ts | 16 + .../utils/sanitizeHtmlPreview.ts | 47 +++ .../serializeAdvancedTextEditorContent.ts | 16 + .../src/modules/ai/hooks/useAiChatEditor.ts | 38 +- ...EngineComponentKeyHeadlessComponentMap.tsx | 4 + ...nEmailBlockSettingsSingleRecordCommand.tsx | 14 + ...ordDetailRelationSectionDropdownToMany.tsx | 1 - .../components/FormAdvancedTextFieldInput.tsx | 37 +- .../RelationOneToManyFieldInput.tsx | 1 - .../constants/SidePanelPagesConfig.tsx | 2 + .../useOpenEmailBlockSettingsInSidePanel.ts | 22 ++ .../components/EmailAlignmentInput.tsx | 72 ++++ .../EmailBlockSettingsFieldInput.tsx | 87 ++++ .../components/EmailBoxSidesInput.tsx | 151 +++++++ .../components/EmailColorInput.tsx | 74 ++++ .../components/EmailPageStyleSection.tsx | 152 +++++++ .../components/EmailSizeInput.tsx | 87 ++++ .../SidePanelEmailBlockSettingsPage.tsx | 245 ++++++++++++ .../components/StyledEmailFieldLabel.tsx | 9 + .../constants/EmailBlockSettingsFields.ts | 340 ++++++++++++++++ .../constants/EmailBodyThemeFields.ts | 24 ++ .../constants/EmailPageThemeFields.ts | 13 + .../types/EmailThemeField.ts | 11 + .../utils/getEffectiveSectionStyleValue.ts | 23 ++ .../utils/getEmailBlockLabel.ts | 23 ++ .../workflow-steps/hooks/useDeleteStep.ts | 4 - .../2-28-upgrade-version-command.module.ts | 4 +- ...rkflow-availability-expression.command.ts} | 2 +- ...lock-settings-command-menu-item.command.ts | 139 +++++++ .../services/unsubscribe-content.service.ts | 6 +- .../__tests__/append-html-footer.util.spec.ts | 43 ++ .../utils/append-html-footer.util.ts | 19 + .../services/file-upload.service.spec.ts | 86 +++- .../services/file-upload.service.ts | 23 ++ .../file/guards/file-by-id.guard.ts | 1 + .../file/interfaces/file-folder.interface.ts | 7 + .../engine/core-modules/i18n/locales/af-ZA.po | 10 + .../engine/core-modules/i18n/locales/ar-SA.po | 10 + .../engine/core-modules/i18n/locales/ca-ES.po | 10 + .../engine/core-modules/i18n/locales/cs-CZ.po | 10 + .../engine/core-modules/i18n/locales/da-DK.po | 10 + .../engine/core-modules/i18n/locales/de-DE.po | 10 + .../engine/core-modules/i18n/locales/el-GR.po | 10 + .../engine/core-modules/i18n/locales/en.po | 10 + .../engine/core-modules/i18n/locales/es-ES.po | 10 + .../engine/core-modules/i18n/locales/fi-FI.po | 10 + .../engine/core-modules/i18n/locales/fr-FR.po | 10 + .../i18n/locales/generated/af-ZA.ts | 2 +- .../i18n/locales/generated/ar-SA.ts | 2 +- .../i18n/locales/generated/ca-ES.ts | 2 +- .../i18n/locales/generated/cs-CZ.ts | 2 +- .../i18n/locales/generated/da-DK.ts | 2 +- .../i18n/locales/generated/de-DE.ts | 2 +- .../i18n/locales/generated/el-GR.ts | 2 +- .../core-modules/i18n/locales/generated/en.ts | 2 +- .../i18n/locales/generated/es-ES.ts | 2 +- .../i18n/locales/generated/fi-FI.ts | 2 +- .../i18n/locales/generated/fr-FR.ts | 2 +- .../i18n/locales/generated/he-IL.ts | 2 +- .../i18n/locales/generated/hu-HU.ts | 2 +- .../i18n/locales/generated/it-IT.ts | 2 +- .../i18n/locales/generated/ja-JP.ts | 2 +- .../i18n/locales/generated/ko-KR.ts | 2 +- .../i18n/locales/generated/nl-NL.ts | 2 +- .../i18n/locales/generated/no-NO.ts | 2 +- .../i18n/locales/generated/pl-PL.ts | 2 +- .../i18n/locales/generated/pseudo-en.ts | 2 +- .../i18n/locales/generated/pt-BR.ts | 2 +- .../i18n/locales/generated/pt-PT.ts | 2 +- .../i18n/locales/generated/ro-RO.ts | 2 +- .../i18n/locales/generated/ru-RU.ts | 2 +- .../i18n/locales/generated/sr-Cyrl.ts | 2 +- .../i18n/locales/generated/sv-SE.ts | 2 +- .../i18n/locales/generated/tr-TR.ts | 2 +- .../i18n/locales/generated/uk-UA.ts | 2 +- .../i18n/locales/generated/vi-VN.ts | 2 +- .../i18n/locales/generated/zh-CN.ts | 2 +- .../i18n/locales/generated/zh-TW.ts | 2 +- .../engine/core-modules/i18n/locales/he-IL.po | 10 + .../engine/core-modules/i18n/locales/hu-HU.po | 10 + .../engine/core-modules/i18n/locales/it-IT.po | 10 + .../engine/core-modules/i18n/locales/ja-JP.po | 10 + .../engine/core-modules/i18n/locales/ko-KR.po | 10 + .../engine/core-modules/i18n/locales/nl-NL.po | 10 + .../engine/core-modules/i18n/locales/no-NO.po | 10 + .../engine/core-modules/i18n/locales/pl-PL.po | 10 + .../core-modules/i18n/locales/pseudo-en.po | 10 + .../engine/core-modules/i18n/locales/pt-BR.po | 10 + .../engine/core-modules/i18n/locales/pt-PT.po | 10 + .../engine/core-modules/i18n/locales/ro-RO.po | 10 + .../engine/core-modules/i18n/locales/ru-RU.po | 10 + .../core-modules/i18n/locales/sr-Cyrl.po | 10 + .../engine/core-modules/i18n/locales/sv-SE.po | 10 + .../engine/core-modules/i18n/locales/tr-TR.po | 10 + .../engine/core-modules/i18n/locales/uk-UA.po | 10 + .../engine/core-modules/i18n/locales/vi-VN.po | 10 + .../engine/core-modules/i18n/locales/zh-CN.po | 10 + .../engine/core-modules/i18n/locales/zh-TW.po | 10 + .../constants/action-tool-label.constant.ts | 4 + .../providers/action-tool.provider.ts | 12 + .../tool-provider/tool-provider.module.ts | 2 + .../email-tool/email-composer.service.ts | 4 +- .../tools/email-tool/email-tool.schema.ts | 8 +- .../types/compose-email-params.type.ts | 3 +- .../__tests__/render-email-body.util.spec.ts | 42 ++ .../render-rich-text-to-html.util.spec.ts | 254 ++++++++++++ .../utils/render-email-body.util.ts | 38 ++ .../enums/engine-component-key.enum.ts | 1 + .../standard-command-menu-item.constant.ts | 16 + .../campaign-variable-pattern.constant.ts | 2 + .../src/modules/emailing/emailing.module.ts | 10 + .../campaign-variable.service.spec.ts | 168 ++++++++ .../message-campaign-draft.service.spec.ts | 297 ++++++++++++++ .../services/campaign-variable.service.ts | 192 +++++++++ .../message-campaign-draft.service.ts | 208 ++++++++++ .../services/message-campaign.service.ts | 47 ++- .../__tests__/save-campaign-tool.spec.ts | 129 ++++++ .../tools/save-campaign-tool.schema.ts | 38 ++ .../emailing/tools/save-campaign-tool.ts | 85 ++++ ...llect-campaign-variable-names.util.spec.ts | 89 +++++ .../render-campaign-body.util.spec.ts | 161 ++++++-- ...ampaign-variable-names-from-string.util.ts | 17 + ...aign-variable-names-from-templates.util.ts | 31 ++ .../collect-campaign-variable-names.util.ts | 22 ++ .../utils/render-campaign-body.util.ts | 70 +--- .../utils/render-campaign-template.util.ts | 4 +- ...sendable-draft-campaign.zod-schema.spec.ts | 110 +++++- .../sendable-draft-campaign.zod-schema.ts | 8 +- .../send-email.workflow-action.spec.ts | 111 ++++++ .../mail-sender/email-workflow-action.base.ts | 21 +- .../__tests__/resolve-email-body.util.spec.ts | 109 +++++ ...orkflow-email-template-string.util.spec.ts | 29 ++ .../utils/resolve-email-body.util.ts | 90 ++++- ...lve-workflow-email-template-string.util.ts | 26 ++ .../tool-backed.workflow-action.ts | 11 +- .../src/constants/EmailImageMimeTypes.ts | 6 + packages/twenty-shared/src/constants/index.ts | 1 + .../twenty-shared/src/types/FileFolder.ts | 1 + .../twenty-shared/src/types/SidePanelPages.ts | 1 + packages/twenty-shared/src/utils/index.ts | 25 +- .../__tests__/campaign-variables.test.ts | 75 ++++ .../__tests__/email-document-schema.test.ts | 231 +++++++++++ .../transform-email-document-strings.test.ts | 60 +++ .../src/utils/tiptap/canvas-theme.ts | 25 ++ .../src/utils/tiptap/email-document-node.ts | 7 + .../tiptap/email-document-schema-version.ts | 1 + .../src/utils/tiptap/email-document-schema.ts | 184 +++++++++ .../tiptap/email-document-string-context.ts | 1 + .../twenty-shared/src/utils/tiptap/index.ts | 14 +- .../src/utils/tiptap/is-canvas-theme.ts | 4 + .../list-campaign-variables-for-fields.ts | 79 ++++ .../src/utils/tiptap/parse-email-document.ts | 23 ++ .../src/utils/tiptap/resolve-canvas-theme.ts | 5 + .../src/utils/tiptap/tiptap-mark-types.ts | 21 + .../utils/tiptap/tiptap-marks-render-order.ts | 9 + .../src/utils/tiptap/tiptap-marks.ts | 48 --- .../src/utils/tiptap/tiptap-node-types.ts | 20 + .../transform-email-document-strings.ts | 104 +++++ 243 files changed, 7620 insertions(+), 569 deletions(-) create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/button.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/columns.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/divider.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/html.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/section.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/utils/block-style.ts create mode 100644 packages/twenty-emails/src/utils/email-renderer/utils/inherited-typography.ts create mode 100644 packages/twenty-emails/src/utils/email-renderer/utils/safe-url.ts create mode 100644 packages/twenty-front/src/modules/activities/emails/hooks/useCampaignEmailEditorVariables.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/components/InsertRail.tsx create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/components/StyledBubbleMenuContainer.tsx create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/constants/AdvancedTextEditorCapabilityExtensions.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/constants/AdvancedTextEditorCoreExtensions.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/constants/EmailDocumentCapabilities.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/constants/FullRichTextCapabilities.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/ButtonNode.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/ColumnNode.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/ColumnsNode.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/DividerNode.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/HtmlNode.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/HtmlNodeView.tsx create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/SectionNode.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/ThemedDocument.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/blocks/readBlockStyleAttribute.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/campaign-variables/CampaignVariableChip.tsx create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/extensions/campaign-variables/CampaignVariableTag.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadEmailImage.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/states/activeEmailEditorState.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/types/AdvancedTextEditorCapability.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/types/AdvancedTextEditorContentType.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/types/UploadedImage.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/__tests__/buildAdvancedTextEditorExtensions.test.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/__tests__/getBlockSelectionTarget.test.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/__tests__/hasEditorExtension.test.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/__tests__/parseCssSizeValue.test.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/__tests__/sanitizeHtmlPreview.test.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/__tests__/serializeAdvancedTextEditorContent.test.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/buildAdvancedTextEditorExtensions.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/getBlockSelectionTarget.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/getBlockStyle.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/hasEditorExtension.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/inlineStyleToCss.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/parseCssSizeValue.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/sanitizeHtmlPreview.ts create mode 100644 packages/twenty-front/src/modules/advanced-text-editor/utils/serializeAdvancedTextEditorContent.ts create mode 100644 packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/OpenEmailBlockSettingsSingleRecordCommand.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/hooks/useOpenEmailBlockSettingsInSidePanel.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/EmailAlignmentInput.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/EmailBlockSettingsFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/EmailBoxSidesInput.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/EmailColorInput.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/EmailPageStyleSection.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/EmailSizeInput.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/SidePanelEmailBlockSettingsPage.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/components/StyledEmailFieldLabel.tsx create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/constants/EmailBlockSettingsFields.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/constants/EmailBodyThemeFields.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/constants/EmailPageThemeFields.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/types/EmailThemeField.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/utils/getEffectiveSectionStyleValue.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/email-block-settings/utils/getEmailBlockLabel.ts rename packages/twenty-server/src/database/commands/upgrade-version-command/2-28/{2-28-workspace-command-1786000000000-sync-discard-draft-workflow-availability-expression.command.ts => 2-28-workspace-command-1785858486000-sync-discard-draft-workflow-availability-expression.command.ts} (98%) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-28/2-28-workspace-command-1785921674941-add-email-block-settings-command-menu-item.command.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/utils/__tests__/append-html-footer.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/utils/append-html-footer.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/utils/__tests__/render-email-body.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/utils/__tests__/render-rich-text-to-html.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/utils/render-email-body.util.ts create mode 100644 packages/twenty-server/src/modules/emailing/constants/campaign-variable-pattern.constant.ts create mode 100644 packages/twenty-server/src/modules/emailing/services/__tests__/campaign-variable.service.spec.ts create mode 100644 packages/twenty-server/src/modules/emailing/services/__tests__/message-campaign-draft.service.spec.ts create mode 100644 packages/twenty-server/src/modules/emailing/services/campaign-variable.service.ts create mode 100644 packages/twenty-server/src/modules/emailing/services/message-campaign-draft.service.ts create mode 100644 packages/twenty-server/src/modules/emailing/tools/__tests__/save-campaign-tool.spec.ts create mode 100644 packages/twenty-server/src/modules/emailing/tools/save-campaign-tool.schema.ts create mode 100644 packages/twenty-server/src/modules/emailing/tools/save-campaign-tool.ts create mode 100644 packages/twenty-server/src/modules/emailing/utils/__tests__/collect-campaign-variable-names.util.spec.ts create mode 100644 packages/twenty-server/src/modules/emailing/utils/collect-campaign-variable-names-from-string.util.ts create mode 100644 packages/twenty-server/src/modules/emailing/utils/collect-campaign-variable-names-from-templates.util.ts create mode 100644 packages/twenty-server/src/modules/emailing/utils/collect-campaign-variable-names.util.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/__tests__/resolve-email-body.util.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/__tests__/resolve-workflow-email-template-string.util.spec.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/resolve-workflow-email-template-string.util.ts create mode 100644 packages/twenty-shared/src/constants/EmailImageMimeTypes.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/__tests__/campaign-variables.test.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/__tests__/email-document-schema.test.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/__tests__/transform-email-document-strings.test.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/canvas-theme.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/email-document-node.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/email-document-schema-version.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/email-document-schema.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/email-document-string-context.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/is-canvas-theme.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/list-campaign-variables-for-fields.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/parse-email-document.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/resolve-canvas-theme.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/tiptap-mark-types.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/tiptap-marks-render-order.ts delete mode 100644 packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/tiptap-node-types.ts create mode 100644 packages/twenty-shared/src/utils/tiptap/transform-email-document-strings.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 614e41286c..809184eb59 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -188,6 +188,7 @@ enum EngineComponentKey { COMPOSE_CAMPAIGN SEND_MESSAGE_CAMPAIGN SEND_MESSAGE_CAMPAIGN_TEST + EMAIL_BLOCK_SETTINGS GO_TO_PEOPLE GO_TO_COMPANIES GO_TO_DASHBOARDS @@ -3726,6 +3727,7 @@ enum FileFolder { Dependencies Workflow EmailAttachment + EmailImage AppTarball GeneratedSdkClient Dpa diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index db2fd54918..bf6a8c689d 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -136,7 +136,7 @@ export interface CommandMenuItem { __typename: 'CommandMenuItem' } -export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'RETRY_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'COMPOSE_CAMPAIGN' | 'SEND_MESSAGE_CAMPAIGN' | 'SEND_MESSAGE_CAMPAIGN_TEST' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS' +export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'RETRY_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'COMPOSE_CAMPAIGN' | 'SEND_MESSAGE_CAMPAIGN' | 'SEND_MESSAGE_CAMPAIGN_TEST' | 'EMAIL_BLOCK_SETTINGS' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS' export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK' @@ -3187,7 +3187,7 @@ export interface Mutation { __typename: 'Mutation' } -export type FileFolder = 'CorePicture' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient' | 'Dpa' +export type FileFolder = 'CorePicture' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'EmailImage' | 'AppTarball' | 'GeneratedSdkClient' | 'Dpa' export type RunAgentMessageRole = 'user' | 'assistant' @@ -9184,6 +9184,7 @@ export const enumEngineComponentKey = { COMPOSE_CAMPAIGN: 'COMPOSE_CAMPAIGN' as const, SEND_MESSAGE_CAMPAIGN: 'SEND_MESSAGE_CAMPAIGN' as const, SEND_MESSAGE_CAMPAIGN_TEST: 'SEND_MESSAGE_CAMPAIGN_TEST' as const, + EMAIL_BLOCK_SETTINGS: 'EMAIL_BLOCK_SETTINGS' as const, GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const, GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const, GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const, @@ -9860,6 +9861,7 @@ export const enumFileFolder = { Dependencies: 'Dependencies' as const, Workflow: 'Workflow' as const, EmailAttachment: 'EmailAttachment' as const, + EmailImage: 'EmailImage' as const, AppTarball: 'AppTarball' as const, GeneratedSdkClient: 'GeneratedSdkClient' as const, Dpa: 'Dpa' as const diff --git a/packages/twenty-emails/src/common-style.ts b/packages/twenty-emails/src/common-style.ts index 0fe58a1255..5dd76e1fbc 100644 --- a/packages/twenty-emails/src/common-style.ts +++ b/packages/twenty-emails/src/common-style.ts @@ -26,7 +26,7 @@ const colors = { blue40: '#5e90f2', }; -export const emailTheme = { +export const canvasTheme = { font: { colors: { highlighted: grayScale.gray60, diff --git a/packages/twenty-emails/src/components/BaseHead.tsx b/packages/twenty-emails/src/components/BaseHead.tsx index a911533f6e..61680ce5dc 100644 --- a/packages/twenty-emails/src/components/BaseHead.tsx +++ b/packages/twenty-emails/src/components/BaseHead.tsx @@ -1,16 +1,16 @@ import { Font, Head } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; export const BaseHead = () => { return ( Twenty email ); diff --git a/packages/twenty-emails/src/components/CallToAction.tsx b/packages/twenty-emails/src/components/CallToAction.tsx index ff384d243a..bce63c056f 100644 --- a/packages/twenty-emails/src/components/CallToAction.tsx +++ b/packages/twenty-emails/src/components/CallToAction.tsx @@ -1,18 +1,18 @@ import { type JSX } from 'react'; import { Button } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; const callToActionStyle = { display: 'inline-flex', padding: '8px 32px', - borderRadius: emailTheme.border.radius.md, - border: `1px solid ${emailTheme.background.transparent.light}`, - background: emailTheme.background.button, - boxShadow: `0px 2px 4px 0px ${emailTheme.background.transparent.light}, 0px 0px 4px 0px ${emailTheme.background.transparent.medium}`, - color: emailTheme.font.colors.inverted, - fontSize: emailTheme.font.size.md, - fontWeight: emailTheme.font.weight.bold, + borderRadius: canvasTheme.border.radius.md, + border: `1px solid ${canvasTheme.background.transparent.light}`, + background: canvasTheme.background.button, + boxShadow: `0px 2px 4px 0px ${canvasTheme.background.transparent.light}, 0px 0px 4px 0px ${canvasTheme.background.transparent.medium}`, + color: canvasTheme.font.colors.inverted, + fontSize: canvasTheme.font.size.md, + fontWeight: canvasTheme.font.weight.bold, width: 'auto', }; diff --git a/packages/twenty-emails/src/components/HighlightedContainer.tsx b/packages/twenty-emails/src/components/HighlightedContainer.tsx index 599cd18211..3b53f96eee 100644 --- a/packages/twenty-emails/src/components/HighlightedContainer.tsx +++ b/packages/twenty-emails/src/components/HighlightedContainer.tsx @@ -1,16 +1,16 @@ import { Column, Container, Row } from 'react-email'; import React, { type JSX } from 'react'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; type HighlightedContainerProps = { children: JSX.Element | JSX.Element[] | string; }; const highlightedContainerStyle = { - background: emailTheme.background.colors.highlight, - border: `1px solid ${emailTheme.border.color.highlighted}`, - borderRadius: emailTheme.border.radius.md, + background: canvasTheme.background.colors.highlight, + border: `1px solid ${canvasTheme.border.color.highlighted}`, + borderRadius: canvasTheme.border.radius.md, padding: '24px 48px', } as React.CSSProperties; diff --git a/packages/twenty-emails/src/components/HighlightedText.tsx b/packages/twenty-emails/src/components/HighlightedText.tsx index 9ca8c04a9f..76b07a85ae 100644 --- a/packages/twenty-emails/src/components/HighlightedText.tsx +++ b/packages/twenty-emails/src/components/HighlightedText.tsx @@ -1,15 +1,15 @@ import { Text } from 'react-email'; import { type JSX } from 'react'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; const highlightedStyle = { - borderRadius: emailTheme.border.radius.sm, - background: emailTheme.background.colors.highlight, + borderRadius: canvasTheme.border.radius.sm, + background: canvasTheme.background.colors.highlight, padding: '4px 8px', - fontSize: emailTheme.font.size.lg, - fontWeight: emailTheme.font.weight.bold, - color: emailTheme.font.colors.highlighted, + fontSize: canvasTheme.font.size.lg, + fontWeight: canvasTheme.font.weight.bold, + color: canvasTheme.font.colors.highlighted, }; type HighlightedTextProps = { diff --git a/packages/twenty-emails/src/components/Link.tsx b/packages/twenty-emails/src/components/Link.tsx index d7ea615593..a265e96cf2 100644 --- a/packages/twenty-emails/src/components/Link.tsx +++ b/packages/twenty-emails/src/components/Link.tsx @@ -1,7 +1,7 @@ import { type JSX } from 'react'; import { Link as EmailLink } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; const linkStyle = { textDecoration: 'underline', @@ -19,7 +19,7 @@ export const Link = ({ value, href, color }: LinkProps) => { href={href} style={{ ...linkStyle, - color: color ?? emailTheme.font.colors.tertiary, + color: color ?? canvasTheme.font.colors.tertiary, }} > {value} diff --git a/packages/twenty-emails/src/components/MainText.tsx b/packages/twenty-emails/src/components/MainText.tsx index fe8f09a46f..b30aa071e9 100644 --- a/packages/twenty-emails/src/components/MainText.tsx +++ b/packages/twenty-emails/src/components/MainText.tsx @@ -1,18 +1,18 @@ import { type JSX } from 'react'; import { Text } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; type MainTextProps = { children: JSX.Element | JSX.Element[] | string; }; const mainTextStyle = { - fontFamily: emailTheme.font.family, - fontSize: emailTheme.font.size.md, - fontWeight: emailTheme.font.weight.regular, - color: emailTheme.font.colors.primary, - lineHeight: emailTheme.font.lineHeight, + fontFamily: canvasTheme.font.family, + fontSize: canvasTheme.font.size.md, + fontWeight: canvasTheme.font.weight.regular, + color: canvasTheme.font.colors.primary, + lineHeight: canvasTheme.font.lineHeight, }; export const MainText = ({ children }: MainTextProps) => { diff --git a/packages/twenty-emails/src/components/ShadowText.tsx b/packages/twenty-emails/src/components/ShadowText.tsx index 138e23e6c7..147899ce50 100644 --- a/packages/twenty-emails/src/components/ShadowText.tsx +++ b/packages/twenty-emails/src/components/ShadowText.tsx @@ -1,18 +1,18 @@ import { type JSX } from 'react'; import { Text } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; type ShadowTextProps = { children: JSX.Element | JSX.Element[] | string; }; const shadowTextStyle = { - fontSize: emailTheme.font.size.sm, - fontWeight: emailTheme.font.weight.regular, - color: emailTheme.font.colors.tertiary, + fontSize: canvasTheme.font.size.sm, + fontWeight: canvasTheme.font.weight.regular, + color: canvasTheme.font.colors.tertiary, margin: '0 0 12px 0', - lineHeight: emailTheme.font.lineHeight, + lineHeight: canvasTheme.font.lineHeight, }; export const ShadowText = ({ children }: ShadowTextProps) => { diff --git a/packages/twenty-emails/src/components/SubTitle.tsx b/packages/twenty-emails/src/components/SubTitle.tsx index fdfd4b2b58..e5b5443ade 100644 --- a/packages/twenty-emails/src/components/SubTitle.tsx +++ b/packages/twenty-emails/src/components/SubTitle.tsx @@ -1,17 +1,17 @@ import { type JSX } from 'react'; import { Heading } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; type SubTitleProps = { value: JSX.Element | JSX.Element[] | string; }; const subTitleStyle = { - fontFamily: emailTheme.font.family, - fontSize: emailTheme.font.size.lg, - fontWeight: emailTheme.font.weight.bold, - color: emailTheme.font.colors.highlighted, + fontFamily: canvasTheme.font.family, + fontSize: canvasTheme.font.size.lg, + fontWeight: canvasTheme.font.weight.bold, + color: canvasTheme.font.colors.highlighted, }; export const SubTitle = ({ value }: SubTitleProps) => { diff --git a/packages/twenty-emails/src/components/Title.tsx b/packages/twenty-emails/src/components/Title.tsx index fc8336b8c6..a4320d70d5 100644 --- a/packages/twenty-emails/src/components/Title.tsx +++ b/packages/twenty-emails/src/components/Title.tsx @@ -1,17 +1,17 @@ import { type JSX } from 'react'; import { Heading } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; type TitleProps = { value: JSX.Element | JSX.Element[] | string; }; const titleStyle = { - fontFamily: emailTheme.font.family, - fontSize: emailTheme.font.size.xl, - fontWeight: emailTheme.font.weight.bold, - color: emailTheme.font.colors.highlighted, + fontFamily: canvasTheme.font.family, + fontSize: canvasTheme.font.size.xl, + fontWeight: canvasTheme.font.weight.bold, + color: canvasTheme.font.colors.highlighted, }; export const Title = ({ value }: TitleProps) => { diff --git a/packages/twenty-emails/src/emails/send-invite-link.email.tsx b/packages/twenty-emails/src/emails/send-invite-link.email.tsx index a4c8e58a67..d29b3ba13e 100644 --- a/packages/twenty-emails/src/emails/send-invite-link.email.tsx +++ b/packages/twenty-emails/src/emails/send-invite-link.email.tsx @@ -1,6 +1,6 @@ import { Trans } from '@lingui/react'; import { Img } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; @@ -55,7 +55,7 @@ export const SendInviteLinkEmail = ({ ), 1: , diff --git a/packages/twenty-emails/src/emails/validate-approved-access-domain.email.tsx b/packages/twenty-emails/src/emails/validate-approved-access-domain.email.tsx index 95d3480d20..05104d01b2 100644 --- a/packages/twenty-emails/src/emails/validate-approved-access-domain.email.tsx +++ b/packages/twenty-emails/src/emails/validate-approved-access-domain.email.tsx @@ -1,6 +1,6 @@ import { Trans } from '@lingui/react'; import { Img } from 'react-email'; -import { emailTheme } from 'src/common-style'; +import { canvasTheme } from 'src/common-style'; import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; @@ -56,7 +56,7 @@ export const SendApprovedAccessDomainValidation = ({ ), 1: , diff --git a/packages/twenty-emails/src/utils/email-renderer/email-renderer.tsx b/packages/twenty-emails/src/utils/email-renderer/email-renderer.tsx index 4ae8fda576..d63b68042f 100644 --- a/packages/twenty-emails/src/utils/email-renderer/email-renderer.tsx +++ b/packages/twenty-emails/src/utils/email-renderer/email-renderer.tsx @@ -1,23 +1,61 @@ -import { Body, Head, Html } from 'react-email'; +import { type ReactNode } from 'react'; +import { Body, Container, Head, Html } from 'react-email'; import { type JSONContent } from '@tiptap/core'; +import { type CanvasTheme, resolveCanvasTheme } from 'twenty-shared/utils'; import { mappedNodeContent } from 'src/utils/email-renderer/renderers/render-node'; +const BASE_STYLE_RESET = `blockquote,h1,h2,h3,img,li,ol,p,ul{margin-top:0;margin-bottom:0}`; + +const themedBody = (theme: CanvasTheme, children: ReactNode) => ( + + + {children} + + +); + export const reactMarkupFromJSON = (json: JSONContent | string) => { if (typeof json === 'string') { return json; } const jsxNodes = mappedNodeContent(json); + const canvasTheme = resolveCanvasTheme(json.attrs?.canvasTheme); + return (