Commit Graph

6260 Commits

Author SHA1 Message Date
Priyanshu Bartwal 7babc049f5 [Twenty-Front]: Record board column drag and drop functionality (#22323)
Closes #22321 

- Used `@dnd-kit` library for core drag and drop logic.
- Tried to keep as much similar to #21304 as possible.


https://github.com/user-attachments/assets/bff100cf-d727-4281-a5fa-b010a373f189



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22323?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: Charles Bochet <charles@twenty.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
2026-07-10 13:47:59 +00:00
Raphaël Bosi 60f5964c64 Run front components in a sandboxed opaque-origin iframe (#22588)
Front components run untrusted third-party React in a Web Worker. That
worker previously shared the host origin, so it could reach
origin-scoped storage (the metadata-store IndexedDB, the
`twenty-sign-out` BroadcastChannel), cookies, and same-origin resources.

This runs the worker inside a `sandbox="allow-scripts"` (no
`allow-same-origin`) iframe, giving it an opaque origin where the
browser denies localStorage, cookies, IndexedDB, and BroadcastChannel
outright. The worker is kept inside the iframe (rather than a bare
iframe) so untrusted code always runs off the main thread; the
remote-dom render path is unchanged.

- **Transport:** host ↔ iframe ↔ worker over a re-transferred
`MessagePort` (`ThreadMessagePort`); a small bootstrap script is inlined
into the iframe via `srcdoc` (bundled at build time by a prebuild step)
and relays the port to the worker it spawns. Messages across the
boundary use a typed discriminated union with a single parse/guard.
- **Network:** under the opaque origin, direct fetches to the Twenty API
would be `Origin: null`, so the component source and SDK modules are
fetched through an allowlisted, credential-omitting `hostFetch` bridge
and blobbed inside the worker. The allowlist is single-sourced on the
host (http(s) origins only) and carried in the render context. The
bridge is mandatory (rendering fails closed if it is missing), refuses
redirects except for GET/HEAD to the known file-storage URLs, and caps
response body size.
- **SDK loading:** SDK client modules now load inside the worker through
the bridge, replacing the host-side SDK-blob state/effect/provider with
a pure `getSdkClientUrls` URL builder.
- **Isolation tests:** a unit test locks the sandbox attribute
(`allow-scripts`, never `allow-same-origin`); a browser test asserts the
worker actually gets an opaque origin with storage denied, probing
cookies by writing one rather than reading an empty jar.

Also adds a "List Companies" seed front component that queries workspace
data via the SDK client (exercising the bridge end-to-end),
single-sources the command-menu confirmation-modal result event name and
detail type in `twenty-shared` (previously a hand-synced duplicate), and
decomposes the renderer (bridge, sandbox, worker orchestration) into
small single-purpose utils with unit tests.

## How it works

```mermaid
sequenceDiagram
    autonumber
    participant Host as Host window (twenty-front · host origin)
    participant Frame as Sandboxed iframe (allow-scripts · opaque origin)
    participant Worker as Worker (untrusted component · opaque origin)
    participant API as Twenty API (host origin)

    rect rgb(238,242,248)
    Note over Host,Worker: 1 — Boot handshake
    Host->>Frame: create iframe sandbox="allow-scripts", srcdoc = inlined bootstrap script
    Host->>Host: MessageChannel + ThreadMessagePort(port1)<br/>exports = host API + hostFetch
    Frame-->>Host: READY
    Host->>Frame: INIT + transfer port2
    Frame->>Worker: spawn inlined Worker + re-transfer port2
    Worker->>Worker: ThreadMessagePort(port)<br/>exports = render / updateContext
    Note over Host,Worker: Port now entangles Host ↔ Worker directly
    end

    rect rgb(246,240,248)
    Note over Host,Worker: 2 — Render
    Host->>Worker: render(connection, { componentUrl, sdkClientUrls, hostFetchOrigins, token })
    Worker->>Worker: override globalThis.fetch<br/>(Twenty origins → hostFetch)
    end

    rect rgb(248,244,238)
    Note over Worker,API: 3 — Network via hostFetch bridge (opaque Origin:null cannot reach the API directly)
    Worker->>Host: hostFetch(componentUrl, Bearer)
    Host->>Host: origin allowlist + credentials:'omit'
    Host->>API: fetch(componentUrl)
    API-->>Host: source
    Host-->>Worker: { status, headers, body }
    Worker->>Host: hostFetch(sdkClientUrls.core / .metadata)
    Host-->>Worker: SDK module sources
    Worker->>Worker: blob each source in its own opaque origin → import() → run untrusted React
    end

    rect rgb(238,248,242)
    Note over Worker,Host: 4 — Render mirror
    Worker->>Host: remote-dom mutations (RemoteConnection)
    Host->>Host: RemoteReceiver → RemoteRootRenderer → host DOM
    end

    Note over Worker: Opaque origin ⇒ browser denies localStorage,<br/>cookies, IndexedDB, BroadcastChannel
```
2026-07-10 13:10:30 +00:00
Marie f667ba500c fix(front): clean stale morph relations from metadata store on object deletion (#22681)
## Problem

After deleting a custom object (e.g. `meeting`), the app crashes with
"Sorry, something went wrong" on pages that load records referencing
that object through a morph relation. The console shows:

```
Target object metadata item not found for target (morph target meeting)
```

It reproduces on the machine that used the object before deletion but
not on a fresh machine, which points at a stale client metadata store
rather than a server issue.

## Root cause

Every field carries its own server-provided `morphRelations` array; a
morph relation field (note/task/timeline targets, etc.) lists every
object it can point to, including the deleted one. When an object is
deleted, `useDeleteOneObjectMetadataItem` and the SSE `delete` handler
only remove the deleted **object** and its own fields from the metadata
store. The sibling morph fields on other objects keep their now-dangling
`morphRelations` entry pointing at the deleted object.

Those stale entries were only meant to be cleaned up later by a
collection-hash-triggered `network-only` refetch. When that
reconciliation does not win, `generateDepthRecordGqlFieldsFromFields`
can't resolve the deleted morph target in `objectMetadataItems` and
throws, crashing the page.

## Fix

Clean `morphRelations` entries referencing the deleted object from the
field metadata store at deletion time, so the store stays
self-consistent immediately instead of relying on an async refetch.
Applied in both paths that handle object deletion:

- `useDeleteOneObjectMetadataItem` (the client performing the deletion)
- `MetadataStoreSSEEffect` delete handler (other tabs/clients receiving
the event)

The throw in `generateDepthRecordGqlFieldsFromFields` is intentionally
left in place so any genuine future metadata inconsistency still
surfaces rather than being silently swallowed.

## Test

Added a unit test for the cleaning util covering: morph relations
targeting the deleted object are removed, only changed fields are
returned, non-morph fields are untouched, and nothing is returned when
no relation targets the deleted object.
2026-07-10 14:55:25 +02:00
Abdul Rahman ffdda50afc remove nestjs-query auto-resolver from index-metadata (#22775)
## Summary

Removes the `NestjsQueryGraphQLModule.forFeature` block from
`IndexMetadataModule`, continuing the incremental migration off
`@ptc-org/nestjs-query`.

- Drops the dead auto-generated read surface: `index` and
`indexMetadatas` queries, the `IndexConnection` /
`IndexObjectMetadataConnection` types, and the `Index.objectMetadata`
field. No client consumes these — the frontend reads indexes via
`ObjectMetadata.indexMetadatas`.
- Keeps `IndexMetadataDTO` nestjs-query-compatible (`@Authorize`,
`@FilterableField`, `@QueryOptions`, `@IDField`) because
`ObjectMetadataDTO` still references it via
`@CursorConnection('indexMetadatas')` until object-metadata is migrated.
- Hand-written `createOneIndex` / `deleteOneIndex` mutations and the
`indexFieldMetadataList` resolve-field are unchanged.
- Deletes the now-obsolete `index-metadatas` integration test and
regenerates the GraphQL schema artifacts (frontend + client-sdk).

## Breaking change

This is an intentional GraphQL schema breaking change
(`api-breaking-changes` CI will flag it)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22775?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. -->
2026-07-10 14:00:23 +02:00
Thomas Trompette b682162f31 fix(front): render +N button for right-edge-clipped chips in expandable list (#22748)
## Problem

Fixes #22383. Overflowing relation & multi-select chip cells didn't show
the "+N" overflow button, so hidden records/values were unreachable.
There were several distinct causes behind this, addressed below.

Before
<img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 43
13"
src="https://github.com/user-attachments/assets/1001d47b-0f54-4fbd-a5cb-83a5c32c35ac"
/>

After
<img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 41
54"
src="https://github.com/user-attachments/assets/d4f5cf2f-fda4-422e-b72d-2df3fc81a06f"
/>

## Changes

**1. Overflow detection missed right-edge-clipped chips**
`isFirstOverflowingChildElement` used `childElement.offsetLeft >
containerElement.clientWidth` (left edge past the container), which is
false when a chip is only right-edge clipped. Switched to a right-edge
check: `childElement.offsetLeft + childElement.offsetWidth >
containerElement.clientWidth`.

**2. Multi-select never used the overflow list when unfocused**
`MultiSelectFieldDisplay` rendered a plain clipped `MultiSelectDisplay`
when not focused, and only used `ExpandableList` on focus. That
non-focused fallback painted over the focused "+N" and hid it. It now
always renders through `ExpandableList` with
`isChipCountDisplayed={isFocused}`, matching
`RelationFromManyFieldDisplay`, so the count shows on focus only (not
idle).

**3. The hover portal was never actually focused**
`FieldFocusContextProvider` silently ignored its `isFocused` prop (`({
children }: any)` + hard-coded `useState(false)`), so
`RecordInlineCellAnchoredPortal`'s `<FieldFocusContextProvider
isFocused={true}>` had no effect and the hovered cell's display always
saw `isFocused=false`. That's why "+N" never appeared on hover for any
multi-value field. The portal now uses the existing
`FieldFocusStaticFocusedProvider`, fulfilling the intent — so relations,
multi-select, emails and phones all surface their "+N" on hover.

**4. "+N" was hard to read on multi-select**
The "+N" chip has a transparent background (shared component), and the
hover portal let the base layer's colored option chips bleed through it.
Gave the hover portal content an opaque `background.primary` so nothing
bleeds through; the "+N" component itself is untouched, so
relations/emails/phones keep their existing look.

## Test plan

- Hover a relation or multi-select field whose values overflow the cell:
the "+N" button appears (and is readable), and clicking it lists the
hidden records. When not hovered, no "+N" shows.
- Verify fully-overflowing rows still show the correct "+N" count.
2026-07-10 13:22:03 +02:00
Abdul Rahman 4ab36a630b remove nestjs-query from app-token and drop unused createOneAppToken mutation (#22765)
## What

Migrates `app-token` off `@ptc-org/nestjs-query` and removes the
auto-generated `createOneAppToken` mutation, which was unused dead API
surface.

## Why

The `createOneAppToken` mutation was reachable only from the schema — no
frontend query, SDK caller, or test used it. It was also non-functional
(its input couldn't set the token `value`), a leftover from
nestjs-query's default `create.one` being left enabled. Real app tokens
(refresh, password-reset, email-verification, invitation, OAuth,
enterprise) are all created directly via the repository in ~14 services,
none of which touched this mutation.

## Notes

- ⚠️ This removes `AppToken`, `createOneAppToken`,
`CreateAppTokenInput`, and `CreateOneAppTokenInput` from the `/metadata`
schema, so the **api-breaking-changes check will flag it**


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22765?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: Weiko <corentin@twenty.com>
2026-07-10 16:14:13 +05:30
github-actions[bot] f8d3555fe7 i18n - translations (#22779)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22779?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: github-actions <github-actions@twenty.com>
2026-07-10 12:05:07 +02:00
github-actions[bot] 09496a0f98 i18n - translations (#22745)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22745?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: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-07-10 11:56:49 +02:00
martmull 23cae2040a Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.

This makes assets always bundled files:

- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.

Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.

Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-07-10 09:18:52 +00:00
Raphaël Bosi 9c405384f3 Only propose configured apps during onboarding install step (#22712)
## What

- Onboarding "Install your first apps" now proposes only apps that are
actually installable: it intersects the onboarding list with
`findManyMarketplaceApps`, which the backend already filters to listed +
configured apps (all required server variables set).
- If none are available, the step auto-skips. If the marketplace query
fails, it shows an intentional fallback (heading + Skip) instead of
silently skipping or rendering an empty install card.
- `findManyMarketplaceApps` now accepts `universalIdentifiers`, so
onboarding fetches and configuration-checks only its own apps instead of
the entire catalog.

## Why

Previously the step rendered all hardcoded apps regardless of
configuration, only borrowing logos from the marketplace, so a user
could be offered an app the admin never configured. This centralizes
onboarding availability on the marketplace's existing logic and keeps
the query bounded as the marketplace grows.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22712?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. -->
2026-07-09 18:12:11 +02:00
Raphaël Bosi f06ba08b16 Fix onboarding locale reverting to English after signup (#22675)
During onboarding the UI reverted to English after email verification.
The chosen locale was never stored on the user, so the workspace member
seeded from it inherited `en`, and after workspace activation
`loadCurrentUser` reactivated `en` and flipped the whole UI to English
regardless of the browser language.

Two changes:

**Backend (the actual fix):** the `signUp` resolver received
`signUpInput.locale` but only used it for the verification email,
creating the user without it (so it defaulted to `en`). It now passes
the locale into `signUpWithoutWorkspace`, and the SSO create-a-workspace
path forwards `locale` as well. The user, and the workspace member
created from it at activation, now keep the chosen locale, so the
post-activation reactivation no longer forces English.

**Frontend:** carry the active locale across the workspace subdomain
redirect (in `useBuildSearchParamsFromUrlSyncedStates`) so the freshly
loaded subdomain renders `/verify` in the right language before the
current user loads, instead of briefly falling back to the browser
default. Minor, only affects the case where the chosen locale differs
from the browser language.
2026-07-09 14:08:09 +00:00
Raphaël Bosi c8c38b6cd2 Fix onboarding entry animations not triggering on first load (#22724)
The staggered entry animations on the onboarding welcome/sign-in and
sync-emails screens sometimes didn't play on first load, though a
refresh reliably fixed them.

Root cause: `OnboardingTransitionOutlet` wrapped pages in
`<AnimatePresence initial={false}>`. framer-motion propagates that
`initial: false` through `PresenceContext` to every descendant motion
component on the outlet's first render, so any
`OnboardingStepAnimatedItem` that mounts during that first commit snaps
straight to its final state instead of animating. Because onboarding
pages are preloaded (`lazyWithPreload`), a warm in-app navigation
renders the page synchronously in that first commit and the cascade is
suppressed; a cold load/refresh mounts it a commit later via Suspense,
so it animates.

Fix: drop `initial={false}`. Descendants no longer inherit a blocked
initial state, so the cascade animates regardless of preload timing. The
page-level fade now also runs on first entry, matching the treatment
already used on every step-to-step transition and cold load.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22724?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. -->
2026-07-09 15:32:58 +02:00
github-actions[bot] a3d9efd94a i18n - translations (#22725)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22725?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: github-actions <github-actions@twenty.com>
2026-07-09 13:35:36 +02:00
Félix Malfait 6897fff632 Rebuild email composer recipient fields as a structured chip input with person resolution and autocomplete (#22668)
# Why

The To/Cc/Bcc fields reused `FormMultiTextFieldInput`, the workflow
Tiptap tag editor, with recipients stored as a comma-separated string.
That caused every reported issue: duplicates were allowed, the field was
locked to one 32px line with a hidden horizontal scrollbar, chips did
nothing on click, `First Last <email>` could not even be typed (space
committed a tag) and was rejected by the backend when pasted, chips
could not be edited, invalid addresses only failed server-side after
pressing Send, and there was no autocomplete at all.

## The model

A recipient is `{ address, displayName? }`. Person and workspace member
are never stored in composer state; they are resolved live from the
address at render time, mirroring how `MatchParticipantService` links
`messageParticipant.handle` to `personId`/`workspaceMemberId` on the
receive side. Entities appear at the edges (autocomplete in, chip
display out); state, dedupe, validation, and send operate on addresses
only. The send path is unchanged: `SendEmailInput.to/cc/bcc` stay
comma-separated bare addresses.

# What changed

New module `activities/emails/recipients/` (the workflow editor is
untouched; its other consumers are unaffected):

- **`EmailRecipientsFieldInput`**: wrapping chip rows (up to ~3 lines,
then scroll), commit on Enter/Tab/comma/semicolon/blur, space commits
only when the buffer is already a valid email, paste parses RFC 5322
lists (names, quoted commas, semicolons, newlines), case-insensitive
dedupe with a flash on the existing chip, invalid addresses become red
chips that disable Send, double-click or keyboard editing in place with
Escape revert, Backspace select-then-delete, arrow-key chip navigation,
Ctrl/Cmd+Enter commits a pending buffer or sends when the buffer is
empty.
- **Person resolution**: chips resolve against People
(`emails.primaryEmail`, case-insensitive) and workspace members,
rendering avatar + name when known and degrading to a plain address chip
otherwise.
- **Chip menu**: person/member header, Copy email, Edit, Remove, and Add
as person for unknown addresses (creates the Person; the chip upgrades
in place).
- **Autocomplete**: blends context people (company you are composing
from, or the company behind a person/opportunity), ranked people search,
workspace members with a Team member badge, and a literal "Use this
email" row ranked first when the typed buffer is a valid address.
Suggestions exclude addresses already present in any field. Enter picks
the highlighted or top row.
- **Prefill**: replies and drafts preserve participant display names
(`getEmailDraftPrefillFromMessage`, `useReplyContext`).
- `useEmailComposerState` holds `EmailRecipient[]` per field and blocks
send on invalid recipients; the recipient-limit warning is surfaced
again in the composer.
- The Send Email engine command passes the record context so context
suggestions work from the record page action.
- `EmailsFilter` was missing from the shared `LeafFilter` union, so
nothing could filter on `emails.primaryEmail`; added (additive).
- New dependency `addressparser@1.0.1` in twenty-front, the same package
and version the server already uses to parse inbound mail headers, so
both sides parse identically. Tiny, dependency-free, browser-safe.

# Decisions and tradeoffs

- Person resolution matches on `emails.primaryEmail` only,
case-insensitively via per-address `ilike` filters (no `%` wildcards,
`%_\` escaped). `additionalEmails` is a JSONB array and not cleanly
filterable through the GraphQL filter API today; the server-side matcher
checks additional emails too, so a chip may show as a plain address even
though the send still links to the person via participant matching.
- Chip flash-on-duplicate replays its CSS animation by remounting the
chip subtree (nonce in the React key), chosen over animation-restart
hacks; the remount is invisible.
- Keyboard chip selection keeps DOM focus on the input and tracks a
virtual `selectedChipIndex` (`aria-activedescendant`) instead of roving
focus across chips: one focus point, no focus juggling, standard
combobox listbox pattern.
- `flushSync` (precedent: `Dropdown.tsx`) focuses and places the caret
after entering chip-edit mode; the alternative was a useEffect on
editing state.
- Suggestion rows `preventDefault` on mousedown so picking a suggestion
never blurs the input (blur would first commit the half-typed buffer as
a junk chip).
- Cmd/Ctrl+Enter inside a recipient field: with a non-empty buffer it
commits the buffer only; with an empty buffer it sends via an `onSubmit`
prop wired to `handleSend`. Not commit+send in one stroke: `handleSend`
holds a same-render closure over composer state, so sending in the same
event would read the pre-commit recipients. E2E also showed the side
panel's own ctrl+Enter hotkey never fires while any form field is
focused (focus-stack scoping, applies to the old composer too), which is
why the field triggers the submit itself.
- Enter with suggestions open picks the highlighted (or top) suggestion,
Gmail-style. When the typed buffer is itself a valid email, the literal
row is ranked first so Enter keeps meaning "add what I typed".
- Suggestions are disabled while editing a chip (the edit buffer holds
`Name <email>` text, a poor search query).
- Dedupe blocks within a field; across fields typed duplicates are
allowed (sometimes intentional), but suggestions exclude addresses
already present in any of To/Cc/Bcc.
- Chip menu actions never navigate: navigating the side panel (or main
view) unmounts the composer and silently destroys the draft, since
composer state is component-local with no draft persistence. "Add as
person" creates the record and shows a snackbar while the chip upgrades
in place; the person header row is informational. "Open person"
navigation should come back once drafts survive navigation.
- The reply composer gets no context record: its widget target record is
the message thread, not a person/company, and replies already prefill
participants.
- If two people share a primary email, the last fetched match wins for
chip display (no ambiguity UI).
- "Add as person" splits the display name on the first space for
firstName/lastName, the same heuristic the contact-creation manager uses
server-side.

# Deferred

- Display names on the wire (`Name <email>` in outbound headers): needs
`SendEmailInput` / `EmailComposerService.validateEmails` changes
server-side.
- Drag chips between To/Cc/Bcc; collapse-on-blur to one line with a "+N
others" summary.
- Frequency/recency ranking of suggestions from `messageParticipant`
aggregates.
- "Open person" from the chip menu, pending draft persistence across
navigation.

# Verification

Unit tests cover the parser, formatter round-trip, merge/dedupe, and the
field state machine (commit, dedupe flash, edit, cancel, keyboard
selection). Typecheck, lint, and the email module suites pass, plus the
shared and side-panel suites.

Every flow was also driven end to end with Playwright against seeded
data: prefill resolution, context and typed suggestions, keyboard
navigation and picks, dedupe flash, RFC 5322 paste, invalid chips gating
Send, wrapping, in-place editing, chip menus, clipboard copy, Add as
person with live chip upgrade, Cc/Bcc exclusions, and the Ctrl+Enter
send path (the mutation reached the server; it failed only on the seeded
account's missing refresh token, expected outside a real provider
connection).

Screenshots of each verified behavior:
https://claude.ai/code/artifact/1743f05d-422e-43d0-bbea-a34a0470c180

---
_Generated by [Claude
Code](https://claude.ai/code/session_0199wDARiw48GqVTpgWzbXWw)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22668?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. -->
2026-07-09 13:11:09 +02:00
Félix Malfait 3dd7dfde5a feat(billing): collect credit card in-app when starting subscription from billing page (#22706)
## Context

On the billing page, a trialing workspace without a payment method that
clicks "Increase" (credits section) or "Subscribe Now" (subscription
card) goes through the end-trial confirmation and then gets redirected
to the Stripe billing portal to add a card. We already have an in-app
card collection flow (`AddCreditCardModal`, Stripe PaymentElement on a
SetupIntent) used by the AI chat credits banner and the global end-trial
information banner. The billing page was the last surface still bouncing
users to the portal.

## What this does

Applies the same pattern as `AIChatNoMoreBillingCreditsBanner` and
`InformationBannerEndTrialPeriod` to the billing page:

- When the workspace is trialing and `billingHasPaymentMethod ===
false`, the `endTrialPeriod` modal now renders `AddCreditCardModal`
(in-app card entry) instead of the confirmation modal. Once the card is
added, the subscription starts via `endTrialPeriod({
skipPaymentMethodRedirect: true })`, and the user stays on the billing
page.
- When a payment method exists (or the flag is unknown), the existing
"Start Your Subscription" confirmation modal is unchanged, including its
portal-redirect fallback if the backend reports no card.
- Both entry points share the same modal instance
(`BILLING_MODAL_IDS.endTrialPeriod`), so the "Increase" button in the
credits section and the "Subscribe Now" header action both get the
in-app flow. Button labels are unchanged: they state the outcome, and
the modal explains the card step.
- `AddCreditCardModal` now closes only after `onPaymentMethodAdded`
resolves, so the form keeps a visible loading state until the
subscription is confirmed instead of closing instantly while activation
runs in the background. This also benefits the AI chat and information
banner flows that share the component.

## What stays the same

- No backend changes. `endSubscriptionTrialPeriod` still verifies the
payment method live against Stripe and calls
`ensureDefaultPaymentMethod` before activating, so the card added
through the SetupIntent becomes the default charged card.
- 3DS or redirect-based payment methods return to the billing page with
the `start-subscription-after-payment-method` param, which the globally
mounted `EndTrialAfterPaymentMethodGater` already handles.
- The trial banner ("Trial ends X, please add card details") keeps its
portal flow on purpose: its semantic is add a card while keeping the
trial, whereas `AddCreditCardModal` starts the subscription immediately.
Converting it would need a variant of the form that skips the auto-start
param. Same for the past-due "Update payment" path. Both are possible
follow-ups.
- All strings reuse existing Lingui msgids, no catalog changes needed.

## Testing

- `oxlint`, `oxfmt` and `nx typecheck twenty-front` pass.
- All 17 `settings/billing` jest suites pass.
- Could not drive the Stripe flow end-to-end in this environment (no
Stripe test keys); the composition is identical to the two surfaces
already shipped, and the state matrix (`hasPaymentMethod` false / true /
unknown, trialing / not trialing, permission gating) was traced through
both entry points.
2026-07-09 12:53:29 +02:00
github-actions[bot] a0cf4cc9e1 i18n - translations (#22714)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22714?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: github-actions <github-actions@twenty.com>
2026-07-09 11:46:19 +02:00
Raphaël Bosi e38d587602 Welcome animation when a workspace is set up (#22622)
## Desktop


https://github.com/user-attachments/assets/87999ff5-9257-4fed-90e0-781322b1db98

## Mobile



https://github.com/user-attachments/assets/0d115cb6-7bca-4229-8fde-0f188e862b00


Greets the user the moment they finish onboarding: a halftone Twenty
mark (brand purple) assembles from scattered particles, shimmers, then
bursts outward to reveal the freshly loaded workspace behind it. Plays
once, ~3s, with a reduced-motion fallback.

`PageChangeEffect` sets a transient `isWelcomeAnimationVisibleState`
atom at the onboarding to workspace redirect seam (gated by
`shouldShowWelcomeAnimationOnNavigate`); `WelcomeOverlay`, mounted in
`WorkspaceAppProviders`, plays it and clears it. Transient by design, so
it never replays on reload.

## Why a canvas rendered on another thread

The overlay plays at the exact moment the workspace mounts behind it,
which is one of the heaviest main-thread stretches in the app (metadata,
providers, first data fetch). Anything animating on the main thread
competes with that work:

- framer-motion and plain main-thread `requestAnimationFrame` visibly
stuttered and froze during the mount, because long tasks starve rAF.
- CSS keyframes stay smooth (they run on the compositor), but can't
drive a ~2900-particle halftone with per-particle physics (staggered
assemble, moving shimmer band, directional burst).

So the halftone runs as a Canvas 2D particle system inside a Web Worker,
drawing to an `OffscreenCanvas` transferred from the main thread. The
whole render loop lives off the main thread, so app-mount jank can't
reach it. A main-thread path is kept as a fallback for browsers without
`OffscreenCanvas` / `transferControlToOffscreen`, and Safari workers
(which lack rAF) fall back to a fixed-interval loop. The renderer is a
pure, DOM-free module so it bundles cleanly into the worker; dot colors
come from CSS vars read on the main thread and passed in.

Also fires for every onboarding completion path and skips the billing
`PlanRequired` detour.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22622?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. -->
2026-07-09 08:47:07 +00:00
Etienne cc7b41db0e feat(ai-chat): inject current date & per-message timestamps into agent context (#22632)
## Summary

Gives the AI chat agent temporal awareness by injecting the current date
into
the system prompt and a per-message "sent at" timestamp into each user
message,
formatted in the member's timezone. Also hardens all timezone formatting
against
the `"system"` sentinel value, which was crashing the stream job.

## What changed

**Message timestamps (new)**
- Added `injectMessageTimestamps` util: prepends a
`<message_timestamp>Sent: …</message_timestamp>`
text part to each user message before it's sent to the model, so the
agent can
  reason about "yesterday", "last week", etc.
- `loadMessagesFromDB` now stores the message time in the canonical
`metadata.createdAt` slot (ISO string, JSON-serializable for the BullMQ
job
payload) instead of a non-typed top-level `createdAt` field that nothing
read.
- Migrated the AI chat message pipeline from the generic `UIMessage` to
the
typed `ExtendedUIMessage` (`chat-execution.service`,
`extract-code-interpreter-files`,
`replace-unsupported-file-parts`, and related types), since
`metadata.createdAt`
  is declared on `ExtendedUIMessage`.

**Current date in context**
- System prompt now includes `Current date: …` formatted in the member's
timezone
  (`system-prompt-builder.service`).
- Settings › AI prompt preview mirrors the same `Current date` line.

**Timezone safety (bug fix)**
- Workspace members default `timeZone` to the `"system"` sentinel, which
is only
  resolvable client-side. Passing it (or any invalid IANA zone) to
`Intl.DateTimeFormat` throws `RangeError: Invalid time zone specified:
system`,
  which was failing the stream job.
- Added `getValidTimeZoneOrUndefined`, which returns a valid IANA zone
or
`undefined` (letting the runtime fall back to its default). Used in both
`injectMessageTimestamps` and `formatCurrentDate`. This mirrors the
existing
  `isValidTimeZone` convention in the calendar module.

## Notes / follow-ups

- For members who never changed `timeZone` from `"system"`, timestamps
fall back
to the server's default zone (UTC). To honor their real local time, the
frontend would need to send the browser-detected zone with the chat
request
(the same way calendar/charts already pass a resolved zone). Not
included here.

## Test plan

- [x] `inject-message-timestamps.util.spec.ts` — covers timestamp
injection,
assistant messages untouched, invalid `createdAt`, and the `"system"`
      timezone no longer throwing.
- [ ] Send a chat message and confirm the agent sees the correct
date/time.
- [ ] Verify a member with `timeZone = "system"` no longer crashes the
stream job.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22632?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. -->
2026-07-09 07:02:56 +00:00
neo773 3a5545c753 chore: remove IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED flag (#22680)
Messaging/calendar webhook subscriptions are now always on; drop the
feature flag gate and its enum/public-flag registration.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22680?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. -->
2026-07-08 23:35:35 +02:00
Weiko 97e21c3403 feat(ai): optionally preselect fast or smart model when opening Ask AI (#22679)
Allow front components to preselect the AI model when opening the Ask AI
side panel with a preprompt.

- Add an optional `model: 'FAST' | 'SMART'` to the preprompt in the
  openSidePanelPage AskAI params of the front-component SDK.
- openAskAiPageWithPreprompt resolves FAST to the workspace fast model
  and SMART to the workspace default (null selection = pinned default
  model), writing to agentChatUserSelectedModelState.
2026-07-08 22:52:38 +02:00
Marie 5f3f734b34 fix(front): page header title overlap, Cmd+K on page layout pages & stable tooltip id (#22678)
## Summary

Three independent front-end fixes.

### 1. Settings page header title overlaps the breadcrumb

On settings detail pages (e.g. an app's logic function), a long centered
title visually overlapped the breadcrumb.

`PageCardHeader` renders the header as a CSS grid (`minmax(0, 1fr) auto
minmax(0, 1fr)`) and the centered title used `justify-self: center`.
With grid, `justify-self: center` sizes the item to its own content
width (up to its `max-width`) instead of to its grid track, so a long
title grew wider than the center track and spilled sideways over the
breadcrumb column — its `overflow: hidden` only clipped its own children
to that oversized box, not to the track.

Fix: let the centered title fill and shrink to its grid track so it
clips (with ellipsis) inside its own column instead of overflowing into
the breadcrumb.
- Center column: `auto` → `minmax(0, auto)` so it can shrink when space
is tight.
- Centered title: `justify-self: center` → `justify-self: stretch` +
`min-width: 0`.

### 2. Cmd+K does nothing on standalone page layout pages

On standalone page layout pages (`/page/:pageLayoutId`, used to render
app front components), the command menu shortcut (Cmd+K) did nothing.

Cmd+K is a global hotkey with a modifier, so it only runs when the
active focus-stack config has `enableGlobalHotkeysWithModifiers: true`.
`RecordIndexPage` and `RecordShowPage` explicitly reset the focus stack
to enable it, but `PageChangeEffect` had no case for
`AppPath.PageLayoutPage`, so the stack kept a stale config (commonly the
Settings config, which disables modifier hotkeys) and swallowed the
shortcut.

Fix: add a `PageLayoutPage` case in `PageChangeEffect` that resets the
focus stack with modifier hotkeys enabled (mirroring `RecordShowPage`),
plus a new `PageFocusId.PageLayoutPage` value.

### 3. Ever-changing / unstable tooltip element id

`OverflowingTextWithTooltip` built its element id from `title-id-${+new
Date()}`, so a new id (the current epoch time in ms) was generated on
every render — the id visibly kept increasing in the DOM. This is
unstable (the tooltip anchor `#id` churns on each render) and
collision-prone (two tooltips rendering in the same millisecond get the
same id, producing duplicate DOM ids and an ambiguous anchor).

Fix: derive the id from React's `useId()` so it is stable per instance
and unique. The colons `useId()` produces are stripped, since the id is
used inside a CSS selector (`anchorSelect={#${id}}`) where colons are
invalid.

## Test plan

- [ ] Open a settings detail page with a long title (e.g. an app logic
function named `maintain-account-team-member-name-on-created`) and
confirm the title no longer overlaps the breadcrumb, and truncates with
an ellipsis when space is tight.
- [ ] Navigate to a standalone page layout page (an app's
front-component page) and confirm Cmd+K opens the command menu,
including after coming from Settings.
- [ ] Inspect an overflowing title/tooltip in DevTools and confirm its
`id` stays stable across re-renders (no longer increments), and tooltips
still show on hover of truncated text.
2026-07-08 18:51:03 +02:00
martmull 0ea0c23556 refactor(app-marketplace): rename featured to vetted (#22674)
## What

Renames the application-registration "featured" flag to "vetted" across
the backend, frontend, GraphQL schema/DTOs, and the marketplace UI.

"Vetted" better describes what the flag actually does today: it marks an
app as reviewed and approved by the Twenty team (a trust signal), rather
than "featured" which reads as spotlighting/promotion. The admin toggle
description was already "Mark this app as reviewed and approved".

## How

- Renamed `isFeatured` -> `isVetted` on the `ApplicationRegistration`
entity, DTOs (`MarketplaceApp`, `MarketplaceAppDetail`,
`UpdateApplicationRegistrationPayload`), services, GraphQL fragments,
and the settings/admin UI (labels: "Featured" -> "Vetted", "Featured
only" -> "Vetted only", etc.).
- Renamed the `MARKETPLACE_FEATURED_APPLICATIONS` constant/file to
`MARKETPLACE_VETTED_APPLICATIONS`.
- Regenerated GraphQL client artifacts (`generated-metadata`,
`generated-admin`, `twenty-client-sdk`).

### Database

The `isFeatured` column is renamed in place to `isVetted` via a single
2.20 fast instance command (`ALTER TABLE ... RENAME COLUMN`). No new
column, no data-copy backfill.

- Since all 2.19 commands (including the existing `isFeatured` backfill)
complete before any 2.20 command runs, the rename carries over the
values that backfill set.
- The entity uses `@WasRenamedInUpgrade` so the upgrade-aware layer
queries the old column name until the rename step runs during an
upgrade.

## Testing

- `nx typecheck` and `nx lint:diff-with-main` pass for twenty-server and
twenty-front.
- Ran `database:reset` on a fresh dev DB: the 2.19 `isFeatured` backfill
runs first, then the 2.20 rename; the column ends up as `isVetted` (and
`isFeatured` no longer exists), values preserved.
- Booted the server: the `@WasRenamedInUpgrade` decorator validates
against the upgrade sequence, and GraphQL introspection confirms all
four types expose `isVetted` and none expose `isFeatured`.
- Ran the three `graphql:generate` configs and the SDK metadata client
generator so the committed generated files match the generator output
(field ordering included).

## Notes

- The `api-breaking-changes` check flags the removal of the `isFeatured`
GraphQL field — that is expected and inherent to this rename.
- Translation catalogs (`locales/`) are intentionally not touched here
since they are managed via Crowdin; new English strings render via
Lingui's default-message fallback until translated.
2026-07-08 16:41:58 +00:00
Marie e751c589ca [Billing for self hosts] Add info in description (#22660)
Clarify how the same enterprise key is reused for one prod + one dev
instance.
Suggest to save / store enterprise key in clear somewhere.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22660?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: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-08 15:54:09 +00:00
martmull 9423af7f67 feat(server): add public marketplace resolver for vetted app catalog (#22647)
## What

Adds a public GraphQL resolver so unauthenticated clients (the public
website) can read the listed/vetted marketplace catalog without a
workspace token.

- `MarketplacePublicResolver` (metadata schema) exposes two public
queries guarded by `PublicEndpointGuard` + `NoPermissionGuard`:
  - `publicMarketplaceApps`
  - `publicMarketplaceAppDetail(universalIdentifier)`
  
Both delegate to the existing `MarketplaceQueryService` (no new logic,
no new REST routing). The existing workspace-guarded
`findManyMarketplaceApps` / `findMarketplaceAppDetail` queries are
untouched.
- Adds a shared `ApplicationCategory` type in `twenty-shared` (known
values plus `string` for backward compatibility) used to type
`ApplicationManifest.category`. A warning is logged server-side when an
app declares a category outside the known set.

## Why

This is the backend half of the public apps marketplace on the website.
Splitting it out so the server-side catalog exposure can be reviewed
independently from the website UI.

## Follow-up

The website PR (the `/apps` marketplace UI) consumes
`publicMarketplaceApps` and should merge after this one.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01GBfegArtJcoiTLSsnWPH8R)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22647?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: martmull <martin@twenty.com>
2026-07-08 15:43:17 +00:00
Pratik Mahajan 72d728ea8e fix(messaging): add sender name to IMAP/SMTP From headers (#22603)
Manual IMAP/SMTP outbound emails were being composed with a
bare email address in the From header, so recipients did not
see the sender's display name.
Gmail already built a proper sender header from Google profile
data, but manually configured IMAP/SMTP accounts had no
equivalent path and fell back to the raw address only.

Fix this by storing the optional sender display name in the
IMAP/SMTP/CALDAV connection parameters, exposing it through
the settings flow and metadata API, and reusing a shared
From-header formatter when composing outbound messages.
The formatter now builds a properly encoded sender header when
a name is available and falls back to the bare email address
when it is not, keeping the behavior safe for blank or missing names.
Gmail keeps using its existing Google-derived display name source;
this change only brings manual accounts up to the same header
formatting standard and removes duplicated formatting logic between
outbound drivers.

After this change, manual SMTP sends and IMAP draft creation include
the configured sender name in the From header, while blank names are
normalized away instead of producing malformed headers.
Existing manual account names are preserved when updates omit the
field, and edited accounts can still explicitly clear it through the
settings flow.

Add focused utility coverage for the shared From-header formatter.

Fixes: #22608

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22603?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: neo773 <neo773@protonmail.com>
2026-07-08 20:29:25 +05:30
Félix Malfait b5b6e110b0 Merge app Public URL and Public Domains settings into one App URL section (#22666)
## Context

The app settings tab showed two sections for the same concept: a "Public
URL" block with a very technical description (Permissions-Policy, COOP,
COEP), a blue info banner about the legacy `/s/` endpoint, and a
separate "Public Domains" block. Confusing, and scary-sounding for apps
whose routes are not really "public".

## Changes

**One merged "App URL" section** (still only rendered for apps that
actually expose HTTP-triggered functions):
- Title renamed to the neutral "App URL" with a one-line description:
"This app's routes are served from this URL. Add a custom domain to use
your own."
- Read-only copyable base URL input, custom domain list card right below
it.
- Removed the info banner and all header/CORS jargon.

**Always show the real URL**: `getFunctionsBaseUrl` now takes
`serverBaseUrl` and falls back to `${serverBaseUrl}/s` instead of
returning `undefined`, so self-hosted instances (no dedicated function
domain) see their actual base URL instead of nothing. This centralizes
the fallback previously duplicated in `FrontComponentRenderer` and
`getLogicFunctionHttpUrl`.

**"Public Domain" renamed to "Custom Domain"** in all user-facing
strings (add card, footer button, detail page, snackbars). Internal
identifiers, GraphQL types and routes keep the `PublicDomain` name to
match the backend entity.

**Polish**:
- Domain rows and the add card use a world icon instead of the mail
icon.
- Row description shows "Added x days ago" instead of a raw ISO
timestamp, via a new shared `useGetAddedRelativeDateDescription` hook
also adopted by the approved access domains card that had the same
inline helper.
- `FrontComponentRenderer` consumes `functionsBaseUrl` from
`useGetLogicFunctionHttpUrl` instead of re-deriving it from the same
atoms.
- User docs updated accordingly.

## Testing

- Ran the app locally (Postgres/Redis + server + front), synced a
fixture app with an HTTP-triggered logic function, and verified the
merged section renders correctly with the copyable URL and Add Custom
Domain card, no banner, no duplicate section.
- `getLogicFunctionHttpUrl` unit tests updated and passing (7/7), `npx
nx typecheck twenty-front` and `lint:diff-with-main` green.
- Locale catalogs are untouched; Crowdin sync regenerates them from the
new source strings.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01NyEWoybMBq62cSNfAFdJAT)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22666?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. -->
2026-07-08 16:44:26 +02:00
Raphaël Bosi ad74f75e6a Make onboarding steps responsive on mobile (#22659)
https://github.com/user-attachments/assets/5840936c-d9b8-412f-bfec-2d5af768660c



The onboarding flow was built for a fixed 440px design with no
responsive handling, so on a phone the steps were cramped: oversized
padding/gaps, a decorative import-preview illustration that clipped, and
side-by-side name fields.

Adds targeted `@media (max-width: 768px)` rules (using the existing
`MOBILE_VIEWPORT`):
- Shared step page: smaller padding and gap on mobile (cascades to every
step).
- `UpgradeFreeTrial`: its own mobile padding (it overrides the base
padding).
- Header: reduced side padding.
- Import preview: hide the floating calendar cards on mobile (their
fixed offsets are tuned for the 440px card).
- Create profile: stack the avatar + name fields vertically on mobile.

Verified each step at 375px via Storybook; desktop is unchanged (media
queries gated to <=768px).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22659?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. -->
2026-07-08 16:05:26 +02:00
Thomas Trompette 48730df0d2 feat(workflow): scaffold core workflowVersion entity + trigger cache (phase 0) (#21674)
## What

**Phase 0 (scaffold)** of migrating `workflowVersion` data to **core**.
Gated by `IS_WORKFLOW_VERSION_IN_CORE_ENABLED` with **no behavior
change** — nothing reads or writes the new core entity yet.

## Plan

`workflowVersion` becomes a thin **workspace shell** over a core entity
(the `dashboard`/`pageLayout` pattern), so navigation, the metadata
relations, and the record UI keep working while the heavy data
(`triggers`, `steps`) lives in core. Trigger dispatch will derive from
active core versions via a per-workspace cache, letting us **eliminate**
the denormalized `workflowAutomatedTrigger` object. `workflow` and
`workflowRun` stay as workspace objects.

Phases: **0 — scaffold (this PR)** → A — backfill + dual-write → B —
switch reads to core → C — drop the workspace `trigger`/`steps` columns
+ the `workflowAutomatedTrigger` object.

## Included
- **Core `WorkflowVersionEntity`** (`extends WorkspaceRelatedEntity`) —
stores version data, with triggers as an **array** (`triggers:
WorkflowTrigger[]`), a long-due shape change. Storage only: dispatch
reads the primary trigger, so behavior stays single-trigger for now.
- **Fast create-table instance command** for `core."workflowVersion"`
(v2.19.0).
- **`IS_WORKFLOW_VERSION_IN_CORE_ENABLED`** feature flag.
- **Per-workspace automated-trigger cache provider** deriving
CRON/DATABASE_EVENT dispatch from the active version's trigger —
groundwork for removing `workflowAutomatedTrigger`.

## Notes
- `WorkspaceRelatedEntity`, **not** `SyncableEntity`: this is user
runtime data (like `connectedAccount`/`apiKey`/`file`), not
application-manifest metadata.
- No frontend behavior; the generated `FeatureFlagKey` enums are updated
to include the new flag.
2026-07-08 12:50:03 +00:00
Parship Chowdhury 6554440bb1 fix: dropping favorites to the last position in a open folder (#22360)
Fixes #9213 

Before:


https://github.com/user-attachments/assets/0855f9c1-07c7-4080-8b5f-94fbe3c8b505



After:


https://github.com/user-attachments/assets/6ae26e89-9fe5-4ebe-b512-d8287ed2c070



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22360?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. -->

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
2026-07-08 14:28:15 +02:00
Brahm Lower be68c0cbab fix: corrected grammar in approved domain message (#22646)
title says it all- quick grammar fix in the workspace approved access
domains form

<img width="1484" height="610" alt="image"
src="https://github.com/user-attachments/assets/bb4ce7a5-6ba4-40a7-a1f9-57c3e8d2dde0"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22646?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. -->
2026-07-08 13:05:33 +02:00
Aressand 14e4b5bd31 fix(rls): prefill RLS predicate fields when creating related records (#22620)
## What / Why

Creating a record from a relation section (e.g. adding a child record
from its
parent's record page) fails with **"Record does not satisfy security
constraints"** for any role restricted by row-level permissions.

Root cause: `useAddNewRecordAndOpenSidePanel` builds the create payload
with only
the label field and the parent FK. Fields required by the role's RLS
predicates
(e.g. `owner = current workspace member`) are missing, so the server
rejects the
insert in `validateRLSPredicatesForRecords` with
`RLS_VALIDATION_FAILED`.

`useCreateNewIndexRecord` (the record table "+ New" path) already
handles this via
`buildRecordInputFromRLSPredicates()`. The relation-section creation
path was
simply never updated — same bug class, different entry point.

## How

Spread `buildRecordInputFromRLSPredicates()` into the create payload in
`useAddNewRecordAndOpenSidePanel`, mirroring `useCreateNewIndexRecord`.
The record
is then created with the RLS-required fields prefilled (e.g. owner =
current
member), so it passes server-side validation.

No behavior change for roles without RLS predicates:
`buildRecordInputFromRLSPredicates()`
returns an empty object when there are none.

## Test plan

Requires row-level permissions (Enterprise) enabled.

1. Create a role with an RLS predicate `owner IS current workspace
member`.
2. Assign it to a non-admin user; create a parent record owned by that
user.
3. As that user, open the parent record and add a child record from a
relation
   section (the "+" on a one-to-many / many-to-one relation field).
4. **Before:** "Record does not satisfy security constraints".
**After:** the child record is created, with owner prefilled to the
current member.

Also verified via REST against a self-hosted instance: inserting the
child record
without the owner field is rejected (HTTP 400, RLS_VALIDATION_FAILED);
inserting it
with `ownerId = current member` succeeds (HTTP 201).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22620?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. -->
2026-07-08 13:03:45 +02:00
nitin 4b127451b4 Fix root domain /authorize rendering workspace-scoped consent page (#22641)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22641?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. -->
2026-07-08 14:11:24 +05:30
martmull 1914b11de2 Unify featured/vetted app terminology to featured (#22648) 2026-07-08 09:56:24 +02:00
martmull 435073e9c5 Display featured applications in marketplace (#22635)
## After
<img width="1060" height="589" alt="image"
src="https://github.com/user-attachments/assets/74dfadcf-8698-4404-81c6-b309cc4cbf79"
/>
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/0e1a3644-04bc-4208-aa77-3842d9db9cc8"
/>
<img width="797" alt="image"
src="https://github.com/user-attachments/assets/0456ecce-607a-4705-8a89-c77029bfb6ac"
/>

- Remove IS_MARKETPLACE_SETTING_TAB_VISIBLE feature flag
- add vetted toggle in admin app tab
- added people data labs, last contact and call recorder to default
vetted applications

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22635?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: prastoin <paul@twenty.com>
2026-07-07 17:05:54 +02:00
Thomas Trompette 35d3f9b89d fix(ai-chat): sort message parts by orderIndex on reload (#22629)
## Problem

When an AI chat conversation is reloaded from the DB (page refresh or
initial load), message parts are returned without guaranteed ordering.
The renderer groups reasoning/thinking steps only when they are
**contiguous** — so if `reasoning` parts land after the `text` part,
thinking blocks appear below the final answer, and can appear duplicated
or split.

This only surfaces with reasoning models (OpenRouter, etc.) because
those produce multiple reasoning/tool/text parts per message, making
ordering observable. Simple text-only messages aren't affected.

## Root cause

`AgentChatService.getMessagesForThread()` fetches `parts` via a TypeORM
relation with no ORDER BY on `orderIndex`. The DB can return parts in
any order.

`mapDBMessagesToUIMessages()` then calls `dbMessage.parts.map(...)`
directly, without sorting.

A parallel server-side utility (`mapDBPartsToUIMessageParts.ts`) already
sorts by `orderIndex` — this fix makes the frontend fetch path
consistent with it.

## Fix

Sort parts by `orderIndex` before mapping to UI parts in
`mapDBMessagesToUIMessages.ts`.

```ts
parts: [...dbMessage.parts]
  .sort((a, b) => a.orderIndex - b.orderIndex)
  .map(mapDBPartToUIMessagePart),
```

`orderIndex` is already included in `GetChatMessagesDocument` — no
schema or query changes needed.

## Test

1. Open Ask AI with a reasoning model (e.g. via OpenRouter).
2. Run a prompt that produces thinking steps.
3. Hard-refresh the page.
4. Thinking blocks should appear collapsed above the final answer, not
below it.

Closes #22386

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22629?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. -->
2026-07-07 15:52:54 +02:00
Raphaël Bosi 71ad0fb5fc Remove logo from onboarding trust badges (#22628)
Removes a logo from the trusted-by cluster on the onboarding
import-contacts step and deletes the unused asset.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22628?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. -->
2026-07-07 14:28:47 +02:00
Thomas Trompette 99f54d9ea8 fix(front): honor user-set label on LINKS/URL social links (#22586)
## Problem

On LINKS and URL fields, recognized social links **always** rendered the
derived handle (e.g. `@cristiano`) and ignored any user-set `label`.
This was a regression: `SocialLink` did `getDisplayValueByUrlType(...)
?? label`, and since a provider always matches for social links, `label`
was never reached. Adding the Instagram/TikTok/Bluesky providers in
v2.16 widened the set of affected links.

| Input | Expected | Before |
|-------|----------|--------|
| `instagram.com/cristiano`, label `Cristiano Ronaldo Official` |
`Cristiano Ronaldo Official` | `@cristiano` |

## Fix (display half of #22265)

- `SocialLink`: prefer a non-empty `label`; only derive the handle from
the URL as fallback (then `href`). Prop widened to `string \| null`.
- `LinksDisplay` / `LinkDisplay` / `URLDisplay`: pass the **raw,
nullable** label into `SocialLink` instead of a pre-coalesced string, so
derivation still works when no label is set. `URLDisplay` passes
`label={null}` (URL fields have no label) so handles still render.
- Stories: dropped `label` args the old code silently ignored (keeps
existing visual snapshots stable) and added a `WithCustomLabel` story
asserting precedence.

## What's left (not in this PR)

The **label input in the UI** (issue's second half) is intentionally
deferred. `MultiItemFieldInput` carries the in-progress edit as a single
string and seeds edits with the URL only, so exposing a Label field
cleanly requires a small generalization of that shared component (not a
JSON-serialization workaround). That change needs manual in-app
verification and will be a follow-up.

## Verification

- `twenty-ui` typecheck clean; oxlint clean on all changed files;
`getDisplayValueByUrlType` tests pass (38).
- Added Storybook `play` assertion for the custom-label case.

Fixes #22265 (display half). Related: #16414.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22586?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. -->
2026-07-07 13:03:29 +02:00
martmull 2a495c3477 feat(app): allow claiming ownership of unclaimed app registrations (#22609)
## After

<img width="653" height="703" alt="image"
src="https://github.com/user-attachments/assets/ebe800da-b00b-4239-99a9-e157f7bfd7a0"
/>
<img width="634" height="711" alt="image"
src="https://github.com/user-attachments/assets/00a048bf-36b1-489f-a080-1ed2d069e625"
/>


## Context

App registrations track their owner via `ownerWorkspaceId`. Curated /
catalog / CLI apps are seeded **unclaimed** (`ownerWorkspaceId: null`).
Until now there was no way to take ownership of an unclaimed app from
the UI — the only ownership action was **Transfer ownership**, which
requires the caller to already be the owner, so it can't act on a
null-owner app.

This PR adds a way to **claim** an unclaimed app registration, and makes
the owner always visible on the detail page.

## Behaviour

Admin panel → app registration detail → General tab:

- The **Owner** row is now always shown — an **Unclaimed** tag when
there's no owner workspace (previously the row was hidden).
- Danger zone buttons are ownership-aware:
- **Unclaimed** app → **Delete app** + **Claim ownership** (claims it
for the current workspace).
  - **Owned** app → **Delete app** + **Transfer ownership** (unchanged).

Transfer is hidden for unclaimed apps because transferring requires the
caller to already own the registration.

## Changes

**Backend**
- New `claimOwnership` service method: looks the registration up
globally, rejects it if it already has an owner, otherwise assigns
`ownerWorkspaceId` to the caller's workspace.
- New `claimApplicationRegistrationOwnership` mutation, guarded by
`WorkspaceAuthGuard` + `SettingsPermissionGuard(APPLICATIONS)` (same
guards as transfer).
- New `ClaimApplicationRegistrationOwnershipInput` DTO
(`applicationRegistrationId`).

**Frontend**
- **Claim ownership** button (shown only when the registration has no
owner workspace); opens a confirmation modal and calls the new mutation.
- **Transfer ownership** button now renders only for owned
registrations.
- The **Owner** row in the general info card is always displayed, with
an `Unclaimed` tag when there is no owner.

**Generated**
- Regenerated the checked-in GraphQL artifacts (`twenty-front` metadata,
`twenty-client-sdk` schema/types) against the live server so codegen
output matches.

## Verification
- `nx typecheck twenty-front` and `nx typecheck twenty-server` pass.
- `oxlint` + `oxfmt` pass on all changed source files.
- Codegen is idempotent — re-running the three `graphql:generate`
configs + `generate-metadata-client` produces no diff.
- Verified end-to-end on the running app against the seeded unclaimed
`Twenty CLI` registration (Owner shows `Unclaimed`; Danger zone shows
Delete + Claim ownership).

https://claude.ai/code/session_01U7rbxhBSUQRWBbdP5TmAgZ
2026-07-07 11:07:21 +02:00
Raphaël Bosi c55bfab11e Fix side panel open/close animation glitches in the page header and panel content (#22598)
When the side panel opens or closes, the pinned header button (e.g. New
Company) was flat-clipped for the whole 300ms animation: the ⋮ toggle
mounts instantly and shifts the button's slot, framer's `layout` prop
compensates with a transform that the surrounding `overflow: hidden`
containers don't follow, and the ResizeObserver-driven rerenders re-seed
that transform every frame. Removing `layout` lets the button follow
plain reflow, which cannot clip.

The panel content also squeezed during the animation (labels
re-truncating at every intermediate width) because the inner panel was
`width: 100%` of the width-animating wrapper. Pinning it to
`var(--side-panel-width)` turns the animation into a rigid drawer slide;
drag-resize is unaffected since it writes the same CSS variable.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22598?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. -->
2026-07-07 10:40:26 +02:00
Raphaël Bosi d3b79320b1 Remove book a call step from onboarding (#22597)
The book a call screen was shown as a dedicated onboarding step after
sending team invites. It is no longer part of the flow: the
`BOOK_ONBOARDING` status, its pending user var, the
`skipBookOnboardingStep` mutation and the `BookCallDecision` screen are
removed, and onboarding completes right after the plan step.

The `/book-call` Cal.com page remains, reachable only from the "Book a
Call" link on the upgrade screen, with a back link to `/plan-required`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22597?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. -->
2026-07-07 10:34:20 +02:00
martmull ee28ae363f feat(files): use direct-to-storage upload for email and AI-chat attachments (#22610)
## Context

Follow-up to the direct-to-storage upload work (#22449 / #22531 / #22533
/ #22576). That migrated files-field, attachments and workflow uploads
off the buffered path. This PR does the same for the **last two
user-facing upload surfaces**: email attachments and AI-chat files.

## What this does

- Adds `EmailAttachment` and `AgentChat` to the server's
`DIRECT_UPLOAD_FILE_FOLDERS` allowlist. Both folders already resolve
through the workspace-custom-application path in
`resolveUploadLocation`, so no other server change is needed.
- Routes the two frontend hooks through the existing
`useDirectFileUpload` handshake (`createFileUpload` → `PUT` →
`completeFileUpload`):
- `useUploadEmailAttachment` → `FileFolder.EmailAttachment` (keeps its
existing `MAX_ATTACHMENT_SIZE` client check — email has a real send-size
limit).
  - `useAiChatFileUpload` → `FileFolder.AgentChat`.

Each hook keeps its public signature and return shape, so call sites are
unchanged. No schema change and no codegen needed — the
`CreateFileUpload`/`CompleteFileUpload` documents and the `FileFolder`
enum values already exist in `generated-metadata` from #22576.

## Why these are safe to migrate

Both server services (`file-ai-chat`, `file-email-attachment`) just
`writeFile` (store) and return a signed URL — no synchronous processing
of the bytes at upload time — so the store-and-reference direct-upload
flow fits exactly, same as files-field/workflow.

## Out of scope

`CorePicture` (avatars, member/workspace pictures, logos) stays on the
buffered path on purpose: small images that go through server-side image
handling and are served inline, where the 10 MB body limit is already
appropriate.

## Tests

Extends the `FileUploadService` unit spec with an `it.each` asserting
`createFileUpload` supports the `EmailAttachment` and `AgentChat`
folders.

## Verification

`typecheck` and `lint:diff-with-main` green on both `twenty-front` and
`twenty-server`. (The server jest suite couldn't run in my local sandbox
due to an unrelated config-import quirk present on a clean `main`
checkout too — CI runs it normally.)

https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d

---
_Generated by [Claude
Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22610?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. -->
2026-07-07 10:26:04 +02:00
Marie 8a4bcd1445 (Billing for self hosts) Tie enterprise key to server (#22464)
# Enterprise key: bind to a server, free dev instances, self-serve
transfer, shorter license

## Summary

Enterprise keys were being reused across multiple instances (e.g. one
prod + one dev, or several environments), which broke seat accounting
and made licensing ambiguous. This PR ties each enterprise key to a
**single server**, while giving customers a legitimate, self-serve way
to run a **free development instance** and to **move their key** when
they replace a server.

## Product behavior

### 1. Enterprise key is bound to one server
- The first server to validate an enterprise key **claims** it
(claim-on-first-use). From then on, that key is bound to that one server
(until unbound - see 3.).
- Any other instance that presents the **same key from a different
server is hard-rejected**: it does not receive a license, so enterprise
features stay off there.
- Each instance has a stable server identifier. If one isn't set, the
instance generates and persists one automatically on first validation
(in keyValuePair table), so existing customers generally don't need to
do anything (unless they have disabled config variables in db then they
should add it to .env).

### 2. Free development instance
- Every enterprise subscription gets **one free, non-billable
development instance** in addition to its production instance.
- An instance registers as development by declaring its instance type as
`development` (done by default when validating the enterprise key, then
can be toggled from UI or by updating value in keyValuePair table).
- The free dev slot is only granted while there is an **active
production instance** on the same subscription (so it's a perk for
paying customers, not a way to run for free).
- Only **one** dev instance can be active at a time per subscription,
and it is **not counted as a billable seat**.

### 3. Self-serve unbind / rebind (transfer)
- Admins can **release** the binding from the enterprise settings, which
frees the key so it can be **claimed by a new server**.
- This is the intended path when **sunsetting an instance and standing
up a new one** (migration, re-hosting, disaster recovery): release on
the old/dead box, then the new box claims it on its next validation.
- To prevent abuse, releases are **rate-limited (10 per rolling 30
days)**; hitting the limit shows a clear message.

### 4. Automatic release of dead servers
- If a bound server stops checking in for **14 days**, its binding is
considered stale and is **auto-released**, so a replacement can claim
the key without any manual step. This covers the case where the old
server is already gone and can't release itself.

### 5. Shorter license validity (30 → 7 days)
- The license (validity token) now expires after **7 days** instead of
30. The daily background refresh keeps healthy instances licensed
transparently.
- This limits the value of copying a license from one instance to
another, since a copied license now stops working within a week.

### 6. License issuance is rate-limited
- Issuing a new license is capped at **twice per 24h, independently for
production and for development**. This tolerates the normal daily
refresh (including small drift between runs) while blocking bursts of
license minting for cloned instances.
- Hitting this limit never revokes an existing, still-valid license —
the current one keeps working until it expires; the manual "refresh"
button just reports that the daily limit was reached.

## What changes for existing self-hosted customers

**If you run a single production instance with one enterprise key:**
nothing to do. On the next validation your instance reports its server
identifier, claims the binding, and keeps working.

**If you reuse one key across several instances (e.g. prod + dev, or
multiple environments):** only the **first** instance to validate keeps
its license. The others will **lose enterprise features**. To migrate:
- Keep your production instance as-is (it claims the binding).
- For a secondary/testing box, mark it as a **development instance**
(set the instance type to `development`) to use the free dev slot — no
extra cost.
- If you genuinely need multiple production instances, you'll need
**separate subscriptions/keys** for each.

**If you're replacing a server (decommissioning + rebuilding):**
- **Release** the binding from enterprise settings on the old instance,
then start the new one — it will claim the key automatically.
- If the old server is already gone, just wait for the **14-day
auto-release**, or contact support.

**Legacy instances that can't persist a server identifier
automatically:** set the server identifier explicitly in your
environment configuration (the instance logs a message telling you to do
so).

**Offline instances:** because licenses now last 7 days, an instance
that can't reach our licensing endpoint for more than a week will lose
enterprise features until it can check in again.

> A migration email will be sent to affected customers separately.

## Technical implementation (brief)

- Binding state lives in the **subscription's billing metadata** (bound
server id + last-seen timestamps for prod and dev, release timestamps,
and license-issuance timestamps). No new database is introduced on the
licensing side; the billing provider's subscription metadata is the
source of truth.
<img width="976" height="413" alt="metadata_3"
src="https://github.com/user-attachments/assets/ccc64822-e177-4223-a65a-4a4602aedf0e"
/>

- On each validation, a pure **binding resolver** takes the reported
server id + instance type + current metadata and returns `allowed` (with
the metadata to persist and whether the seat is billable) or `rejected`.
It handles claim-on-first-use, staleness/auto-release, the
dev-requires-active-prod rule, and the single-dev-slot rule.
- **Rate limits** (release + license issuance) use a shared
sliding-window helper stored as pruned timestamp lists in the same
metadata, so the metadata self-cleans and never grows unbounded. License
issuance uses **separate windows per instance type**.
- The self-hosted instance **generates and persists a server
identifier** if none is configured, and sends it (plus instance type) as
instance metadata on validation.
- A rejected binding returns a specific error code; the instance
**revokes its stored license** on that code. A license-issuance
rate-limit instead **throws a typed exception that surfaces to the
manual refresh** while leaving the existing license untouched; the daily
refresh job swallows it.
- License lifetime is a configurable duration (defaulted from 30 to **7
days**), clamped to the subscription's cancellation date when sooner.
2026-07-06 18:07:03 +02:00
Weiko 8580cd6f27 feat(ai): open Ask AI side panel with a preprompt in two modes (#22582)
Add the ability to open the Ask AI side panel pre-filled with a prompt
from any frontend component, with a mode to control whether the message
is sent automatically or left for the user to review.

- agentChatPrepromptState: holds the pending preprompt and its mode
(PREFILL = fill only, SEND = fill and auto-submit)
- useOpenAskAiPageWithPreprompt: seeds the new-thread draft, opens a
fresh Ask AI thread and stores the preprompt intent
- AgentChatPrepromptEffect: applies the intent once the chat editor and
send listener are mounted, either restoring the editor content or
dispatching the send event and clearing the editor

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22582?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. -->
2026-07-06 15:38:28 +00:00
Raphaël Bosi 62c7e8f6b4 Polish onboarding v2 verify animation and step screens (#22585)
https://github.com/user-attachments/assets/1d9b8dc2-ef01-4202-a97e-b41cab048f87





A few polish tweaks to onboarding v2:

- **Verify/workspace-creation animation:** emphasize the key phrase of
each message in medium weight, the rest regular (e.g. "Creating your
**workspace**…").
- **Wider content column:** 340px → 440px. Collapses to full width on
mobile via the existing `max-width: 100%` on every consumer.
- **Sticky disabled buttons:** step submit buttons now stay disabled
from submit through navigation instead of briefly re-enabling once the
mutation resolves.
- **Fewer pulse loaders:** stop the pulsing logo from flashing when
navigating between onboarding steps (removed the step-page Suspense
fallback loader). The verify animation, cold-boot gates, and sign-in
fallbacks are unchanged.

Note: the reworded activation messages get new Lingui catalog IDs, so
non-English locales fall back to English until catalogs are
re-extracted.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22585?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. -->
2026-07-06 14:44:44 +00:00
Paul Rastoin 6c40c7b91a Deterministic system field universal identifier (#22565)
# Introduction

Close twentyhq/core-team-issues#2641

Auto-provisioned field metadata used to get its `universalIdentifier`
from three unrelated sources: random `v4()` on the server when creating
custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc
`v5` derivation in the SDK manifest build. This PR unifies all of them
behind the shared `getFieldUniversalIdentifier` derivation:

```
universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName)
```

## Ownership model

The rollout is built on an explicit split of who owns a field's
universal identifier:

- **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`, `position`, `searchVector`) are
**server-owned**. Their universal identifiers are always the
deterministic derivation, on **every** application (standard,
workspace-custom, installed). Clients cannot provide custom values: a
temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects
any non-derived system field identifier at migration build time. This
check stands in until system fields are generated exclusively server
side by the metadata side-effect engine and stripped from client inputs
— at which point it becomes structurally impossible to send one.
- **`name` is a default field, not a system field**: it is
auto-provisioned when absent (server side for custom objects, SDK side
for application objects) but authors can define their own. It is only
derived where it is guaranteed to be auto-provisioned. In particular,
standard objects keep their **historical hardcoded** `name` identifiers:
the standard app authors its `name` fields like any installed app would,
and moving those identifiers would break every installed application
referencing them (e.g. views on `opportunity.name`).
- **User-created and author-provided fields** keep random / explicit
identifiers, untouched.

## Server

- `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of
the existing type/`isSystem` checks, that each system field's
`universalIdentifier` equals the deterministic derivation. Runs for
every object creation going through the migration orchestrator: app
sync, custom object creation, standard provisioning
- `build-default-flat-field-metadatas-for-custom-object.util.ts` derives
the system field identifiers (and the auto-provisioned `name`) with
`getFieldUniversalIdentifier` instead of `v4()`
-
`build-default-relation-flat-field-metadatas-for-custom-object.util.ts`
derives both the forward and the reverse default relation field
identifiers deterministically
- `generateMorphOrRelationFlatFieldMetadataPair` accepts optional
`sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so
callers can inject deterministic values; user-created relations still
default to `v4()`

## twenty-shared

- `STANDARD_OBJECTS` system field identifiers (the 8) are now computed
at module load via `buildStandardObjectSystemFields`; `name` and every
other identifier keep their hardcoded values
- New snapshot test pinning **every** universal identifier of
`STANDARD_OBJECTS`: any identifier change now requires an explicit
snapshot update and should ship with a coordinated backfill

## SDK (breaking, pre-GA)

- `generateDefaultFieldUniversalIdentifier` delegates to
`getFieldUniversalIdentifier` and now requires
`applicationUniversalIdentifier`
- Reverse default relation field identifiers are derived from the
field's real coordinates (standard object UID + actual field name, e.g.
`targetRocket` on `attachment`) instead of the legacy custom-object UID
+ synthetic `${fieldName}Inverse` hash input. Field *names* are
unchanged
- The manifest build threads the application universal identifier
through default field injection (two-pass over object configs)
- `twenty dev:add` now resolves the application universal identifier
upfront and refuses to scaffold anything until `defineApplication`
declares one — no more `fill-later` placeholder for the app UID in
generated files

## Upgrade

A 2.19 **workspace command** backfills existing
`fieldMetadata.universalIdentifier` rows to the deterministic
derivation. Coverage follows the ownership model:

- **The 8 system fields**: taken over for **every application**,
whatever value they currently hold. This is both safe and required now
that sync rejects non-derived values — leaving a row unconverged would
make its application unsyncable
- **`name`**: workspace-custom app → always taken over
(server-generated, no author to clobber); installed applications → only
rows still carrying the legacy SDK derivation are recomputed,
author-provided identifiers are never touched; standard app → never
touched (hardcoded in `STANDARD_OBJECTS`)
- **Default relation fields**: workspace-custom app → forward fields on
custom objects and reverse fields on the standard relation objects;
installed applications → legacy-derivation probe only

All identifiers of a workspace are updated inside a single transaction,
then the command flushes the field-metadata-related workspace caches and
bumps the metadata version.

Stored `applicationRegistration.manifest` snapshots are intentionally
**not** rewritten: installs and upgrades always sync from the
`manifest.json` inside the resolved package (npm/tarball), the stored
column is only used for display/marketplace purposes.

## Breaking behavior for old packages (fail closed)

Packages built with an older SDK carry legacy system field identifiers
in their tarball `manifest.json`. Installing or upgrading such a package
now fails with an explicit `INVALID_SYSTEM_FIELD` validation error
("universal identifier is not deterministic") instead of silently
mismatching against the backfilled rows and triggering a destructive
delete+create. The remediation is to rebuild the package with the new
SDK; the backfill has already converged the installed rows, so the
rebuilt manifest syncs cleanly.

## Test plan

- [x] `twenty-sdk` unit tests (526 tests) and typecheck
- [x] `twenty-shared` unit tests (1635 tests) including the
`STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte
identical to `main`
- [x] Lint and typecheck clean on all touched packages
- [x] Integration: create a custom object and verify system + default
relation field identifiers match the deterministic derivation
(`create-one-object-metadata-deterministic-field-universal-identifiers`,
13 assertions passing)
- [x] Integration: `failing-sync-application-object-system-fields`
extended with a non-derived system field identifier case; all
identifiers in the spec pinned deterministically so snapshots embedding
expected/actual values are stable across runs (verified with a double
run)
- [x] Integration: all application sync suites pass with the derived
system field identifiers now required by the
`buildDefaultObjectManifest` test helper (9 suites, 20 tests)
- [x] Full test-database reset: standard app provisioning and seeded
workspaces pass the new validation
- [x] SDK manifest build verified on the postcard example app: all
auto-generated default field identifiers match the derivation
- [ ] Run
`upgrade:2-19:backfill-deterministic-field-universal-identifiers`
(dry-run then real) on a seeded workspace and verify identifier
convergence with a rebuilt app manifest
2026-07-06 13:34:33 +00:00
martmull 0706c7c1bc feat(front): upload files directly to storage for files-field, attachments and workflow (#22576)
## Context

Final step of the direct-to-storage upload work (follows #22449
endpoints, #22531 reaper, #22533 content-verify). The server can now
hand the client an upload URL so bytes go straight to storage instead of
being buffered through the Node process (the original OOM problem). This
PR switches the frontend to that flow for the three in-scope surfaces.

## What this does

Adds **`useDirectFileUpload`** — the shared hook that runs the
handshake:

1. `createFileUpload({ filename, size, fileFolder, fieldMetadataId? })`
→ `{ fileId, uploadUrl, contentType, expiresAt }`
2. `PUT` the raw file to `uploadUrl` with `Content-Type: contentType`
3. `completeFileUpload({ fileId })` → `FileWithSignedUrl` (`{ id, path,
size, createdAt, url }`)

Routes the three existing upload hooks through it, **keeping each hook's
public signature and return shape unchanged** so no call sites change:

| Hook | Folder |
|---|---|
| `useUploadFilesFieldFile` (FILES fields) | `FilesField` |
| `useUploadAttachmentFile` (attachments — the Attachment object's
`file` FILES field) | `FilesField` |
| `useUploadWorkflowFile` (workflow send-email attachments) | `Workflow`
|

Adds the `CreateFileUpload` / `CompleteFileUpload` gql documents and
regenerates `generated-metadata` types (+19 lines, scoped to the two new
operations).

## Out of scope

- AI-chat (`AgentChat`) and email-attachment (`EmailAttachment`) uploads
keep the legacy buffered mutations — those folders aren't in the
server's direct-upload allowlist (`[FilesField, Workflow]`).
- Workflow serverless-function code is saved via metadata mutations, not
the file path.

## Notes

- The legacy `uploadFilesFieldFile` / `uploadWorkflowFile` mutations
still exist server-side and remain used by the out-of-scope surfaces, so
this is non-breaking.
- Local storage routes the `PUT` to the token-authenticated streaming
endpoint (`SERVER_URL/file-upload/:id?token=…`); S3 uses a presigned
`PUT`. CORS is already enabled globally on the server and the token
rides in the query string (no cookies), so the browser upload works
cross-origin.

## Verification

`typecheck` and `lint:diff-with-main` green on `twenty-front`; codegen
ran against a live metadata schema so the generated file matches the
drift check. No existing tests/stories cover these hooks.

https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d

---
_Generated by [Claude
Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22576?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. -->
2026-07-06 15:28:57 +02:00
Raphaël Bosi 29920738dc Fix stale token race forcing re-login on verify pages (#22573)
Landing on /verify often forced users to refresh and log in again. The
culprit is a logout side effect triggered by a stale token: when a
previous session's token pair is still in localStorage, boot queries use
it, fail, and the failed token renewal reacts by logging the user out
(onUnauthenticatedError clears the token pair). That logout fires while
the loginToken exchange is running, so it can wipe the fresh session
that was just stored.

Fix: clear the stale token pair right before exchanging the loginToken
(in useVerifyLogin, so both /verify and /verify-email are covered) —
with no stale token to renew, the logout side effect never fires against
the new session. Also removes the redundant clientConfig gate on the
verify effect, stops that same logout side effect from redirecting users
off /verify-email mid-verification, and always re-enables app redirects
after loading the user.

Note: opening a loginToken link now replaces an existing valid session
instead of keeping it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22573?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. -->
2026-07-06 14:58:32 +02:00
Raphaël Bosi 11edd56505 Don't list headless front components in the widget picker (#22578)
Front components can be marked headless (`isHeadless: true`), meaning
they render no UI and only run logic. Both the record-page and dashboard
widget pickers were listing every front component, including headless
ones, which have nothing to render as a widget.

This filters out headless front components where each picker reads them
from `FIND_MANY_FRONT_COMPONENTS`. The downstream select-item mapping,
keyboard-navigation list, and the "Front Components" group guard all
derive from that array, so filtering once excludes them everywhere and
hides the section when every front component is headless.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22578?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. -->
2026-07-06 12:20:05 +02:00
Raphaël Bosi 4cd05b2b3e Reverse pinned command menu item order on record header (#22577)
## Before
<img width="502" height="102" alt="CleanShot 2026-07-06 at 12 10 42@2x"
src="https://github.com/user-attachments/assets/da186911-5a1f-446f-a590-1af4a191554f"
/>

## After
<img width="500" height="102" alt="CleanShot 2026-07-06 at 12 10 17@2x"
src="https://github.com/user-attachments/assets/6e7bb914-55af-4968-a15a-fb17378fffc7"
/>

Pinned command menu items in the record page header rendered
left-to-right by position, putting the first item on the left. They
should read the other way: first item on the right, last on the left.

Fixed with `flex-direction: row-reverse` on the items container so the
reversal is purely visual. The DOM/source order stays in position order,
so the responsive overflow logic still keeps the highest-priority items
visible and keyboard/screen-reader order is unaffected.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22577?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. -->
2026-07-06 12:17:30 +02:00
neo773 904957ea1e message campaign redesign (#22508)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22508?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. -->
2026-07-05 13:44:34 +02:00