Commit Graph

13144 Commits

Author SHA1 Message Date
Kylen Nguyen 407273cff2 fix(docs): correct typos in multiple files (#22173)
Corrects spelling errors across three documentation files.

- `indivual` -> `individual` in `./CLAUDE.md`
- `accesible` -> `accessible` in
`./packages/twenty-docs/user-guide/getting-started/how-tos/navigate-around-twenty.mdx`
- `editting` -> `editing` in
`packages/twenty-docs/user-guide/views-pipelines/overview.mdx`

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

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-06-25 15:43:08 +02:00
Akash! 87329c8810 fix(ask-ai): resolve stream subscription race condition on new thread… (#21916)
## Description
Resolves a race condition in the Ask AI feature where the first
assistant reply in a newly created thread does not stream into the UI
and only appears after sending a second message.

### What's Changed
- **Immediate Thread Subscription:** Updated `useAgentChat.ts` to
immediately set `currentAiChatThread` to the newly generated `threadId`
instead of deferring it until after the `SEND_CHAT_MESSAGE` mutation
finishes.
- **The Bug:** Previously, the backend worker processed the AI chat job
so quickly that the stream completed and fired the `message-persisted`
event *before* the frontend established the SSE subscription.
- **The Fix:** By setting the thread ID immediately, the
`useAgentChatSubscription` hook now properly connects and listens to the
SSE stream before the backend begins emitting chunks, guaranteeing the
first message streams seamlessly.

### How to Test
1. Open the Ask AI panel and start a completely new thread.
2. Send an initial message (e.g., "Hello!").
3. Observe that the AI's response successfully streams into the chat
without needing a workaround or page refresh.

Closes #21694

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-25 15:41:56 +02:00
Raphaël Bosi c3183f7828 Forward parent commits to Argos visual regression dispatch (#22174)
Part of the Argos orphan-build fix. The dispatch now lists the
merge-base plus its ancestors (up to 100) and forwards them as
`parent_commits`, so the self-hosted Argos can walk back to the nearest
commit with a reference build instead of orphaning when the exact
merge-base lacks one.

Companion to twentyhq/twenty-argos#11 (deploy that first) and the
ci-privileged change that passes the input through to build creation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22174?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 15:21:43 +02:00
nitin 4f429565e1 call recorder polishes (#22170)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22170?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 18:27:43 +05:30
Weiko 864ea452b4 fix mobile side panel close (#22169)
The side panel close (X) button was hidden on all mobile views, while
the back button only renders when there is navigation history. When the
side panel is opened at the root (e.g. viewing a record directly with a
single-item navigation stack), neither button was shown, leaving no way
to dismiss the panel on mobile.

Keep the close button available on mobile whenever there is no back
button to fall back on, so the panel is always dismissable.

## Before


https://github.com/user-attachments/assets/61891d25-26b8-4ba4-8b05-73fd44f92d89

## After


https://github.com/user-attachments/assets/41342722-bcaf-420b-83bb-3cafaec49516
2026-06-25 14:55:04 +02:00
Charles Bochet d8cb4aa15b fix(filter): resolve filter-derived date defaults with Temporal (fixes create/update crash in date-filtered views) (#22124)
## Symptom

Creating or updating an Opportunity on a board view crashes with:

```
Uncaught TypeError: e.split is not a function
    at splitDateString (date-fns) → parseISO → isMatchingDateFilter
    → isRecordMatchingFilter → <group-by optimistic effect> → createOneRecord
```

`e` is a non-string (a `Date` object, shown as `{}` in the debugger).
Distinct from the `null` case fixed in #22029.

## Root cause

Reproduced on a live board filtered by **Close date — Is relative —
"This quarter"** (a `DATE_TIME` field).

When you create a record in a filtered view, `useCreateNewIndexRecord`
derives default field values from the view's filters via
`buildRecordInputFromFilter` → `buildValueFromFilter`. For a date field,
`computeValueFromFilterDate` returned a JS **`Date` object** (`new
Date()` / `new Date(value)`), assigned to the new record verbatim. The
optimistic record's `closeDate` was then a `Date`, not an ISO string,
and the group-by optimistic effect matched it via `parseISO(dateObject)`
→ `dateString.split is not a function`, crashing every create/update in
the view.

## Fix

`computeValueFromFilterDate` now returns timezone-aware **ISO strings**
via Temporal, mirroring what `turnRecordFilterIntoGqlOperationFilter`
produces for the same filters — so a record created in a date-filtered
view actually satisfies its own filter:

- **`DATE_TIME`** → an instant. Date-only filter values (a `DATE_TIME`
"is" filter stores `yyyy-MM-dd`, no time) are resolved to the start of
day in the user's time zone, matching how the filter operands are built
— `Temporal.Instant.from()` alone would `RangeError` on them.
- **`DATE`** → a plain date `yyyy-MM-dd` resolved in the **user's time
zone** (`Temporal.Now.plainDateISO(timeZone)`). A bare `new
Date().toISOString()` would use the UTC date, which near midnight is the
wrong calendar day for non-UTC users, so the new record could miss its
own `IS_TODAY`/relative filter. The time zone is threaded from
`useUserTimezone` (same source the filter side uses).
- `IS_BEFORE` subtracts 1 day for `DATE` / 1 minute for `DATE_TIME` (the
`-1 minute` special-case moved out of `buildRecordInputFromFilter`,
which now just assigns the string).

No `Date` object ever reaches `parseISO`, and the value matches the
filter operand, so the optimistic card lands in the right place.

## Tests

- `buildValueFromFilter.spec.ts`: every date operator returns an ISO
**string** (round-tripped to the expected instant); a `DATE` block
asserts date-only `yyyy-MM-dd` output and that `IS_TODAY` resolves to
the correct calendar day **per time zone** at a UTC day boundary
(`2024-03-20` UTC vs `2024-03-21` Asia/Tokyo); a date-only `DATE_TIME`
`IS` value resolves to start-of-day in the user time zone (UTC vs
America/New_York) without throwing.
- `buildRecordInputFromFilter.test.ts`: filter-derived date values are
ISO strings, not `Date` objects.

## Verified locally (Chrome)

Reproduced the exact prod scenario on the **By Stage Opportunities
Kanban board** (the group-by optimistic effect) with a **Close date — Is
relative** filter:

- Created a card in a column → **no `split is not a function` crash**;
the card got a valid Close date (`now` for the relative filter, e.g. `25
Jun 2026 13:10`). Console clean.
- Also verified a **table view + Close date — Is** filter: created
record gets a valid start-of-day value (`25 Jun 2026 00:00`).

Both `IS_RELATIVE` (→ now instant) and the date-only `IS` (→
start-of-day in the user tz) paths produce string values that the
optimistic matcher handles without throwing.
2026-06-25 14:51:42 +02:00
Charles Bochet fe1a8ad5f0 fix(ci): patch danger to decline gzip, fixing ERR_STREAM_PREMATURE_CLOSE on Node 24 (#22171)
## Problem

The `danger-js` check (`twenty-utils:danger:ci`) started failing
intermittently with:

```
FetchError: Invalid response body while trying to fetch
https://api.github.com/repos/twentyhq/twenty/pulls/<n>/files: Premature close
  errno: 'ERR_STREAM_PREMATURE_CLOSE'
```

It fails before the Dangerfile even runs, while fetching PR files / diff
/ commits. The existing retry wrapper
([#22151](https://github.com/twentyhq/twenty/pull/22151)) reduced it but
can't absorb longer GitHub-API windows, so checks still go red.

## Root cause

Not "node-fetch is old" generically — a specific recent regression:

- Node **22.23.0 / 24.17.0** shipped a security fix for CVE-2026-48931
(http.Agent response-queue poisoning) that attaches a `'data'` listener
to idle keep-alive sockets.
- `node-fetch@2` misreads that listener as an unclean connection close —
but only on **gzip-encoded responses without `Content-Length`**, which
is exactly what `api.github.com` returns.
- The GitHub-hosted runners rolling into the patched Node 24.17.x in
recent weeks is why this surfaced now.

See
[danger/danger-js#1515](https://github.com/danger/danger-js/issues/1515),
[nodejs/node#63989](https://github.com/nodejs/node/issues/63989).

## Why this approach

- `node-fetch@2` can't be removed downstream — Danger imports it
directly, and it's pervasive transitively (gaxios/googleapis). Dropping
it is an upstream migration.
- We don't want to pin an old Node version.

So: bump `danger` 13.0.4 → 13.0.8 and backport
[danger/danger-js#1516](https://github.com/danger/danger-js/pull/1516)
via a yarn patch — set `compress: false` on Danger's shared `api()`
wrapper. GitHub then returns identity-encoded responses with
`Content-Length`, and node-fetch's faulty premature-close detector never
fires. Negligible bandwidth cost on these small JSON payloads; explicit
caller overrides are preserved via an `=== undefined` guard.

## Changes

- `packages/twenty-utils/package.json` — `danger` → patched 13.0.8
- `yarn.lock` — registers the `danger@patch:` resolution
- `.yarn/patches/danger-npm-13.0.8-48aba2788c.patch` — the `compress:
false` fix

## Verification

- Patch dry-run applies cleanly against pristine danger 13.0.8 source.
- Inspected yarn's materialized patched cache package — the `compress`
fix is present in the linked `distribution/api/fetch.js`.
- Confirmed the failing calls (`getPullRequestInfo` /
`getPullRequestCommits` / `getPullRequestDiff`) all route through
`this.api` → the patched wrapper.

## Lifecycle

Temporary backport. When #1516 ships in a Danger release, drop the patch
and bump to that version (flagged in a comment inside the patch). The
existing CI retry wrapper stays as defense-in-depth.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22171?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 14:32:25 +02:00
Thomas des Francs 2aeacf341f Flatten system object pickers (#22161)
## Summary
- Flatten system object entries into the first-level workflow object
pickers.
- Flatten dashboard Source and record-page Field picker advanced/system
entries into the main searchable list.
- Keep regular entries first, place system/advanced entries at the
bottom, and cap page-layout picker height at 340px.
- Add keyboard selection support to workflow object pickers through
`SelectableList`.

## Review notes
- Removed now-unused Advanced submenu state, submenu headers, and
duplicated filtering paths.
- Kept the width behavior scoped to each existing dropdown; the shared
page-layout wrapper only controls height/scrolling.
- No blocking issues found in the final reviewed diff.

## Screenshots

### Workflow record type picker
| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/dc3f7ef933bdca6c39599c45c827db0f27f01e7d/before-workflow-record-type-advanced.png"
width="320" /> | <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/2ee195a8562e13d0f4307e7891ab60cfaecae8cf/after-workflow-record-type-flat.png"
width="320" /> |

### Dashboard Source picker
| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/45f57728ec7643c7048e857829e977d49b9b365c/before-dashboard-source-tall.png"
width="420" /> | <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/f8b864ea5e3d04a00b9b6a613e8bf97dd545933c/after-dashboard-source-340px.png"
width="420" /> |

### Record page Field picker
| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/ea5f14ccac6c0d9b3d953b4047dc7491702f756a/before-record-field-advanced.png"
width="320" /> | <img
src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/9d0717cf3cfcb47ed17d595ba83b19a37718c3eb/after-record-field-flat.png"
width="320" /> |

## Checks
- `npx oxfmt --check` on touched files
- `git diff --check`
- `npx tsc -p tsconfig.json --noEmit --pretty false --noErrorTruncation
| rg
"(ChartDataSourceDropdownContent|FieldWidgetFieldDropdownContent|PageLayoutDropdownContentContainer|WorkflowObjectDropdownContent|WorkflowEditTriggerDatabaseEventForm|WorkflowEditActionFindRecords|WorkflowEditActionPickRecord|ChartSettingItem)"`
returned no touched-file diagnostics
- Browser verification: dashboard Source top/bottom, record-page Field
top/bottom, workflow Record Type top/bottom, and ArrowDown selection in
workflow Record Type menu


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22161?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 14:10:30 +02:00
Abdul Rahman db5338cf32 Feat: group records by many to one relation (#22123)
## Group records by relation (Kanban + Table)

Adds grouping by `MANY_TO_ONE` relation fields on both board and table
views, reusing the existing `ViewGroup` storage (`fieldValue = related
record id`).

- **New group** record picker to create relation-backed groups (board
column + table row)
- Relation-aware group headers (name/avatar), filtering, and drag-drop
(writes the FK join column) — all using one canonical `${name}Id` column
- Sort menu hides alphabetical options when grouping by a relation (no
comparable title)
- A group whose backing record no longer exists renders a "Deleted" chip
instead of a blank header
- **Backend:** allow `MANY_TO_ONE` relations as the Kanban
`mainGroupByField` in the flat-view validator


https://github.com/user-attachments/assets/267077a6-2667-4506-b178-eee420a16f20



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22123?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 13:55:50 +02:00
Weiko 108cc5b6a5 API key creation triggers unnecessary ORMEntityMetadatas cache recomputation (#22168)
## Context

Creating an API key only changes role/apiKey-related data, but the
workspace migration runner was also invalidating and recomputing the
metadata caches (`ORMEntityMetadatas` and `graphQLResolverNameMap`) on
it.

The root cause is a single `if` block in
`getLegacyCacheInvalidationPromises` that gated **all** caches with an
`||` condition combining the metadata and role/permission conditions:

```ts
if (
  shouldIncrementMetadataGraphqlSchemaVersion ||
  shouldInvalidateRoleMapCache ||
  shouldInvalidateRolesPermissionsCache
) {
  // recomputes role caches AND ORMEntityMetadatas + graphQLResolverNameMap
}
```

So any role-only change (such as API key creation, which sets
`shouldInvalidateRoleMapCache`) also recomputed `ORMEntityMetadatas` —
an expensive recomputation that does not depend on role data.

## Fix

Split the combined block into two independent blocks, each gated by its
own condition:

- `shouldIncrementMetadataGraphqlSchemaVersion` → invalidate/recompute
only the metadata-derived caches: `ORMEntityMetadatas` and
`graphQLResolverNameMap`
- `shouldInvalidateRoleMapCache ||
shouldInvalidateRolesPermissionsCache` → invalidate/recompute only the
role/permissions caches

`graphQLResolverNameMap` is built from `flatObjectMetadataMaps` (see
`WorkspaceResolverNameMapCacheService`), so it is grouped with
`ORMEntityMetadatas` in the metadata block rather than the role block.

## Result

- Role-only changes (e.g. API key creation) no longer trigger
unnecessary `ORMEntityMetadatas` / `graphQLResolverNameMap`
recomputation.
- Metadata-only changes no longer recompute the role/permissions caches.
2026-06-25 13:53:58 +02:00
Raphaël Bosi 19bbd59b53 Support CSS imports in front-components via runtime style injection (#22150)
Front-components compile to a remote-dom worker, so a CSS import like
`import 'twenty-ui/style.css'` can't load a stylesheet and was breaking
the build at the manifest step.

This makes the build inline an imported CSS file as a runtime `<style>`
injection that flows through the existing style bridge into the host.
Because the CSS is bundled alongside that same build's hashed class
names, an app's styling matches its own twenty-ui version regardless of
which version the host ships — no server, manifest, or host changes
needed.

The manifest extractor keeps the no-op CSS loader (it executes the
bundle in Node, where `document` is undefined); the inject plugin runs
only in the real build and the dev watcher.
2026-06-25 13:31:00 +02:00
Rashad Karanouh 2af5370749 v1.1.16 — Restore Partner slug in side panel and Notes tab (#22165)
## Summary

**Package version:** `1.1.16`

- Adds **slug** to the Partner record-page `FIELDS_WIDGET` view so it
appears in the side panel for admins and partners (partners remain
update-locked on slug via `partner.role.ts`).
- Restores the **Notes** tab on the custom Partner `RECORD_PAGE` layout
— the marketplace v2 layout replaced the platform default but only
included Home + Timeline.

## Test plan

- [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0
errors
- [ ] `yarn twenty dev --once` on a local partners workspace — sync
succeeds
- [ ] Admin: open a Partner record full page → slug visible under Name
in side panel; **Notes** tab present and can create a linked note
- [ ] Partner role (My Profile): slug visible, not editable; Notes tab
works
- [ ] After merge: `deploy` + `install` on prod partners workspace

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22165?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 13:20:53 +02:00
github-actions[bot] 1284099940 i18n - website translations (#22162)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22162?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-06-25 13:19:57 +02:00
Emmanuel Hernández Bazán 981ee6a2d7 fix: stabilize tooltip anchor ID with useRef to prevent hover glitch (#21888)
## Context

Tooltip components were generating a new anchor ID on every render. This
caused a flicker/glitch when hovering: the tooltip would briefly
disappear and reappear because React reconciled the changed ID as a
different element.

## Solution

Changed the anchor ID generation from inline (re-created every render)
to `useRef` (stable across renders). The ID is now created once on mount
and stays the same for the lifetime of the component.

## Test plan

- [x] Hover over any element with a tooltip — no flicker or
disappear/reappear behavior
- [x] Multiple tooltips on the same page still work independently

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21888?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: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-25 13:12:11 +02:00
neo773 ee71a382de IMAP support non RFC compliant servers (#22153)
Some non complaint IMAP server don't send `UIDNEXT` UIDNEXT is the next
message id you subtract with 1 to get total current messages

This does a fallback to searching all UIDs and taking the highest

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22153?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 16:15:13 +05:30
github-actions[bot] a87bc4bed6 i18n - website translations (#22160)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-25 12:31:53 +02:00
martmull a20ebfa880 feat(applications): remove the application custom settings tab (#22156)
## Summary

Removes the application **custom settings tab** feature. This is one
half of #22059, split out so it can be reviewed/merged independently
from the variable-types enrichment.

## Changes

- Remove the `SettingsApplicationCustomTab` component and its tab
entry/rendering in `SettingsApplicationDetails`.
- Stop syncing `settingsCustomTabFrontComponent` from application
manifests — `ApplicationManifestMigrationService` now only syncs the
default role.
- Deprecate the now-unused fields (kept for backward compatibility, no
longer read or synced):
- `ApplicationDTO.settingsCustomTabFrontComponentId` (GraphQL
`@deprecated`)
-
`ApplicationManifest.settingsCustomTabFrontComponentUniversalIdentifier`
- the `settingsCustomTabFrontComponentId` column comment on
`ApplicationEntity`

The DB column is intentionally **not dropped**, so existing
installations upgrade cleanly.
2026-06-25 12:29:45 +02:00
Abdullah. 5a657129f0 Split website stepper into home-stepper and product-stepper sections (#22149)
Reorganizes the flat `sections/stepper/` (30 files — two distinct
steppers plus shared code) into two product-feature-style sections, and
moves the shared code to the shared layers.

- **`sections/home-stepper/`** — the home-page stepper. Renamed
`Stepper` → `HomeStepper` (and the home components → `HomeStepperLottie`
/ `HomeStepperSteps` / `HomeStepperVisualFrame`) for symmetry with
`ProductStepper`. Shell at the root + `components/`/`data/`/`utils/` +
barrel.
- **`sections/product-stepper/`** — the product-page stepper, same
structure.

The 3 files both steppers shared can't live in a shared *section* —
`check-conventions` forbids a section importing another section. So they
moved to the shared layers:
- `StepperProgressRail`, `StepperSwipeDeck` → `ui/`
- `useBreakpointStepSync` → `platform/motion`

Both consumer pages repointed (`@/sections/home-stepper`,
`@/sections/product-stepper`); the row-gap allowlist in
`check-conventions.mjs` updated to the new paths; explanatory comments
stripped across the moved files (CSS-in-template comments and `'use
client'` kept).

Pure reorganization — no behavior change. typecheck + lint + build all
green.
2026-06-25 12:24:22 +02:00
github-actions[bot] 904d3d6bff i18n - website translations (#22158)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22158?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-06-25 12:20:32 +02:00
Abdullah. 551a792291 Rework website TasksVisual to match twenty-front tasks tab (#22143)
- Rework `TasksVisual` to match twenty-front's tasks tab: per-group
bordered cards with row dividers, `TODO`/`DONE` headers, the `+ Add
task` button, and twenty-front's checkbox, row, and target-chip styling.
- Enable the checkbox to toggle a task between `TODO` and `DONE`.

Marketing data kept intuitive (single assignee, full dates, action-led
titles).

<img width="859" height="608" alt="image"
src="https://github.com/user-attachments/assets/085c225f-b121-44e6-aaec-553c02fd2367"
/>
2026-06-25 12:20:18 +02:00
Abdullah. d6556221d9 Tidy website ImportVisual markup and CSS (#22148)
Markup/CSS tidy-up of `ImportVisual` — no visual change.

- Collapse the redundant `Grid` wrapper into `Root` (now
`flex-direction: column`) and drop the no-op `justify-content: center`
(the child was already full-width).
- Hoist the duplicated `font-size: previewFontSize(md)` to the mapping
`Row` (the `sm` example keeps its override).

<img width="574" height="604" alt="image"
src="https://github.com/user-attachments/assets/dcc72567-5c13-4d54-884a-f3120cc5a345"
/>
2026-06-25 15:12:08 +05:00
martmull 29e0327063 fix(server): allow moving menu items into a folder created in the same sync (#22130)
## Context

Fixes
[core-team-issues#2593](https://github.com/twentyhq/core-team-issues/issues/2593).

When reorganizing navigation menu items by moving existing items into a
**newly created folder** within a single deploy, the sync failed with
`Parent navigation menu item not found`, forcing a two-step deploy
(create the folder first, then move the items into it).

## Root cause

Migration entities are validated in the fixed order **delete → update →
create** (`workspace-entity-migration-builder.service.ts`). When items
are moved into a new folder in one sync, the items are *updated* (adding
`folderUniversalIdentifier`) while the folder is *created* — but the
update phase runs before the create phase, so the folder isn't yet in
the optimistic maps.

The **creation** validator already handles "parent doesn't exist yet" by
also checking `remainingFlatEntityMapsToValidate`. The **update**
validator couldn't: `FlatEntityUpdateValidationArgs` explicitly omitted
that field, so it only looked at the optimistic maps and threw.

## Changes

- `universal-flat-entity-update-validation-args.type.ts` — stop omitting
`remainingFlatEntityMapsToValidate` from the update args.
- `workspace-entity-migration-builder.service.ts` — pass
`createdFlatEntityMaps` (entities being created in the same migration)
into update validation.
- `flat-navigation-menu-item-validator.service.ts` — resolve the parent
folder against both the optimistic maps and the to-be-created entities,
mirroring the creation validator.
- Integration test — sync an item, then in a second sync create a folder
and move the item into it, asserting it succeeds in a single deploy.

The change is generic and type-safe: all other update validators receive
the new field and simply ignore it. `createdFlatEntityMaps` is
`MetadataUniversalFlatEntityMaps<T>`, matching the field's type.

## Test plan

- [x] Added integration test `should move existing menu items into a
folder created in the same sync`
- [ ] CI green

https://claude.ai/code/session_017pmBkho9Fh6Vjv8WA4m9YE

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22130?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 12:04:18 +02:00
Abdullah. 491b9e954a Rework website EmailsVisual to match twenty-front emails tab (#22146)
- Rework `EmailsVisual` to match twenty-front's emails tab: bordered
thread card with dividers, `Inbox` header sizing, and twenty-front's
row/sender/date colors and hard-clipped sender names (kept `+ Compose`).
- Replace the "not shared" row with four fully-populated shared threads
(subjects/bodies sourced from twenty-server's dev seed), two
participants each, full `MMM D, YYYY` dates.

<img width="575" height="607" alt="image"
src="https://github.com/user-attachments/assets/68d1fd6d-e030-4f3c-b775-687f7e1ab173"
/>
2026-06-25 15:02:44 +05:00
github-actions[bot] cdd1aba3e0 i18n - docs translations (#22154)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22154?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-06-25 11:32:08 +02:00
Charles Bochet f416f81548 fix(ci): retry Danger.js on transient GitHub API fetch errors (#22151)
## Problem

The `danger-js` job in **CI Utils** has been failing across most PRs.
The failure is not a real Danger violation — it's a transient network
error fetching the PR diff/commits from the GitHub API:

```
Failed to fetch GitHub pull request files: FetchError: Invalid response body while trying to fetch
https://api.github.com/repos/twentyhq/twenty/pulls/XXXXX/files?page=1&per_page=100: Premature close
    at Gunzip.<anonymous> (.../node_modules/node-fetch/lib/index.js:400:12)
  errno: 'ERR_STREAM_PREMATURE_CLOSE',
  code: 'ERR_STREAM_PREMATURE_CLOSE'
```

GitHub closes the gzipped HTTP response mid-stream, and Danger's bundled
`node-fetch` has **no retry** on a dropped connection — so any single
blip fails the whole check.

## Why is this happening now?

Nothing on our side changed at the boundary where failures started. The
Node 24.16 bump landed Jun 8 (the job stayed green for 2+ weeks after),
Danger has been pinned at `13.0.4` for months, and there are **zero
commits** to `.nvmrc`, `twenty-utils/package.json`, or the
`yarn-install` action since Jun 22.

What changed is GitHub's API reset rate, and it changed abruptly:

| Day | Failures | Successes | Failure rate |
|-----|----------|-----------|--------------|
| Jun 23 | 1 | 48 | ~2% (green) |
| Jun 24 | 38 | 204 | ~16% |
| Jun 25 | 23 | 25 | **~48%** |

The same PR passes on one run and fails on the next (e.g. one PR shows
up as both pass and fail; another failed 3 runs in a row) — a code bug
can't flip outcomes on identical input, only an infrastructure flake
can. When GitHub's connection-reset rate was ~0% we never noticed; now
that it's in the tens of percent, roughly half of all PRs trip it.

## Fix

Wrap the Danger invocation in a small retry loop (3 attempts, 5s
backoff) so the check absorbs these transient fetch errors instead of
red-flagging the PR. Applied to both the `danger-js` and `congratulate`
jobs since they share the same failure mode.

This is the correct mitigation rather than a code revert — there's no
change on our side to revert. 3 attempts drop a ~48% single-shot failure
rate to ~11%, and a less-degraded ~16% rate to well under 1%. If
GitHub's reliability recovers, the retries simply stop firing and cost
nothing.

This is a CI-only change — no application code is touched.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22151?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-light.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 11:27:51 +02:00
martmull 6a36fc3ba8 fix(sdk): evict compiled manifest modules from require cache in dev mode (#22129)
## Problem

`yarn twenty dev` crashes after running for a while with:

```
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
```

The crash happens during the **Manifest Build** phase, after the process
has been up for a long time (~35 min in the reported case) — the
signature of a slow memory leak in the long-running watch process, not a
single oversized operation.

Fixes the issue described in `core-team-issues#2560`.

## Root cause

`manifest-extract-config-from-file.ts``loadModule()` compiles each
entity file with esbuild (`bundle: true`, so the whole dependency graph
is inlined), writes it to a unique `mkdtemp` directory, and loads it
with `appRequire(tempFile)`.

`createRequire` shares Node's global module cache (`Module._cache`), so
**every loaded temp module is retained in the require cache forever**.
Two things made this unbounded:

1. `mkdtemp` generates a fresh random directory each call, so cache keys
never collide — entries purely accumulate.
2. The `finally` block removed the temp dir **from disk** but never
evicted the `require.cache` entry, so the evaluated, fully-bundled
module object stayed pinned in the JS heap.

In dev mode the orchestrator re-runs `buildManifest` on **every** file
change and watcher rebuild (`scheduleSync()`), and each rebuild compiles
& `require()`s ~one module per entity file (53 in the reported case).
Over a session of editing, thousands of large bundled module objects
pile up in `require.cache` until V8's heap is exhausted → FATAL OOM.

## Fix

Delete the temp module from the require cache after loading it, so
memory stays bounded to a single rebuild. Because `bundle: true` inlines
the whole graph, the temp module is the only cache entry per load, so
deleting it lets the bundled object be GC'd.

```ts
} finally {
  delete appRequire.cache[tempFile];
  await remove(tempDir);
}
```

## Test

Adds a regression test (`manifest-extract-config-from-file.spec.ts`)
that runs `extractManifestFromFile` repeatedly and asserts no
`twenty-manifest` temp modules accumulate in the require cache.

https://claude.ai/code/session_0168hN9yEYvjZbqdz7PckKn8

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22129?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 11:21:51 +02:00
nitin db687c7407 chore(call-recorder): drop FAILED status schema bridge (#22134)
Cleanup of the call-recorder data layer + the SDK 2.16 bump and its
fallout, now that the `FAILED_UNKNOWN → FAILED` rename shipped in
`twenty/v2.16.0` (#22062).

- **Drop the schema bridge.** `executeCurrentSchemaMutation` and the
integration-test compatibility filter existed only to work around
servers exposing `FAILED_UNKNOWN`. Deleted the bridge;
`updateCallRecording` / `completeCallRecordingIngestion` call
`client.mutation(...)` directly. Integration test iterates all
`CallRecordingStatus` values.
- **Bump SDK** `twenty-sdk` / `twenty-client-sdk` to `2.16.0`.
- **One export per file.** Move `CallRecordingUpdateFields` to its own
type file; extract the duplicated media-file shape into
`CallRecordingMediaFile`.
- **Migrate the Recall webhook to `serverRouteTriggerSettings`** (2.16
dropped `serverWebhookTriggerSettings` + its declarative
`workspaceIdResolver`). The webhook is now a **resolver**
(`recall-webhook`) that verifies the Svix signature, reads
`twentyWorkspaceId` from the Recall bot metadata, and returns `{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`; the
platform dispatches to a new **target** function
(`process-recall-webhook`) in the resolved workspace, where
`CoreApiClient` is workspace-scoped. Resolver UID/route unchanged, so
the registered Recall endpoint URL stays valid. Failures now throw →
HTTP 500 (Svix retries) instead of returning 401/400.

Verified: typecheck, 213 unit tests, oxlint, oxfmt all green. **Not yet
verified end-to-end against a live server** — call-recorder is the first
app on `serverRouteTriggerSettings`, so a real Recall webhook should be
tested through the resolver→target path before relying on it.
2026-06-25 11:06:58 +02:00
neo773 dc371ef6e7 rename sync-completion methods to avoid confusion with stage setters (#22138)
`markAsCompletedAndMarkAsCalendarEventListFetchPending` was just
`markAsCalendarEventListFetchPending`
with a prefix, so dropping the prefix silently turned a sync-completion
into a plain stage reset

Renamed to markAsCalendarEventSyncCompleted / markAsMessageSyncCompleted
so they
no longer share a tail with the stage setters. Mirrors the existing
markAsFailed naming. No behavior change.

Sanity check: replayed the original #22015 diff through two isolated
review agents, identical prompt,
only the names differing. With the old names the reviewer explicitly
cleared the branch as safe; with
the new names it flagged the missing completion as high severity. The
rename makes the mistake visible.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22138?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 11:06:38 +02:00
Félix Malfait 5242ddf458 feat(apps): let front components open a record in the side panel (#22140)
## Why

Front components (apps) could `navigate()` to a record's **full page**,
but there was no way to open a specific record in the **side panel**.
More generally, `openSidePanelPage` could navigate to a `SidePanelPages`
enum page but couldn't pass the context most pages need.

## What

`openSidePanelPage`'s params are now a **discriminated union keyed on
`page`**, so each page declares its own typed payload (instead of a flat
bag of optionals whose validity silently depends on `page`). This is
also safer: pages that can't render without context can't be "opened"
into a broken panel.

Wired the param-bearing pages host-side, each bridging to its existing
internal hook:

| `page` | Params | Bridges to |
|---|---|---|
| `ViewRecord` | `recordId`, `objectNameSingular`,
`resetNavigationStack?` | `useOpenRecordInSidePanel` (full-page fallback
on mobile / unsupported objects) |
| `EditRichText` | `recordId`, `objectNameSingular`, `fieldName?` |
`useOpenRichTextInSidePanel` |
| `ComposeEmail` | `connectedAccountId`, `threadId?`, `defaultTo?`,
`defaultSubject?`, `defaultInReplyTo?`, `pageTitle?`, `pageIcon?` |
`useOpenComposeEmailInSidePanel` |
| `ViewFrontComponent` | `frontComponentId`, optional
`recordId`+`objectNameSingular`, `pageTitle`, `pageIcon?`,
`resetNavigationStack?` | `useOpenFrontComponentInSidePanel` |
| *(any other page)* | `pageTitle`, `pageIcon?`,
`shouldResetSearchState?` | `navigateSidePanel` |

`CommandOpenSidePanelPage` now takes the union directly, so headless
command-menu items can open any of these. Threaded through `twenty-sdk`
→ `twenty-front-component-renderer` → host
(`useFrontComponentExecutionContext`), with unit tests per page and the
mobile/unsupported fallbacks.

## Deliberately deferred: `MergeRecords`

`useOpenMergeRecordsPageInSidePanel` takes `objectNameSingular` /
`objectRecordIds` at **hook-init** (it calls `useObjectMetadataItem` /
`useLazyFindManyRecords` at render), so it can't be driven by runtime
app params without refactoring that hook + its current caller. Left out
of this PR — better as its own change.

## Worth a second look (reviewers)

- **`ViewFrontComponent`** lets an app open a front component by id.
Within an app that's clean composition; whether an app should be able to
target *another* app's component is a scoping/security question. The
render still runs under the app's access token, so cross-app fetches
would fail auth — but flagging it explicitly.

## Security note

Side-panel record/page views render natively under the **user's**
session/Apollo client, not the app's scoped token — RLS/field
permissions are enforced as if the user opened it themselves. Same trust
model as `navigate(AppPath.RecordShowPage, …)`.

## Follow-up

A separate PR will centralize the mobile + `canOpenObjectInSidePanel`
guard inside `useOpenRecordInSidePanel` (currently duplicated across
callers, missing in others).

## Validation

> [!NOTE]
> Dependencies wouldn't install in this environment (flaky network
during `yarn install`), so lint / typecheck / jest weren't run locally —
relying on CI. The diff was reviewed manually for type-consistency,
including the discriminated-union narrowing in the host switch.

https://claude.ai/code/session_01AAJFXzsCeoj6BeP3ofiTKQ
2026-06-25 10:47:39 +02:00
neo773 885effb3d8 fix(calendar): mark channel completed on no-events fetch (#22137)
No-events branch left channels stuck on syncStatus=ONGOING and stopped
bumping the active metric (flatlined dashboard), since
markAsCalendarEventListFetchPending only resets the stage.

Switched it to markAsCompletedAndMarkAsCalendarEventListFetchPending so
status, syncedAt, throttle and the metric reset properly, matching the
messaging side. Regression from #22015.
2026-06-25 10:46:56 +02:00
Abdullah. 20f8321dca Rework website FilesVisual to match twenty-front files tab (#22147)
- Rework `FilesVisual` to match twenty-front's files tab: bordered file
card with dividers, `All` header sizing, and twenty-front's row
background and date color (kept the bordered `+ Add file`).
- Four files, one per category (pdf/xlsx/pptx/png), with full `MMM D,
YYYY` dates. File-icon colors already matched `useFileIconColors`.
2026-06-25 10:43:51 +02:00
martmull fa008b4728 chore(create-twenty-app): align scaffold with unified app test/lint/typecheck configs (#22128)
## What

We have unified the unit + integration test, lint, and typecheck
configuration across `packages/twenty-apps/internal` and
`packages/twenty-apps/public`. This PR updates the `create-twenty-app`
scaffolded project template so a freshly generated app complies with
that same setup.

### Scaffold template changes
(`packages/create-twenty-app/src/constants/template/`)

- **`package.json`**: add `typecheck` (`tsgo --noEmit -p
tsconfig.spec.json`) and `test:unit` scripts; add
`@typescript/native-preview` + `vite-tsconfig-paths` devDeps; bump
`vitest` → `^4.0.0` and `packageManager` → `yarn@4.13.0`.
- **`vitest.config.ts`**: resolve path aliases via the
`vite-tsconfig-paths` plugin (matching the unified apps) instead of the
ad-hoc `resolve.tsconfigPaths`.
- **`vitest.unit.config.ts`** (new): unit test config targeting
`src/**/*.test.ts`, identical to existing apps.
- **`tsconfig.json`**: `moduleResolution: node → bundler`, `target:
es2018 → es2020`.
- **`src/__tests__/application-config.test.ts`** (new): a sample unit
test so a fresh scaffold's `test:unit` passes out of the box.
- **`.github/workflows/ci.yml`**: run Lint → Typecheck → Unit tests
alongside the integration tests, mirroring the central
`ci-twenty-apps.yaml` pipeline.
- **`README.md`**: document the new `lint` / `typecheck` / `test:unit`
commands.

### CLI package changes

- **`tsconfig.json` / `tsconfig.lib.json`**: exclude the new
`template/vitest.unit.config.ts` from the CLI's own typecheck/build
(just like the existing `vitest.config.ts` exclusion) — its `vitest` /
`vite-tsconfig-paths` imports aren't deps of the CLI package.

## Notes

- The template's committed `yarn.lock` is an intentional empty seed; the
scaffold runs `yarn install` at creation time, so the new deps
regenerate cleanly — no lockfile change needed.
- The sample unit test asserts `typeof APP_DESCRIPTION === 'string'`
(not truthiness) because the description is optional at scaffold time
and can be empty.

https://claude.ai/code/session_01XiJpkiQG3TiDjKncx2XSQq

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22128?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 10:23:51 +02:00
Paul Rastoin f04db9751f fix(client-sdk): bundle metadata client into a single self-contained file (#22085)
## Problem

A front component that imports `MetadataApiClient` from
`twenty-client-sdk/metadata` crashes at render time:

```
FrontComponent error: Failed to resolve module specifier "./chunk-Dqa2HsxW.mjs".
Invalid relative url or base scheme isn't hierarchical.
```

(hash differs per build). The equivalent component using `CoreApiClient`
from `twenty-client-sdk/core` works fine.

## Root cause

The front-component renderer loads each SDK client as a **single
in-memory blob-URL module** and only rewrites the two bare specifiers it
knows (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`). A
blob-URL module cannot resolve a **relative** `import … from
"./chunk-*.mjs"` (blob URLs aren't hierarchical), and that chunk isn't
served anyway.

Only two entrypoints are externalized by the front-component build
(`FRONT_COMPONENT_EXTERNAL_MODULES`) and thus served as blob modules:
`core` and `metadata`. Everything else (`rest`, `generate`) is bundled
into the component and is unaffected. Of those two:

| client | how `dist/*.mjs` is produced | self-contained? |
|---|---|---|
| **core** | esbuild single-file bundle (`compileGeneratedClient`),
re-run per workspace at server runtime by `replaceCoreClient` |  |
| **metadata** | the shared multi-entry Vite build, which hoists shared
code into a relative `chunk-*.mjs` |  |

The metadata client is built once at package-build time (it is not
workspace-specific) and was shipped straight from the multi-entry Vite
output, keeping the unresolvable relative chunk import.

## Regression trace

This was **not** broken on arrival — it regressed via a transitive
bundler swap:

| Date | Commit | Event |
|---|---|---|
| 2026-05-20 | `a26fe3bb65` | Metadata-client-in-front-components
shipped; `twenty-client-sdk` on **Vite 7 (Rollup)** |
| 2026-06-08 | `d2e7dc0e74` (#21309, *"security: bump vulnerable direct
dependencies"*) | Bumped **Vite 7 → 8**, introducing **Rolldown 1.0.3**
(no rolldown entries in the lockfile before this commit) |

Vite 7 is Rollup-based; Vite 8 uses Rolldown. The breaking artifact is
literally a `\0rolldown/runtime.js` shared chunk — a Rolldown construct
that could not have existed before the bump. So the metadata
front-component path worked from 2026-05-20 until the 2026-06-08
security dependency bump silently changed the bundler and split out the
shared runtime chunk.

## Fix

Build the metadata client as its **own single-entry Vite library**
(`vite.metadata.config.ts`) so its output is a single self-contained
file with no shared chunk. `core` / `rest` / `generate` stay in the main
multi-entry build (`vite.config.ts`); shared config (`isExternal`,
`entryFileNames`) is factored into `vite.shared.ts`. The build pipeline
runs `vite build && vite build -c vite.metadata.config.ts`.

The server picks this up automatically: `SdkClientGenerationService`
ships the pre-built package `dist/` and only regenerates the **core**
client; it never regenerates metadata. No server-side change required.

## Regression guard (e2e)

The postcard example's `card.front-component.tsx` previously used
`CoreApiClient` only, so this metadata-only regression had no e2e
coverage. It now loads and round-trips all three SDK clients (`Core`,
`Metadata`, `Rest`) via an SDK health panel, and the e2e asserts the
blob-served `core` + `metadata` probes reach `ok` — which only happens
if those bundles resolve and function. A future chunk-import regression
in either blob module would crash the component on load and fail the
test.

## Verification

- `npx nx build twenty-client-sdk` succeeds.
- `dist/metadata.mjs` / `dist/metadata.cjs`: **0** `chunk-*` imports,
**0** relative imports; both load and export `MetadataApiClient` +
`MetadataSchema`.
- `dist/metadata/index.d.ts` types still emitted.
- `npx nx typecheck` + `npx nx lint twenty-client-sdk` pass; postcard
app typecheck + lint pass.

## Notes

- `dist/` is not committed (CI builds it); a running server must rebuild
`twenty-client-sdk` for the fix to take effect.
- The e2e was validated statically (typecheck + lint); running it
end-to-end requires a live stack with a seeded postcard record.
2026-06-25 10:19:18 +02:00
Abhinav A P cf91b87892 fix(server): skip defaultValue null check for relation/morph fields on update (#21875)
## Description

Updating any metadata property (e.g. `description`, `label`) of an
existing **non-nullable RELATION** field fails with:

```
INVALID_FIELD_INPUT: Default value cannot be null for non-nullable fields
```

A relation field has no literal `defaultValue` (it's always `null`), so
the update-path validator rejects every required relation. **Creating**
the same field is fine — only **updates** fail.

This also blocks any incremental app re-sync (`yarn twenty dev --once`)
whose diff touches a required relation field.

## Fix

Added a guard in
`FlatFieldMetadataValidatorService.validateFlatFieldMetadataUpdate()`
using the already-imported `isMorphOrRelationUniversalFlatFieldMetadata`
utility to skip the `defaultValue === null` check for relation/morph
field types:

```diff
 if (
+  !isMorphOrRelationUniversalFlatFieldMetadata(
+    flatFieldMetadataToValidate,
+  ) &&
   flatFieldMetadataToValidate.isNullable === false &&
   flatFieldMetadataToValidate.defaultValue === null
 ) {
```

### Why this works:
- Relation fields represent foreign key relationships, not columns with
literal defaults
- The same guard is already used at line 144 in the same method for
relation-specific validation
- The create path (`validateFlatFieldMetadataCreation`) never had this
check, which is why creation always worked
- No new imports needed — `isMorphOrRelationUniversalFlatFieldMetadata`
is already imported on line 14

## Verification
- `npx nx build twenty-server`  compiles successfully

Fixes #21751

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

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-06-25 10:05:31 +02:00
Rashad Karanouh feac2df216 chore(website): remove partners marketplace route lint guard (#22121)
## Summary

Follow-up to #22120. Removes the `check-partners-marketplace-routes.mjs`
lint guard and its `project.json` wiring — the profile fix is just
`force-dynamic` on the page; the extra script is not needed.

## Changes

- Delete
`packages/twenty-website/scripts/check-partners-marketplace-routes.mjs`
- Restore `project.json` lint command to run only
`check-conventions.mjs` (as before #22120)

## Context

The guard was added in #22120 but the removal commit did not land before
merge. This PR cleans that up. No runtime behavior change.

## Test plan

- [ ] `nx lint twenty-website` (or CI) passes without the removed script

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22121?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 05:12:12 +00:00
Félix Malfait b841b92cb5 fix(billing): widen embedded add-card modal and align its buttons (#22141)
## Context

Follow-up to #22125. The embedded "Add your credit card" modal had two
visual issues:
1. **Too narrow** — it used `narrowWidth` (~368px), which capped the
Stripe Payment Element too tightly (cramped card fields, country
dropdown, and the Google Pay / Cash App / bank-account accordions).
2. **Mismatched buttons** — the submit used `MainButton` (onboarding's
filled CTA) while Cancel used `Button` (the settings/`ConfirmationModal`
outlined style), so the two never matched.

No new components or behavior — purely prop/component swaps within
existing primitives.

## Changes

- **`AddCreditCardModal`**: drop `narrowWidth`, use `size="medium"` so
the Payment Element has room.
- **`AddPaymentMethodForm`**: replace `MainButton` with `Button`
(`variant="secondary"`, `accent="blue"`, `fullWidth`), using `Button`'s
built-in `isLoading` spinner. Both buttons are now full-width outlined
`Button`s, matching the sibling `StartSubscriptionConfirmationModal`
(which renders `ConfirmationModal`'s blue-accented confirm + plain
cancel). Removed the now-unused `MainButton`/`Loader` imports and
`StyledButtonContainer`.

## Notes
- Frontend-only; no GraphQL/schema/codegen impact.

https://claude.ai/code/session_01VU7SfrSgaYWr2AhVL8DMfu

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22141?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 06:52:49 +02:00
martmull 4277f8f04f fix(sdk): render CLI OAuth success page on localhost (twenty dev) (#22131) 2026-06-25 06:11:49 +02:00
github-actions[bot] 7a23a3d851 i18n - docs translations (#22142)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22142?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-06-25 05:33:22 +02:00
Félix Malfait 94dbcc27a9 feat(billing): embed credit card form in the add-card trial-end modal (#22125)
## Context

When a trialing workspace (trial without a credit card) clicks **Add
Credit Card** from the "End trial period" banner or the AI-chat
usage-limit banner, the modal currently redirects the browser to
Stripe's hosted billing portal to collect the card. Since we already
embed the Stripe Payment Element in onboarding, this brings the same
in-app experience to the trial-end modal so the whole flow stays inside
Twenty.

## Why the onboarding flow couldn't be reused as-is

The onboarding embed (`createSubscriptionPaymentIntent` /
`SubscriptionPaymentForm`) **creates a new subscription** with
`payment_behavior: 'default_incomplete'`. In the trial-end case the
customer **already has a trialing subscription**, so that path throws
`BILLING_SUBSCRIPTION_INVALID`. The correct primitive here is a
**SetupIntent** against the existing customer: collect + save the card,
then end the trial.

A standalone SetupIntent attaches the card to the customer but does
**not** make it the default (the Stripe portal used to do that for us),
so the trial-end invoice would have no payment method. The backend now
backfills the customer default before charging.

## Changes

**Backend**
- `StripeCustomerService`: `createSetupIntent()` for an existing
customer, and `ensureDefaultPaymentMethod()` which sets the customer
default only when none is already set (won't clobber a portal-chosen
default).
- `BillingPortalWorkspaceService.createPaymentMethodSetupIntent()`:
returns a SetupIntent client secret for the current non-canceled
subscription's customer.
- `BillingSubscriptionService.endTrialPeriod()`: ensures a default
payment method before `trial_end: 'now'`.
- New `createBillingPaymentMethodSetupIntent` mutation +
`BillingSetupIntent` DTO; SDK schema snapshot synced.

**Frontend**
- `AddPaymentMethodForm`: Stripe Elements (`mode: 'setup'`), confirms
with `redirect: 'if_required'` so the common card case stays in-app; 3DS
still redirects and is finished by the existing
`EndTrialAfterPaymentMethodEffect`.
- `AddCreditCardModal`: hosts the embedded form.
- Both trial-end banners (`InformationBannerEndTrialPeriod`,
`AIChatNoMoreBillingCreditsBanner`) open the embedded modal instead of
redirecting when no card is on file; the AI-chat path preserves its
thread context in the 3DS return URL.

## Flow

1. User clicks **Add Credit Card** → embedded modal opens.
2. Card entered → `createBillingPaymentMethodSetupIntent` →
`confirmSetup({ redirect: 'if_required' })`.
3. Non-3DS: confirms inline → `endSubscriptionTrialPeriod` →
subscription active, no redirect.
4. 3DS: redirects to `?startSubscriptionAfterPaymentMethod=true` →
existing effect finishes activation.
5. Self-hosted instances without a Stripe publishable key fall back to
the existing portal redirect (the form renders an unavailable state).

## Notes for reviewers
- The metadata GraphQL types were regenerated by hand (codegen needs a
live `/metadata` server, which wasn't available in the authoring
environment); a `graphql:generate --configuration=metadata` run against
a live backend should be a no-op.
- Local `typecheck`/`lint` could not be run in the authoring environment
(dependency install was blocked); relying on CI to validate.
- Scope is intentionally limited to the two trial-end banner modals. The
Settings → Billing "update payment method" link still uses the Stripe
portal.

https://claude.ai/code/session_01VU7SfrSgaYWr2AhVL8DMfu

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22125?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 04:49:31 +02:00
Félix Malfait 179ab2f066 feat(billing): clarify subscription, credits, and add-credits sections (#22126)
## What & why

Redesigns the **content** of the workspace billing settings page so the
numbers that matter are surfaced clearly. Strictly reuses existing
design-system components — no new UI primitives, no color/font changes,
same section structure. All pricing values are pulled from the pricing
config (nothing hardcoded).

## Changes

**Subscription card** (`SettingsBillingSubscriptionInfo.tsx`)
- **Seats** row shows per-seat pricing inline, e.g. `16 × $25 / seat /
mo` (interval-aware unit).
- **"Credits by period" → "Monthly credits"** (interval-aware: "Yearly
credits" on yearly plans).
- New **"Total per month"** row (→ "Total per year" on yearly) with a
helper line via the existing `Label`: `$400 seats + $2,000 credits ·
next charge <date>`. The total is computed at the billing interval, so
it equals the actual next charge.
- **"Switch to Yearly · save 20%"** — discount computed from real
monthly vs. yearly config prices (only appended when > 0).

**Credit usage card** (`SettingsBillingCreditsSection.tsx`)
- Denominator stays total available (allocation + rollover); figures
shown in full (`3,856`, not `3.86k`).
- "Base Credits" → **"This month's credits"** + a **"Rolled over"** row
rendered as `+1,856`.
- Adds a blue `Info` note stating the rollover rule: *"Unused credits
roll over, up to 2× your plan's credits (max 4,000)."* — verified
against the backend cap (`billing-credit-rollover.service.ts`:
`rolloverAmount = min(unused, tierQuantity)`).

**Add-credits section** (`ResourceCreditPriceSelector.tsx`)
- "Resource credits" → **"Add monthly credits"** with a description
clarifying it stacks on the plan and adjusts the bill.
- On selection, shows the resulting **new total** and **new rollover
cap** (2× the new allocation).

**Shared logic**
- New `useBillingSubscriptionCost` hook centralizes the
seats/credits/total math so the subscription card and the add-credits
selector stay consistent.
- Yearly-discount calculation lives in `useBillingWording`
(`getYearlyDiscountPercent`) next to the existing price logic.

All new figures degrade gracefully — the Total row and the add-credits
summary only render when fully computable, so trial/edge states never
show partial numbers.

## Testing

⚠️ `nx typecheck` / `lint` could **not** be run in the authoring
environment (dependency install couldn't complete due to flaky network).
The changes were reviewed manually against the generated GraphQL types
(`BillingPlanKey` = `{ENTERPRISE, PRO}`, `unitAmount: number`,
`creditAmount: Maybe<number>`, nullable
`interval`/`currentPeriodEnd`/`quantity` — all guarded) and a 4-angle
cleanup pass (reuse/simplify/efficiency/altitude). **Please let CI run
typecheck + lint to confirm.**

https://claude.ai/code/session_01GNhCHPfD1SRzAiBACXGCf7

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22126?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 04:13:06 +02:00
Luiz Gabriel 19bdf2122a docs(user-guide): remove unsupported Between operator for Date filters (#22136)
## What

The Filters & Sorting user guide lists a **Between** operator for
**Date** fields, but this operator is not currently supported by the
product.

This removes it from the Date operators table.

| Field Type | Before | After |
|---|---|---|
| Date | Equals, Before, After, **Between**, Is empty | Equals, Before,
After, Is empty |

## Why

Avoids documenting a capability that does not currently exist.

The shared `ViewFilterOperand` enum does not define an `IS_BETWEEN`
operand, and the Date filter operands map does not include a Between
operator.

Related to #20932.

## Notes

- Documentation-only change.
- Only the canonical English source is edited.
- Localized copies under `packages/twenty-docs/l/*` are expected to be
regenerated by the existing docs translation workflow.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22136?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 04:07:35 +02:00
nitin 811fecc119 chore(apps): move call-recorder to public, remove stale meeting-bot duplicate (#22132)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22132?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 00:13:13 +05:30
martmull 14e4656fe3 Update TypeScript moduleResolution to bundler (#22127)
## Summary
Updates the TypeScript `moduleResolution` configuration from `"node"` to
`"bundler"` across template and example tsconfig files. This aligns with
modern TypeScript best practices for bundler-based projects.

## Changes
- Updated `moduleResolution` setting in
`packages/create-twenty-app/src/constants/template/tsconfig.json`
- Updated `moduleResolution` setting in
`packages/twenty-apps/examples/hello-world/tsconfig.json`
- Updated `moduleResolution` setting in
`packages/twenty-apps/examples/postcard/tsconfig.json`

## Details
The `"bundler"` module resolution strategy is the recommended approach
for projects using modern bundlers (Vite, Webpack, etc.) as it provides
better compatibility with ESM and package.json exports field resolution.
This change ensures that new projects created from the template and
example applications follow current TypeScript best practices.

https://claude.ai/code/session_01XBdmaN1bpnE7DiH1XwBuX4
2026-06-24 20:00:59 +02:00
Thomas des Francs e22d31c553 Close side panel after command menu actions (#22118)
## What changed

- Close the command side panel after eligible headless command-menu
actions complete or are confirmed.
- Cover delete, restore, permanent destroy, create view, import, see
deleted, and hide deleted record commands.
- Keep create-record behavior unchanged so a newly created record can
still open in the side panel.
- Add focused Jest coverage for the affected command components and the
create-record exception.

# Before


https://github.com/user-attachments/assets/36632578-3cab-47db-8f6d-350cd6fce683

# After


https://github.com/user-attachments/assets/c9dba19b-7f4a-44aa-ae7f-57660c5a1e96

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-24 19:15:01 +02:00
Charles Bochet ad61d6d8a3 fix(server): dispatch each cron trigger exactly once (#22113)
## Problem

App/logic-function crons occasionally fire **twice, ~1 minute apart**.
The most visible symptom is a notification cron sending the same Discord
DM (or channel post) at e.g. `17:00` and again at `17:01`.

## Root cause

`CronTriggerCronJob` runs every minute (`* * * * *`) and re-dispatches
any logic function whose pattern is "due" according to `shouldRunNow`:

```ts
const diff = Math.abs(prevTriggerDate.getTime() - now.getTime());
return diff < rootCronIntervalMs; // 60_000
```

The detection window (`60_000ms`) is **equal to** the 60s tick interval.
So when a root tick drifts across a minute boundary (runs slightly
early/late, or BullMQ fires a catch-up), two adjacent ticks can both see
the *same* trigger as "within the last 60s" and each enqueue a
`LogicFunctionTriggerJob`. The dispatch isn't idempotent, so the
function runs twice.

## Fix

Make dispatch idempotent, keyed on the trigger itself:

- New `getMatchingTriggerTimestamp(pattern, now)` returns the epoch-ms
of the matched trigger (stable regardless of *when* within the window
the root job runs), or `null`. `shouldRunNow` now delegates to it —
behaviour unchanged.
- Before enqueuing, `CronTriggerCronJob` claims a
`logic-function-cron:{workspace}:{function}:{triggerTs}` key in the
`EngineLock` cache. A second tick that resolves to the same trigger
finds the key and skips.

Distinct triggers always have distinct timestamps (hence distinct keys),
so a later legitimate run is never suppressed. The TTL (2 min) only
needs to outlive the detection window.

## Notes

- `WorkflowCronTriggerCronJob` uses the same `shouldRunNow` pattern and
has the same latent double-dispatch; left out of this PR to keep it
focused, but the new helper makes the same guard a small follow-up.
- The cache `get`-then-`set` isn't atomic; for the observed failure mode
(ticks ~1 min apart, sequential) it's reliable. A Redis `SET NX` would
also close the rare concurrent-multi-instance race.

## Test plan

- [x] `should-run-now.utils.spec.ts` extended: two ticks within one
window resolve to the same timestamp; out-of-window and invalid patterns
return `null`. All 8 pass.
- [x] `oxlint --type-aware` + `oxfmt` clean on changed files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22113?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 19:14:19 +02:00
Raphaël Bosi 02120aac42 Add v2 create-workspace onboarding screen (#22075)
https://github.com/user-attachments/assets/30d69db2-ef50-48b5-8233-d9a36511b5e8

Builds the second step of the new onboarding flow on top of #22027: the
v2 "Create your workspace" screen, shown inside `/welcome-v2` at the
`WorkspaceCreation` step.

What changed:
- New `SignInUpV2Header` (back chevron + Twenty logo) and
`SignInUpWorkspaceCreationFormV2` (left-aligned title/subtitle, logo
upload, Name + Subdomain fields, "Create workspace"), wired into
`SignInUpV2` for the workspace-creation step.
- When a subdomain is taken, a box now lists 3 server-verified-available
alternatives. Backend `SubdomainAvailabilityDTO` returns
`suggestedSubdomains` via a new `findAvailableSubdomains` helper.
- The shared `useWorkspaceSubdomainField` hook is extended additively
(new `suggestions` + `applySuggestionValue`) so the v1 `/welcome` screen
is untouched.

Reviewer notes:
- `generated-metadata/graphql.ts` was hand-patched (metadata codegen
needs a running server).
- Storybook: `Pages/Auth/SignInUpV2 → WorkspaceCreation`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22075?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 17:13:20 +00:00
github-actions[bot] fdea2d6e1e i18n - docs translations (#22122)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22122?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-06-24 19:09:14 +02:00
Emmanuel Hernández Bazán 33d2441a93 fix(ux): input error messages no longer overlap adjacent fields in forms (#21886)
## Context

`InputErrorHelper` used `position: absolute` which took the error
message out of normal document flow. In multi-field grid layouts (e.g.
the HTTP Request workflow node editor), this caused the error text to
render on top of the input below it instead of pushing it down.

## Solution

Removed `position: absolute` from `InputErrorHelper`. The error message
now participates in normal document flow and pushes subsequent content
downward, as expected in modern forms.

## Test plan

- [x] Open the HTTP Request node editor, fill in a field incorrectly —
error message appears below the field without overlapping the next input
- [x] Verify single-field forms still display error messages correctly

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21886?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: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-24 19:07:33 +02:00
Parship Chowdhury 6ee5413951 chore(vite): replace vite-tsconfig-paths with resolve.tsconfigPaths (#22100)
### Summary
Migrates main monorepo packages from the `vite-tsconfig-paths` plugin to
vite’s built-in path resolution.

Vite 8 showing this warning when the plugin is detected:
> The plugin "vite-tsconfig-paths" is detected. Vite now supports
tsconfig paths resolution natively via the resolve.tsconfigPaths option.
You can remove the plugin and set resolve.tsconfigPaths: true in your
Vite config instead.

### References
- https://vite.dev/config/shared-options#resolve-tsconfigpaths
- https://vite.dev/guide/features#paths
- https://github.com/vitejs/vite/pull/21781

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

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
2026-06-24 19:03:18 +02:00
Rashad Karanouh 500f441807 fix(website): force-dynamic partner profiles to stop OpenNext 404 cache (#22120)
## Summary

Partner profile pages (`/partners/profile/[slug]`) returned **404 on
every slug** on OpenNext/Cloudflare while `/partners/list` showed live
partners from the same API.

PR #21963 fixed the list with `export const dynamic = 'force-dynamic'`
but only added `dynamicParams = true` on profiles. That is not
sufficient on OpenNext — the Worker kept serving **cached prerendered
404s** even when `TWENTY_PARTNERS_API_KEY` was present at runtime.

This PR mirrors the list page: **`force-dynamic` on the profile route**,
plus a small lint guard so both marketplace routes stay dynamic.

## Root cause

Partner data is fetched server-side from
`https://partners.twenty.com/s/partners` using
`TWENTY_PARTNERS_API_KEY`. That key is a **Wrangler runtime secret**
(not in `dev.env` / `prod.env`, not available during CI build — by repo
convention).

| Route | Before | Behavior |
|-------|--------|----------|
| `/partners/list` | `force-dynamic` (#21963) | Fetches at request time
on Worker → works |
| `/partners/profile/[slug]` | static + `dynamicParams = true` | Build
prewarm often empty; OpenNext served cached 404 |

## Fix

- Add `export const dynamic = 'force-dynamic'` to
`profile/[slug]/page.tsx` (keep `dynamicParams = true`).
- Add `scripts/check-partners-marketplace-routes.mjs` — fails lint if
list or profile drop `force-dynamic`.
- Wire guard into `project.json` `lint` target (runs before existing
`check-conventions.mjs`).

**No infra changes.** We intentionally did not add a GitHub Actions
secret for the API key — that would contradict the documented pattern
(`wrangler secret put` only).

## Verification

- [x] `node scripts/check-partners-marketplace-routes.mjs` → OK
- [x] `npx jest src/partners-marketplace` → 36/36 pass
- [x] Deployed to **dev** (`deploy-website`, env `dev`, ref
`rk-partner-profile-404`)
- [x] `curl -sI
https://twenty-main.com/partners/profile/atlasprods-technologies-llp` →
**HTTP 200**
- [x] Browser: list → profile link loads

## Test plan

- [ ] CI lint + tests green
- [ ] After merge: deploy prod when ready (`environment: prod`, confirm
`website`)
- [ ] Spot-check `https://twenty.com/partners/profile/<slug>` → 200

## Out of scope

- Build-time `generateStaticParams` prewarm (would need a separate infra
discussion; not required once profiles are `force-dynamic`)
- Per-slug `/s/partner-by-slug` endpoint (optional perf follow-up)

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