80fb91c033dfd367f17ba58e2095526faa016e70
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1d755983ff |
Feat/advanced text editor capability presets (#23657)
# 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*
`</html>`, 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 `</html>`**, outside the
document, where Gmail strips it. Legally significant, since an
unsubscribe link is required. Now inserted before `</body>`.
- **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.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23657?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>
Co-authored-by: Félix Malfait <felix@twenty.com>
|
||
|
|
d37ed7e07c |
Optimize merge queue to only run E2E and integrate prettier into lint (#18459)
## Summary - **Merge queue optimization**: Created a dedicated `ci-merge-queue.yaml` workflow that only runs Playwright E2E tests on `ubuntu-latest-8-cores`. Removed `merge_group` trigger from all 7 existing CI workflows (front, server, shared, website, sdk, zapier, docker-compose). The merge queue goes from ~30+ parallel jobs to a single focused E2E job. - **Label-based merge queue simulation**: Added `run-merge-queue` label support so developers can trigger the exact merge queue E2E pipeline on any open PR before it enters the queue. - **Prettier in lint**: Chained `prettier --check` into `lint` and `prettier --write` into `lint --configuration=fix` across `nx.json` defaults, `twenty-front`, and `twenty-server`. Prettier formatting errors are now caught by `lint` and fixed by `lint:fix` / `lint:diff-with-main --configuration=fix`. ## After merge (manual repo settings) Update GitHub branch protection required status checks: 1. Remove old per-workflow merge queue checks (`ci-front-status-check`, `ci-e2e-status-check`, `ci-server-status-check`, etc.) 2. Add `ci-merge-queue-status-check` as the required check for the merge queue |
||
|
|
b3c95744ef |
Fix twenty-emails build (#17760)
## Context
I've seen errors in twenty-emails build where I18n from @lingui/core was
resolved to two different declaration files
```typescript
src/components/BaseEmail.tsx:20:19 - error TS2719: Type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n' is not assignable to type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n'. Two different types with this name exist, but they are unrelated.
Types have separate declarations of a private property '_locale'.
20 <I18nProvider i18n={i18nInstance}>
~~~~
node_modules/@lingui/react/dist/shared/react.b2b749a9.d.ts:42:5
42 i18n: I18n;
~~~~
The expected type comes from property 'i18n' which is declared here on type 'IntrinsicAttributes & Omit<I18nContext, "_"> & { children?: ReactNode; }'
```
Seems to be related to https://github.com/twentyhq/twenty/pull/17380
The tsconfig simplification changed how vite plugin resolves types during build. With the inherited moduleResolution: "node", the plugin and source code resolved @lingui/react's types differently, creating two incompatible I18n types from the same package.
## Fix
Add moduleResolution: "bundler" to packages/twenty-emails/tsconfig.json, aligning with twenty-front, twenty-ui, twenty-shared should fix the issue
|
||
|
|
41dd9856e6 |
fix(twenty-front): fix tsconfig to properly typecheck all files with tsgo (#17380)
## Summary This PR fixes the `tsconfig` setup in `twenty-front` so that `tsgo -p tsconfig.json` properly type-checks all files. ### Root Cause The previous setup used TypeScript project references with `files: []` in the main `tsconfig.json`. When running `tsgo -p tsconfig.json`, this checks nothing because `tsgo` requires the `-b` (build) flag for project references, but the configs weren't set up for composite mode. ### Changes **Simplified tsconfig architecture (4 files → 2):** - `tsconfig.json` - All files (dev, tests, stories) for typecheck/IDE/lint - `tsconfig.build.json` - Production files only (excludes tests/stories) **Removed redundant configs:** - `tsconfig.dev.json` - `tsconfig.spec.json` - `tsconfig.storybook.json` **Updated references:** - `jest.config.mjs` → uses `tsconfig.json` - `eslint.config.mjs` → uses `tsconfig.json` - `vite.config.ts` → uses `tsconfig.json` for dev **Type fixes (pre-existing errors revealed by proper typechecking):** - Made `applicationId` optional in `FieldMetadataItem` and `ObjectMetadataItem` - Added missing `navigationMenuItem` translation - Added `objectLabelSingular` to Search GraphQL query - Fixed `sortMorphItems.test.ts` mock data ## Test plan - [ ] Run `npx nx typecheck twenty-front` - should pass - [ ] Run `npx nx lint twenty-front` - should work - [ ] Run `npx nx test twenty-front` - should work - [ ] Run `npx nx build twenty-front` - should work - [ ] Verify IDE type checking works correctly |
||
|
|
dc93cf4c59 |
feat: add TypeScript Go (tsgo) for faster type checking (#17211)
## Summary - Add `@typescript/native-preview` (tsgo) for dramatically faster type checking on frontend projects - Configure tsgo as default for frontend projects (twenty-front, twenty-ui, twenty-shared, etc.) - Keep tsc for twenty-server (faster for NestJS decorator-heavy code) - Fix type imports for tsgo compatibility (DOMPurify, AxiosInstance) - Remove deprecated `baseUrl` from tsconfigs where safe ## Performance Results | Project | tsgo | tsc -b | Speedup | |---------|------|--------|---------| | **twenty-front** | 1.4s | 60.7s | **43x faster** | | **twenty-server** | 2m42s | 1m10s | tsc is faster (decorators) | tsgo excels at modern React/JSX codebases but struggles with decorator-heavy NestJS backends, so we use the optimal checker for each. ## Usage ```bash # Default (tsgo for frontend, tsc for backend) nx typecheck twenty-front nx typecheck twenty-server # Force tsc fallback if needed nx typecheck twenty-front --configuration=tsc # Force tsgo on backend (slower, not recommended) nx typecheck twenty-server --configuration=tsgo ``` ## Test plan - [x] `nx typecheck twenty-front` passes with tsgo - [x] `nx typecheck twenty-server` passes with tsc - [x] `nx run-many -t typecheck --exclude=fireflies` passes - [ ] CI tests pass |
||
|
|
b27a97f2c5 |
feat: enforce @/ alias for imports and fix all relative parent imports (#16787)
## Summary This PR enforces the use of `@/` alias for imports instead of relative parent imports (`../`). ## Changes ### ESLint Configuration - Added `no-restricted-imports` pattern in `eslint.config.react.mjs` to block `../*` imports with the message "Relative parent imports are not allowed. Use @/ alias instead." - Removed the non-working `import/no-relative-parent-imports` rule (doesn't work properly in ESLint flat config) ### VS Code Settings - Added `javascript.preferences.importModuleSpecifier: non-relative` to `.vscode/settings.json` (TypeScript setting was already there) ### Code Fixes - Fixed **941 relative parent imports** across **706 files** in `packages/twenty-front` - All `../` imports converted to use `@/` alias ## Why - Consistent import style across the codebase - Easier to move files without breaking imports - Better IDE support for auto-imports - Clearer understanding of where imports come from |
||
|
|
39e7f6cec3 |
setup localization for twenty-emails (#9806)
One of the steps to address #8128 How to test: Please change the locale in the settings and click on change password button. A password reset email in the preferred locale will be sent.   Todo: - Remove the hardcoded locales for invitation, warn suspended workspace email, clean suspended workspace emails - Need to test invitation, email verification, warn suspended workspace email, clean suspended workspace emails - The duration variable `5 minutes` is always in english. Do we need to do something about that? It does seems odd in case of chinese translations. Notes: - Only tested the password reset , password update notify templates. - Cant test email verification due to error during sign up `Internal server error: New workspace setup is disabled` --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
7d7955fc65 |
Move capitalize into twenty-shared (#9414)
capitalize had been moved into twenty-shared. Let's remove the duplicates in server and front ! |
||
|
|
08a9db2df6 |
Add Twenty Shared & Fix profile image rendering (#8841)
PR Summary: 1. Added `Twenty Shared` Package to centralize utilitiies as mentioned in #8942 2. Optimization of `getImageAbsoluteURI.ts` to handle edge cases  --------- Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
a654205dbc |
chore: set up twenty-emails config so build isn't needed in development (#3619)
* chore: set up twenty-emails config so build isn't needed in development * fix: fix script dependency * chore: use @vitejs/plugin-react-swc * Remove useless dependancy * Fix typing * chore: use baseUrl in twenty-emails * chore: fix docker server prod build * refactor: optimize Docker file and tsconfig * fix: fix WORKDIR in docker --------- Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
e358d677f9 |
Move emails to dedicated package (#3542)
* Add new package * Add twenty-emails package * Use generated files from twenty-emails in twenty-server * Fix deleted file * Import emails templates properly |