Commit Graph

6374 Commits

Author SHA1 Message Date
Thomas Trompette 48f4c1661b fix(sse): resync records on reconnect and recover from silent query listener errors (#23357)
## Context

Live updates stop working in prod in a way that only a page refresh
fixes. Two independent holes in the SSE self-healing path, both silent.

## 1. Reconnecting restored the stream but never the data

Events emitted while the stream was down are not replayed, so any record
change during the gap stayed missing from the UI indefinitely. On
reconnect `SSEClientEffect` called `resyncMetadataStore()` and
dispatched `SSE_CLIENT_RECONNECTED_EVENT_NAME`, but the only listeners
were `AgentChatMessagesFetchEffect` and
`AgentChatStreamKeepAliveEffect`. No record surface listened. Metadata
self-healed, records did not.

This fires on every deploy, laptop sleep and network blip, and the
reconnect backoff is a uniform random draw up to 2 minutes, so the gap
is routinely long.

The event was also incomplete. It was dispatched on graphql-sse
transport reconnects only. When the keep-alive watchdog or an error set
`shouldDestroyEventStream` and `SSEEventStreamEffect` built a
replacement stream, nothing was dispatched at all.
`useTriggerEventStreamCreation` now dispatches it from the creation path
too, for every stream that replaces an earlier one in the tab.

Each surface is wired to the resync path it already uses, through an
optional `onSseReconnected` on `useListenToEventsForQuery`. That hook is
the single funnel every SSE subscriber already goes through, so the tab
reloads exactly what it declared an interest in and nothing else:

- Record table: reset virtualization, plus
`useRefetchAggregateQueriesForObjectMetadataItem` for the header count,
which is served by a separate aggregate query that the row reset does
not touch.
- Record board: `triggerRecordBoardInitialQuery({ shouldResetScroll:
false })`. Scroll position preserved.
- Workflow versions: `shouldWorkflowRefetchRequest`.

These are the same resets each component already runs on every record
event, so the only new thing is the trigger. `SSEClientEffect` keeps the
metadata store resync, which is genuinely global; that also fixes the
matching gap on the metadata side, since `resyncMetadataStore()` used to
be called straight from the graphql-sse `connected` callback and so
never ran for a watchdog- or error-driven stream re-creation.

**Known gap:** the record show page and workflow run detail are not
covered. Both read through `useFindOneRecord`, but their subscription
lives in a sibling component with no access to `refetch`. Wiring them
needs either `refetch` exposed from `RecordShowEffect` /
`useWorkflowRun`, or the subscription moved into the data owner — the
latter changes subscription lifetime, and `useListenToEventsForQuery`
unregisters by `queryId` on unmount regardless of other consumers. Left
out pending a decision.

## 2. A network error on `addQueryToEventStream` silently unsubscribed a
view forever

`handleError` in `SSEQuerySubscribeEffect` only reacted to
`CombinedGraphQLErrors`. On Apollo Client v4 a network failure or a 5xx
from a rolling pod surfaces as `ServerError` or a plain `Error`, so the
handler was a complete no-op: no Sentry capture, no stream teardown, and
`syncAdditions` returned before recording the listener as active.

Since neither `requiredQueryListeners` nor `activeQueryListeners`
changed, the driving effect never re-ran. That query stayed unregistered
server-side for the rest of the session while the stream looked healthy
and every other view kept updating live. Recovery required remounting
the component or refreshing.

The recovery now runs for every error type.
`getGraphqlErrorExtensionsFromError` is called unconditionally: it
accepts `unknown` and reads `extensions` off any object-shaped error, so
an error carrying a gracefully-handled `code` is still recognised as one
whether or not it is a `CombinedGraphQLErrors`.

Note: errors without extensions now reach Sentry, since
`isGracefullyHandledEventStreamError` returns false for them. That is
new noise during outages, but this failure class is currently completely
invisible.

## Testing

- `nx typecheck twenty-front` and oxlint `--type-aware` + oxfmt pass.
- Verified on a local instance with an A/B/A run. A row inserted
straight into Postgres emits no SSE event, which is exactly the state
after a disconnect; restarting the server then forces a reconnect. With
the fix the row appears and the count updates with no page reload;
reverted to `main` it stays invisible indefinitely. Redis confirmed the
stream had reconnected and re-registered its queries in the negative
run, so that result is the missing resync rather than a dead stream.
- Fix 2 is **not** exercised at runtime — it needs a network-level
failure on the `addQueryToEventStream` mutation specifically. Reasoned
about only.
- There are no existing tests for the `sse-db-event` module.

## Known gap

A tab's very first stream is not treated as a reconnection, since
`isRecreatedEventStream` is derived from
`lastSseEventReceivedTimestampState` already being set. If that first
stream connects but never receives its first message and is then
replaced, no resync is dispatched. That is the separate issue of
`SSEKeepAliveEffect` being gated on `sseEventStreamReady`, which is
itself only set by the first message: a stream that never becomes ready
is never watched and never torn down. Not addressed here.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23357?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-27 16:21:08 +00:00
Raphaël Bosi 3c48e27b2e Fix stuck onboarding route on failed chunk preload (#23359)
Fixes [Sentry 7604159654](https://sentry.io/issues/7604159654/)
(v2.20.0, Mobile Safari). The onboarding router preloads 7 lazy chunks
on entry; Vite's CSS preload for SyncEmails rejected and three defects
compounded:

- `void SomePage.preload()` discarded the promise, so it became an
unhandled rejection and the user got a raw `Unable to preload CSS for
/assets/...css` snackbar.
- `lazyWithPreload` cached the *rejected* promise and rendered via
`throw preload()`. React pings on the rejection, re-renders, the
component throws the same settled rejected thenable, the ping listener
de-dupes, and the route hangs on its loader forever.
- `checkIfItsAViteStaleChunkLazyLoadingError` only matched Chrome's
message, so `AppErrorBoundary`'s reload recovery never fired for the
CSS-preload or Safari variants.

`lazyWithPreload` now records the failure in state instead of
rethrowing, so the thenable thrown into Suspense always fulfills,
`preload()` returns void and can never reject, and the render path
throws the real `Error` to the boundary, which reloads.

Two things worth knowing for review: `React.lazy` is not a substitute
here (its initializer has no synchronous fast path, so it suspends even
when the module is already loaded, reintroducing the loader flash #22392
removed), and the failure is deliberately sticky because Vite marks the
dep `seen` before attempting it, so an in-document retry loads the JS
without its CSS and silently renders an unstyled page.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23359?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-27 14:47:58 +00:00
Raphaël Bosi 590ae069e8 Support workspace member Me filter for relation fields in dashboards (#23282)
<img width="3024" height="1484" alt="CleanShot 2026-07-27 at 15 04
22@2x"
src="https://github.com/user-attachments/assets/a57797b7-dbe9-4748-aeff-18667f1f69bb"
/>


Fixes #20225

Workspace member "Me" filters worked for standard actor fields (Created
by / Updated by) in dashboard widgets but not for relation fields
pointing to a workspace member (e.g. "Account owner"). In the
advanced-filter UI, picking such a relation forced a relation traversal
and never produced a filter you could set to "Me".

For a many-to-one relation targeting workspaceMember, the
relation-target sub-menu now offers a "filter by record" entry that
creates a direct relation filter with the same "Me" multi-select picker
used by view filters. Traversal (e.g. "Account owner -> Name") is
preserved. No backend change is needed: the stored value matches view
filters and is already resolved server-side.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23282?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-27 13:51:05 +00:00
github-actions[bot] dbbad1bffa i18n - translations (#23367)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-27 15:30:31 +02:00
Raphaël Bosi 2899058b5f Warn users before front components navigate to an external site (#23270)
https://github.com/user-attachments/assets/af3fb042-d066-4e0c-9348-f86ea92a6fcd



Front component anchors render a real host `<a>`, so clicking a link to
another domain performed an uncontrolled full-page navigation. This adds
a phishing-resistant "you're leaving Twenty" confirmation modal before
navigating to an external origin (Fixes
[#23260](https://github.com/twentyhq/twenty/issues/23260)).

The renderer intercepts external anchor clicks in
`createHtmlHostWrapper` and hands the destination to a host callback via
context; twenty-front owns the modal (reuses `ConfirmationModal`) and a
per-application list of trusted origins persisted in localStorage. A
"Don't ask again for this site" checkbox (checked by default) skips the
modal next time for that app.

Scope is external cross-origin http(s) links only; same-origin links
keep native behavior. External links always open in a new tab, so a
component can never navigate the Twenty tab away, even once its origin
is trusted. The modal is rendered by the trusted host, so components
cannot style or suppress it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23270?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-27 13:21:38 +00:00
Raphaël Bosi 56245a35af Stop leaking the refresh token in the social SSO redirect URL (#23061)
The Google/Microsoft callback for a sign-in with no target workspace
redirected to `/sign-in-up?tokenPair={...}`, putting a 60-day refresh
token in a query string. Those persist in browser history, `Referer`
headers and access logs.

It now carries a single-use, 5-minute opaque token in the URL fragment,
which the frontend exchanges over POST. Browsers never send the fragment
on the wire, so the token stays out of access logs, proxies and
`Referer` headers entirely. Redemption claims the row with a `DELETE`
guarded on `revokedAt`/`deletedAt` being null, so concurrent requests
cannot each mint a refresh token and a revoked token cannot redeem.
Enterprise SSO (OIDC/SAML) already used a POST exchange and is
unchanged.

```mermaid
sequenceDiagram
  participant Browser
  participant Server
  participant DB

  Note over Browser,Server: before, the redirect carried access + 60-day refresh in ?tokenPair
  Browser->>Server: GET /auth/google/redirect
  Server->>DB: store sha256(token), expires in 5 min
  Server-->>Browser: 302 /sign-in-up#ssoExchangeToken=opaque
  Note over Browser: fragment never sent back to any server
  Browser->>Server: POST getAuthTokensFromSSOExchangeToken
  Server->>DB: guarded DELETE, single-use claim
  Server-->>Browser: access + refresh token, in the response body
```

Since the token is single-use, the refresh token is minted at redemption
instead of at callback, so an abandoned redirect leaves an inert expired
hash rather than a live credential.

Redemption lives in its own `SignInUpSSOExchangeTokenEffect` +
`useRedeemSSOExchangeToken`, mirroring the existing
`VerifyLoginTokenEffect` + `useVerifyLogin` pair, so
`SignInUpGlobalScopeFormEffect` only loses the vulnerable branch. Like
`useVerifyLogin`, the hook clears any stale token pair before
exchanging. The effect reads `window.location.hash` live and strips it
synchronously, which doubles as the StrictMode double-invocation latch.

Remaining exposure is the browser itself (history until the synchronous
strip, client-side scripts), same as any fragment-based OAuth response.
`loginToken` on the workspace-targeted branch still travels as
`/verify?loginToken=` and is replayable for 15 minutes; moving it to the
fragment too is a separate change.

A fast instance command adds a unique partial index on `("type",
"value")` for live SSO exchange tokens, so redemption is an index lookup
instead of a full scan of the shared token table and at most one row can
ever match.
2026-07-27 12:58:42 +00:00
Shinu Cherian b81ca99162 fix(twenty-front): hide layout editor UI when SystemPermissionFlag.LAYOUTS is missing (#23303) (#23343)
## Description
Fixes #23303.

This PR ensures that the layout editor UI and customization entry points
are hidden and protected when a user lacks the
`SystemPermissionFlag.LAYOUTS` permission flag.

### Changes Made:
1. **`useEnterLayoutCustomizationMode.ts`**: Added
`useHasPermissionFlag(PermissionFlagType.LAYOUTS)` check inside
`enterLayoutCustomizationMode` to return `false` and prevent entering
customization mode if the user lacks permission.
2. **`WorkspaceSection.tsx`**: Updated the sidebar `WorkspaceSection`
component to render the layout edit button (`IconTool`) only when
`hasLayoutsPermission` is `true`.
3. **`ObjectLayout.tsx`**: Disabled customize and reset layout controls
in the Data Model Object Details settings page if the user lacks
`LAYOUTS` permission.
4. **`useEnterLayoutCustomizationMode.test.tsx`**: Added unit tests to
verify that `useEnterLayoutCustomizationMode` correctly guards layout
customization initialization based on permission.

## Testing
- Added unit tests for `useEnterLayoutCustomizationMode` permission
checks.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23343?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-27 12:57:24 +00:00
Thomas Trompette 22a01dc1c6 Fix: password reset link returns FORBIDDEN for logged-in users (#21248) (#23335)
## Problem

Fixes #21248.

After upgrading, workspace members who open a password reset link while
a token pair still exists in local storage get a generic `You do not
have permission to perform this action.` (FORBIDDEN) error, blocking
account recovery.

## Root cause

Every GraphQL request passes through
`GraphQLHydrateRequestFromTokenMiddleware` before any resolver. If a
token is present it validates it; if no token is present it
short-circuits and lets the request through unauthenticated.

The reset flow was only ever designed for the unauthenticated case (the
user is logged out, so no token exists). Two intended changes broke that
assumption:

- A token pair now persists in local storage at reset time (unified
`accessOrWorkspaceAgnosticToken` + tokenPair moved off session cookies
into local storage).
- The Apollo auth link attaches `authorization: Bearer <token>` whenever
any token pair exists, regardless of the operation.

So the public `validatePasswordResetToken` /
`updatePasswordViaResetToken` operations now arrive with a token that
the middleware rejects, producing FORBIDDEN before the resolver runs.
Note: the `PublicEndpointGuard` / `NoPermissionGuard` on these resolvers
both just `return true` — they do not inspect headers and are not the
gate. The middleware is.

## Fix

Add a generic `skipAuthToken` operation-context flag. The auth link
omits the `Authorization` header when a request sets it, staying
agnostic of any specific operation or endpoint. The two public reset
operations opt in at their call site in `PasswordReset.tsx`.

This restores the exact unauthenticated path the flow was designed for,
regardless of whether a token pair happens to sit in local storage.
Nothing is reverted; all authenticated traffic is unaffected.

## Testing

Ran the built frontend against a local backend, logged in so a
`tokenPairState` was present in local storage, then opened a reset link
and inspected the outgoing `ValidatePasswordResetToken` request:

- Request headers: `accept`, `content-type`, `x-locale` only. No
`authorization` header, despite a token pair being present.
- With an invalid token the response is the resolver-level `Token is
invalid` error (it reaches the resolver) instead of the middleware's
FORBIDDEN.
- With a valid token the query succeeds (`validatePasswordResetToken`
returns the email + `hasPassword`) and the Set/Change Password form
renders, so the recovery flow completes.

Lint and typecheck pass on the changed files.
2026-07-27 12:32:51 +00:00
Guillaume Flambard 710d4da4b1 fix(emails): bump @react-email/render to ^2.0.6 to fix empty transactional email bodies (#23323)
## Problem

Fixes #23307. Every transactional email (workspace invite, password
reset,
email verification, etc.) is delivered with an **empty body** — no
title, text,
or CTA.

## Root cause

`twenty-server` pins `@react-email/render` directly at `^1.2.3`:

```jsonc
// packages/twenty-server/package.json
"@react-email/render": "^1.2.3",
```

In 1.2.3, `render()` reads `renderToReadableStream` **before** the email
template's async Suspense boundary (i18n/locale load) has resolved. The
result
is the Suspense fallback marker instead of the real markup:

```html
<!DOCTYPE html ...><!--$!--><template></template><!--/$-->
```

This was fixed upstream in `@react-email/render@2.0.6`
(*"await stream.allReady before reading renderToReadableStream
output"*).
`twenty-emails` already resolves a 2.x render via `react-email@6.5.0`,
so the
server's direct pin was simply stale — the two were out of sync.

## Fix

Bump the direct pin to `^2.0.6` (resolves to `2.1.0`) and regenerate the
lockfile. The server's `render()` imports now use the fixed 2.x.

> Note: a `1.2.3` entry remains in `yarn.lock` — it is an internal
transitive
> pin of `@react-email/components@0.5.3`, not the server render path, so
it is
> expected and harmless.

## Verification

Rendering `SendInviteLinkEmail` through the real `render()` (Node 24)
now
returns full markup (5.7 kB) with no Suspense marker and the resolved
invite
link + workspace content, instead of the empty fallback.

A jest unit test was intentionally not added: `@react-email/render` 2.x
uses a
dynamic import that jest's CJS runtime rejects ("A dynamic import
callback was
invoked without --experimental-vm-modules") — which is exactly why the
existing
email specs mock `render`. The fix was verified with a standalone Node
script.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23323?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-07-27 11:37:09 +02:00
Thomas Trompette 44ed0d5498 Fix phantom targetId field in workflow triggers for morph relations (#23290)
## Problem

In a workflow **record-change trigger** on `noteTarget`, the output
variables offered a `targetId` field that doesn't exist. A morph
relation reaches the frontend as a single field named `target` (the
per-target morph fields are grouped by `morphId`), so the output-schema
generators synthesized its foreign-key column as `` `${field.name}Id` ``
→ `targetId`. But `noteTarget` has no `targetId` column; its FKs are one
per target type: `targetCompanyId`, `targetPersonId`,
`targetOpportunityId`, etc. The phantom `targetId` never matched
anything in the event payload.

## Fix

New helper `getRelationIdFieldNames` returns the actual FK id column(s)
for a relation field:
- normal relation → `[`${name}Id`]`
- morph relation → one column per `morphRelations` target, via the
existing `computeMorphRelationGqlFieldJoinColumnName`
(`targetCompanyId`, `targetPersonId`, ...).

Used by the two output-schema generators:
- `generateRecordEventOutputSchema` — the record-change trigger output
variables (the reported symptom).
- `generateRecordOutputSchema` — record output for
form/find/update-record output schemas.

Scoped strictly to the output schema; no workflow component changes.

## Testing

- Unit tests updated to assert per-target columns instead of the phantom
`targetId` (both generators). 28 passing.
2026-07-27 09:35:06 +00:00
github-actions[bot] 32a031ac0b i18n - translations (#23336)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-27 09:19:30 +02:00
Guillaume Flambard 1e5b8e6db4 fix(front): add accessible labels to icon-only options dropdown triggers (#23325)
## Problem

Refs #23127. Icon-only `LightIconButton` "more options" triggers
(`IconDotsVertical`) render without an accessible name, failing WCAG
4.1.2 (button-name) — screen readers announce nothing for them.

## Fix

Add `aria-label={t`More options`}` to the affected dropdown triggers.
`LightIconButton` already forwards `aria-label` and sets `aria-hidden`
on the icon when a label is present, so this is purely additive — no
behavioral or visual change.

Scoped to a coherent set of options-menu triggers (attachments, public
domains, SSO, connected accounts, field group config). Other unlabeled
icon buttons can follow in separate PRs.

## Verification

`oxlint --type-aware` and `oxfmt` pass on all changed files. The label
is i18n-wrapped via the existing `useLingui` macro already imported in
each component.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23325?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-27 07:10:49 +00:00
martmull 4f9fd6f674 feat(applications): restore the application custom settings tab (#23256)
## Summary

Restores the application **custom settings tab** feature that was
removed in #22156. This reverts that removal so applications can again
expose a custom settings tab via a front component.

## Changes

- Restore the `SettingsApplicationCustomTab` component and its tab
entry/rendering in `SettingsApplicationDetails`.
- `ApplicationManifestMigrationService` syncs
`settingsCustomTabFrontComponent` from application manifests again
(`syncDefaultRoleAndSettingsCustomTab`), resolving the front component
from `settingsCustomTabFrontComponentUniversalIdentifier`.
- Remove the deprecation annotations added by #22156:
- `ApplicationDTO.settingsCustomTabFrontComponentId` (drop GraphQL
`@deprecated`)
-
`ApplicationManifest.settingsCustomTabFrontComponentUniversalIdentifier`
- the `settingsCustomTabFrontComponentId` column comment on
`ApplicationEntity`
- Regenerate the corresponding GraphQL schema/types to drop the
`@deprecated` reason.

The DB column was never dropped, so no schema migration is required.


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23256?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-07-27 06:52:04 +00:00
github-actions[bot] a94f2443b3 i18n - translations (#23328)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-26 22:55:56 +02:00
neo773 3fb29db28a Feat/email settings v2 (#23180)
Settings pages changes

- Add `displayName`
- Unsubscribers Page

<img width="1496" height="844" alt="Screenshot 2026-07-22 at 8 52 15 PM"
src="https://github.com/user-attachments/assets/69bc1993-4547-4a64-83a6-b47fef1a4e40"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23180?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-07-26 22:48:30 +02:00
github-actions[bot] 0b44864f5f i18n - translations (#23315)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-26 12:32:26 +02:00
eeshsaxena 472c7c1edc fix(workspace): open edit panel for PAGE_LAYOUT sidebar items (#23293)
Fixes #22649.

Custom page-layout links in the sidebar (like "Star History") couldn't
be removed. Clicking them in edit mode did nothing.

**Root cause**

`handleNavigationMenuItemClick` in `WorkspaceSection.tsx` switches on
`item.type`. `FOLDER` and `LINK` have explicit cases that call
`openNavigationMenuItemInSidePanel`. `PAGE_LAYOUT` fell through to
`default`, which calls `openViewOrRecordEditPanelAndNavigate`. That
function only opens the side panel when `objectMetadataItem` is defined
- PAGE_LAYOUT items don't have one - so the panel never opened.

**Fix**

Add a `PAGE_LAYOUT` case that calls `openNavigationMenuItemInSidePanel`
directly, using the item's own label and icon. Same pattern as `LINK`.

**How to test**

1. Create a custom page link in the sidebar (Settings > Workspace > Add
menu item > Page layout).
2. Click the wrench icon to enter edit mode.
3. Click the custom page item - the edit side panel should now open.
4. Verify you can remove it from the sidebar.


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

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-26 10:24:07 +00:00
github-actions[bot] 4eb5ad9e32 i18n - translations (#23313)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-26 10:40:10 +02:00
Gueye Papa Djadji 93066ae800 fix(a11y): add aria-label to navigation drawer collapse button (WCAG … (#23287)
…4.1.2)
Fixes #23131

Added `aria-label` to the navigation drawer collapse/expand button 
(LightIconButton with IconLayoutSidebarLeftCollapse/RightCollapse), 
which previously had no accessible name for screen readers.

Verified with axe DevTools scan on localhost — the button-name 
violation for this element no longer appears.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23287?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: Thomas Trompette <thomas.trompette@sfr.fr>
2026-07-26 08:32:02 +00:00
github-actions[bot] 70e1e94e55 i18n - translations (#23288)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 17:11:17 +02:00
Félix Malfait 6cc7ed7570 Make solo tabs first-class: derived presentation, native editing, unified widget header (#23109)
## Why

Full-page record tabs (Timeline, Tasks, Notes, Files, Emails, Calendar,
Flow) were encoded by storing a `CANVAS` layout mode. That made them a
separate species: editing one didn't feel native (no drag handles, no
way to add a second widget, the tab couldn't adapt), and the widget
pipeline was full of `layoutMode === CANVAS` branches.

This PR replaces the stored mode with two derived rules and one unified
header grammar:

> **Presentation is derived from content, never stored.**
> A list tab with exactly **one widget** renders it **solo**
(full-bleed, it owns the tab). Anything else is a **stack** of boxed
cards. **Edit mode always shows the stack structure.**

No widget taxonomy, no per-type branches: any lone widget owns its tab.

## What

**Presentation model**
- `getTabPresentation({ widgets, layoutMode, isInEditMode })`: solo iff
a list tab has exactly one widget in view mode; grid tabs (dashboards)
and edit mode are always stacks. The pinned left panel is always a
column (a surface rule, not a widget rule).
- Solo view rendering is identical to the old CANVAS rendering
(container height, internal scroll).
- Stacked widgets in the main tab area get one bounded slot rule
(`max-height` + own scroll) so no widget swallows the tab;
pinned/side-column stacks keep their flowing behavior. This only binds
on user-composed mixed tabs, which could not exist before.

**Native editing (the point of the PR)**
- Every record-page tab is edited through the same vertical-list editor:
drag handle, reorder, remove, add widget. Add a second widget to a
Timeline tab and it becomes a stack; remove back down to one and it's
solo again. Nothing is stored, nothing to migrate.
- Fixes the stuck-drag bug found while testing the preview: widgets
publishing header info republished a fresh object on every render
(activity cards build their action from non-memoized hook returns), and
since the widget chrome reads that state above the widget content, any
tab with an activity card sat in an infinite render loop. The loop
starved React's transition lane, which dnd-kit's drop teardown waits on,
so the drag clone and drop outlines froze on screen after a drop. The
header hook now republishes only on real value changes and routes
onClick through a stable wrapper, so callers need no memoization. The
page-layout drag provider also disables the Feedback drop animation so
clone cleanup is synchronous at drop time.

**Unified widget header API**
- A widget's content can publish header info to its chrome via
`usePublishWidgetHeaderInfo({ count, primaryAction })`: a count rendered
in grey next to the title, and a primary action (icon button with
accessible name) on the right in view mode. Instance-scoped state keyed
by widget id, so third-party widgets (front components) can use the same
seam later; the hook no-ops outside a page layout (stories, previews)
and is safe to call with inline, non-memoized values.
- A solo widget's header only appears when the widget published
something: the tab label already names it, so a bare title row adds
nothing. Timeline/Flow tabs stay exactly as today.
- Emails, Tasks, Notes, Files, Calendar publish their count (query
totals, not loaded-page lengths) and action (Compose, New task, New
note, Add file) and stop rendering internal title rows ("Inbox 12", "All
5"): exactly one header per widget everywhere, same grammar.
`ComposeEmailButton`, `AddTaskButton` and the title/button plumbing in
`NoteList`/`AttachmentList`/`TaskList` are deleted.

**Object-aware tabs**
- The hardcoded `SYSTEM_OBJECT_TABS` title allowlist is gone. A tab
renders based on whether the target object supports its widgets: widgets
that read through a relation (Tasks, Notes, Files, Timeline) require the
relation field to exist and be active, while Emails and Calendar
aggregate through the messaging timeline, so a missing participants
relation is fine (Company) and a deactivated one is an explicit opt-out.
System objects on the shared default layout keep exactly Home +
Timeline, now by derivation instead of hardcoded titles.

**Data cleanup**
- Seeds (frontend defaults, server standard template, `twenty app`
scaffolder, docs) write `VERTICAL_LIST`;
`PageLayoutTabLayoutMode.CANVAS` is `@deprecated`, kept read-only for
layouts persisted before this change (they render correctly through the
derivation; no data migration, by design: an in-place flip can't pass
the widget-position/tab-layoutMode validator atomically, and it isn't
needed).
- Locale catalogs are intentionally untouched: the i18n pipeline
extracts and translates the new header labels on main; they fall back to
their English source until then.

## Deliberate view-mode changes (approved)

- A lone widget of any type now owns its tab full-bleed: lone Fields tab
(mobile/side panel), lone rich-text Note tab, lone chart, and the
message-thread page lose their card box.
- Activity tabs show the unified header (title, grey count, + action)
instead of their internal "Inbox 12"-style rows.

Everything else is pixel-parity, including solo scroll behavior and
dashboards.

## Test plan
- `nx typecheck twenty-front` / `twenty-server`: clean; oxlint/oxfmt on
the changeset: clean
- 239 suites / 1474 tests across page-layout, activities, side-panel
pass, including new tests for `getTabPresentation` (count-based,
edit-mode override) and `usePublishWidgetHeaderInfo` (publish, cleanup
on unmount, no-op outside a widget, referential stability across
re-renders with inline actions, latest-onClick wrapper)
- `getTabsRenderableForTargetObject` tests covering missing vs
deactivated relations, Emails/Calendar without a participants relation,
and non-relation widgets
- Stuck-drag repro verified fixed end to end against a local stack with
an instrumented dnd-kit: before the fix the affected tab committed ~65
renders/second at idle and drops never tore down; after it, idle commits
are flat and every drop cleans up
2026-07-24 15:02:02 +00:00
github-actions[bot] 482b88bcda i18n - translations (#23283)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 16:39:25 +02:00
Thomas Trompette cbcfba0de2 feat(workflow): dedicated updateWorkflowVersionTrigger mutation + close version CRUD holes (#23207)
Prerequisite for the workflow-core soft-ref migration: every
`workflowVersion` content write must go through a dedicated,
draft-guarded server mutation so it can later be wrapped in a
transactional core mirror. This closes the generic-CRUD holes that let
writes bypass that path.

## Part A - dedicated `updateWorkflowVersionTrigger` mutation
The builder saved a version's trigger through generic
`updateOneWorkflowVersion` - the only content write not going through a
dedicated mutation. Added:
- Server: `updateWorkflowVersionTrigger(input: { workflowVersionId,
trigger })` resolver +
`WorkflowVersionStepWorkspaceService.updateWorkflowVersionTrigger`,
draft-guarded via `getValidatedDraftWorkflowVersion` then
`updateWorkflowVersionStepsAndTrigger` (reuses existing write logic).
- Front: `useUpdateWorkflowVersionTrigger` now calls the dedicated
mutation instead of `useUpdateOneRecord`.

## Part B - restrict generic `updateOneWorkflowVersion`
`validateWorkflowVersionForUpdateOne` previously allowed writing
`trigger`, `position`, `workflowId` (re-parenting),
`coreWorkflowVersionId`, and let `steps: null` slip through on a draft.
It now rejects any update that sets `steps`, `trigger`, `status`,
`workflowId`, or `coreWorkflowVersionId`, or that clears the `name`,
while still allowing a plain rename. (A name-only allowlist was tried
first but blocked legitimate renames - at the pre-hook the generic
update payload is not single-key - so it was replaced by this denylist,
verified live.)

## Part C - close the destroy/restore hole
`workflowVersion` had no `destroyOne/destroyMany/restoreOne/restoreMany`
query hooks, so a caller with object permission could hard-destroy any
version (including active) or resurrect one with no validation. Added
pre-hooks that forbid all four via the API ("Method not allowed"),
matching the existing forbidden generic mutations (`createOne`,
`deleteMany`, ...). Rationale: there is no legitimate API use for
standalone version destroy/restore - retention purging happens through
the trash-cleanup cron (internal, not hook-gated) and restore happens
through the workflow-restore cascade or create-draft-from-version.

## Tests
Integration specs that set a trigger through the generic mutation were
migrated to the new `updateWorkflowVersionTrigger` mutation (new
`update-workflow-version-trigger.util.ts`). Unit test for the front hook
updated.

## Verification
- `twenty-server` + `twenty-front` typecheck: clean.
- oxlint + oxfmt on all changed files: clean.
- `graphql.ts` regenerated for the new mutation; its types match the
server DTOs exactly. Local `graphql:generate` introspects a running
server, so it only succeeds against a server built from this branch - CI
regenerates against the PR server and verifies.


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

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-24 14:32:09 +00:00
martmull 25b0b2601f Replace admin app rollout buttons with upgrade-application CLI command (#23212)
## What

Removes the two rollout buttons from the admin application detail page
and replaces the upgrade flow with a CLI command that can be run
directly from a server or worker pod. Also restructures application stop
into its own module with a kill switch CLI command, and surfaces stopped
apps in workspace settings.

### Removed

- "Install on all workspaces" button (General tab,
`SettingsAdminApplicationRegistrationGeneralToggles`), its confirmation
modal and tooltip
- "Upgrade existing installations" button
(`SettingsApplicationRegistrationGeneralStats`), its confirmation modal
and batch size input
- `backfillApplicationInstallation` and
`upgradeRegistrationApplications` admin GraphQL mutations and their
frontend documents / generated types
- `BackfillApplicationInstallationJob` (its only trigger was the removed
mutation); `UpgradeApplicationsJob` is kept since the auto-upgrade flow
still enqueues it

Per review, the "install on all workspaces" flow is dropped without a
CLI replacement for now; a dedicated command will be added when needed.

### application:upgrade command

Located in `application-upgrade/commands`, registered in
`ApplicationUpgradeModule`:

```
yarn command:prod application:upgrade \
  --application-registration-universal-identifier <universalIdentifier> \
  [--batch-size 5] \
  [--workspace-id <id> --workspace-id <id2>] \
  [--workspace-count-limit 10] \
  [--dry-run] [--yes]
```

- `--workspace-id` (repeatable) restricts the upgrade to specific
workspaces; `--workspace-count-limit` caps how many installations are
upgraded (max 50, for canary rollouts)
- `--batch-size` and `--workspace-count-limit` are validated as positive
integers, max 50
- `--dry-run` reports how many (and which) workspaces would be upgraded,
without upgrading
- Without `--dry-run`, a confirmation prompt shows the app, target
version and impacted workspaces; the run then executes exactly the
confirmed set; `--yes` skips the prompt for non-interactive usage

The upgrade plan is computed by a new
`ApplicationUpgradeService.findApplicationsToUpgrade`, and batches run
through a new `upgradeApplications` method — both reused by
`upgradeAllApplications`, so the auto-upgrade job path is unchanged.

### Application kill switch (per review)

Global mechanism only — a per-workspace stop had no demonstrated
operational need and added a Redis key format, execution branching, CLI
options and tests; an isolated workspace issue can be handled directly
in the DB or Redis with the same effort.

- `ApplicationStopService` moved to a dedicated `application-stop/`
folder with its own `ApplicationStopModule` (imported and re-exported by
`ApplicationModule`)
- `stop` / `remove` methods that enable or clear the Redis-backed global
kill switch; the logic function executor checks it before executing
- `application:kill-switch` command with a positional action,
confirmation prompt (shows the installation count) and `--yes` bypass:

```
# Enable the kill switch (stop is the default action)
yarn command:prod application:kill-switch stop -u <universalIdentifier> [-y]
yarn command:prod application:kill-switch -u <universalIdentifier>

# Remove the kill switch
yarn command:prod application:kill-switch remove -u <universalIdentifier> [-y]
```

### Stopped apps surfaced in workspace settings (per review)

- Dedicated `isApplicationStopped(applicationUniversalIdentifier)` query
backed by the kill switch, fetched with `network-only` policy solely by
the application detail page — listing applications triggers no extra
Redis reads
- Application detail page shows a danger banner when the app is stopped:
"We are currently encountering issues with this app, its behavior may be
degraded while we work on a fix."

## Test

- `npx nx typecheck twenty-server` / `npx nx typecheck twenty-front`
pass
- `npx nx lint:diff-with-main` passes for both packages
- `application-stop.service.spec.ts` covers stop, remove, caching and
fail-open behavior
- Verified end to end locally: ran the kill switch command on a seeded
workspace and confirmed the banner renders on the app detail page
(screenshot shared separately)
2026-07-24 14:31:25 +00:00
github-actions[bot] 86d0e15a6a i18n - translations (#23281)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 16:20:55 +02:00
Raphaël Bosi d1c6b8ee72 Show relation record labels instead of UUIDs in dashboard charts (#23163)
https://github.com/user-attachments/assets/d012a013-2c90-49a1-a27e-b8e4b684a84f



Charts grouped by a relation without a sub-field rendered raw FK UUIDs
on axis ticks, legends and tooltips. The server now batch-resolves the
grouped record ids to their label identifier through a permission-scoped
query and formats every bucket with the record's display name.
Unresolvable records (deleted or not readable) render as Unknown and
their ids are stripped from the response payload. Same-named records get
an ordinal suffix so their buckets don't merge. Covers bar, line and
pie, plain and morph relations.

```mermaid
flowchart TD
    A["Dashboard widget load"] --> B["Chart data service<br/>(bar / line / pie)"]
    B --> C["executeGroupByQuery:<br/>group by relation FK id,<br/>ORDER BY target label identifier,<br/>scoped to source object permissions"]
    C --> D["filterOutEmptyChartBuckets"]
    D --> E{"Bare relation axis?<br/>(no sub-field)"}

    subgraph RL["ChartRelationLabelService.resolveRelationLabels"]
        direction TB
        G1["Collect distinct record ids<br/>per target object"] --> G2["Batch SELECT label identifier columns,<br/>scoped to TARGET object permissions"]
        G2 --> G3["buildRawLabelByRecordId:<br/>display name per record"]
        G3 --> G4["buildUniqueRelationLabels:<br/>suffix duplicates, Unknown for unresolved"]
    end

    E -- No --> H["formatDimensionValue per bucket"]
    E -- Yes --> G1
    G4 --> H
    H --> I["Strip unresolved ids from<br/>formattedToRawLookup"]
    I --> J["Chart DTO to frontend"]
```

The chart settings sub-field dropdown gains a Record option to group by
the related record itself, and now only offers sub-fields the backend
accepts (system fields like a workspace member's updatedBy were
selectable but rejected at query time). Chart-data errors are now logged
server-side.

Also fixes two latent bugs on this path: sorting a bare-relation chart
by field threw `Cannot orderBy unknown field: agentId`, and the pie
chart truncated slices before sorting. The AI dashboard tool guidance
and the seeded dashboards no longer force the sub-field workaround.

The group-by query orders buckets by the related record's label
identifier at the database level (the engine now accepts ordering by a
target field when grouping by its id), so with more than 100 distinct
related records the surviving buckets match the label order.
2026-07-24 14:12:22 +00:00
github-actions[bot] b61134ea5b i18n - translations (#23268)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 14:14:44 +02:00
Raphaël Bosi 3ee8fc0973 Add front component skeleton loader (#23261)
Front components (dashboard widget, side panel, settings preview) showed
blank space during their entire load. They now show a shimmering
full-area skeleton continuously, from the lazy chunk load through
metadata fetch, token/SDK wait, and worker boot, until the real UI
mounts.

The skeleton is threaded down as an optional `loadingFallback` prop so
the shared `twenty-front-component-renderer` package stays
dependency-free (react-loading-skeleton stays in twenty-front). The
command-menu headless component opts out by not passing a fallback, so
it stays blank as before.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23261?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-24 14:09:16 +02:00
Félix Malfait 3a8f086d15 Converge drag and drop on shared dnd-kit primitives, remove @hello-pangea/dnd (#23211)
Follow-ups recorded in #23023, done in one pass.

## Shared primitives

- Folded `PageLayoutWidgetSortableItem` and `PageLayoutWidgetDropLine`
into the shared `DragDropItemSortableCell` / new `DragDropItemDropLine`
(new `data`, `dropLine`, `highlightWhileDragging`, `hasTransition`
props).
- Added generic `DragDropProviderDragStartEvent` (and
DragMove/DragOver/DragEnd/DropTarget) helpers and deleted the 7 copied
`Parameters<...>` extractions across the dnd hooks.
- Replaced the `useMovePageLayoutWidgetUp/Down` implementations (~140
lines) with `moveWidgetWithinTabInDraft`.
- Migrated the remaining page-layout test suites onto
`pageLayoutDraftFixtures`.

## Tab reordering off Pangea

- Tabs are sortable cells on the same provider as widget drags,
segregated by dnd type, so widget drops on tab buttons keep working
while tabs reorder.
- Reordering is ID based (`reorderTabInDraft`: insert before the hovered
tab), which keeps the pinned first tab in place without index
arithmetic.
- Preserved overflow behaviors: the dropdown stays open while a tab drag
is in flight, dropping a tab on the "+N More" button appends it and
opens the dropdown, and both the visible strip and the overflow list
have end drop zones.

## Fields configuration editors off Pangea

- Group reorder, field reorder and cross-group field moves now run on
the shared cells (same drop line and end-zone patterns).

## DraggableList off Pangea

- `DraggableList` / `DraggableItem` keep their consumer-facing API — the
~9 consumers now type their handlers with a local
`DraggableListDropResult` instead of pangea's `DropResult` — but run on
the shared sortable cells; each list's uuid group doubles as its dnd
type so nested lists stay isolated from page-level providers.
- Items register their index in a list-scoped registry so the end drop
zone can resolve the append index at drop time (with insert-before
semantics an item could otherwise never reach the last position).
- Deleted three dead files that only existed for pangea plumbing (the
side panel navigation placeholder, `getCssCompatibleDraggableProps`, the
orphaned `recordGroupPendingDragEndReorderState`).

## Record table row drag off Pangea

- Rows register through `useSortable` directly on the row element — no
wrapper div, so row CSS, sticky cells and virtualization stay untouched
— with the grip cell wired as the drag handle via the shared sortable
handle ref context.
- Both table modes (virtualized flat list and record groups) share a
`DragOverlay` clone that replaces pangea's virtual-mode `renderClone`,
and end drop zones per record group (and after the virtualized list)
allow dropping after the last row or into an empty group.
- The drop handlers keep their pangea-shaped result object, retyped as a
local `RecordDragDropResult`, so the position computation logic is
untouched.

## Pangea removed

`@hello-pangea/dnd` is gone from `package.json` and the lockfile, along
with its orphaned transitive entries (`css-box-model`, `raf-schd`,
`react-redux`, `redux`). Nothing in the repo imports it anymore.

## Dashboards: cross-tab widget drag for grids

react-grid-layout drags never enter dnd-kit, so the bridge hit-tests the
pointer against the tab buttons' `data-page-layout-tab-drop-target-id`
rects during grid drags, highlights the hovered tab through state, and
on drop moves the widget to the destination grid below its existing
content (`moveWidgetToGridTabInDraft`, `buildTabWidgetLayouts`). The
grid's own post-drag layout commit is suppressed once so it does not
overwrite the cross-tab move.

## Fixes found while testing

- With `feedback: 'clone'`, the drag source is its own initial drop
target and its placeholder is a DOM clone taken at drag start, so the
drop line rendered into the source got baked into the placeholder and
stuck there for the whole drag. The line is now hidden on the source
cell, leaving a single indicator at the actual target.
- Reorderable tabs collapsed to text height and sat top-aligned next to
"+ New Tab" because the sortable cell wrapper defaults to `display:
block; height: auto`, breaking the tab height chain — the tab list now
uses the cell's `fill` mode so tabs stretch to the strip height again.

## Testing

Playwright against the dev app:
- Record page: widget reorder up and down in the pinned column (single
blue drop line at the target), drag to another tab via its tab button
(highlight + move), drag back into content at a specific position,
chained cross-tab moves, tab reorder with vertical drop line, new tab
creation.
- Overflow (narrow viewport): drop a tab on "+N More" (appends last,
dropdown opens), reorder inside the dropdown (stays open), drag a tab
from the dropdown back to the visible strip.
- Dashboard: grid drag within a tab, cross-tab drag onto a tab button
(hover highlight, widget lands below destination content, remaining
widgets keep their positions), save and reload persistence in both
directions.
- Fields editor: field reorder, group reorder, field move across groups,
plus the Move Up / Move Down widget actions.

Since the pangea-removal commits:
- Typecheck, oxlint and oxfmt green over the full front source; unit
suites green including the migrated `useStartRecordDrag` test (jest
needed a scoped transform exemption for `@preact/signals-core` once
dnd-kit reached the side-panel suites).
- Storybook visual regression unchanged across ~700 stories — expected,
since the migrated surfaces render identical DOM at rest (drop lines and
drag overlays only exist mid-drag).
- The tab strip fix reverses the exact regression mechanism: the
sortable cell wrapper defaulted to `display: block; height: auto`,
collapsing the tab height chain next to the full-height "+ New Tab"
button; `fill` restores the stretch.

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


<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23211?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-07-24 14:04:13 +02:00
github-actions[bot] d0863dd1f7 i18n - translations (#23253)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 12:10:12 +02:00
Félix Malfait 1fdb5605f1 feat: kanban, calendar and grouped-table layouts for relation field widgets (#23112)
## Context

A relation field widget on a record page can already embed a
record-scoped view rendered as a **table** (`FieldDisplayMode.TABLE`) —
e.g. a Company's Opportunities. This brings **kanban, calendar and
grouped-table** to that same embedded view, so the board/calendar stays
scoped to *this* record's related records (not a standalone all-records
widget — that was the earlier #23003 approach, closed).

Builds directly on the merged dashboard widget layouts (#22963), reusing
its renderer, draft/save pipeline, and settings dropdowns.

## Approach — extend the existing "Table" display mode

The relation field widget already stores a `viewId` and renders it
through the layout-agnostic `RecordTableWidgetRendererContent` (which
branches on the embedded view's `type`), scoped to the current record
via `RecordFilterValueDependenciesContext`. So rendering + persistence
already work for any widget view type — only the authoring UI and one
server gate were missing. **No new `FieldDisplayMode`, no data
migration.**

## Server

- `view-widget-upsert.service.ts`: a field widget in table display mode
(`isFieldTableWidget`) could already persist viewFields/filters/sorts
through this path, but was **blocked from updating view settings**
(`type` / group-by / calendar), pinning its embedded view to a table.
The widget-type guard earlier in the method already rejects every widget
kind other than record-table and field-table, so the now-redundant
record-table-only guard on the view-settings branch is dropped. The
allowed-widget-view-types check and the downstream group-by /
calendar-field validations still apply equally.

## Frontend

- **One merged Layout picker.** The field widget's Layout dropdown lists
**Field / Card / Table / Kanban / Calendar** in a single flat list — you
pick Kanban directly, instead of "Display as: Table" first and a
separate embedded-view layout second. Picking a view layout selects the
`TABLE` display mode under the hood, seeds the record-scoped embedded
view on first use (with a default group-by / date field), and applies
the layout in the same click. Kanban/Calendar are disabled with a hint
("Needs a Select field" / "Needs a Date field") when the relation target
can't support them — same gating as the dashboard picker. The row's icon
and description reflect the effective selection (e.g. Kanban), and the
dropdown mounts the draft-init effect so switching straight from
Field/Card to Kanban works before the table renderer has ever mounted.
- **Contextual rows** (Group by / Date field / Calendar view / Hide
empty groups) extracted from the dashboard panel into a reusable
`WidgetViewLayoutSettingsRows` (source object passed in — fixed to the
relation target; no Source / Limit rows) and surfaced under the picker
while a view layout is active. Its standalone layout row is hidden here
(`isLayoutRowHidden`) since layout lives in the merged picker.
- Reuses the dashboard draft snapshot + `upsertViewWidget` save pipeline
and the group-by/calendar dropdown components unchanged.

## Scope

- **One-to-many relations only** (matches the existing
`getFieldWidgetAvailableDisplayModes` gate; junction / many-to-many stay
table-only — a pre-existing inconsistency left untouched here).
- Field-widget **calendars inherit the dashboard's behavior** (month
read-only by default; day/week + drag-to-reschedule only behind
`IS_CALENDAR_WEEK_VIEW_ENABLED`), since it's literally the same
renderer.

## Tests

- Server integration
(`upsert-view-widget-view-settings.integration-spec.ts`): a FIELD +
TABLE widget can switch its embedded view to `KANBAN_WIDGET` (with
group-by) and `CALENDAR_WIDGET` (with date field), and the kanban
group-by validation still applies through the newly-opened path.
- Front unit: `getWidgetViewLayoutSettingsItemIds` (keyboard-nav row ids
per layout/flag/group state).

## Follow-ups (intentionally not in this PR)

- Migrate the dashboard settings panel onto the shared
`WidgetViewLayoutSettingsRows` (kept out to avoid churning the
just-merged #22963 file; behavior-preserving refactor).

https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf
2026-07-24 12:01:53 +02:00
github-actions[bot] 09e20eee5f i18n - translations (#23246)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 10:49:02 +02:00
martmull 91f0b77fdb Only suggest vetted apps in onboarding install step (#23222)
The onboarding "Install your first apps" step now only suggests
marketplace apps where `isVetted` is true.
2026-07-24 10:44:35 +02:00
martmull fb52635d2a Add defineUninstallLogicFunction hook for applications (#23227) 2026-07-24 10:41:22 +02:00
Rehuz 7f1e3d3541 fix(admin): prevent fallback to 'latest' string when dockerhub tags c… (#22885)
Fixes #22849 

### What changed?
When the `AdminPanelVersionService` hits a DockerHub API error (or
filters out all valid tags), it was previously hardcoded to return
`'latest'`. This caused the frontend to display `Latest version:
latest`.

I updated the GraphQL DTO to make `latestVersion` nullable, and modified
the service fallback and unit tests to return and expect `null` instead.

The frontend (`SettingsAdminVersionDisplay`) already has logic to handle
a falsy version and gracefully display `No latest version found`, so
this backend fix entirely resolves the UX issue without touching the
frontend.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22885?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-24 09:25:46 +02:00
github-actions[bot] 8b7ac02464 i18n - translations (#23226)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-23 21:33:32 +02:00
neo773 59eead238d message list member backfill (#23176)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23176?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-24 00:55:46 +05:30
neo773 97d2b52a71 fix(front): duplicate junction chip after Add New in relation picker (#23185)
Clicking Add New in a junction relation picker rendered the newly
created target twice until a page reload. The handler appended the
created junction to the source record's store field after awaiting the
mutation, but useCreateOneRecord's post-optimistic effect had already
attached it, so the same junction id ended up in the field array twice
(single row in DB). Removes the redundant manual append in both to-many
picker flows.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23185?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-24 00:55:30 +05:30
Raphaël Bosi 5c7915ba56 Skip install apps onboarding step for invitees (#23214)
Users joining an existing workspace via an invitation link were shown
the install apps onboarding step after skipping the email-connect step,
even though the backend never assigns them that step (and apps they
selected were silently not installed).

The frontend optimistic transition unconditionally mapped SYNC_EMAIL to
APPS_INSTALLATION. It now checks workspaceMembersCount === 1, like the
existing PROFILE_CREATION to INVITE_TEAM transition, so invitees go
straight to profile creation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23214?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-23 16:39:19 +00:00
Félix Malfait 65d399c70d feat(page-layout): drag widgets across tabs on record pages (#23023)
## What

When editing a record page layout, you can now drag a widget out of one
tab and into another. The most common case works: drag from the left
column (the pinned first tab, in full mode) into the tab you're
currently viewing.

Ways to move a widget across tabs:
- **Into the visible tab's content** — drop it into another
vertical-list tab's list to place it at a precise index. A blue line
shows exactly where it will land.
- **Onto a tab button** — drop a widget onto another tab's button to
move it to that tab; the button highlights while hovered.
- **Into an empty tab / the end of a tab** — an end-of-list drop zone
(wrapping the add-widget area) accepts the widget, so an empty tab is a
valid drop target and widgets can be appended to the end of a populated
one.

Within-tab reordering keeps working as before, now with the same blue
drop-line indicator.

## How

The record-page widget list is migrated from `@hello-pangea/dnd` to
`@dnd-kit/react` (already used elsewhere in the app, e.g.
navigation-menu-item and record-board). A single `DragDropProvider`
spans the left column, the tab bar, and the active tab content, which is
what makes cross-list drag possible — Pangea scopes each list to its own
context, so cross-tab drag wasn't expressible there.

- Widgets are dnd-kit sortables grouped by `tabId`; dropping into a
different group is a cross-tab move.
- The drop line uses `useSortable().isDropTarget` on the targeted
widget, plus an end-of-list droppable for append/empty-tab.
- Each record-page tab button is a `useDroppable` target for widgets,
opt-in per vertical-list tab so canvas/grid tabs keep their native
placement.
- The drag lifecycle lives in one router hook
(`usePageLayoutWidgetDragAndDrop`) that routes to two pure, unit-tested
draft utils (`moveWidgetWithinTabInDraft`, `moveWidgetToTabInDraft`).
The side-panel "Move to tab" action shares the same
`moveWidgetToTabInDraft` util, so drag and menu paths converge on one
mutation.
- Drop resolution reuses the shared module #23071 landed:
`getDestinationIndex` compensates same-tab downward moves for the
source-removal shift so the drop line and the landing slot agree, and
the shared `preventNativeDragStart` guard stops links/images inside
widget content from starting a native URL drag.

## Scope

Deliberately staged to the record-page widget list. Not included
(follow-ups):
- Tab reordering and the field-config editors still use
`@hello-pangea/dnd`; finishing the full page-layout removal of Pangea is
separate.
- Grid/dashboard cross-tab drag (the source there is
`react-grid-layout`, which needs a cross-system bridge).
- #23071 has merged and this branch sits on it; the remaining
convergence is a follow-up: fold
`PageLayoutWidgetSortableItem`/`PageLayoutWidgetDropLine` into the
shared `DragDropItem*` cells, export a generic drag-event-type helper to
delete the 7 copied `Parameters<...>` extractions, replace
`useMovePageLayoutWidgetUp/Down` with `moveWidgetWithinTabInDraft`, and
migrate the remaining page-layout test suites onto
`pageLayoutDraftFixtures`.

## Testing

- Unit tests for `moveWidgetToTabInDraft` and
`moveWidgetWithinTabInDraft` (incl. the non-vertical destination guard);
the three suites now share one fixture module
(`page-layout/testing/pageLayoutDraftFixtures`).
- The downward off-by-one is covered by the shared `getDestinationIndex`
unit tests from #23071.
- Full `page-layout` suite green (161 files / 1055 tests), plus
typecheck, lint, and format.
- The drag interaction itself (drop precision incl. downward same-tab
drops, line/highlight, clone feedback) still needs a manual pass in the
running app.
2026-07-23 15:06:20 +02:00
github-actions[bot] d20e5378fd i18n - translations (#23200)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-23 12:23:37 +02:00
Raphaël Bosi f5a9adcb76 Add post-onboarding AI chat setup behind a feature flag (#23120)
https://github.com/user-attachments/assets/fec7076f-4e46-4c39-84d7-68e4340244ac



After finishing onboarding, users now land in a full-screen AI chat that
helps them set up their workspace, instead of going straight to their
default view. The welcome overlay's title flies into the chat's first
message so the handoff reads as one continuous motion: the slide plays
alone, the title swaps in place pixel-exactly (a regular-weight clone of
the target line is crossfaded in mid-flight to morph the font weight),
then the rest of the text fades in.

All of it sits behind `IS_ONBOARDING_AI_CHAT_ENABLED` (default off, not
registered as a public flag). With the flag off, onboarding behaves
exactly as it does today — the welcome overlay still plays and the user
lands on their home view.

Layout follows the Figma: the nav drawer stays visible and the chat
renders in a panel-styled container with an "Onboarding" header,
matching the expanded side panel.

Also fixes two pre-existing bugs the feature surfaced:
- On billing instances the completion redirect raced the lazy
`PaymentSuccess` page, which silently skipped the welcome animation on
the no-card trial path. The redirect now defers while a checkout is
pending, and `PaymentSuccess` always confirms through
`useLoadCurrentUser` so freshly served feature flags are respected.
- `useDefaultHomePagePath` could conclude its `/settings/profile`
empty-workspace fallback from a transiently empty metadata store and
strand the user there; it now waits for both object metadata and
navigation menu items before deciding.

Reviewer notes:
- `AgentChatRuntimeEffects` no longer keys off side-panel state, so
`modules/ai` stops importing `modules/side-panel`. The two
visibility-scoped effects moved into `AiChatTab`.
- `/workspace-setup` is deliberately URL-addressable rather than
onboarding-only: the collapse control in the header is a general
expand/collapse toggle (paired with a new expand button in the side
panel top bar), and gating the route would break refresh and
browser-back. It is still authenticated-only.
- The design's second, LLM-authored paragraph is not implemented —
starting an assistant turn with no user message needs server-side work.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23120?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-23 12:16:11 +02:00
Weiko e15b9efc2d Deprecate workspace metadataVersion and stop consuming it in the frontend (#23189)
## Context

Follow-up to #23164. Now that the metadata GraphQL response cache and
the workspace SDL cache are keyed on flat-map hashes,
`workspace.metadataVersion` no longer drives any cache invalidation.
This PR is the next stage of retiring it: the frontend stops consuming
the field entirely, and the public GraphQL field is marked deprecated so
external API consumers get a migration signal.

## What changed

**Frontend stops consuming `metadataVersion`:**
- `userQueryFragment.ts` no longer selects the field.
- `currentWorkspaceState.ts` drops it from the workspace `Pick`.
- `apollo.factory.ts` no longer attaches the `X-Schema-Version` request
header.

Dropping the header retires the "your workspace has been updated, please
refresh the page" error rewrite on the server (it only fired when the
header was present, and only on requests that had already failed
validation). Metadata staleness detection is unaffected: the frontend
has been running on collection hashes plus SSE since the
minimal-metadata work, so that path stays intact. Stale clients now
surface a raw validation error instead of the friendly message, which we
consider an acceptable trade for deleting the mechanism.

**Server marks the field deprecated:**
- `workspace.entity.ts`: `@Field({ deprecationReason: 'No longer used
for metadata cache invalidation, will be removed' })`.

**Regenerated (CI-enforced surfaces):**
`twenty-front/src/generated-metadata`, and `twenty-client-sdk`'s
generated schema, which now carries `@deprecated(reason: ...)`. The
`admin` codegen config produced no changes.
`packages/twenty-sdk/generated` is intentionally untouched: no in-repo
command produces it, CI does not drift-check it, and its committed
snapshot lags the live schema, so regenerating it here would pull
unrelated schema drift into this PR; it will pick up the directive on
its next routine refresh.

## Deployment notes

- No ordering constraint with #23164: removing a field selection and a
request header is backward compatible against any server, and old
frontend bundles keep working during the rollout because the field still
exists and the server-side header check is still in place. Same release
is fine.
- The follow-up server cleanup (removing the `X-Schema-Version` check in
`use-graphql-error-handler.hook.ts`, the per-request `metadataVersion`
reads and seed in `middleware.service.ts`/`jwt-auth.guard.ts`, and the
REST heal block) must wait until the release containing this PR has
shipped, since a deployed frontend still selecting the field would break
`GetCurrentUser` if the field were removed first.

After that cleanup, the only remaining `metadataVersion` consumers are
the five pinned upgrade commands (2.8 through 2.20), which hold the
column and `WorkspaceMetadataVersionService` until that upgrade window
closes; the physical column drop then follows the two-phase pattern used
for `gridPosition`.

## Validation

- Server and frontend typecheck, lint, and format pass; the apollo
factory test suite passes unchanged (it fixtures the field but never
asserted the header).
- Live introspection against a server running this branch returns
`isDeprecated: true` with the reason on `Workspace.metadataVersion`.
- CI's pending-codegen check covers the regenerated surfaces
(`data`/`metadata`/`admin` configs and
`twenty-client-sdk:generate-metadata-client`).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23189?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-23 09:57:17 +02:00
Scarab Systems d1c70ab0bf Fix dashboard record table widget aggregate persistence (#23008)
## Summary

- Update dashboard record-table widget aggregate changes to write into
the widget draft while page layout edit mode is active.
- Include `aggregateOperation` when saving record-table widget view
fields through `upsertViewWidget`.
- Persist aggregate operations server-side for widget view-field create,
update, and clear flows.
- Add frontend utility tests and backend integration coverage for widget
aggregate create/update/clear behavior.

Fixes #22934.

## Why

Dashboard record-table widgets use their own draft view state while a
page layout is being edited. The aggregate footer path was resolving
fields through the normal current-view flow and then trying to persist
immediately, which can miss widget draft fields and fail before the save
flow runs.

This change keeps aggregate edits in the widget draft during page layout
editing, then saves the aggregate operation with the rest of the widget
view configuration.

## Validation

- `npx nx lint twenty-front`
- `npx nx typecheck twenty-front`
- `npx nx test twenty-front --configuration=ci`
- `npx nx build twenty-front`
- `npx nx build twenty-server`
- `npx nx lint twenty-server --configuration=ci`
- `npx nx typecheck twenty-server`
- `npx nx test twenty-server --configuration=ci`
- `npx nx jest --config ./jest-integration.config.ts --logHeapUsage
--runTestsByPath
test/integration/metadata/suites/view/upsert-view-widget.integration-spec.ts`
- `git diff --check`

Disclosure: I used AI-assisted coding tools while preparing this PR. I
reviewed the changes myself, tested them, and take responsibility for
the implementation and any follow-up revisions needed.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23008?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-23 08:07:50 +02:00
github-actions[bot] 3ae09cd581 i18n - translations (#23187)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-22 19:13:27 +02:00
Paul Rastoin 987995636f Sync metadata store after view child entity mutations server response (#23150)
## Summary

- View persist hooks
(`usePerformView(Sort|Field|Filter|Group|FieldGroup|FilterGroup|)APIPersist`)
now write successful mutation results back to the metadata store
(`addToDraft` / `updateInDraft` / `removeFromDraft` + `applyChanges`),
following the existing pattern from `usePerformViewAPIUpdate`.
- Previously the store only updated via SSE, so until that landed, save
flows diffed against stale view data and re-sent creates with the same
id — failing server-side with a duplicate key error.

Fixes TWENTY-SERVER-HQM
2026-07-22 19:05:22 +02:00
Raphaël Bosi eddb56d26e Fix uneven padding in the onboarding trust badge (#23174)
The "+10k" trust badge on the onboarding import contacts step had 2px
left padding vs 10px right, so the PwC logo hugged the left edge. The
badge now has equal padding on both sides so its content is centered.
<img width="347" height="168" alt="image"
src="https://github.com/user-attachments/assets/05e18541-827e-4ace-99ce-7e8c4201cd1b"
/>
2026-07-22 16:07:55 +02:00
Thomas Trompette de3889e04a fix: use aria-current instead of aria-selected on navigation drawer items (#23160)
## Fixes

Closes #23129

## Problem

Navigation drawer items render an `<a>` (or React Router `Link`) with
`aria-selected="true"`. `aria-selected` is only valid on roles such as
`option`, `tab`, `row`, or `gridcell`, not on links, so axe flags WCAG
4.1.2 (aria-allowed-attr): "ARIA attribute is not allowed:
aria-selected=true".

## Fix

Use `aria-current="page"` instead. It is the WAI-ARIA recommended way to
mark the current item in a navigation, it is a global attribute valid on
any role (link, button, or div), and it correctly communicates the
active page to screen readers.


`packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx`:

    - aria-selected={active}
    + aria-current={active ? 'page' : undefined}

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23160?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-22 15:19:09 +02:00
github-actions[bot] 271f9f833b i18n - translations (#23171)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-22 14:55:44 +02:00
Raphaël Bosi 0c964aee10 Fix blank pages and add error recovery in onboarding (#23069)
Onboarding showed a blank page in two places: before the plan/payment
step, and briefly after the welcome animation.

`ChooseYourPlan` returned `null` while its `ListPlans` query loaded,
leaving the step empty during the crossfade. It now renders the step
loader, and the plans query is warmed from an earlier onboarding step so
the content is usually already there.

The post-completion redirect lands transiently on `/`, whose route
element was `<></>`, so the welcome animation could reveal an empty
page. It now renders a skeleton, and the `null` Suspense fallbacks on
payment-success and book-call are replaced too.

A failed `ListPlans` query was worse than a blank frame: `PLAN_REQUIRED`
redirects every route back to itself, so the user was locked out of the
product with no way to retry. That step and the billing settings page
now show a retryable error state.

One related fix found on the way: `BlankLayout` had no error boundary,
so a render-time throw anywhere in sign-in or onboarding took down the
whole app. `DefaultLayout` already had one.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23069?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-22 14:46:52 +02:00