Commit Graph

13889 Commits

Author SHA1 Message Date
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] 763d31a859 chore: sync AI model catalog from models.dev (#23298)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23298?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-25 08:43:55 +02:00
Abdullah. c102a22375 fix: lift axios/tar/brace-expansion/body-parser in server fixture lockfiles (Dependabot) (#23291)
## Summary

Follow-up to #23267: lifts **axios, tar, brace-expansion, body-parser**
in the two **twenty-server fixture projects** that were deliberately
excluded from the apps sweep because of checksum coupling:

- `application-package/constants/seed-dependencies`: axios 1.16.1 ->
1.18.1, body-parser 1.20.5 -> 1.20.6, brace-expansion 2.1.1 -> 2.1.2 and
5.0.6 -> 5.0.7, tar 7.5.16 -> 7.5.20
- `logic-function/.../common-layer-dependencies`: brace-expansion 2.1.1
-> 2.1.2

All moves fit the declared ranges (recursive `yarn up`), so both diffs
are lockfile-only.

## Checksum coupling

`seed-dependencies` is read at runtime by
`getDefaultApplicationPackageFields` and its content is pinned by stored
constants (first 32 hex chars of SHA512; package.json hashes the
re-serialized JSON). The lockfile change therefore regenerates
**`DEFAULT_YARN_LOCK_CHECKSUM`** in
`get-default-application-package-fields.util.ts`. `package.json` is
byte-untouched, so `DEFAULT_PACKAGE_JSON_CHECKSUM` stays.

Verified in order: the hash formula reproduces both *current* constants
before regenerating; the new constant matches the new lockfile content;
`yarn install --immutable` passes in both fixture projects.
`common-layer-dependencies` has no checksum coupling (copied + installed
at Lambda layer build time).

## Deliberately not covered

**sharp** stays at 0.34.5 in seed-dependencies: every path is
minor-locked at `^0.34.5` (including twenty-sdk latest); pending the
twenty-sdk range decision.

## Alerts

Clears the axios (10), tar (4), brace-expansion (3) and body-parser (1)
Dependabot alerts on these two manifests.
2026-07-24 18:11:29 +00:00
github-actions[bot] ab3d921218 i18n - docs translations (#23292)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23292?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 19:05:04 +02:00
Etienne 4851489ebc fix(ai-chat) fix AI chat tool-output spill leaks: spill learn_tools, cap navigation tools, truncate on spill failure (#23286)
## Context

AI chat spills tool outputs larger than `MAX_INLINE_TOOL_OUTPUT_BYTES`
(16 kB) to a file and lets
the model page them back with `search_output` / `extract_json_paths`.
Three paths bypass this and
let unbounded payloads into conversation history:

1. **`learn_tools` is never spilled.** Tool schemas go inline whatever
their size.
2. **Navigation tools have no inline cap.** They are exempt from
spilling by design (they page
   spilled files), but nothing bounds their own output.
3. **Spill failure falls back to full inline.** On any spill error the
service returns the complete
   payload with only a warning appended.

## What changed

- **`learn_tools` now spills.** `createLearnToolsTool` takes `{
excludeTools?, spillLargeOutput? }`
(same shape as `createExecuteToolTool`); chat execution enables it. Only
the bulky `tools`
schemas are spilled; `message` / `notFound` / `suggestions` stay inline
and the response carries
a `spilledTools` envelope (fileId, preview, hint) pageable via
`extract_json_paths`. MCP is
  unchanged.
- **Navigation tools get a hard inline cap.** Still never spilled, but
output above 16 kB is
head+tail truncated with a marker telling the model to narrow the query
or page with `offset`.
- **Spill failure truncates instead of inlining.** The fallback returns
head+tail within the 16 kB
  budget with the original byte size in the marker, keeping the warning.
- New `truncateHeadTail` util: byte-budgeted, marker-aware, UTF-8
codepoint-safe.

## Test plan

- `tool-output-spill.service.spec.ts`: spill envelope unchanged,
under-budget passthrough,
navigation cap for both tools (budget respected, marker mentions
`offset`, no file written),
  truncated fallback on spill failure with warnings preserved.
- `learn-tools.tool.spec.ts`: no spill without the option, inline under
budget,
`message`/`notFound`/`suggestions` intact when spilled, spill-failure
warnings surfaced.
- `truncate-head-tail.util.spec.ts`: budget, head+tail+marker,
multibyte-safe cuts.
- 63 tests across 6 suites; `lint:diff-with-main` and `typecheck` green.

## Post-deploy

Watch the `AiChatToolOutputTokens` histogram (p95 should collapse to ~4k
tokens) and the
`AiChatInputTokens` / `AiChatCacheReadTokens` ratio on GPT-5-class
models.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23286?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 16:57:35 +00:00
Etienne 3a1067ec6d fix(server): index page-layout FKs to fix workspace cleanup timeout (#23289)
The cleanSuspendedWorkspacesJob cron timed out every run (Sentry monitor
"a timeout check-in was detected"): hard-deleting soft-deleted
workspaces hung on `DELETE FROM core.pageLayout`, hit the 10s query
timeout, rolled back, so those workspaces were never destroyed and got
retried hourly.

Root cause: the FKs in the pageLayout -> pageLayoutTab ->
pageLayoutWidget tree had no usable index on the referencing column. The
existing indexes lead with workspaceId and are partial ("deletedAt" IS
NULL), so ON DELETE CASCADE / SET NULL fell back to full sequential
scans of the shared core tables per deleted row; on layout-heavy
workspaces this exceeded 10s.

- Add non-partial FK-column indexes on pageLayoutTab(pageLayoutId) and
pageLayoutWidget(pageLayoutTabId)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23289?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 16:50:57 +00:00
martmull 45f92b6763 feat(billing): make logic function executions free for exempt apps (#23255)
## Problem

Workspaces get 5 free credits/month to run logic functions, AI and
workflows. When a user imports their mailbox with the
onboarding-suggested **Call Recorder** and **Last contact** apps, each
imported message/calendar event fires those apps'
database-event-triggered logic functions, and each execution bills a
flat 100 micro-credits. A single import can fire tens of thousands of
executions and drain the entire monthly allowance before the user has
done anything else.

The trigger pipeline has no notion of "this came from sync", and logic
function executions are metered per record (one job per imported
record), so the burn is unavoidable today.

## Approach

Keep a static list of billing-exempt app identifiers
(`MARKETPLACE_BILLING_EXEMPT_UNIVERSAL_IDENTIFIERS` — Call Recorder and
Last contact) and check it in the logic-function executor's billing step
via a small `isBillingExemptApplication(universalIdentifier)` utility.
When the running app is exempt, the per-invocation meter records
`creditsUsedMicro: 0` and skips the credit decrement.

Scope is deliberately narrow: only the automatic per-invocation meter is
exempted. Anything the function itself charges via `chargeCredits` (the
separate `/app/billing/charge` endpoint) and any AI token usage keep
billing and keep their enforcement, so a free app can still charge for
real paid work (e.g. Call Recorder's per-recording charge, People Data
Labs enrichment) and AI usage still throws on credit exhaustion.

There is no DB column, migration, cache, admin UI, or per-registration
state — the exemption is derived entirely from the app's
`universalIdentifier` against the in-memory list, so it applies
uniformly to fresh and existing installations.

## Changes

- `isBillingExemptApplication` utility over the exempt-apps constant,
with a unit test.
- Logic-function executor consults the utility to decide
`creditsUsedMicro` (0 for exempt apps, 100 otherwise) and only
decrements credits for non-exempt invocations.

## Notes / follow-ups

- This fixes the billing drain but not the execution burst: an import
still fires the real isolate executions for zero user-visible benefit
over the apps' existing batch backfill. Suppressing database-event
triggers during historical import is a complementary follow-up worth
doing for infra cost and rate-limit reasons.

## Test plan

- [x] `nx typecheck twenty-server` / `nx typecheck twenty-front`
- [x] Server unit tests (`isBillingExemptApplication`) pass
- [ ] Manual: install Call Recorder / Last contact, import a mailbox,
confirm credits are not consumed by their logic function executions
while AI usage and in-app charges still bill
2026-07-24 18:23:17 +02:00
Thomas Trompette d6c186a71e Fix system objects bypassing role object permission overrides (#23280)
## Bug

Fixes #23062 (security). A workspace member whose role denies all object
access could still read/mutate system-object records (messages, calendar
events, and related system objects). System objects bypassed explicit
role-level object permissions.

## Root cause

In `workspace-roles-permissions-cache.service.ts`, the per-object
permission helper resolved values as:

```ts
(isSystem ? true : (overrideValue ?? defaultValue))
```

For every non-workflow, non-workspace-member system object this forced
`read`/`update`/`softDelete`/`destroy` to `true`, so an explicit deny
override on the role was never consulted.

## Fix

Flip the precedence so an explicit role override wins, and the
`isSystem` default only applies when the role provides no override:

```ts
overrideValue ?? (isSystem ? true : defaultValue)
```

Because the override fields are `boolean | undefined`, `??` correctly
honors an explicit `false` while still falling back to the system
default (`true`) when the role has no override row for that object.

Workflow objects (settings-gated via the `WORKFLOWS` flag) and
workspace-member objects (settings-gated, always readable) are handled
in separate branches and are unchanged, so their intended defaults do
not regress.

## Testing

- `nx lint:diff-with-main twenty-server` passes.
- Typecheck: no new errors from this change (pre-existing unrelated
failures in `twenty-shared` date-filter utils only).
- Manually verified on a local instance that a deny-all role no longer
has read access to system objects.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23280?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 15:49:52 +00:00
Paul Rastoin bc7922da7d fix: move add-agent-foreign-key-to-role-target instance command to 2.25 (#23285)
## What

Moves the `add-agent-foreign-key-to-role-target` fast instance command
from `2.24.0` to `2.25.0`.

Introduced in #23206, the command was registered under version `2.24.0`.
Since `TWENTY_CURRENT_VERSION` is now `2.25.0`, `2.24.0` is an
already-released version, so its instance commands do not re-run on
upgrade and the foreign-key migration would never execute.

This is the same issue #23271 fixed for the message-list-members
backfill workspace command.

## Changes

- Moved the command file from `upgrade-version-command/2-24/` to `2-25/`
(renamed the file prefix).
- Updated the decorator from `@RegisteredInstanceCommand('2.24.0', ...)`
to `('2.25.0', ...)`.
- Updated the import in `instance-commands.constant.ts` to the new
relative path and reordered both the import and the array entry to sit
after the 2-24 commands.

The timestamp (`1784820332810`) and command logic (`up`/`down`) are
unchanged.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23285?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 15:32:33 +00:00
Abdullah. 0d876eb714 fix: lift axios/tar/brace-expansion/body-parser across app lockfiles (Dependabot) (#23267)
## Summary

Sweeps the **twenty-apps lockfiles** for this week's advisory wave:
recursive `yarn up` for **axios, tar, brace-expansion, body-parser** in
each of the 12 apps with open Dependabot alerts (hello-world, postcard,
self-hosting, twenty-partners, call-recorder, people-data-labs,
twenty-discord, twenty-exa, twenty-fireflies, twenty-last-contact,
twenty-linear, twenty-slack).

All moves fit the declared ranges (apps carry these transitively via
`twenty-sdk`, whose `axios ^1.16.0` and deep tar/brace chains are
carets), so the diff is **lockfile-only** across all 12 manifests - no
resolutions, no `package.json` changes. axios -> 1.18.x, tar -> 7.5.20
(critical GHSA-23hp-3jrh-7fpw chain), brace-expansion -> 1.1.16 / 2.1.2
/ 5.0.7, body-parser -> 1.20.6 / 2.3.0.

The second commit narrows scope to apps only: the twenty-server fixture
projects (seed-dependencies, common-layer-dependencies) move to a
dedicated PR because seed-dependencies' yarn.lock is checksum-coupled to
`DEFAULT_YARN_LOCK_CHECKSUM` in
`get-default-application-package-fields.util.ts`; it also drops
accidentally committed `.yarn/install-state.gz` artifacts.

## Deliberately not covered

- **sharp**: every path is minor-locked at `^0.34.5` (including
twenty-sdk latest) - separate PR bumping twenty-sdk's range.
- **react-router / react-router-dom**: no fixed release on the 6.x line
(fix is the v7 major); tracked separately.

## Verification

- Vulnerable-version scan across all 12 lockfiles: no axios <1.18, tar
<7.5.19, brace-expansion below 1.1.16/2.1.2/5.0.7, or body-parser below
1.20.6/2.3.0 remains.
- `yarn install --immutable` passes in each app.
- All fix versions clear the 3-day npm age gate.
2026-07-24 15:21:32 +00:00
Abdul Rahman 7067f6ef88 fix: honor agent rolePermissionConfig in record CRUD (#23248)
## Summary
- Agent tools were built with the agent’s `rolePermissionConfig`, but
record CRUD ignored it and re-resolved permissions from `authContext`
(app `defaultRoleId`)
- CRUD services now pass `rolePermissionConfig` through
`CommonApiContextBuilder` and the common query runner, so repository
access matches the agent role
- Workflow/chat paths already use the same role for auth and
`rolePermissionConfig`, so their behavior should be unchanged

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23248?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 15:21:27 +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
Etienne 0bba75dd18 fix(ai-chat): stop duplicate tool_use ids from bricking threads (#23277)
## Issue

Some AI chat threads become permanently broken. Every turn fails with an
Anthropic 400:

messages.5.content.1: tool_use ids must be unique



The error is on the message *history*, so once a thread is in this state
every subsequent turn fails too, not just the one that triggered it. It
surfaces most visibly when aborting a thread and continuing it, but the
abort is incidental: it just replays the already-corrupted history.

## Root cause

A single tool call gets persisted as **two message parts sharing one
`toolCallId`**. Confirmed in the DB for the affected thread, one
assistant message held:

- `tool-extract_json_paths` and `dynamic-tool`, both
`toolu_01FXxxfBYP27NZEvA6AZ3AJc`
- `tool-search_output` and `dynamic-tool`, both
`toolu_01VqnFrYdGCERAc2dbG3zAyx`

On the next turn `convertToModelMessages` turns each pair into two
`tool_use` blocks with the same id, which Anthropic rejects.

## Why it happens

It is not two concurrent calls. It is one call the AI SDK classifies
inconsistently across its own stream chunks.

The chat only exposes a small set of directly-callable tools
(`execute_tool`, `learn_tools`, `load_skills`, `ask_questions`, plus
native/preloaded ones). Registry tools like `extract_json_paths` and
`search_output` are reachable only through `execute_tool`. When the
model shortcuts that and calls one directly, the name is not in the
active `ToolSet`, and the SDK does this:

1. On `tool-input-start`, `dynamic` is derived from the tool set:
`tools[name]?.type === "dynamic"`. The tool is absent, so `dynamic:
false`, and a **static** `tool-<name>` part is created.
2. On finalization the unknown tool throws `NoSuchToolError`.
`repairToolCall` intentionally skips name errors (`return null`), so the
SDK re-emits the call with a hardcoded `dynamic: true`. That error
routes to the **dynamic** path and creates a second `dynamic-tool` part
with the same id.

The UI-message builder keeps static and dynamic tool parts in separate
buckets, each searched by `toolCallId` independently, so the mid-call
static-to-dynamic flip produces two parts for one call. Both persist and
break the next turn.

## Fix

Two independent read/convert-path passes, both in
`sanitizeMessagePartsForModel` in `chat-execution.service.ts`, running
before `convertToModelMessages`:

1. **`finalizeDanglingToolParts`** now dedupes tool parts by
`toolCallId` (first-wins), keeping the `input-streaming` filter ahead of
the dedup so a leading streaming duplicate can't strand the call.
Because it runs before conversion and on the write paths too,
already-corrupted threads are un-bricked on their next turn with no
migration.
2. **`guideUncallableToolCallsToMetaTool`** addresses the behavior that
caused it: when the model calls a tool that is not directly callable, it
appends the `learn_tools` -> `execute_tool` flow to that failed tool
result, so the model reads how to reach the tool. Detection is
structural (a failed tool part whose name is not in the active tool
set), not string-matched against the SDK's error wording.

Unit tests added for both. Typecheck, lint, and the suite pass.

Note: the stale duplicate rows already in the DB are harmless (deduped
on every read); no migration is required.

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 16:46:27 +02: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
Paul Rastoin 308e4de7a6 TDD: twenty-ui render coverage in the front-component sandbox — golden tests cataloging every sandbox gap (fully green) (#23203)
## What — TDD coverage layer, no fixes

Renders every twenty-ui component export inside the real front-component
sandbox (opaque-origin iframe + remote-dom worker, real SDK esbuild
pipeline) through the existing Playwright-backed storybook vitest suite,
and expresses **every sandbox gap as a golden known-failure test**. This
PR deliberately ships **zero fixes** and is **fully green (267/267)**:
each known issue has a test asserting the exact current broken behavior.
A golden test fails on regression (an unexpected component starts
failing) AND on fix (the documented failure disappears) — so every
future fix must flip its golden assertion to the strict one, whose
acceptance criteria are spelled out in each story's comment.

- One gallery fixture per submodule under
`src/__stories__/twenty-ui-gallery/` (~150 components)
- `component-gallery.tsx`: each component renders inside its own error
boundary; failures are aggregated with component name and error message
(`data-failed-messages`) so one crash cannot mask the rest
- Behavior-level stories that a fake fix cannot pass (MutationObserver
must actually fire; a link click must reach `hostApi.navigate`)
- `vitest.config.ts` re-export so the storybook in-UI "Run tests" button
works

## Current state: 267/267 green — mergeable; 18 golden tests encode the
catalog below

## Failure catalog (each entry = golden tests asserting today's broken
behavior)

### 1. `MutationObserver.observe is not a function` — 6 failing tests

`@remote-dom/polyfill` ships `MutationObserver` as an empty class:
construction succeeds, the first `.observe()` call crashes.

| Failing test | twenty-ui export(s) | Call site |
|---|---|---|
| Input React + Preact | `Radio`, `RadioGroup`, `CardPicker` |
`@base-ui/react` field internals observe form state |
| Surfaces React + Preact | `AppTooltip` | twenty-ui observes
`document.body` to track anchors |
| Worker Platform APIs: Mutation Observer React + Preact | (behavior
test) | asserts the observer actually fires on a React-driven insertion
— a no-op stub cannot pass it |

Validated fix (branch history, commit 94b96d01e79e): implement real
mutation semantics **locally in the worker** by tapping the same
`@remote-dom/polyfill` hooks
(`insertChild`/`removeChild`/`setAttribute`/`removeAttribute`/`setText`)
remote-dom already uses to mirror mutations to the host. The worker tree
is the source of truth — no host bridge needed.

### 2. `getComputedStyle is not a function` — 4 failing tests

The remote-dom `Window` polyfill does not implement `getComputedStyle`;
`@base-ui/react` Collapsible calls it on open.

| Failing test | twenty-ui export(s) |
|---|---|
| Layout React + Preact | `AnimatedEaseInOut`,
`AnimatedExpandableContainer` |
| Json Visualizer React + Preact | `JsonTree`, `JsonArrayNode`,
`JsonObjectNode`, `JsonNestedNode` (all via `JsonNestedNode`'s
Collapsible) |

Validated fix (commit 7af577054fd9): `getComputedStyle` is synchronous
and layout only exists host-side, so a bridge is impossible — an
inert-but-valid stub (`0s` durations, `none` animation names) is the
honest terminal state.

### 3. No router context in the sandbox — 5 failing tests

react-router's `Link` reads `NavigationContext`, which has no provider
inside the worker: `Cannot destructure property 'basename' of
'useContext(...)' as it is null`.

| Failing test | twenty-ui export(s) |
|---|---|
| Navigation React + Preact | `RawLink`, `UndecoratedLink` |
| Data Display React + Preact | `LinkChip` |
| HostApi: Router Link | acceptance test — the link must render AND its
click must reach `hostApi.navigate` |

Validated fix (commit 221c9c353dc8): SDK build plugin wraps the
component tree in a low-level react-router `<Router>` whose custom
navigator forwards `push`/`replace` to the SDK `navigate` host API (NOT
a MemoryRouter, which would render links but swallow clicks into
in-memory history). Falls back to a pass-through provider when the app
has no react-router-dom dependency.

**Security finding bundled in that commit:** clicking a mirrored `<a>`
performs a native host-page navigation before the async worker
round-trip can `preventDefault` — a front component can escape the
sandbox by rendering a link. Fix: apply the renderer's existing
preventDefault-then-forward guard (already used for form submits) to
anchor clicks, keeping `target="_blank"` native. The Router Link story's
flip-to acceptance assertions (click must reach `hostApi.navigate`) will
hard-crash the vitest browser the moment links render without this guard
— by design.

### 4. Open `Modal` hangs the React runtime — 1 failing test

base-ui Dialog portal with `isOpen` never commits under the React
runtime (no error thrown); works under Preact.

| Failing test | twenty-ui export |
|---|---|
| Modal Open React (Modal Open Preact passes) | `Modal` |

Isolated in its own fixture so the hang cannot mask the rest of the
surfaces gallery (which keeps a closed Modal for mount coverage). Root
cause not yet identified — first experiment: portal into a worker-owned
container instead of the polyfilled `document.body`.

### 5. monaco cannot load in the sandbox — 2 failing tests

`@monaco-editor/react` lazy-loads monaco via script injection,
impossible in the polyfilled worker DOM (opaque-origin CSP, no script
loading). The CodeEditor wrapper mounts; monaco's `onMount` never fires.

| Failing test | twenty-ui export |
|---|---|
| Code Editor React + Preact | `CodeEditor` |

Probably wont-fix in the worker: if front components need a code editor,
the path is a host-rendered privileged component. The failing test is
the documentation.

### Also worth knowing (no failing test possible yet)

- `ResizeObserver` / `IntersectionObserver` / `matchMedia` are absent in
the worker: components mount without them today only because nothing
crashes at mount — behavior like popup auto-resize and `useIsMobile` is
silently wrong. Real fix is a host bridge (async observers on the
component's own mirrored elements only — never the host document).
Behavior tests should land with that fix.
- `Icon`/`IconsProvider`/`useIcons` are excluded from galleries by
choice: they dynamic-import the multi-MB Tabler catalog; direct icon
imports are the supported pattern in bundled front components.

## Iteration plan

Each fix is one commit + one set of stories flipping green, in suggested
order:

1. Worker-local MutationObserver (6 tests) — cherry-pick base:
94b96d01e79e
2. `getComputedStyle` + observer/matchMedia stubs (4 tests) —
cherry-pick base: 7af577054fd9
3. Router provider + anchor click guard (5 tests) — cherry-pick base:
221c9c353dc8
4. Open-Modal React hang investigation (1 test)
5. CodeEditor: decide wont-fix + keep the failing story or convert to a
documented skip (2 tests)

## How to run

```
npx nx run twenty-front-component-renderer:storybook:prebuild
cd packages/twenty-front-component-renderer
npx vitest run --config vitest.storybook.config.ts --project storybook
```
2026-07-24 14:38:19 +00: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
martmull 940d150775 chore(self-hosting): upgrade to latest twenty CLI tooling (#23279)
## What

Upgrades the internal `self-hosting` app to the latest Twenty CLI
tooling, bringing it in line with the other actively-maintained apps in
the monorepo.

- `twenty-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency +
devDependency)
- `twenty-client-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency +
devDependency)
- `engines.twenty`: `>=2.19.0` → `>=2.23.0`
- Regenerated `yarn.lock` to match.

The `twenty` CLI ships inside `twenty-sdk`, so this pulls the app onto
the same CLI version every other recently-updated app (call-recorder,
people-data-labs, twenty-partners, real-estate, last-contact, postcard)
already uses.

## Verification

- `yarn typecheck` passes
- `yarn lint` passes (0 warnings, 0 errors)
- `yarn test:unit` passes (3/3)

Integration tests (`yarn test`) require a running Twenty server and were
not run in this environment.

## Notes

Scope is limited to the CLI/SDK tooling. Framework deps (React 18) were
left untouched since they are not tied to the CLI version and vary
across apps.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23279?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:26:42 +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
Weiko b036d67ec9 Configure async ClickHouse inserts for pageview events (#23274)
## Context

Pageview tracking goes through the `trackAnalytics` mutation on the
metadata API and is persisted through the unified event pipeline before
the mutation resolves.

ClickHouse inserts already use:

```text
async_insert = 1
wait_for_async_insert = 1
```

`async_insert` lets ClickHouse buffer and batch small inserts, but
`wait_for_async_insert = 1` still keeps the API request open until that
buffer is flushed successfully. For sparse pageview inserts, the buffer
timeout can therefore account for most of the request duration and
contribute to metadata API tail latency.

## What this changes

- Adds a named `ClickHouseService.insert` option for overriding
`async_insert_busy_timeout_max_ms`.
- Caps the pageview buffer wait at 100 ms.
- Keeps `wait_for_async_insert = 1`.
- Leaves workspace, object, usage, application-log, and other event
inserts on the existing default timeout.

## Why this approach

This removes the avoidable buffer wait from the pageview request path
without changing the delivery guarantees of the event pipeline.

In particular, this does **not** use `wait_for_async_insert = 0` or
fire-and-forget writes. The API still receives an acknowledgement only
after ClickHouse flushes the pageview successfully, and insert/schema
errors still propagate through the existing handling.

The 100 ms value caps only the batching wait. It does not impose a 100
ms deadline on the complete ClickHouse request.

## Expected impact

- Lower ClickHouse span duration for pageview tracking.
- Lower tail latency for metadata API requests that emit pageviews.
- No behavior or durability change for other event types.

The trade-off is that pageviews may be flushed in smaller batches. The
setting remains scoped to the pageview table so higher-value event
streams keep their current batching behavior.

## Testing

- Added coverage for the optional ClickHouse busy-timeout setting.
- Added coverage verifying that only pageview inserts receive the 100 ms
override.
- Existing insert failure/retry behavior remains covered.
- `twenty-server` typecheck passes.
- Focused test result: 23 tests passed.

## Post-deploy verification

- Compare pageview ClickHouse span p95/p99 before and after deployment.
- Compare metadata API p95/p99.
- Check ClickHouse asynchronous-insert failures.
- Watch ClickHouse part creation and merge pressure for unexpected
growth.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23274?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:01:23 +00:00
Paul Rastoin 6a8457d8c5 fix(ci): diff upgrade mutation guard against the merged base, not stale PR base (#23278)
The `server-previous-version-upgrade-mutation-guard` check falsely flags
upgrade commands that landed on `main` as being added/modified by
unrelated PRs (e.g. [run on
#23207](https://github.com/twentyhq/twenty/actions/runs/30097349274/job/89494554178)
flagged six `2-23`/`2-24` files the PR never touched).

## Cause

The guard action diffs `git diff "$BASE_SHA" HEAD` where the PR caller
passes `base_sha: github.event.pull_request.base.sha`.

On a `pull_request` event `HEAD` is the `refs/pull/N/merge` ref, whose
first parent is the *current* tip of `main` it was merged with. But
`pull_request.base.sha` is pinned to the base at the last branch sync
and lags behind. Any upgrade command merged into `main` after that point
exists in `HEAD` but not in the stale base, so the two-dot diff
attributes it to the PR.

Verified against the real merge ref for #23207: diffing against
`base.sha` reproduces the six false offenders from the failing run;
diffing against the merge ref first parent is clean.

## Fix

Derive the base from the merge ref first parent (`HEAD^1`, the actual
merged `main` tip), falling back to `base.sha` only when `HEAD` is not a
merge commit (non-mergeable PR).

The merge-queue caller in `ci-merge-queue.yaml` is unaffected: it passes
`merge_group.base_sha`, which already matches its checkout.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23278?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 16:01:36 +02:00
Paul Rastoin dee653dfa6 fix: move message list member backfill command to 2.25 (#23271)
## What

Moves the `backfill-message-list-members-junction-target` workspace
upgrade command from `2.24.0` to `2.25.0`.

Introduced in #23176 (commit 59eead23), the command was registered under
version `2.24.0`. Since `TWENTY_CURRENT_VERSION` is now `2.25.0`,
`2.24.0` is an already-released version, so its upgrade commands do not
re-run and the backfill would never execute on upgrade.

## Changes

- Moved the command and its module from `upgrade-version-command/2-24/`
to a new `2-25/` directory.
- Updated the decorator from `@RegisteredWorkspaceCommand('2.24.0',
...)` to `('2.25.0', ...)`.
- Renamed `V2_24_UpgradeVersionCommandModule` to
`V2_25_UpgradeVersionCommandModule` and updated its registration in
`workspace-command-provider.module.ts`. The `2-24` module only ever
provided this single command.

The timestamp (`1784567000000`) and command logic are unchanged.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23271?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 13:50:26 +00:00
Paul Rastoin ba5cb6ba15 fix(server): repair missing keyValuePair.applicationId on 2.23 upgrades (#23272)
Fixes #23254

## Problem

Upgrading a self-hosted instance from `2.23.x` to `2.24.0` leaves
`core.keyValuePair` without the `applicationId` column. Database-backed
config loading then fails on startup and on every refresh (~every 15s)
with:

```
column KeyValuePairEntity.applicationId does not exist
```

The frontend shows "Unable to reach the backend".

## Root cause

`AddApplicationIdToKeyValuePairFastInstanceCommand` was added in #23089
(after `2.23.x` shipped) but registered under the already-released
`2.23.0` segment:

```ts
@RegisteredInstanceCommand('2.23.0', 1784659343818)
```

The upgrade cursor is **positional and forward-only**:

- `resolveStartCursor` resumes at `lastAttemptedIndex + 1`. A
fully-upgraded `2.23.x` instance has its cursor at the last `2.23`
workspace command, which sits *after* this newly-inserted fast command
in the sequence. So the runner steps right over it and the DDL never
runs.
- The upgrade-aware metadata layer decides "applied" the same way
(`stepIndex < currentCursor` in
`upgrade-aware-entity-metadata.adapter.ts`). Since the step index is
below the cursor, the column is considered applied and is **not** hidden
from TypeORM SELECTs, so every query references a column that was never
created.

Fresh `2.24.0` installs replay the whole sequence, so only `2.23.x ->
2.24.0` upgrades are affected. The instance log `1 fast instance ... for
2.24.0` confirms the command landed in the `2.23.0` bundle rather than
`2.24.0`.

## Fix

- Add `RepairKeyValuePairApplicationIdFastInstanceCommand` under the
current version (`2.24.0`) with a fresh timestamp, so it sorts last in
the sequence and runs for every existing instance regardless of cursor
position. Its DDL mirrors the original command and is fully idempotent
(`ADD COLUMN IF NOT EXISTS`, `DROP INDEX IF EXISTS` + recreate, `ADD
VALUE IF NOT EXISTS`), so it is a no-op on healthy instances. `down()`
is intentionally empty: the column lifecycle is owned by the `2.23.0`
introduction command.
- Repoint the entity's `@WasIntroducedInUpgrade` to the new command so
the column stays hidden from queries until the repair has actually run,
eliminating the error window during the migration itself.

## Notes

- `2.24.0` (`TWENTY_CURRENT_VERSION`) is the correct target: the upgrade
sequence only covers previous + current versions, so a command under
`2.25.0` (a next version) would not run. If a version bump lands before
this merges, the command should be moved to the new current version.
- Follow-up worth considering: nothing currently prevents registering a
command under a version in `TWENTY_PREVIOUS_VERSIONS`. A startup
validation rejecting that would have caught this at PR time.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23272?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 13:46:59 +00:00
Weiko abe4d7491c Cap nested relation query concurrency (#23252)
## Context

Common API queries load selected relations after fetching the root
records.

Relation loading is batched: one query pipeline loads a relation for all
parent records, so this is not an N+1 problem. However, every sibling
relation currently starts concurrently through `Promise.all`.

Nested relations repeat the same behavior recursively. A wide selection
can therefore submit many independent relation query pipelines at once.

Existing query complexity and record limits restrict what can be
requested, but they do not limit how much database work starts
concurrently.

## What this changes

This PR adds a request-local FIFO concurrency limiter for nested
relation loading.

- At most four `findRelations` pipelines execute concurrently.
- One limiter is created for the outer relation-loading call.
- The same limiter is shared by every recursive level.
- Queued work starts as permits become available.
- Permits are released in `finally`, including when a query fails.

Conceptually:

```text
Before:
all sibling relations -> database concurrently
nested siblings       -> more database work concurrently

After:
all sibling relations -> FIFO queue -> at most 4 database pipelines
nested siblings       -> same FIFO queue and same limit
```

Note: Also addressing
https://github.com/twentyhq/twenty/pull/23251#discussion_r3644510597
2026-07-24 13:27:26 +00:00
Paul Rastoin 9390c28cb6 ci: report ci-shared-status-check on merge_group (#23276)
Follow-up to #23275. `ci-shared` was the one required check left off
that batch, so `ci-shared-status-check` still sits at "Expected -
Waiting for status to be reported" and blocks the merge queue.

Same fix as the other workflows: add a `merge_group` trigger and gate
`changed-files-check` with `if: github.event_name != 'merge_group'`. On
a queued candidate, `changed-files-check` skips, `shared-test` (gated on
`any_changed`) cascades to skipped, and the `always()`-gated
`ci-shared-status-check` job reports success in seconds. The full suite
still runs on `pull_request`.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23276?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 15:27:07 +02:00
Paul Rastoin 29747f5d6b ci: report required status checks on merge_group so the queue isn't blocked (#23275)
Follow-up to #23216, which added the `merge_group`-triggered
`upgrade-mutation-guard`. This makes the merge queue actually usable.

## Why

The merge queue waits for every **required status check** to report a
conclusion on the `merge_group` candidate commit, and there is no
queue-only subset: it uses the branch's required status checks. Our
required `ci-*-status-check` contexts only trigger on `pull_request`, so
on a queued PR they sit at "Expected - Waiting for status to be
reported" and block the queue until the status-check timeout (60 min),
which then counts them as failed.

Only `upgrade-mutation-guard` (from #23216) triggers on `merge_group`,
so today it is the only check that reports in the queue.

## What

Add a `merge_group` trigger to each of the seven required-check
workflows and short-circuit the expensive work so the check reports
success in seconds, while the full suite keeps running on `pull_request`
to gate PRs. `upgrade-mutation-guard` stays the only check the queue
genuinely validates against `main`.

Mechanism: on `merge_group` the root jobs skip, everything downstream
cascades to `skipped`, and the `always()`-gated `*-status-check` job
runs, sees no failing needs, and succeeds. Kept as the same job in the
same workflow so the required-check context is byte-identical to the
PR-level one (a separate pass-through workflow could register a
different context and not satisfy branch protection).

Per workflow:

- **ci-front**: trigger only. It already cascades -
`changed-files-check` is `pull_request`-only and `front-sb-build` gates
on `push || any_changed`, so nothing runs on `merge_group`.
- **ci-server**: trigger + `if: github.event_name != 'merge_group'` on
the three ungated root jobs (`changed-files-check`,
`upgrade-changed-files-check`,
`server-previous-version-upgrade-mutation-guard`). The guard would
otherwise fail on `merge_group` since `pull_request.base.sha` is empty
there; the queue-side guard in `ci-merge-queue.yaml` already covers that
case.
- **ci-sdk / ci-website / ci-test-docker-compose**: trigger + the same
guard on `changed-files-check`.
- **ci-twenty-apps**: trigger + the guard on `discover` (its
`ci`/`integration` jobs gate on `discover` output, so they cascade off).

## Settings note

This complements the branch-protection changes for the queue (enable the
queue, max group size 1, per #23216). If the branch has **other**
required checks beyond these seven whose workflows are
`pull_request`-only, they need the same `merge_group` treatment or the
queue will wait on them too.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23275?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 15:18:26 +02:00
Paul Rastoin bb4e427196 ci: run upgrade mutation guard in the merge queue against main (#23216)
Follow-up to #23215 (merged). Rebased on `main`.

## Why

#23215 fixes an instance of a class of bug: an upgrade command whose
version is chosen at `generate:instance-command` time from
`TWENTY_CURRENT_VERSION`, then left behind when `main` bumps the version
before the PR merges (base-drift). The command ships one minor early and
instances already on the newer version skip it forever.

The existing `server-previous-version-upgrade-mutation-guard` in
`ci-server.yaml` runs on `pull_request`, so it validates against the
PR's base. When the base is stale (main moved after the branch was cut),
the guard reads the branch's own `TWENTY_CURRENT_VERSION` and the check
passes even though the command is now a version behind main. That is
exactly how the original bug slipped through.

## What

The version-directory and append-only-timestamp validation is extracted
into a shared composite action,
`.github/actions/upgrade-mutation-guard`, diffed against a
caller-supplied `base_sha`. It is called from two places:

- **`ci-server.yaml`** (PR-level guard, `base = pull_request.base.sha`)
for fast feedback. The job keeps its existing name/check. This replaces
~290 lines of inline shell.
- **`ci-merge-queue.yaml`** (new, `merge_group`-triggered, `base =
merge_group.base_sha`). GitHub builds each merge-queue candidate on top
of the current tip of `main`, so the checks read
`TWENTY_CURRENT_VERSION` and the existing per-directory timestamps from
main's real state at merge time.

Because the candidate is rebased onto main, base-drift is caught by
construction: the same validation simply runs where the base is
guaranteed current. No origin/main comparison hack; the logic now lives
in one place.

## Bypass semantics

The guard has two independent checks, and they are treated differently
on purpose:

- **Version-directory check** keeps its
`ci:allow-previous-version-upgrade-mutation` bypass, a deliberate,
reviewed escape hatch for legitimately touching a previous-version
directory. The PR-level guard reads the label directly; the merge-queue
guard resolves it from the queued PR (the `merge_group` event carries no
labels) and passes it to the composite action, which skips only the
version-directory step.
- **Timestamp / append-only check has no bypass.** The old
`ci:allow-upgrade-command-timestamp-exception` label is removed. A fake
or out-of-order timestamp rewinds the upgrade cursor and re-hides
already-applied columns, so there is no "allowed" version of it: the
timestamp just has to be configured correctly (real epoch millis,
strictly greater than every existing command in the same version
directory). If a blocking existing max is itself a fabricated future
timestamp, re-slot that command to its real merge epoch rather than
reaching for a bypass.

Preventing previous-version mutation is the guard's primary purpose. In
the merge queue the guard job always runs and skips only the
version-directory step when the bypass label is set, so it reports a
real success/failure (the required check never resolves to a skipped
state, and a label-lookup failure fails closed) and the timestamp check
always runs.

## Requires a settings change (not in this diff)

Enabling the merge queue and marking the check required are
branch-protection settings, not file changes. After merge, an admin
needs to:

1. Enable the merge queue for `main` in branch protection.
2. Add `CI - Merge Queue / upgrade-mutation-guard` to the merge queue's
required checks.

## Notes

- Composite action, not a `workflow_call` reusable workflow,
deliberately: converting the `ci-server.yaml` job to a reusable-workflow
call would rename its status check to
`server-previous-version-upgrade-mutation-guard / ` and break that
required-check mapping in branch protection. A composite action dedups
the logic while keeping both callers' check names intact.

---------

Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com>
2026-07-24 14:37:45 +02:00
github-actions[bot] c0c5ba33e8 i18n - translations (#23269)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23269?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 14:20:10 +02: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 923035bf48 Extract front component host wrapper into hooks (#23262)
Refactors `createHtmlHostWrapper` into composable hooks
(`useHtmlHostElementProps`, `useComposedElementRef`,
`useCaretPreservingElementRef`) as groundwork for the geometry mirror.

Behavior-focused, no feature change:
- Caret preservation moves to a stable ref + `useLayoutEffect`
re-assertion (covered by the caret suites). Highest regression surface
in the series, isolated here for focused review.
- The remote `ref` prop is now swallowed via `INTERNAL_PROPS` instead of
leaking onto host elements.

First of three PRs splitting the geometry mirror work.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23262?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:40 +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
Abdullah. d53ef11fe2 fix: bump linkify-it to 5.0.2 (Dependabot) (#23236)
Bumps **linkify-it -> 5.0.2** (sole descriptor `^5.0.1`, caret already
permits it; recursive `yarn up`, lockfile-only, no resolution). Clears
[1799](https://github.com/twentyhq/twenty/security/dependabot/1799)
(GHSA-v245-v573-v5vm, high). `yarn install --immutable` passes. 5.0.2
published 2026-07-01, clears the age gate.
2026-07-24 16:38:58 +05:00
Raphaël Bosi bb22b216db Fix front components rendering a blank panel in Firefox (#23213)
Fixes #22973

In Firefox, front components rendered a blank panel with only
`DataCloneError: Exception object could not be cloned` in the console.
Accessing `caches` in the opaque-origin sandbox worker throws a Gecko
`Exception` (worker-side CacheStorage code up to v2.22, or any component
code touching it since), and `@quilted/threads` posts thrown values raw
over the MessagePort. Firefox cannot structured-clone these exceptions,
so the error report itself failed and the render promise never settled.

Thread errors are now flattened to clonable payloads and rehydrated on
the other side, so the real error surfaces in the error box instead of
silently hanging the panel. Also makes the CacheStorage guards
exception-safe (v2.23 already moved that code host-side, which removed
the main trigger).

Verified end to end in stock Firefox 149: before, a component touching
`caches` hangs silently; after, render rejects with the full
`NS_ERROR_FAILURE` diagnostic. Chromium behavior unchanged.

```mermaid
sequenceDiagram
    participant Host as Host (React)
    participant Worker as Sandbox worker (null origin)
    participant Threads as @quilted/threads

    rect rgb(250, 235, 235)
    note over Host,Threads: Before — Firefox hangs
    Host->>Worker: render(component)
    Worker->>Worker: throws Gecko Exception<br/>(typeof caches)
    Worker->>Threads: postMessage(rawException)
    Threads--xHost: DataCloneError:<br/>Exception could not be cloned
    note over Host: CALL_RESULT never arrives<br/>render() promise never settles → blank panel
    end

    rect rgb(232, 245, 233)
    note over Host,Threads: After — error surfaces
    Host->>Worker: render(component)
    Worker->>Worker: throws Gecko Exception
    Worker->>Threads: serialize → { name, message, stack }
    Threads->>Host: postMessage(clonable payload)
    Host->>Host: rehydrate → Error, reject render()
    note over Host: error box shows NS_ERROR_FAILURE
    end
```

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23213?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 13:14:47 +02:00
github-actions[bot] ae2c8978d1 i18n - docs translations (#23258)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 13:12:20 +02:00
Weiko e5c9fcf058 Add PostgreSQL connection pool pressure metrics (#23251)
## Summary
- Add pool gauges for total, idle, waiting, and maximum connections
- Record PostgreSQL connection acquisition duration and failures
- Instrument core, workspace primary, and optional replica data sources
- Add unit tests covering gauges, acquisition timing, failures, and
deduplication


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23251?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 13:03:12 +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] a3a6a55051 i18n - docs translations (#23250)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 11:26:33 +02:00
martmull fdb8865933 test: cover uninstall logic function hook execution (#23249)
Follow-up to #23227 ([review
comment](https://github.com/twentyhq/twenty/pull/23227#pullrequestreview-4771629391)):
adds integration coverage for the `defineUninstallLogicFunction` hook
execution on uninstall.

## What

New integration suite
`successful-uninstall-application-logic-function-hook.integration-spec.ts`
that drives the real `syncApplication` / `uninstallApplication` GraphQL
flow and asserts on the executor wiring:

- When the synced manifest declares an `uninstallLogicFunction`,
uninstalling the application resolves the hook and calls
`LogicFunctionExecutorService.execute` exactly once, before deletion,
with the `{ version }` payload.
- When the manifest declares no uninstall hook, uninstalling the
application does not call the executor.

The executor is spied via the running app container
(`getAppProviderByClassName`) and stubbed to a success result, so the
test verifies the server-side resolution/trigger path deterministically
without depending on the local function runtime.

## Test plan

- `npx jest --config ./jest-integration.config.ts
successful-uninstall-application-logic-function-hook` passes (2/2).
- Typecheck green for twenty-server; oxlint and oxfmt clean on the new
file.


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

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

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-24 09:26:04 +00:00
martmull f793f3c5a9 Fix application logos resolving to null on install and sync (#23245)
## Problem

Application icons are no longer resolved: `logo` is `null` on
`FindManyApplications` / `FindOneApplication`, so app chips and the
settings applications table fall back to initials avatars.

## Root cause

Manifests produced by the current SDK carry the logo path in
`manifest.application.logo`; `logoUrl` is deprecated and stripped by
`normalizeApplicationAssets` (external URLs are dropped, relative ones
are moved to `logo`). Three server call sites still read only the
deprecated `logoUrl`:

- `application-sync.service.ts` (`syncApplication`): wrote `logo:
manifest.application.logoUrl ?? null` on every install/upgrade/dev sync,
overwriting `application.logo` with `null`
- `application-sync.service.ts` (`buildVirtualDryRunFlatApplication`):
same read on the dry-run path
- `application-install.service.ts` (`ensureApplicationExists`): same
read on the create path

Since both `Application.logo` and the `Application.logoUrl` resolve
field derive from that column, icons went null everywhere.

Separately, OAuth-only apps (e.g. "Twenty CLI" from dynamic client
registration) never get a logo at all: `OAuthRegisterInput` accepts
`logo_uri` but the registration controller dropped it, so
`applicationRegistration.logoUrl` also resolves to null for those.

## Fix

- Read `manifest.application.logo ?? manifest.application.logoUrl ??
null` at all three sites, matching the fallback already used by
`importLogoFile` and `fromManifestApplicationToDisplayFields`
- Persist `logo_uri` into `applicationRegistration.logo` on OAuth
dynamic client registration; `buildLogoUrl` passes absolute URLs
through, so the consent screen and app chips can resolve it

Existing rows that were already nulled will self-heal on the next app
upgrade/sync, since the sync path rewrites `logo` from the manifest.

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

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 10:52:11 +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 abc82d66b7 Consolidate per-queue worker tuning in one explicit config file (#23229)
## Context

Follow-up to the worker configuration analysis and to the worker pool
split rolled out in twentyhq/twenty-infra#805/#806. Worker tuning was
previously spread across two partial constants (`QUEUE_WORKER_OPTIONS`,
`MESSAGE_QUEUE_PRIORITY`), and most queues silently relied on implicit
BullMQ defaults.

## What this PR does

Introduces a single dedicated file to pilot worker behavior per queue:


`src/engine/core-modules/message-queue/message-queue-worker-config.constant.ts`

`MESSAGE_QUEUE_WORKER_CONFIG` declares, for **every** queue, an
explicit:
- `priority` (applied when enqueuing, lower runs first)
- `concurrency`
- `lockDuration`
- `maxStalledCount`
- `boundedShutdownDrain`

Explicitness is enforced at compile time: the record is typed
`Record<MessageQueue, { priority: number; workerOptions:
Required<MessageQueueWorkerOptions> }>`, so adding a queue without
declaring its full configuration is a type error, and no field can be
omitted.

Wiring changes:
- `message-queue.explorer.ts` passes
`MESSAGE_QUEUE_WORKER_CONFIG[queueName].workerOptions` when creating
workers
- `bullmq.driver.ts` reads the enqueue priority from the same record
- `message-queue-worker-options.constant.ts`,
`message-queue-priority.constant.ts` and
`ai-stream-lock-duration.constant.ts` are removed (the AI stream lock
duration is inlined into the one config entry that used it)

## Behavior

No behavior change — the previously implicit BullMQ defaults
(concurrency 1, lockDuration 30s, maxStalledCount 1) are now spelled out
per queue, and the existing overrides (`ai-stream-queue`: concurrency 20
/ 10 min lock / no stall retry / bounded shutdown drain;
`logic-function-queue`: concurrency 10) and all priorities are carried
over unchanged.

## Validation

- `npx nx typecheck twenty-server` 
- `oxlint --type-aware` + `oxfmt --check` on changed files 
- `npx jest "message-queue"` (7 tests) 

Companion infra PR: twentyhq/twenty-infra#808 moves the worker pool
topology (replicas, resources, queue filters) into a dedicated
`workers.yaml` per environment.

Session: https://claude.ai/code/session_01TL6Te48Lkys5NxyG9j2Nz6
2026-07-24 10:45:16 +02:00