Commit Graph

6325 Commits

Author SHA1 Message Date
Weiko 632114e5e2 fix(front): hide sub-item tree connector in navigation drag preview (#22442)
## Context

Dragging a navigation menu sub-item cloned the whole row as the floating
drag preview, which included the vertical tree-connector bar on the
left. Hide that connector inside the moving clone (marked by dnd-kit
with [data-dnd-dragging]) so the preview shows only the icon and label.
The static placeholder left in the list and the other items keep their
connectors, so the list layout is unchanged.

## Before



https://github.com/user-attachments/assets/8fc04a28-e1d2-49e0-88c1-ef03f89475c2


## After


https://github.com/user-attachments/assets/dbcd3cc5-8a51-40a3-98de-a8ea3d440774
2026-07-02 14:21:08 +02:00
Weiko 63a0b0ab96 fix(front): render pinned command-menu buttons inline in page header (#22446)
## Context
Pinned command-menu items (isPinned: true) stopped appearing as inline
buttons next to the command-menu/burger control and only showed up in
the side panel's "Pinned" list.

Root cause: PR #21308 replaced the flex-based PageHeader with the
grid-based PageCardHeader. The old header sized the title with `flex: 0
1 auto` (content width) and the action container with `flex: 1 1 0`
(grows to fill), so the pinned-buttons wrapper — itself a `flex: 1 1 0`
element that measures its own available width to decide how many buttons
fit inline — had room to expand. PageCardHeader inverted this: it put
the title in the flexible `minmax(0, 1fr)` track and the action area in
the content-sized `auto` track. With the pinned wrapper empty on first
paint, the `auto` track collapsed to zero, the measured container width
was 0, and the "wait until measured" guard kept the visible inline count
pinned at 0 forever — a deadlock where nothing ever rendered inline and
every pinned item fell through to overflow.

Fix: give the non-centered header the same intent as the old flex layout
— title track content-sized/shrinkable (`minmax(0, auto)`), action track
flexible (`minmax(0, 1fr)`). The centered variant already placed the
action area in a `1fr` track, so it is unchanged. Both tracks keep a 0
minimum, so long titles still clip without causing horizontal overflow.

## Before
<img width="1299" height="140" alt="Screenshot 2026-07-02 at 13 05 20"
src="https://github.com/user-attachments/assets/948f5ded-1a9f-4329-825e-313924a829fe"
/>

## After
<img width="1296" height="238" alt="Screenshot 2026-07-02 at 13 05 11"
src="https://github.com/user-attachments/assets/64edd5f2-b307-4119-9158-813e39f813aa"
/>
2026-07-02 14:20:57 +02:00
Félix Malfait 5a4ebca226 refactor(server): unify the two metadata override mechanisms into one (#22417)
## Unify the two metadata override mechanisms into one

Twenty had **two** override mechanisms:

- **`standardOverrides`** — a bespoke JSONB column on
`objectMetadata`/`fieldMetadata` with typed DTOs and a per-locale
`translations` map, resolved by two i18n-aware resolvers.
- **`OverridableEntity.overrides`** — a flat, registry-driven JSONB blob
on view / view-field / view-field-group / command-menu-item /
page-layout-tab / page-layout-widget, resolved by a plain spread.

This PR collapses them into **one** concept: a single `overrides` blob,
one registry-driven overridable set, one i18n-aware read path, and one
write path (`computeMetadataOverridesBlob`, extracted in #22404).

Object/field **stay on `SyncableEntity`** (not reparented to
`OverridableEntity`) so their `isActive` default stays **FALSE** — this
sidesteps the `isActive` default conflict entirely.

### GraphQL breaking change (accepted)

The `standardOverrides` field is **removed** with no deprecation alias —
`overrides` (a `JSON` scalar) is exposed instead on `Object` and
`Field`. Product confirmed negligible external usage; the front-end has
no hand-written consumer (only generated types), which are regenerated
here.

### Commit structure (reviewable commit-by-commit)

1. **Unified resolver + parity harness** —
`resolveEffectiveEntityProperty` is a strict superset of the three
legacy resolvers; a corpus parity spec compares it against a *frozen
reference* of the old logic across every locale, `isStandardApp` branch
and override shape.
2. **Registry-driven** — object/field presentation props tagged
`isOverridable` + `translatable`; the overridable/translatable sets are
derived from the registry (a test asserts they equal the legacy
hardcoded lists).
3. **Rename + swap + delete** — `standardOverrides` → `overrides` across
entities, DTOs, flat/universal types, producers, the ~12
resolve/write/create/sync call sites, mocks and specs; the reconciler's
two compare entries collapse to one; the three legacy resolvers, both
DTOs and the hardcoded constants/types are deleted.
4. **Migration (zero-downtime, two-phase)** — split across two releases
so a rolling deploy never drops a column a previous-release pod still
`SELECT`s:
   - **2.19 fast** — add the `overrides` column (schema only).
- **2.19 slow** — backfill `overrides` from `standardOverrides` in
`runDataMigration` (kept out of the schema transaction so the bulk write
doesn't hold the ACCESS EXCLUSIVE lock; skipped on fresh installs, which
have no data to copy).
- **2.20 fast** — drop the legacy `standardOverrides` column (gated by
`TWENTY_NEXT_VERSIONS`, so it stays dormant until the instance reaches
2.20).
5. **Front/client-SDK regen** — regenerated metadata GraphQL types.
6. **Integration specs + i18n** — updated the standard object/field
update integration specs + snapshots, and the reworded validator message
catalog entry.

### Rolling-deploy safety

`standardOverrides` is retained through 2.19 and only dropped in 2.20,
mirroring the codebase's deferred-drop convention
(`isUIReadOnly`/`isCustom`). During the 2.19 rollout both columns exist,
so old and new pods coexist without "column does not exist" errors. The
backfill lives in a slow `runDataMigration` (per the
`no-data-mutation-in-fast-instance-command` rule) so it doesn't stall
reads.

### `isActive` guard

The migration never reads or writes `isActive`; the backfill asserts the
active-row count is unchanged and aborts otherwise. Verified on a real
DB: apply + revert preserves the blob **and** the nested `translations`
map, with `isActive` counts identical before/after.

### Verification (local)

- `nx typecheck twenty-server` + `nx typecheck twenty-front` — green
- `nx lint:diff-with-main twenty-server` (oxlint `--type-aware` + oxfmt)
— green
- `nx test twenty-server` — green (unit + parity + registry + migration
tests)
- `nx run twenty-server:test:integration:with-db-reset` — green
- `database:reset` applies the 2.19 phases and leaves **both** columns
present (2.20 drop stays dormant); backfill + revert round-trip verified
on a real DB
- Metadata integration suites (standard object/field update, application
sync) pass end-to-end against the two-column schema
- Metadata GraphQL types regenerated against a booted server; zero
`standardOverrides` references remain in application code (only the
migration commands + the legacy schema baseline)

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-02 12:01:15 +02:00
Abdul Rahman 3bbc08d41f refactor(schema): reorganize IndexField and related types (#22439)
## Summary

Querying `indexMetadatas { indexFieldMetadatas { ... } }` on the
`/metadata` GraphQL endpoint fails with a 500:

> Nest could not find IndexFieldMetadataDTOAuthorizer element (this
provider does not
> exist in the current context)

The `@CursorConnection('indexFieldMetadatas', ...)` decorator on
`IndexMetadataDTO` makes nestjs-query auto-generate a relation resolver
that injects an authorizer for `IndexFieldMetadataDTO`. That authorizer
is never provided, because the DTO was never registered as a resolver in
`IndexMetadataModule` — so the field has been broken since it was
introduced in #7162.

Since the working, DataLoader-backed `indexFieldMetadataList` field
already exposes the same data (and is what the frontend uses), this PR
removes the dead connection instead of wiring up the authorizer.

## Changes

- Remove `@CursorConnection('indexFieldMetadatas', ...)` from
`IndexMetadataDTO`
- Regenerate frontend metadata GraphQL types
(`twenty-front/src/generated-metadata`)
- Regenerate client SDK metadata schema/types
(`twenty-client-sdk/src/metadata/generated`)

## Notes

- Not a breaking change in practice: the removed field always threw, so
no consumer can have been relying on it. Callers now get a standard
GraphQL validation error suggesting `indexFieldMetadataList` instead of
an internal server error.
- Verified locally: the failing query now returns `Cannot query field
"indexFieldMetadatas" on type "Index". Did you mean
"indexFieldMetadataList"?` and `indexFieldMetadataList` continues to
work.

Fixes [sonarly issue #54098](https://sonarly.com/issue/54098?type=bug)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22439?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-02 11:32:39 +02:00
Raphaël Bosi 717b297bd1 Add Last contact app to onboarding v2 installable apps (#22433)
Adds the Last contact app to the list of installable apps shown in the
onboarding v2 install-apps step, alongside Call recorder and Enrichment.

Wired in both the frontend list (label + description) and the backend
reward/install allow-list so it can be selected, installed server-side,
and credited.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22433?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-02 08:56:08 +00:00
Thomas des Francs d8cc81cb91 Improve billing settings UI (#22377)
## Summary

- Refresh the settings billing subscription and credits cards with
clearer status, usage, and action states.
- Add the credit-package picker flow and route past-due/cancellation
actions to billing management instead of credit modals.
- Clean up related billing UI helpers and formatting.

## Screens

### regular
<img width="1007" height="781" alt="image"
src="https://github.com/user-attachments/assets/dc9c0c59-8cda-422a-a0d3-56292421a744"
/>

### downgrading
<img width="1049" height="818" alt="image"
src="https://github.com/user-attachments/assets/0dc3dea8-5b55-4cf2-bc92-997cf6e9bebc"
/>
<img width="1048" height="901" alt="image"
src="https://github.com/user-attachments/assets/fba805ad-f523-444b-93a0-2011b6b43443"
/>

### Trialing

without card
<img width="1008" height="903" alt="image"
src="https://github.com/user-attachments/assets/d5dd11b6-4c92-4102-ac22-ad1ad4e9fbfb"
/>

with card
<img width="1008" height="806" alt="image"
src="https://github.com/user-attachments/assets/0dbeb00e-890e-4448-94fe-0cc0ef411e8d"
/>


### Past due & Unpaid

<img width="1052" height="860" alt="Past due"
src="https://github.com/user-attachments/assets/25ba53ef-6e74-4b4c-bfe1-d6c74e165917"
/>
<img width="1138" height="860" alt="Unpaid"
src="https://github.com/user-attachments/assets/bb16fe65-84e4-40a7-8951-90b01030aded"
/>

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-07-01 17:02:54 +00:00
Félix Malfait 55ed4b7adb feat(sdk): translate front-component strings with t()/Trans/useTranslate (#22301)
## What

Lets app **front components** localize the strings they render,
extending the
existing application-translation pipeline (which today only covers
manifest
labels) to component source. App authors mark strings with a small,
familiar
API; the build extracts and bakes them; the runtime resolves them for
the
user's locale.

```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';

<Trans>Loading postcard…</Trans>
<Trans context="card-title">Untitled</Trans>            // disambiguation
const empty = t('No content yet…');                     // works outside JSX
<p>{t('Saved {count} cards', { count })}</p>            // interpolation
const STATUSES = [{ id: 'draft', label: msg('Draft') }]; // lazy descriptor
```

## How

- **Runtime** (`twenty-sdk/front-component`): `t()` (eager, usable
anywhere —
event handlers, helpers, module scope), `msg()` (lazy descriptor),
`<Trans>`
(reactive JSX), `useTranslate()` / `useLocale()`. Source-string
fallback,
`{name}` interpolation, and `context` disambiguation. No build-time
macro —
  these are plain runtime functions.
- **Extraction**: a `ts-morph` scan collects `t()`/`msg()`/`<Trans>`
strings
from component source into the same `locales/*.json` catalogs the
manifest
  pipeline already writes (`twenty dev:translations-extract`).
- **Delivery**: `twenty dev:build` bakes the compiled per-locale catalog
into
each front-component bundle via an esbuild banner, so the runtime
resolves
with **no server or renderer changes**. Locale comes from the execution
  context that already flows to the worker.

The catalog key and `generateMessageId` hashing are shared between the
node
extractor and the browser runtime; `<Trans>` text whitespace is
normalized
identically on both sides so multi-line elements resolve.

## Design notes

- Reuses the existing `extract → compile → manifest.translations`
contract and
`generateMessageId`, so component strings flow through the same
machinery as
  manifest labels.
- Self-contained in `twenty-sdk` + a shared pure helper; the server is
untouched.

## Scope / follow-ups

- `twenty dev` (watch) does not bake catalogs yet — preview shows source
strings; use `twenty dev:build` (documented). Wiring the watcher is a
follow-up.
- Usage is documented in twenty-docs under **Apps → Translations**
  (`developers/extend/apps/translations`).

## Tests

Unit tests for the catalog-key/interpolation helpers, the runtime
resolver
(hit/miss/context/fallback/interpolation), and the ts-morph extractor
(static `t`/`msg`/`<Trans>`, dynamic-skip, dedup, multi-line
whitespace), plus a
compile test for context→messageId. Verified with an adversarial review
pass.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

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

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22301?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>

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-01 18:50:35 +02:00
Thomas des Francs ad31227f7c Fix BlockNote placeholder alignment + paddings (#22409)
## Before

<img width="660" height="958" alt="image"
src="https://github.com/user-attachments/assets/017e73c3-cd55-47e3-9470-3f6c31152663"
/>


## After

<img width="1347" height="995"
alt="file-c55b9d7bc3bd212f18f8c77a0318eaef"
src="https://github.com/user-attachments/assets/3f96af62-6509-4b58-a740-cd87856422cd"
/>
2026-07-01 18:22:59 +02:00
martmull 27dea0ed0b Add installed workspaces view to application registration (#22359)
## After
<img width="895" height="344" alt="image"
src="https://github.com/user-attachments/assets/33591753-f248-45ce-b32d-cc1112f50579"
/>
<img width="889" height="425" alt="image"
src="https://github.com/user-attachments/assets/469ee228-9abb-486f-b2ec-9efb490bb2c8"
/>
<img width="766" height="343" alt="image"
src="https://github.com/user-attachments/assets/2d88444a-6d98-4f97-8e5d-109197cfad27"
/>


## Summary
Add a new "Installed workspaces" section to the application registration
settings page that displays all workspaces that have installed a given
application, with pagination support.

## Key Changes
- **Backend Service**: Added `getInstalledWorkspaces()` method to
`ApplicationRegistrationService` that queries installed applications
across workspaces with pagination support
- **Backend DTO**: Created
`ApplicationRegistrationInstalledWorkspacesDTO` and
`InstalledWorkspaceDTO` to structure the response with workspace details
(id, displayName, logo, version), total count, and hasMore flag
- **GraphQL Resolver**: Added
`findApplicationRegistrationInstalledWorkspaces` query resolver with
pagination (page parameter, default page size of 10) and proper
authorization guards
- **Frontend Component**: Created
`SettingsApplicationRegistrationInstalledWorkspaces` component that:
- Displays installed workspaces in a table with workspace logo, name,
and version
  - Shows initial 3 workspaces with "Show all" button to expand
- Implements pagination with "Show more" button to load additional pages
  - Handles empty state (returns null if no workspaces installed)
- **GraphQL Query**: Added
`FindApplicationRegistrationInstalledWorkspaces` query document for
frontend data fetching
- **Integration**: Integrated the new component into
`SettingsApplicationRegistrationGeneralTab`

## Implementation Details
- Pagination uses offset-based approach with configurable page size (10
workspaces per page)
- Query results are ordered by workspace displayName and id for
consistent ordering
- Soft-deleted applications and workspaces are excluded from the list
and counts
- Apollo Client's `fetchMore` with `updateQuery` merges paginated
results into the cache
- Component respects existing authorization (API_KEYS_AND_WEBHOOKS
permission required)
- Uses existing UI components (Table, Card, Avatar, Button) from
twenty-ui library
- Supports internationalization with Lingui

## Screenshots
The new "Installed workspaces" section on the app registration General
tab (admin app detail page), captured against a local instance with a
demo app installed in 14 workspaces. The three PNGs are committed under
`.github/assets/screenshots/installed-workspaces/` and render inline in
the **Files changed** tab of this PR:

- `1-first-3-show-all.png` — Collapsed: the first 3 installed workspaces
(avatar + name + installed version) with a "Show all" button.
- `2-expanded-show-more.png` — "Show all": the first page of 10
workspaces, with a "Show more" button (more remain).
- `3-all-paginated.png` — "Show more": all 14 workspaces loaded, button
gone.

Review in cubic:
https://cubic.dev/pr/twentyhq/twenty/pull/22359?utm_source=github

https://claude.ai/code/session_012nWtviSBdfFeHEASTtwvJ7
2026-07-01 14:38:51 +00:00
Raphaël Bosi 72bcc78e36 Let the sign-in screen scroll when its content overflows the viewport (#22397)
On the sign-in screen, when a step's content is taller than the viewport
(e.g. many workspaces to choose from), it grew past the fixed-height
background and overflowed the page.

Make the shared background scroll instead, so every sign-in step scrolls
when its content overflows and stays centered when it fits.

## Before


https://github.com/user-attachments/assets/0226daee-0cd9-454c-9f4b-257cfab61bfb


## After


https://github.com/user-attachments/assets/7a535099-f0f0-431b-86ad-9fa8121638db


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22397?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-01 16:16:27 +02:00
Raphaël Bosi 4d96ec489b Add smooth page transitions to onboarding v2 (#22392)
## Before


https://github.com/user-attachments/assets/d2fcd5ce-7e34-4f07-9a52-cac8acdc37cd

## After


https://github.com/user-attachments/assets/5c245949-cff9-41b2-802d-3deeb562efa6


On a full-page load of a v2 onboarding URL (the post-signup
workspace-subdomain redirect), Lingui's `I18nProvider` renders `null`
until the locale chunk async-activates, so the app is blank for ~2s
before the verify step appears. Steps also hard-cut and flashed a loader
between each other.

- Show a pulsing-logo loader until the locale activates (a gate above
`I18nProvider`), scoped to onboarding v2 paths so every other page is
unchanged.
- Cross-fade between steps and preload their chunks on entry, so
navigating never flashes the loader.

Frontend-only; i18n loading itself is untouched.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22392?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-01 14:50:13 +02:00
Raphaël Bosi 2e6077383b Add install your first apps onboarding V2 step (#22347)
https://github.com/user-attachments/assets/5326d48f-1842-4db1-bc7c-94852145c035


<img width="838" height="754" alt="CleanShot 2026-06-30 at 16 25 05@2x"
src="https://github.com/user-attachments/assets/5c7d53d7-4d65-4e35-aed1-edf0c104e140"
/>


Adds an "Install your first apps" step to the V2 onboarding, shown right
after import-contacts. It lets users opt into installing marketplace
apps (Call recorder and People Data Labs for now) during onboarding.

- New backend `OnboardingStatus.APPS_INSTALLATION` (between SYNC_EMAIL
and PROFILE_CREATION); V1 auto-skips it.
- The primary button sends the selected app ids to the server via
`triggerInstallAppsOnboardingStep`, which enqueues a dedicated job that
installs them asynchronously so onboarding isn't blocked. Skip continues
without installing.
- The workspace is credited per app on successful installation. Credits
are env-driven via `ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP`,
shown as "Earn +N free credits (1 per tool)".

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22347?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-01 12:25:16 +00:00
Parship Chowdhury 49a80c72d2 feat: show group-by context in record show breadcrumb (#22247)
Fixes [#837](https://github.com/twentyhq/core-team-issues/issues/837)

On the record show page, extend breadcrumb pagination when the current
view is grouped: (`rank/total in {viewName} -> {groupValue}`)
Example: `Tasks / Schedule follow-up call (1/1,800 in By Status -> To
do)`



https://github.com/user-attachments/assets/7038d1f5-57e5-4e85-a3e6-09ac46c5b824



https://github.com/user-attachments/assets/88134fa4-e038-4520-a970-ce058a4444b0

<img width="1427" height="173" alt="Screenshot 2026-06-27 202807"
src="https://github.com/user-attachments/assets/842d911e-b4bc-4443-afcd-4c67ed007ae0"
/>
<img width="1426" height="183" alt="Screenshot 2026-06-27 202851"
src="https://github.com/user-attachments/assets/f8d9ee31-b1cb-46fa-9e7c-8876d4b099ff"
/>
<img width="1427" height="178" alt="Screenshot 2026-06-27 203423"
src="https://github.com/user-attachments/assets/196ed694-0edb-4b52-934c-0938d3fd4da2"
/>




<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22247?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>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
2026-07-01 12:02:28 +02:00
Raphaël Bosi 52d3735147 [REQUIRED FOR 2.18 RELEASE] Show the upgrade-plan step at the end of V1 onboarding (#22368)
## What

On V1 onboarding the upgrade-plan step (`ChooseYourPlan`) only appeared
later, once the user happened to create a record, instead of right after
Invite team.

## Why

The frontend advances the onboarding status optimistically in
`getNextOnboardingStatus()` without refetching, and it never emitted
`PLAN_REQUIRED`. So after Invite team the user was locally marked
`COMPLETED` and dropped into the app; the backend's real `PLAN_REQUIRED`
only surfaced on a later `GetCurrentUser` refetch.

## Fix

Make `getNextOnboardingStatus()` billing-aware so it mirrors the
backend: return `PLAN_REQUIRED` in the terminal branches when
`isBillingEnabled && billingSubscriptions.length === 0` (using
`billingSubscriptions` to match the backend's any-subscription check).
The navigate hook already routes `PLAN_REQUIRED` to `/plan-required`, so
no routing change is needed. Self-hosted and existing-subscription flows
are unchanged.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22368?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-06-30 17:59:09 +02:00
Paul Rastoin 36e89c04ad Front fallback flat object search field metadata (#22369)
## Fix: crash on Settings → Object → Search after changing label
identifier

### Problem
Opening the Search section (or changing an object's label identifier)
threw
`t.searchFieldMetadatas is not iterable` in
`SettingsObjectSearchSection`.

### Root cause
`EnrichedObjectMetadataItem.searchFieldMetadatas` is typed as a
non-optional
array, but at runtime it can be `undefined`. `useLoadMinimalMetadata`
stores the
minimal objects with `objectMetadataItems as unknown as
FlatObjectMetadataItem[]`.
The minimal query doesn't select `searchFieldMetadataList`, so the
double-cast
hides that the property is missing. Until the full metadata reload
lands, the
object has no `searchFieldMetadatas`, and
`objectMetadataItemsWithFieldsSelector`
spreads that `undefined` straight through to the component, which
spreads it
(`[...searchFieldMetadatas]`) and crashes.

(`fields`/`indexMetadatas` never hit this because they come from
`Map.get()`,
which is honestly typed as `| undefined` and already falls back to
`[]`.)

### Fix
Guarantee the array contract in `objectMetadataItemsWithFieldsSelector`,
matching
how `fields`/`indexMetadatas` are already defaulted:
`searchFieldMetadatas: flatObject.searchFieldMetadatas ?? []`.

### Tradeoff considered
The "clean" alternative is promoting `searchFieldMetadatas` to its own
metadata-store entity (like `indexMetadataItems`), which would make the
`?? []`
type-mandated via `Map.get`. Rejected for now: it's a medium
cross-package
refactor (new store key, type, selectors, split/reload wiring, plus a
server-side
collection hash for staleness) for an entity that is never independently
mutated —
it only changes as a side effect of label-identifier/field updates, so
independent
caching buys nothing. The selector default fixes the crash with minimal
surface
area; the deeper cleanup (making the `as unknown as` cast honest, or
splitting the
store) can be deferred until search-field metadata becomes directly
editable.
2026-06-30 17:26:22 +02:00
Paul Rastoin 9d361c8bb0 [FIX_TYPECHECK_ON_MAIN] Add missing inviteTeamMaxCreditsReward to OnboardingConfig type (#22370)
## Context

The `twenty-front` typecheck is broken on `main`:

```
src/modules/onboarding/hooks/useInviteTeam.ts:154:27 - error TS2551: Property 'inviteTeamMaxCreditsReward' does not exist on type 'OnboardingConfig'.
```

This is a merge race: one PR started consuming
`onboardingConfig.inviteTeamMaxCreditsReward` in `useInviteTeam.ts`,
while the frontend `OnboardingConfig` type only declared
`inviteTeamCreditsRewardPerUser`. The backend already returns both
fields (`client-config.entity.ts` declares `inviteTeamMaxCreditsReward`
and the service populates it), so this is purely a missing frontend type
field.

## Changes

- Add `inviteTeamMaxCreditsReward: number` to the frontend
`OnboardingConfig` type.
- Add the field to the config mock so `mock-data/config.ts` satisfies
the type.

## Test

`npx nx typecheck twenty-front` passes.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22370?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-06-30 17:25:51 +02:00
nitin 5a5c829129 fix(page-layout): render relation field widgets in table display mode (#22220)
Adding a to-many relation field as a **Table** on a record page rendered
an empty widget (header only) in several cases. This fixes three
independent defects behind that.

- **Morph inverse relations crashed the table.** The host-scoping view
filter (`IS current record`) is built on the relation's inverse field.
When that inverse is a `MORPH_RELATION` (attachments, notes, tasks…),
`getFilterTypeFromFieldType` fell through to `TEXT` and the GraphQL
builder threw `Unknown operand IS for TEXT filter`, unmounting the table
via the ErrorBoundary. `MORPH_RELATION` now classifies as `RELATION`,
and the relation filter resolves the correct morph join column (e.g.
`targetPersonId`) from the current record's object type.
- **Stale `viewId` on field change.** Changing the bound field on a
Table widget kept the previous relation's draft view (wrong
object/fields/filter). Field selection now regenerates the draft view
for the new relation, or clears the stale `viewId` when the new field
can't back a table.
- **Label identifier could be hidden or reordered.** Relation-table
widget views now pin the label-identifier field first and visible on
view creation and save.

Deferred: morph relation filters with arbitrary selected record ids (not
just "current record") — needs target-object identity in the filter
value schema.

**Test:** open a Person → edit layout → add a Field widget → bind a
to-many relation → switch Layout to Table. Previously empty for
`attachments` (morph) and for any field changed on an existing Table
widget; now scoped to the host record.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22220?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-06-30 19:03:04 +05:30
Raphaël Bosi aea6c3832a Credit workspaces for onboarding invite-team signups (#22309)
https://github.com/user-attachments/assets/6591cbb0-2b60-4f25-8b03-26b0da73f0d8

After the invite has been accepted:
<img width="1606" height="286" alt="CleanShot 2026-06-30 at 11 24 47@2x"
src="https://github.com/user-attachments/assets/7becf8a5-04dc-4512-ac7f-951a77e4c0ac"
/>

Adds a dedicated `ONBOARDING_INVITATION_TOKEN` app-token type so
invitations sent during the onboarding invite-team step are
distinguished from regular invites. When an invited person actually
signs up, the inviting workspace is credited 0.5 credits.

Reward eligibility is derived entirely server-side, with no public API
parameter: an invitation is reward-eligible only while the workspace is
in the onboarding invite-team step (`ONBOARDING_INVITE_TEAM_PENDING`), a
flag set once at workspace creation that no public mutation can re-arm.
Both token types stay valid invitations everywhere via a shared
`INVITATION_APP_TOKEN_TYPES`, so invitees still join normally and appear
in invite lists.

Crediting is a best-effort direct call to
`BillingCreditService.creditWorkspaceBalance` from the sign-in-up flow:
it no-ops when billing is disabled and never blocks signup, and is
bounded by a 10-invite-per-workspace cap. No DB migration needed:
`appToken.type` is a text column.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22309?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-06-30 14:49:25 +02:00
Raphaël Bosi 96e5d0f3ed Track total onboarding free credits in an atom (#22348)
The v2 onboarding header shows a "free credits" counter, but every page
fed it a hard-coded `0`, so it never reflected the credits the workspace
would actually receive. This tracks the running total based on the
choices made at each step.

- New `onboardingFreeCreditsState` atom (`{ importContacts, inviteTeam
}`, localStorage-backed) + `useOnboardingFreeCreditsTotal` to sum it
into the header.
- Connecting email sets the import-contacts reward (persisted so it
survives the OAuth redirect); inviting teammates sets `min(count ×
perUser, max)` on submit. Skipping a step contributes 0; the atom resets
at onboarding start.
- Counter scope is import-contacts + invite-team rewards only
(display-credit units already exposed via `onboardingConfigState`).
Plan/trial credits are out of scope.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22348?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-06-30 14:20:50 +02:00
Raphaël Bosi f08b87c478 Fix v2 onboarding dropping to v1 after connecting email (#22351)
Connecting an email during v2 onboarding triggers a full-page OAuth
round-trip that returns to `/` with no query param.
`isOnboardingV2State` was an in-memory atom, so it reset to `false` on
return and the navigation hook routed the user into the v1 onboarding
(same break on a plain refresh).

Fix: back the atom with `sessionStorage`. It survives the same-tab OAuth
redirect and refresh, hydrates synchronously (`getOnInit`), and is
auto-cleared by the existing `sessionStorage.clear()` on sign-out. The
`onboardingV2=true` URL-param plumbing stays, since it carries the flag
across the cross-subdomain signup hop.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22351?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-06-30 11:33:48 +00:00
neo773 9f3ebaaf22 feat(messaging): sync draft emails and edit them in the thread composer (#22178)
Stop excluding drafts from sync across all three providers (Gmail DRAFT
label, Microsoft/IMAP Drafts folder) and add an isDraft boolean field on
Message so drafts are queryable by the API and AI agents.

Drafts render in the thread with a Draft tag; clicking one opens the
existing reply composer pre-filled with the draft's recipients, subject
and body, and Send reuses the existing send-email flow.

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

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-06-30 13:07:53 +02:00
Paul Rastoin b0d7516951 Deprecate asExpression from field metadata search_vector (#22287)
## Summary

Fully deprecates the cached `asExpression` / `generatedType` settings on
`TS_VECTOR` (searchVector) fields. Previously the generated-column
expression was stored in `FieldMetadataSettings` and kept in sync via
imperative recompute side-effects. It is now **derived at DDL time**
from the `searchFieldMetadata` rows that describe which fields feed the
search vector, making `searchFieldMetadata` the single source of truth
and removing a whole class of cache-drift bugs.

This is delivered across the milestones tracked in #2587 and coordinates
with the frontend migration (#1428).

## Why

- The searchVector expression lived in two places (stored
`settings.asExpression` + the actual generated column), kept consistent
by bespoke side-effects (`recompute-search-vector-on-field-rename`,
label-identifier recompute, etc.).
- The frontend reconstructed the searchable-fields list by
**regex-parsing** the stored `asExpression`.
- Both are brittle. Deriving the expression from `searchFieldMetadata`
rows at build/run time removes the cache and the parsing.

## What changed

### Server - data model & derivation
- Introduce the `tsVectorFieldMetadata` relation on
`searchFieldMetadata` (`tsVectorFieldMetadataId` / universal identifier)
linking each searchable-field row to its target `TS_VECTOR` field.
- New runtime derivation
`deriveSearchVectorAsExpressionForTsVectorField`
(`flat-search-field-metadata/utils/...`) used by the create-object and
update-field handlers to generate the column expression from
`searchFieldMetadata` rows.
- Remove `asExpression` / `generatedType` from stored settings:
`FieldMetadataSettings.TS_VECTOR` is now `null`; the column builder
(`generate-column-definitions.util.ts`) hardcodes `generatedType:
'STORED'` and requires the derived expression.
- Delete the imperative recompute side-effects and the
`compute-search-vector-universal-settings-from-object-manifest` path;
drop the `settings` block from all 28 standard
`compute-*-standard-flat-field-metadata` utils.

### Server - migration runner
- New `rebuildSearchVector` marker on `update-field` actions: the
orchestrator synthesizes targeted column rebuilds
(`compute-search-vector-rebuild-target-universal-identifiers.util.ts` +
the deprioritize aggregator) only when a searchFieldMetadata change or
indexed-field rename actually requires it - instead of rebuilding on
every settings touch.
- Deferrable FKs + in-flight ID resolution so a `searchFieldMetadata`
row and its `TS_VECTOR` field can be created in the same transaction
(deterministic UUIDs).

### Frontend (contract change, #1428)
- New `SearchFieldMetadataDTO` + dataloader exposing
`searchFieldMetadataList` on object metadata.
- `SettingsObjectSearchSection` now reads
`objectMetadataItem.searchFieldMetadatas` instead of parsing
`asExpression`; new `SearchFieldMetadataItem` type, fragment, and
mapping updates.

### Upgrade commands (2.18)
-
`2-18-instance-command-fast-...-add-ts-vector-field-metadata-id-to-search-field-metadata`
-
`2-18-instance-command-fast-...-make-search-field-metadata-fks-deferrable`
-
`2-18-instance-command-slow-...-backfill-ts-vector-field-metadata-id-on-search-field-metadata`

(These were relocated from 2.16 to 2.18 and re-timestamped into an
ordered block - add column -> make FK deferrable -> backfill data -
since 2.16/2.17 are released.)

### Tests
- Updated search-vector side-effect integration specs to assert behavior
(search works) rather than the now-removed `asExpression`; removed the
obsolete expression-validation specs; refreshed the application-sync
snapshot (`universalSettings: null`).

## Upgrade / compatibility notes
- Existing workspaces keep their stored `settings` until a later
cleanup; nothing reads it anymore. The new derivation drives all DDL
going forward.
- Schema changes are gated behind the 2.18 instance commands above.

## Known follow-up (separate PR)
https://github.com/twentyhq/core-team-issues/issues/2620
- The column rebuild (`DROP`/`ADD` of the `searchVector` STORED column)
cascade-drops its GIN index and does not recreate it - a pre-existing
regression on `main` inherited here. A follow-up PR will fix the rebuild
handler to recreate the GIN index and add a 2.18 workspace command to
recompute every search vector + strip the deprecated settings.
(Planned.)

## Test plan
- [ ] `npx nx typecheck twenty-server` / `twenty-front`
- [ ] `npx nx lint:diff-with-main twenty-server` / `twenty-front`
- [ ] Server integration: create/update/delete field, rename indexed
field, update object - search returns expected records
- [ ] Run the 2.18 instance commands on a seeded DB; verify
`tsVectorFieldMetadataId` backfilled and FKs deferrable
- [ ] Frontend: object Search settings tab lists the correct searchable
fields (no `asExpression` parsing)

close https://github.com/twentyhq/core-team-issues/issues/2587

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22287?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-06-30 09:16:01 +00:00
Paul Rastoin 460b203b38 ci(twenty-front): show only failing unit tests in CI (#22345)
## What

Adds a custom Jest reporter to **twenty-front** that, in CI, suppresses
passing-test output and surfaces only failures — mirroring what we
already do for **twenty-server**.

## How

- New `packages/twenty-front/jest-failures-only-reporter.cjs` — a
verbatim port of
`packages/twenty-server/jest-failures-only-reporter.js`. It prints a
`FAIL` block per failing suite plus a final "FAILED TEST SUITES
SUMMARY", and otherwise emits only the suite/test totals.
- Wired into `packages/twenty-front/jest.config.mjs` via `...(isCI && {
reporters: ['./jest-failures-only-reporter.cjs'] })`, gated on `CI ===
'true'` exactly like twenty-server.

### Note on the `.cjs` extension

twenty-front's `package.json` sets `"type": "module"`, so a `.js`
reporter is parsed as ESM and `module.exports` throws. Renaming to
`.cjs` keeps the file as CommonJS (Jest requires CJS reporters).
twenty-server is not an ESM package, hence its `.js` extension.

## Testing

- Passing suite (`CI=true npx jest <file>`): output reduced to the
totals summary only.
- Temporary failing suite: shows the `FAIL` block, failure message, and
the failed-suites summary.

Local dev runs (no `CI` env) are unaffected — the default reporter is
used.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22345?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-06-30 08:30:11 +00:00
Etienne 1055764fff fix: reconcile metadata store after object creation so activity targets on new custom objects link correctly (#22331)
## Summary

Creating a Task or Note on a record of a newly-created custom object
failed to link them, and the console showed `Missing field 'targetTest…'
while writing result` for `TaskTarget`/`NoteTarget`.

After creating a custom object, the front-end metadata store was left
with an inconsistent morph relation group on the default-relation
objects (`taskTarget`, `noteTarget`, `attachment`, `timelineActivity`).
This reconciles the store from the server after creation so the morph
fields are rebuilt correctly.

## Context / root cause

The DB and server are correct: the new object adds a single member (e.g.
`targetTest`) to the existing `target` morph group (shared `morphId`),
and the server's `objects` query collapses + renames the group to one
`target` field with a full `morphRelations` array.

On the client, though, the metadata store is updated incrementally after
creation:
- The bulk `objects` query stores morph fields already collapsed
(`target` + `morphRelations`).
- The new reciprocal morph member arrives via SSE/mutation as a raw,
un-collapsed field row (`targetTest`, without `morphRelations`), which
`objectMetadataItemsWithFieldsSelector` simply joins in.

This leaves two morph fields on `taskTarget`/`noteTarget` (`target` with
stale members + an un-normalized `targetTest`).
`mapFieldMetadataToGraphQLQuery` then fans `targetTest` out into
non-existent fields (`targetTestCompany`, `targetTestPerson`, …), which
the server omits, breaking the optimistic cache write (`writeFragment`)
and leaving the activity target unlinked in the UI.

This is a regression from the metadata-store incremental-sync refactor
(the create path stopped reconciling reciprocal morph fields on existing
objects).

## Fix

In `useCreateOneObjectMetadataItem`, after the incremental store
updates, call `invalidateMetadataStore()` so the objects/field metadata
is refetched from the server and the morph groups are rebuilt in their
correct collapsed form. This mirrors the existing pattern in
`useDeleteOneObjectMetadataItem`.

## Test plan

- [ ] Create a new custom object.
- [ ] Open a record of that object and create a Task and a Note from it.
- [ ] Verify no `Missing field 'target…'` error in the console and the
task/note is linked (visible in the record's Tasks/Notes and on the
activity target).
- [ ] Confirm existing standard objects (Company/Person/Opportunity)
still link tasks/notes correctly.
- [ ] Confirm object creation still updates the left nav / views as
before.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22331?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-06-30 10:01:24 +02:00
Weiko 87fa0e4c12 fix(front): mark stale AnimatePresence exit page inert so it can't intercept clicks (#22328)
## Context
When navigating between the app and settings sections,
MainAppLayoutOutlet keeps the outgoing page mounted during the
AnimatePresence exit transition, and can leave a stale exit node behind
the active page, notably when the page hosts an app front component
whose Web Worker teardown blocks React from removing it.
The leftover page is invisible (opacity 0) but still captures pointer
events on top of the active route, so e.g. front-component buttons stay
clickable through the settings screen.

The existing `exit={{ pointerEvents: 'none' }}` mitigation is defeated
by descendants that set pointer-events explicitly (the front-component
container uses pointer-events: auto). Tag each transition page with its
route section and mark every non-active one `inert`, which descendants
cannot override, so any stale page is fully non-interactive.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22328?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-06-30 09:34:27 +02:00
vittolago fb800e7a2d fix: improve native print output for dashboards and record tables (#22272)
## Summary

- Adds print-specific snapshots for dashboards so native browser
print/PDF captures charts and front components instead of the app shell.
- Adds record-table print snapshots for object index pages so printable
tables are generated from visible rows and native print avoids
virtualized blank pages.
- Preserves rendered chart layers by rasterizing canvas/SVG content for
print.

## AI-generated disclosure

This pull request was AI-generated by Hermes Agent on behalf of Vittorio
Alfieri. The changes were reviewed and tested locally before submission.

## Screenshots

### Dashboard print

**Before**

![Dashboard print
before](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/dashboard-before.png)

**After**

![Dashboard print
after](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/dashboard-after.png)

### Table records print

**Before**

![Table records print
before](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/tasks-before.png)

**After**

![Table records print
after](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/tasks-after.png)

## Test plan

- [x] `yarn nx typecheck twenty-front`
- [x] `yarn nx build twenty-front`
- [x] Generated dashboard PDFs from the preview build and rasterized
pages to PNG for visual verification.
- [x] Generated Tasks/table-record PDFs from the preview build and
verified the final page contains table content instead of blank pages.


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

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-06-29 22:41:37 +02:00
Raphaël Bosi facdbb5ba8 v2 onboarding: dedicated verify step and upgrade-free-trial as the last step (#22303)
https://github.com/user-attachments/assets/b1ee4f77-c6d7-4638-b9f1-dd801d1cc0db

Completes the onboarding-v2 flow: a dedicated verify step, the
reordering that makes the plan step come last, and the
upgrade-free-trial page itself.

## Verify step (`/verify-v2`)
After the cross-domain token exchange, v2 sign-ups land on a clean
`BlankLayout` "Verifying your email" screen (fading Twenty logo) instead
of the v1 `AuthModal` flashing over the background mock. The redirect
target is chosen from `isOnboardingV2` (read from the Jotai store at
redirect time). The pulsing logo is extracted into a shared
`OnboardingPulsingLogo`, reused by the workspace-activation loader.
`/verify-v2` joins the same exempt lists as `/verify` (ongoing-creation
guard, metadata gater, apollo unauthenticated handler, captcha, page
title) — intentionally not `useShowAuthModal`, which is what drops the
modal.

## Plan step is now last
`getOnboardingStatus` checks `PLAN_REQUIRED` after invite-team instead
of first, so onboarding runs workspace activation → email → profile →
invite → plan. This is what lets the upgrade step be reached as the
final step instead of gating right after sign-up. Applies to both v1 and
v2 (same order).

## Upgrade free trial page (`PlanRequiredV2` → `ChooseYourPlanV2` /
`UpgradeFreeTrial`)
The final step, full-screen under `BlankLayout` via
`OnboardingV2Layout`, matching the Figma (billing card with the Stripe
form, the "Basic / without credit card" option, trial + credits pills).
Reuses the v1 `ChooseYourPlanContent` billing logic
(`SubscriptionPaymentForm`, `useHandleCheckoutSession`). The "+N free
credits" reward comes from
`clientConfig.onboarding.upgradeCreditsReward` (sourced from
`BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD`).

## Also
Fixes a latent staleness in the Apollo `onUnauthenticatedError` handler
— it captured `location` from the memoized client, now read via a ref —
so auth-path exemptions are correct after navigation.

Note: the onboarding step order change affects v1 too (plan becomes its
last step as well).
2026-06-29 16:32:30 +00:00
Thomas des Francs 2bb7fb2e9b Polish settings page titles and admin tables (#22305)
## Summary

- Reuse the shared settings title presentation for read-only and
editable settings page titles.
- Polish settings AI/app icons, breadcrumb cropping, and admin detail
title icons.
- Align admin panel table/card typography and spacing with existing
settings tables.

## Before/After

<img width="1524" height="2214" alt="Settings pages before and after"
src="https://github.com/user-attachments/assets/91037b25-b442-4eb2-b244-1d8280ce2cd9"
/>

<img width="2200" height="3268" alt="Additional settings UI before and
after"
src="https://github.com/user-attachments/assets/f10d7283-7031-4958-8539-649c3067cd31"
/>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22305?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-06-29 17:39:30 +02:00
souheyl gouadria 1b2f2c71c3 Move filter group destruction after filter operations (#22248)
### The current order is:

1. Create filter groups
2. Update filter groups
3. Destroy filter groups ← **happens here**
4. Clean up store (cascade)
5. Create/update/delete filters (which may reference groups just
destroyed)

The fix is to move filter group destruction after filter operations, so
filters that reference those groups get created/updated/deleted first.

### after fix : The persistence order is now:

1. Create filter groups
2. Update filter groups
3. Create/update/delete view filters (these can safely reference groups
that still exist)
4. Destroy filter groups (only after all filter mutations are done)
5. Clean up store (cascade-deleted filters

**root cause :** step 4 happened before step 3, so filter
creates/updates would reference groups that had already been deleted in
the same save cycle ; causing the backend to fail with "Migration
execution failed" when it couldn't resolve the `viewFilterGroupId
`foreign key.

this fixes the bug : #21351

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22248?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: Souheyl Gouadria <souheyl.gouadria@medius.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-29 16:54:15 +02:00
martmull d1854bc7e9 Add marketplace catalog synchronization to admin panel (#22260)
## After

<img width="1476" height="658" alt="image"
src="https://github.com/user-attachments/assets/25a042dd-eabf-4b71-9872-d3b627104634"
/>


## Summary
This PR adds the ability for admins to manually synchronize the
marketplace application catalog from the admin panel. It introduces a
new mutation endpoint and UI controls to trigger catalog synchronization
with user feedback via snackbar notifications.

## Key Changes
- **Frontend (SettingsAdminApps component)**:
  - Added `useSnackBar` hook for user feedback on sync success/failure
- Imported `useMutation` from Apollo Client to handle the sync operation
  - Added `IconRefresh` and `Button` imports for the sync UI control
- Created `handleSyncCatalog` function that triggers the mutation,
refetches app registrations, and displays appropriate snackbar messages
- Added a new "General" section with a "Synchronize catalog" button
above the existing app registrations table
  - Button shows loading state and is disabled while sync is in progress

- **Backend (AdminPanelResolver)**:
- Added `syncMarketplaceCatalog` mutation that queues a
`MarketplaceCatalogSyncCronJob` via the message queue
- Uses `@InjectMessageQueue` decorator to inject the cron queue service
- Includes job deduplication via `id: 'marketplace-catalog-sync'` to
prevent multiple pending sync jobs
  - Protected with `@UseGuards(AdminPanelGuard)` for admin-only access

- **GraphQL Schema**:
  - Added `SyncMarketplaceCatalog` mutation type definition
  - Generated corresponding TypeScript types and mutation document

- **New Files**:
  - Created `syncMarketplaceCatalog.ts` GraphQL mutation document

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22260?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-06-29 12:58:34 +00:00
Weiko a215d3cbdf fix: prevent crash when leaving dashboard edit mode with side panel open (#22292)
## Problem

When creating or editing a dashboard and opening a page layout side
panel (clicking the **"Add widget"** box, or editing a widget via grid
edition), clicking another nav item like **"Opportunities"** crashes the
whole app ("Sorry, something went wrong").

### Steps to reproduce
1. Go to `/objects/dashboards` and click **"+ Add new"** to create a
dashboard.
2. Click the **"Add widget"** box (or add a widget via grid edition and
open its settings).
3. Click another nav item such as **"Opportunities"**.
4. The app crashes.

## Root cause

The side panel stays mounted during its close animation, but the main
context store has already switched to the new page (the record index has
no single targeted record). The still-mounted page layout side panel
page (`SidePanelPageLayoutDashboardWidgetTypeSelect`, chart settings,
etc.) re-renders and calls `usePageLayoutIdFromContextStore`, which
throws `Error: Only one record should be selected`. With no local error
boundary, the throw propagates to the top-level boundary and crashes the
app.

## Fix

`SidePanelRouter` now skips rendering page layout side panel pages
whenever the main context store has no single targeted record — the same
condition `usePageLayoutIdFromContextStore` requires to not throw.
During navigation the panel closes cleanly instead of crashing.

This is safe because `usePageLayoutIdFromContextStore` unconditionally
throws without a single-record selection, so the guard can only skip
pages that would otherwise crash — it cannot break a currently-working
flow. The guard uses the existing `isPageLayoutSidePanelPage` helper, so
it covers all page layout side panel pages (widget type select, chart /
iframe / record table settings, record page field settings, etc.).

## Testing

- Reproduced the crash in the running app, applied the fix, and
confirmed navigating to Opportunities from both the **widget type
select** and the **chart settings** panels now lands on the
Opportunities list with no console or page errors.
- `oxlint --type-aware` and `nx typecheck twenty-front` both pass.
2026-06-29 14:14:42 +02:00
Raphaël Bosi db7d8172f7 Add v2 onboarding invite team page (#22229)
<img width="3024" height="1500" alt="CleanShot 2026-06-26 at 18 09
47@2x"
src="https://github.com/user-attachments/assets/e91f30a5-2763-42a0-9abf-d9fa8400870c"
/>


Adds the v2 onboarding **Invite team** page (`INVITE_TEAM`), shown right
after the create-profile step for the onboarding-v2 cohort. It renders
full-screen under `BlankLayout` via the shared `OnboardingV2Layout`,
matching the Figma (340px column, email inputs with inline remove, dark
Invite, Skip).

Reuses all v1 invite-team logic via a new `useInviteTeam` hook (v1
`InviteTeam` now consumes it too; its UI is unchanged). Routing mirrors
`SyncEmailsV2`/`CreateProfileV2`: new `AppPath.InviteTeamV2`, lazy
route, and an `isOnboardingV2`-gated branch in
`usePageChangeEffectNavigateLocation` (+ tests and a Storybook story).

No backend changes.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22229?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-06-29 13:53:07 +02:00
greymoth 13f80f0d95 fix: ignore IME composition Enter in chat-thread and attachment rename inputs (#22270)
## What's this PR doing?

Two inline rename inputs run their action on `Enter` without ignoring
the `Enter` that confirms an IME composition:

- `AiChatThreadListItem` (renaming an AI chat thread). It also calls
`preventDefault()`, so the composition-commit `Enter` is swallowed and
the half-typed title gets saved.
- `AttachmentRow` (renaming an attachment). The same `Enter` saves the
unfinished name.

When you type with an IME (Japanese, Chinese, Korean), the first `Enter`
after typing confirms the candidate text rather than submitting, so
these handlers fire with text the user hasn't finished entering.

## Why

The codebase already guards this where keyboard handling goes through
`useHotkeysOnFocusedElement` (`if (keyboardEvent.isComposing ||
keyboardEvent.keyCode === 229) return`), and the inline inputs that
don't use that hook add the same check themselves: see
`SettingsAccountsBlocklistInput`, `SettingsDevelopersApiKeysNew`, and
the sign-up workspace forms. These two rename inputs were just missing
it.

## How

Add the same `isComposing || keyCode === 229` guard before the `Enter`
branch. For input without an IME, `isComposing` is `false` and `keyCode`
is `13`, so the rename-on-Enter behavior stays the same. This only skips
the action on the composition-commit key.

I checked the change against the repo's Prettier config locally. I
couldn't add a unit test because jsdom doesn't dispatch real composition
events (`isComposing` stays `false`), so it can't reproduce the
keystroke. Happy to add an e2e test if that's preferred.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22270?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-06-29 08:08:11 +02:00
Félix Malfait d81b3c3fa3 feat(twenty-sdk): extract & compile app translations into the manifest (#22236)
## Summary

**PR 2/4** of the app-metadata-translations stack. Gives app developers
the authoring side, as part of the normal manifest build — and it stays
out of the way of developers who don't translate.

- `twenty-sdk` CLI i18n pipeline: collect translatable strings from the
manifest, generate value-as-key message ids (`sha256(value)` truncated,
byte-identical to the server's `generateMessageId`), a `dev
i18n-extract` command to scaffold per-locale catalog files, and a
compile step folded into `build` that emits `manifest.translations`.
- Opt-in: no `locales/` dir → `compileApplicationTranslations` returns
`undefined` → manifest is unchanged.
- Adds an optional `locale` to the front-component execution context so
components can translate against the host locale.

## Stack
Stacks on #22235 (PR 1/4). Base branch:
`claude/app-translation-1-runtime-resolution`.

## Tests
Unit (vitest): extract/compile round-trip + message-id determinism.

## Verification note
`yarn install` could not complete in the remote dev environment, so
typecheck/lint/tests were not run locally — **CI is the source of
truth**.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22236?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-06-28 07:36:48 +02:00
Félix Malfait 538b180824 feat(dpa): self-serve Data Processing Agreement generator (#22243)
## What

A single, region-aware DPA that serves all customers, generated
automatically from the customer's deployment. Two layers:

1. **Click-through DPA** — recorded at signup (acceptance = execution),
resolving merge fields from the deployment region. Cloud only.
2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA:
preview the agreement, enter legal entity + authorized signatory,
download a PDF pre-signed by Twenty, and store the executed copy against
the workspace with its template version + timestamp. Deep-linkable at
`/dpa` (login-gated) for `twenty.com/dpa`.

## How it resolves

A typed variable matrix (`dpa-region-config.constant.ts`) maps the
deployment region to the contracting Processor entity and terms:

- **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law
France, SCC section dormant.
- **US (custom)** → Twenty, Inc., hosting US, SCC section active.

Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default
EU) behind a `DpaRegionService` seam so it can later become
per-workspace without touching callers. The legal text is verbatim from
the template (generated into `dpa-template.constant.ts` directly from
the source `.docx`); only the 6 merge fields are filled and the SCC
sections (7.2–7.5) stay in the document for every region per the spec —
only field values branch. Sub-processors are deferred to
trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc.
remains merchant of record regardless of Processor).

## UI

Standard list + create-page pattern (mirrors API keys / webhooks): a
list of executed copies (with re-download) — or the agreement preview
when none exists — and a top-right blue **Generate DPA** CTA opening a
standard create page. The "Legal" item is intentionally **not** in the
settings menu; the page is reached via the `/dpa` deep link.

## Notable implementation details

- **PDF** is rendered server-side with `@react-pdf/renderer`. The
built-in standard-14 fonts only encode ASCII and crash on the template's
curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL)
is **subset to a Latin glyph set and embedded as base64 data: URLs** —
no font files to ship or resolve at runtime (works in dev, prod-Docker
and CI).
- New `core.dpaAgreement` table via a fast instance command (FK hash
reproduced to match TypeORM).
- Self-hosted deployments (billing disabled) skip click-through
recording and stamp a prominent "not a valid agreement" banner on the
preview and PDF.

## Tests

- Unit: resolver (per-region entity/law/SCC state, EU default, no
unresolved `{{ }}`, SCC sections present in both regions, self-hosted
notice) and HTML renderer.
- Integration (`test/integration/graphql/suites/dpa`): preview has no
unresolved fields; `generateSignedDpa` renders + persists + returns a
downloadable PDF (asserted with accented input to guard the font
regression); list re-download.

## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in
`dpa-region-config.constant.ts`)

- Registered-office addresses for Twenty.com SAS and Twenty, Inc.
- US deployment governing law (the template only specifies France).
- DPO name and the Twenty pre-signed authorized signatory name/title.

## Out of scope (flagged per spec)

Intra-group legal agreement and any Stripe/billing-entity changes. A
future e-sign provider would plug in at `DpaService.generateSignedDpa` +
the signatory input.

> Draft until the integration test passes in CI and the legal
`TODO_CONFIRM` values are supplied.

https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22243?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-06-27 17:46:38 +02:00
Félix Malfait 2a21eb46c0 perf: cap to-many relation records per parent and inline chips in table (#22206)
## Problem

Record table views that show a to-many relation column (e.g. Workflows
with a "Runs" column) get slow and janky to scroll when some records
have many related records.

Two root causes, found by profiling the page live:

1. **Backend over-fetch + unfairness.** Nested one-to-many relations
were loaded with a single flat limit of `QUERY_MAX_RECORDS_FROM_RELATION
* parentCount` shared across *all* parents in the page (`WHERE
parentColumn IN (ids) LIMIT 60*N`, no per-parent cap). A single hot
parent can consume the entire budget — returning thousands of rows for
one cell, and potentially starving sibling parents of records they
actually have. The `limit * parentCount` shape shows the original intent
*was* a per-parent budget; it was just implemented as a global limit.

2. **Frontend DOM explosion.** `ExpandableList` mounts the *entire*
child array inline (clipped with `overflow: hidden`) when unfocused, and
mounts all children for measurement when focused. A cell with 2,000+
relation chips mounts ~14k DOM nodes — one observed page reached ~55k
nodes for 43 rows, producing 100–300 ms main-thread long tasks on every
scroll.

## Fix

- **Backend:** load one-to-many relations with a true **per-parent** cap
via a `LATERAL` join — each parent runs its own indexed, `LIMIT`-ed scan
that stops after the per-parent budget. This is `O(perParentLimit ×
parentCount)` and never reads or sorts a parent's full relation set. The
per-parent query is built through the workspace query builder (so it
stays schema-qualified and keeps the soft-delete predicate) and wrapped
as a `FROM` subquery; read/row-level permissions are enforced when
records are hydrated by id, as elsewhere in the relation loader.
Many-to-one is unchanged.
- **Frontend:** add an opt-in `maxInlineCount` to `ExpandableList` so
to-many relation cells mount only a small inline preview; the expand
dropdown still renders the full fetched set. Fully backward compatible
(no cap → identical behavior).

## Why LATERAL over a window function

A windowed `ROW_NUMBER() OVER (PARTITION BY parent) <= limit` is correct
and fair too, but a window function **cannot stop early within a
partition** — it must read every matching row (and sort it). Measured on
skewed data (one parent with ~4k children, on the existing single-column
join index, PG16):

| Approach | Time | Buffers | Rows read from the hot partition |
|---|---|---|---|
| Pre-PR (`LIMIT 60×N`) | 1.6 ms | 91 | ~180 total, early-stops, but
**unfair** (starves siblings) |
| Window (`ROW_NUMBER`) | 3.7 ms | 128 | **all ~4k + sort** |
| **LATERAL (`per-parent LIMIT`)** | **0.5 ms** | **57** | **~60, index
early-stop** |

LATERAL matches the pre-PR read cost while being fair, needs no new
index, and scales independently of how large any single relation is.

## Verification

- Backend integration test (`nested-relation-per-parent-limit`): a
parent with 65 children is capped at 60 while a sibling with 3 keeps all
3 — passes.
- `EXPLAIN ANALYZE` on the generated SQL: Index Scan with the `LIMIT`
pushed into the per-parent lateral (early-stop).
- Frontend unit test for the `ExpandableList` cap.
- Manual check on a table cell with 40 related records: exactly 10 chips
mount inline (down from 40), no console errors, chips still clickable
and the overflow count reflects the true total.
2026-06-27 15:12:43 +02:00
Félix Malfait 0e22ae0521 feat: create calendar events on Google and Microsoft accounts (#22231)
## Context

Twenty can import calendar events and send emails, but cannot create
calendar events. This adds calendar event creation on connected
**Google** and **Microsoft** accounts, mirroring the existing email-send
architecture (`message-outbound-manager`).

## What it adds

The capability is exposed three ways, all backed by the same composer →
driver → persist pipeline:

- **GraphQL mutation** `createCalendarEvent` (metadata API)
- **AI agent tool** `create_calendar_event` (flows to MCP
automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission
flag
- **Workflow builder node** "Create Calendar Event" in the **Core**
section, with a full settings form (variable interpolation supported)

CalDAV/IMAP is intentionally out of scope for now (different long pole).

## Design notes

- **Reuse over reinvention** — the created event is run through the
existing inbound formatters (`formatGoogleCalendarEvents` /
`formatMicrosoftCalendarEvents`) and persisted immediately via the
existing `CalendarSaveEventsService`, so it appears in Twenty right away
and is reconciled by the next provider sync (dedup on external id).
Persistence is best-effort.
- **OAuth scopes** — Google already requests `calendar.events`
(read+write), so no change there. Microsoft moves `Calendars.Read` →
`Calendars.ReadWrite`; existing Microsoft accounts must re-consent
(surfaced as a clear "reconnect" error via a missing-scope check).
- **Deliberate invitation semantics** — `sendInvitations` is off by
default. When off, the event is created with **no attendees** on either
provider, so creating an event never silently emails external people.
When on, attendees are attached and notified (Google `sendUpdates: all`,
Microsoft's default). This sidesteps Microsoft Graph having no
per-request suppression.
- **Timezone correctness** — Microsoft Graph interprets `dateTime` as
wall-clock in the supplied `timeZone` and ignores the offset, so the
absolute instant is converted to its wall-clock form before sending
(Google honors the offset directly). Both providers end up scheduling
the same instant.
- **Conferencing** — optional Google Meet
(`conferenceData.createRequest`, with a follow-up `events.get` to
resolve the async link) / Microsoft Teams (`isOnlineMeeting`).
- Attendees are a comma-separated string everywhere (tool input, GraphQL
DTO, workflow input), consistent with `send_email` recipients; the
composer parses to its internal list.

## Test plan

- **Unit**: 45 tests covering the composer (validation, all-day
boundaries, offset enforcement, timezone, scope checks, default-account
resolution), both provider drivers, the dispatcher, and the workflow
step-log builder.
- **Integration**: `createCalendarEvent` on the `/metadata` API fails
closed with a structured error for a non-existent account (the
auth/ownership/validation path that doesn't require provider mocking).
- **Manual**: verified the workflow node appears in the Core section,
the settings form renders and round-trips (edit → autosave → reload),
and the live mutation returns a structured failure for a bogus account.

## Open question for reviewers

The metadata mutation `createCalendarEvent` shares a name with the core
schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for
the CalendarEvent object — they live on different endpoints (`/metadata`
vs `/graphql`) so there's no runtime conflict, but it's a potential
point of confusion for API consumers. Happy to rename (e.g.
`createCalendarEventOnConnectedAccount`) if preferred.

## Out of scope / follow-ups

- CalDAV/IMAP support
- Event update/delete and recurrence
- Existing Microsoft accounts need re-consent for the widened scope


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?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-06-27 14:05:58 +02:00
Etienne 3525187321 fix(ai) - fixes (#22227)
- ai chat author fix (before : "workflow", after : "user")
- https://discord.com/channels/1130383047699738754/1496872385687584768

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22227?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-06-26 17:53:29 +00:00
martmull e747bc3e42 Add backfill installation feature for pre-installed apps (#22199)
## After

<img width="1070" height="514" alt="image"
src="https://github.com/user-attachments/assets/9cbd2ff5-1678-4c2f-84da-074568f37c51"
/>


## Summary
Adds the ability to backfill application installations across all
existing workspaces. This allows admins to retroactively install a
pre-registered application on every active and suspended workspace
through a background job, making the feature idempotent and
non-blocking.

## Key Changes

- **Backend Service**: Added `backfillApplicationOnAllWorkspaces()`
method to `PreInstalledAppsService` that:
  - Validates the application registration exists
  - Iterates through all workspaces using `WorkspaceIteratorService`
  - Installs the app on each workspace
  - Swallows `APP_ALREADY_INSTALLED` errors for idempotency
  - Logs success/failure counts

- **Background Job**: Created `BackfillApplicationInstallationJob` to
process backfill requests asynchronously via the message queue

- **GraphQL Mutation**: Added `backfillApplicationInstallation` mutation
to `AdminPanelResolver` that:
  - Validates the application registration exists
  - Enqueues the background job
  - Returns immediately without blocking the request

- **UI Components**: Enhanced
`SettingsAdminApplicationRegistrationGeneralToggles` with:
- New "Pre-install on new workspaces" toggle for the `isPreInstalled`
flag
  - "Backfill on all workspaces" button with confirmation modal
  - Loading state and success/error snack bar feedback

- **Data Model**: Added `isPreInstalled` field to
`UpdateApplicationRegistrationPayload` input type

- **Tests**: Added comprehensive unit tests for
`PreInstalledAppsService.backfillApplicationOnAllWorkspaces()` covering:
  - Missing registration validation
  - Successful multi-workspace installation
  - Idempotent handling of already-installed errors
  - Proper error propagation for unexpected failures

## Implementation Details

The backfill operation is designed to be:
- **Idempotent**: Already-installed apps are skipped without error
- **Non-blocking**: Runs as a background job via message queue
- **Resilient**: Per-workspace failures don't block other installations
- **Observable**: Logs aggregated success/failure counts for monitoring


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22199?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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-06-26 16:21:27 +00:00
Raphaël Bosi c891258f34 Add v2 onboarding create profile page (#22221)
<img width="3024" height="1498" alt="CleanShot 2026-06-26 at 15 30
23@2x"
src="https://github.com/user-attachments/assets/8b4863a9-66ed-4da1-851b-473cedf71511"
/>

<img width="3022" height="1500" alt="CleanShot 2026-06-26 at 15 29
43@2x"
src="https://github.com/user-attachments/assets/22fc0e94-f670-4638-975c-f06b2b2e25e8"
/>

Adds the v2 onboarding **Create profile** page, shown right after the
import-contacts step (`PROFILE_CREATION`) for the onboarding-v2 cohort.
It renders full-screen under `BlankLayout` via the shared
`OnboardingV2Layout`, matching the Figma (340px column, inline round
avatar uploader + First/Last row, Job Title, dark Continue). The v1
modal flow is untouched and still used for non-v2 users.

Job Title is wired end-to-end: it adds a real `jobTitle` field to the
`WorkspaceMember` standard object (shared metadata constant + flat field
metadata + entity property) and a `2-17` workspace upgrade command to
backfill the field on existing workspaces. Continue persists name +
jobTitle through the existing `updateWorkspaceMemberSettings` mutation,
whose allow-list picks up the new standard field automatically.

Routing mirrors `SyncEmailsV2`: new `AppPath.CreateProfileV2`, lazy
route, and an `isOnboardingV2`-gated branch in
`usePageChangeEffectNavigateLocation` (+ tests and a Storybook story).

Reviewer notes:
- `jobTitle` is **write-only** for now (no read-back path: core
DTO/transpiler/fragment unchanged), and the field is
`isSystem`/non-UI-editable to match its siblings. Easy to surface later
if wanted.
- New `OnboardingProfilePictureUploader` is a compact round avatar
uploader reusing the same upload mutation flow as
`WorkspaceMemberPictureUploader`.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22221?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-06-26 17:34:44 +02:00
Thomas des Francs b84f748237 Fix navigation opened section spacing (#22222)
## Summary

- Remove the collapsed empty `Opened` sidebar section when no opened
object exists.
- Restore the first navigation section top alignment with the settings
`User` section.

## Before/After

Visual verification after the fix: in the current fixture data, the
first visible main sidebar section (`Favorites`) starts at y=92,
matching the settings `User` section at y=92. This confirms there is no
collapsed `Opened` spacer pushing the main drawer content down.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22222?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-06-26 15:58:57 +02:00
Félix Malfait da6a2ee300 fix(ai-chat): keep streams alive on silent SSE death + make the stream job idempotent (#22201)
## Problem

In production, an AI-chat assistant response sometimes freezes
mid-stream (partial text, looks hung), then "picks up again on its own"
later without the user resending and without a known worker restart.

Root cause: the **agent-chat SSE subscription has no keepalive and no
silent-death detection**.

- Delivery is fire-and-forget Redis pub/sub
(`SubscriptionService.publishToAgentChat`) and the resolver returns the
**raw** iterator — unlike `EventStreamResolver`, which heartbeats every
30s via `wrapAsyncIteratorWithLifecycle`.
- During a quiet model/tool gap the connection sends no bytes, so a
proxy/LB/NAT can silently drop it mid-stream. `graphql-sse` neither
surfaces an error nor resumes with `Last-Event-ID`, and **nothing
re-pulls the existing Redis chunk catch-up on reconnect** (it only runs
on thread (re)mount / `message-persisted` refetch).
- So the live view freezes; recovery only happens when the terminal
`message-persisted` fires a full refetch from the DB — the observed
"self-recovery".

This is the **same silent-SSE-death class fixed for the DB event stream
in #21061**, which was never applied to the agent-chat path. The symptom
also matches #21096 (worker logs the job finishing, client never
updates, reload shows the message).

It is **not** queue prioritization, and it is **not** addressed by
#22193 (which only stabilizes the assistant message id and removes
end-of-stream flicker).

A secondary, independent self-recovery path also existed: BullMQ
stalled-job re-run (default 30s `lockDuration`, no idempotency guard)
re-streaming the whole turn → duplicate assistant messages / double
billing.

## Changes

### Commit 1 — keepalive + silent-death recovery (ports the #21061
pattern to agent chat)
- **Shared:** new `keepalive` variant on `AgentChatSubscriptionEvent`.
- **Server:** wrap the agent-chat subscription iterator with
`wrapAsyncIteratorWithLifecycle` — emit a `keepalive` on connect and
every `APPLICATION_KEEPALIVE_INTERVAL_MS` (30s) so the connection keeps
flushing bytes and a dead connection becomes detectable.
- **Client:** track the last received event timestamp (refreshed on
every chunk/keepalive in the SSE `next` sink); new
`AgentChatStreamKeepAliveEffect` forces a resubscribe + messages refetch
after 90s of silence, so the durable Redis chunk list backfills the gap
(`firstLiveSeq` is reset on resubscribe).

### Commit 2 — stream-job idempotency + lockDuration
- Thread a `lockDuration` option through `MessageQueueWorkerOptions` +
the BullMQ driver; set `aiStreamQueue` to 10 min so long streams aren't
falsely stalled.
- Guard `StreamAgentChatJob.handle` with a `streamId`-scoped Redis lock
(`SET NX PX` + compare-and-delete release) so a stalled re-run is
skipped instead of double-processing.

## Verification

⚠️ I could **not run typecheck/lint locally** — `yarn install` could not
complete in this environment (transient registry network aborts before
the link step, so `node_modules` never populated). **Please rely on CI
for type/lint verification.** The changes are written to match existing
conventions; the points most worth a reviewer's eye are the resolver's
iterator typing and the ioredis `set(..., 'PX', ttl, 'NX')` overload.

How to confirm the root cause in prod: a frozen client with the worker
logging `StreamAgentChatJob processed in …ms` and no `[AI_CHAT_NO_TEXT]`
is the silent-death signature (check reverse-proxy idle/buffering). For
the secondary path, watch `aiStreamQueue` `stalled`/re-processed metrics
and duplicate turns around worker restarts.

## Notes / trade-offs
- The 10-min `lockDuration` means a genuinely crashed worker's job isn't
reclaimed for up to 10 min; the client-side keepalive/catch-up recovers
the view independently, and the idempotency lock prevents duplicates.
Faster dead-worker recovery could be a follow-up.
- Touches `useAgentChatSubscription.ts` / `AgentChatRuntimeEffects.tsx`
/ `stream-agent-chat.job.ts`, which #22193 also touches — trivial rebase
expected.

Opened as **draft** pending CI.

https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22201?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-06-26 15:19:34 +02:00
Raphaël Bosi 8f7d6c24dd Add v2 onboarding import contacts page and unify the onboarding v2 shell (#22212)
<img width="3024" height="1668" alt="CleanShot 2026-06-26 at 13 29
28@2x"
src="https://github.com/user-attachments/assets/9bdd0029-45eb-4ddb-859f-eaab9bb61406"
/>

Adds the new v2 onboarding **Import contacts** step (email + calendar
import), shown right after workspace creation in the v2 flow. The
presentational page was designed in a previous PR; this wires it in and
unifies the shell.

**What changed**
- Reuses and unifies the existing v2 onboarding shell: extracts
`OnboardingV2Layout` + `OnboardingV2Header` (the back + logo header, now
with the free-credits pill), and the `SignInUpV2` workspace-creation
step renders through it (old `SignInUpV2Header` removed).
- New `SyncEmailsV2` route (`/sync/emails-v2`) under `BlankLayout`,
wired to the same OAuth/skip hooks as v1 `SyncEmails`.
- The `SYNC_EMAIL` step routes to the new page only when
`isOnboardingV2` is set (mirrors the existing `WorkspaceActivation` →
`WorkspaceActivationV2` branch); the v1 modal is unchanged for the
non-v2 flow.
- No backend changes — reuses the `SYNC_EMAIL` status and
`skipSyncEmailOnboardingStep` mutation.

**Reviewer notes**
- Connect defaults to `METADATA` (private) visibility to match the "Only
you will be able to see your emails and events" note (v1 had a selector
defaulting to `SHARE_EVERYTHING`).
- The header free-credits pill shows `0` for now (no current-workspace
credits source on the frontend yet).
- The back button is hidden on the import page (no meaningful "back"
after workspace creation); unchanged on the workspace-creation step.
2026-06-26 12:54:52 +00:00
Weiko ebe067f65c Fix favorite showing non-readable objects (#22217)
## Context
Navigation menu items backed by objects the user has no read permission
on were correctly hidden from the Workspace section, but Favorites still
showed them. Favorites are user-scoped nav items (tied to
workspaceMemberId), and FavoritesSection only filtered out folder
children (!item.folderId) without ever checking canReadObjectRecords.

The shared upstream filter (filterAndSortNavigationMenuItems)
intentionally does not apply read permissions, because its output also
drives drag-and-drop position math and layout-customization/edit mode,
which need the complete, unfiltered list. So read-permission filtering
belongs at the display layer.

## Fix
New shared hook useReadableNavigationMenuItems that centralizes the
read-permission filtering logic previously duplicated across sections:
wires up objectMetadataItems + views + object permissions around
isNavigationMenuItemReadable
filters folder children and top-level items (dropping folders whose
children are all unreadable)
exposes both raw filtered* outputs and
isLayoutCustomizationModeEnabled-aware display* outputs
FavoritesSection now applies the filter via the hook, so unreadable
favorites are hidden — while still showing everything in
layout-customization mode (consistent with the Workspace section).
WorkspaceSectionContainer refactored to consume the same hook, removing
its inline isItemReadable, the dual-map reduce, and inline filtering.

## Before
### With access
<img width="842" height="570" alt="Screenshot 2026-06-25 at 14 01 38"
src="https://github.com/user-attachments/assets/ae51f3c8-c178-4162-84ce-3fe49cf07987"
/>

### Without access
<img width="987" height="590" alt="Screenshot 2026-06-25 at 14 02 08"
src="https://github.com/user-attachments/assets/94dacea3-bd77-4fcb-868d-353ed513b28c"
/>

## After
### Without access
<img width="1001" height="615" alt="Screenshot 2026-06-25 at 14 02 46"
src="https://github.com/user-attachments/assets/e1ccd5d9-8583-4ff1-ab91-6f6d187925c5"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22217?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-06-26 11:55:09 +00:00
Thomas des Francs fdf9f543ae Fix rounded page layout tab edit outline (#22214)
## Summary
- Round the edited page layout tab outline to match the tab hover
radius.

## Test plan
- `npx oxfmt --check
packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx`
- `npx oxlint --type-aware -c packages/twenty-front/.oxlintrc.json
packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx`
- `npx nx run twenty-front:lint`
- Browser: verified the edited `Notes` tab outline before/after locally.

## Visual
<img width="1108" height="392" alt="clipboard"
src="https://github.com/user-attachments/assets/1cb63180-83a9-4ef8-9c55-2b475eaaa02b"
/>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22214?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-06-26 10:45:51 +00:00
Thomas des Francs bea7857a1c Fix settings page root overflow (#22207)
## Summary

- Clamp the fixed app shell with `overflow: hidden` so long nested
settings scroll content no longer contributes to the document root
scroll range.
- Keeps the object permission page scrolling inside its existing
settings `ScrollWrapper` instead of letting the whole page slide under
the viewport.

## Screenshots

Before: root document can scroll under the bottom of the viewport and
exposes the gray app background.

![Before root
scroll](https://raw.githubusercontent.com/twentyhq/twenty/bonapara/pr-screenshots-settings-root-scroll-20260626/.github/pr-screenshots/fix-settings-root-scroll/before-object-permission-root-scroll.png)

After: the same root scroll attempt leaves the app shell fixed to the
viewport.

![After root
scroll](https://raw.githubusercontent.com/twentyhq/twenty/bonapara/pr-screenshots-settings-root-scroll-20260626/.github/pr-screenshots/fix-settings-root-scroll/after-object-permission-root-scroll.png)

## Browser Validation

Route tested:
`/settings/members/roles/78fa69cb-1237-43b5-bfa5-5a11a47bf781/object/5ab1a16b-7811-471f-ac53-940666c667dd`

- Before on `http://apple.localhost:3001`: `window.scrollTo(0, 9999)`
moved the root to `scrollY=244.5`; `htmlScrollHeight=1287`,
`htmlClientHeight=1043`.
- After on `http://apple.localhost:3002`: the same root scroll attempt
stayed at `scrollY=0`; `htmlScrollHeight=1043`, `htmlClientHeight=1043`.
- The settings content still scrolls internally: wrapper
`scrollHeight=1475`, `clientHeight=955`.

## Checks

- `git diff --check`
- `yarn oxfmt --check
packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx`
- `cd packages/twenty-front && npx oxlint --type-aware -c .oxlintrc.json
src/modules/ui/layout/page/components/DefaultLayout.tsx`


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22207?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-06-26 11:34:19 +02:00
Thomas des Francs 34dc681c0e Use settings icon in settings drawer tab (#22204)
## Summary

- Let the shared navigation drawer tab row accept a custom icon and
accessible label for its navigation tab.
- Use the Settings icon and Settings label when that tab row is rendered
inside the settings drawer.
- Keep the main navigation drawer defaulting to the Home icon.

## Screenshots

### Before

<img width="420" alt="Before: settings drawer navigation tab uses the
Home icon"
src="https://github.com/user-attachments/assets/03b684ba-0f62-4a96-a9f1-8c6138efd9cf"
/>

### After

<img width="420" alt="After: settings drawer navigation tab uses the
Settings icon"
src="https://github.com/user-attachments/assets/3e5f4669-ed7c-4355-be92-c9cfbf160959"
/>

## Validation

- `yarn oxfmt --check
packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx
packages/twenty-front/src/modules/navigation/components/SettingsNavigationDrawer.tsx`
- `git diff --check`
- `yarn nx typecheck twenty-front`

Note: `oxlint` could not run locally because the installed dependencies
are missing the native `@oxlint/binding-darwin-*` optional package.
2026-06-26 09:14:30 +00:00
Raphaël Bosi cb49a7a053 Add v2 onboarding loading screen while creating workspace (#22152)
https://github.com/user-attachments/assets/cc7b1d10-7495-4f21-9311-4c22c0f14771

Adds the full-screen loading screen shown while a new workspace is being
created in the v2 sign-up flow (`SignInUpV2`), building on the v2
"Create your workspace" step.

How it works:
- Submitting the v2 create-workspace form marks the flow as v2
(`isOnboardingV2State`) and creates the workspace. The flag is carried
across the cross-subdomain redirect with an `onboardingV2=true` URL
param, so v2 users land on a new `/workspace-activation-v2` route
instead of v1's `/workspace-activation`.
- `WorkspaceActivationV2` runs the real `activateWorkspace` mutation on
mount and renders the loader: a pulsing Twenty logomark above a stack of
status messages that shift up one at a time, cycling once per second.
There is no faked/minimum duration; it advances to the next onboarding
step as soon as the workspace is activated.
- On activation failure it shows a "Workspace creation failed" screen
with a Retry button.

v1 onboarding is unchanged. Storybook:
`Modules/Auth/SignInUpWorkspaceActivationV2`.

Note: The flashes will be fixed in later PRs

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22152?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-06-26 08:46:34 +00:00
Akash! ce4d0f3447 fix: hide "Create Workspace" button when multi-workspace is disabled (#22202)
## Description

This PR fixes a bug where the "Create Workspace" button was
unconditionally rendered in the workspace switcher dropdown, even when
single-workspace mode was active (`IS_MULTIWORKSPACE_ENABLED=false`).

This created a confusing "dead-end" action for users, as clicking the
button would do nothing (because the backend correctly blocks workspace
creation in this mode, and the frontend skips the redirect).

### Changes made
- Imported the `isMultiWorkspaceEnabledState` atom from client-config.
- Evaluated `isMultiWorkspaceEnabled` inside
`MultiWorkspaceDropdownDefaultComponents`.
- Conditionally rendered the "Create Workspace" `<MenuItem>` only if
multi-workspace is enabled.

Closes #22139


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22202?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-06-26 10:42:10 +02:00
Etienne b625bd1995 fix(ai-chat) - improvements (#22193)
- remove flickering at assistant message streamed end
- add copy code
- leave chat history when navigating to settings

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22193?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-06-26 10:26:40 +02:00