Commit Graph

6167 Commits

Author SHA1 Message Date
Etienne 39e00d5853 feat(workflow): expected output schema for runtime-output steps + validation (#21744)
## Summary

Extends the workflow validation layer (introduced in #21422) and adds a
new
"expected output schema" capability for steps whose output structure is
only
known at runtime.

Some workflow steps (HTTP Request, Code, Logic Function, AI Agent
(coming soon), Webhook
trigger) don't have a statically known output shape, so downstream steps
can't
resolve `{{step.x.y}}` variable paths or validate them. This PR lets
users
declare a **sample/expected output** for those steps, derives an output
schema
from it, and uses that schema both to power variable resolution and to
surface
validation issues at build time.

## What's included

### Expected output schema (shared schemas + types)
- New `expectedOutputSchemaShape` reused across the HTTP request, code,
logic
function and AI agent action settings schemas, plus the webhook trigger
  schema (`expectedOutputSchema` optional loose object).
- Mirrored on the server-side action/trigger settings types.

### Output schema computation (server)
- `workflow-schema.workspace-service` now computes a step's output
schema from
  the user-declared `expectedOutputSchema` sample (via
`getOutputSchemaFromValue`) when no statically computed schema is
available.

### Validation layer (server)
- `STEP_HAS_NO_VARIABLE_REFERENCE` (warning): flags steps of
`VARIABLE_CONSUMING_ACTION_TYPES` (HTTP_REQUEST, CODE, LOGIC_FUNCTION,
SEND_EMAIL, record CRUD) that reference no upstream variable.
- `LOGIC_FUNCTION_OUTPUT_SCHEMA_MISMATCH` /
`AI_AGENT_OUTPUT_SCHEMA_MISMATCH`
(warnings): compare the declared output schema against the expected
sample
using the new shared `getOutputSchemaMismatchIssues` util (missing keys,
  leaf/object mismatches, type mismatches).
- Trigger is now validated alongside steps (trigger type requirements +
  trigger variable references).
- Validation issues no longer return both `suggestions` and
`availablePaths`
  when they are identical (avoids redundant, costly payloads).

### Shared utilities
- New `getOutputSchemaMismatchIssues` (+ tests) in
`twenty-shared/logic-function`.
- Moved `agentResponseSchemaToOutputSchema` from `twenty-front` into
  `twenty-shared/ai` so it can be reused on both sides.

### Frontend
- New `WorkflowExpectedOutputBodyInput` component (JSON sample editor
with
validation) used by HTTP request, code, logic function and AI agent step
  editors.
- New `resolvePersistedStepOutputSchema` util + `useStepsOutputSchema`
update:
  resolves a step's output schema from `outputSchema`, falling back to
  `expectedOutputSchema`, with an AI_AGENT default.
- HTTP request / code / logic function editors persist
`expectedOutputSchema`
  and derive `outputSchema` from it.
- Webhook trigger default settings include `expectedOutputSchema`.


BONUS : iterator loop validation

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21744?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-18 10:31:01 +02:00
Félix Malfait ecd90b78b9 fix(front): stop impersonation from corrupting the impersonator's profile name (#21757)
## Problem

A customer reported that after impersonating another user, **their own
account's first/last name had been permanently changed to the
impersonated user's name** (persisted in the DB, surviving logout), and
their actions showed up under the wrong "Updated by".

The "Updated by = impersonated user" part is expected (while
impersonating you genuinely act as that user). The real bug is the
**durable overwrite of the impersonator's profile name**.

## Root cause

About a week ago `currentUserState` became localStorage-backed with
`getOnInit: true` (`currentWorkspaceMemberState` already was).
Impersonation swaps the auth token and does a full reload, but — unlike
sign-out — it never cleared those cached identity keys.

So after the reload:
1. The atoms hydrate **synchronously from localStorage with the previous
identity** (e.g. the impersonated user, on stop), and because
`currentUser` is now non-null the authenticated UI renders immediately
with that stale identity.
2. The network `loadCurrentUser` then corrects `currentUser` /
`currentWorkspaceMember` **in place**.

If **Settings → Profile** was mounted across that in-place identity
flip, `NameFields` — which seeds local `useState` from
`currentWorkspaceMember` once and auto-saves on change — read the stale
name as a pending edit and debounce-saved it onto the **now-current**
workspace member, persisting one user's name onto another.

Read-only caches (object metadata, permissions) tolerate the same
staleness because nothing writes them back — they're only ever
overwritten by the network. `NameFields` is the one consumer that
*persists* a cached identity value, which is what turns a transient
stale read into a durable write.

## Fix

Two small, complementary layers:

- **`useImpersonationSession`** — clear the cached session identity
(`clearSessionLocalStorageKeys()`) on both `startImpersonating` and
`stopImpersonating`, before the reload. The reload then re-bootstraps
from a clean slate for the correct user (and the brief
stale-**permissions** flash goes away too). The admin's token stash
lives in `sessionStorage` and is untouched; `tokenPairState` has its own
key and is not in the cleared set.
- **`NameFields`** — re-seed the inputs when the workspace-member
**identity** changes, so an identity swap is never mistaken for a user
edit. This closes the underlying footgun regardless of how the identity
changes.

## Testing

- Added `NameFields.test.tsx`: swapping `currentWorkspaceMemberState` to
a different member must **not** trigger `updateWorkspaceMemberSettings`,
while a genuine user edit still saves. Verified the test **fails
without** the `NameFields` fix (it writes the previous member's name
onto the new member) and **passes with** it.
- `nx typecheck twenty-front`, `oxlint --type-aware`, and `oxfmt
--check` all pass on the changed files; full `twenty-front` Jest suite
green.

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

https://claude.ai/code/session_015MVW3gg7CVq5oR572ctisx

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21757?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: Claude <noreply@anthropic.com>
2026-06-17 23:01:30 +02:00
Etienne d99e479be8 feat(billing) - facilitate top up in ai chat (#21645)
Today, when a trialing user hits their AI usage cap inside the Ask AI
chat, ending the trial bounces them to the Stripe billing portal (and,
for card-less users, loses their place in the conversation). This PR
makes activating a paid plan / topping up credits feel seamless from
within the chat:

Trial users with a card on file activate their subscription in place,
without leaving the app.
Trial users without a card are sent to the Stripe payment-method portal
and, on return, the trial is ended automatically and they're dropped
back into the exact Ask AI thread they came from.
Credit-exhaustion and trial banners now reflect whether a payment method
exists (Add Credit Card vs Subscribe Now / End Trial Period) and upgrade
inline via a confirmation modal instead of redirecting to Settings.


Uploading Screen Recording 2026-06-16 at 07.51.12.mov…


https://github.com/user-attachments/assets/4ea77273-da63-4b32-b6f1-5ac9e9560651



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21645?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-17 16:20:11 +00:00
nitin 177afde866 [BREAKING CHANGE] fix chart cache collisions with key-based data plumbing (#21743)
closes
https://discord.com/channels/1130383047699738754/1514946035317997709

This fixes Apollo cache collisions for pie slices and line series by
keeping chart bucket identity as key end-to-end, matching how bar chart
already works.


What changed -- 

- Renamed pie/line chart response identity from id to key in the chart
data path.
- Kept key through frontend chart hooks, types, stories, and
tooltip/drilldown logic.
- Only adapt key to id at actual external boundaries like Nivo and
GraphWidgetLegend.
- Added/updated tests covering cache normalization and chart data
behavior.


before - 

<img width="2600" height="844" alt="CleanShot 2026-06-17 at 20 17 14@2x"
src="https://github.com/user-attachments/assets/b9ee83e9-db4b-423e-8668-a7beb4c4c62e"
/>

after - 

<img width="2614" height="800" alt="CleanShot 2026-06-17 at 20 16 17@2x"
src="https://github.com/user-attachments/assets/674a5417-ffc2-441d-9484-e1126438254c"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21743?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-17 16:13:27 +00:00
Félix Malfait 02a3a3c47c fix(ai): handle dynamic-tool message parts in chat persistence (#21740)
## Summary

Fixes #20558. AI chat streams crashed with `Unsupported part type:
dynamic-tool` whenever the model emitted a *dynamic* tool call (a tool
that isn't part of the bound schema). The assistant message never
persisted, so the user saw a hard failure mid-stream.

## Root cause

The AI SDK v6 emits two flavors of tool parts:
- **Static** — `type: "tool-<toolName>"` (e.g. `tool-execute_tool`)
- **Dynamic** — `type: "dynamic-tool"`, with the name on `part.toolName`

`mapUIMessagePartsToDBParts` recognised tool parts with a homegrown
check:

```ts
part.type.includes('tool-') && 'toolCallId' in part
```

That returns `false` for `'dynamic-tool'` (it contains `-tool`, not
`tool-`), so dynamic parts fell through to `throw new
Error(\`Unsupported part type: ${part.type}\`)` during the
`handleStreamFinish` persistence step. Stack trace from the issue
matches exactly.

The same broken heuristic was duplicated in:
- `packages/twenty-server/.../mapDBPartToUIMessagePart.ts` (reverse
mapper)
- `packages/twenty-front/.../utils/mapDBPartToUIMessagePart.ts`
(frontend mirror — would also throw on a `dynamic-tool` row reloaded
from history)

Meanwhile, two other call sites in the codebase
(`finalize-dangling-tool-parts.util.ts`, `isThinkingStepPart.ts`)
already correctly use the SDK's `isToolUIPart`, which natively
recognises both flavors.

## What this PR does

1. **Switches all three mappers to the SDK's canonical check**
(`isToolUIPart` on the forward path; explicit `dynamic-tool` + `tool-`
startsWith on the reverse paths, where the input is an entity/DTO, not a
UI part).
2. **Persists `toolName`** — the column already existed on the entity,
DTO and GraphQL fragment but nothing wrote it. For static parts the name
is recoverable from `type`; for dynamic parts it's the only place the
name lives, so without it the round-trip is impossible. The shared
denormalisation also helps existing per-tool analytics
(`count-native-web-search-calls-from-steps.util.ts`).
3. **Reconstructs `dynamic-tool` parts on read** (with `toolName`) so
they survive a DB round-trip both on the server and on the frontend
history view.
4. **Adds a round-trip unit test** covering both `dynamic-tool` and a
static tool part to lock the behavior in.

## Architecture notes (called out for review)

- `mapDBPartToUIMessagePart` is duplicated frontend + backend because
the input shape differs (TypeORM entity vs. GraphQL DTO). Out of scope
to consolidate here, but they're drifting — this PR is what that drift
looked like in production. Worth a follow-up to express the shared logic
once over a unified row type.
- I left the existing renderer guard `part.type !== 'dynamic-tool'` in
`AiChatAssistantMessageRenderer.tsx` alone — it's a reasonable UI-side
decision to not attempt to render an unknown dynamic tool generically.
Persistence and history reload now work; rendering of dynamic tool calls
is a separate UX decision.
- No DB migration needed — the `toolName` column already exists. Old
static rows have `toolName: null`; the reverse mapper recovers their
name from the `type` column as before. Old dynamic-tool rows don't exist
(they all threw on write).

## Test plan
- [x] `yarn workspace twenty-server jest map-message-parts.dynamic-tool`
— 5 passed
- [x] `yarn workspace twenty-server jest
finalize-dangling-tool-parts.roundtrip` — still 4 passed (no regression)
- [x] `yarn nx typecheck twenty-server` — clean
- [x] `yarn nx typecheck twenty-front` — clean
- [x] `yarn nx lint:diff-with-main twenty-server` — clean
- [x] `yarn nx lint:diff-with-main twenty-front` — clean
- [ ] Manual: trigger an AI chat that exercises a dynamic tool (e.g. via
an MCP server returning a tool not in the bound schema) and confirm the
stream finishes and the message persists.

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

https://claude.ai/code/session_013EE11eVWtyxmdcbEHVJKoc

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21740?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: Claude <noreply@anthropic.com>
2026-06-17 18:12:21 +02:00
Thomas Trompette 105f9565a5 feat(workflow): surface manual-trigger payload + metadata in variable picker (#21692)
## Summary

Step 2 of the manual-trigger output schema restructuring (expand →
display → migrate → contract).

Builds on the now-merged #21676 (which expanded the runtime payload to
serve `payload` and `metadata` siblings at the trigger root). This PR
**surfaces** those in the variable picker as nested, expandable nodes:

- `trigger.payload.{record fields}` — the record(s) that triggered the
run
- `trigger.metadata.workspaceMemberId` — who triggered it

The flat root fields (`trigger.id`, etc.) remain available, so existing
saved variable references keep working until a later migration phase
moves them.

### Changes
- **twenty-shared**: metadata/payload label constants +
`build-manual-trigger-metadata-node` util + barrel exports.
- **twenty-front**: `computeStepOutputSchema` MANUAL branch now nests
`payload` (RecordNode for SINGLE_RECORD, array Node for BULK_RECORDS,
omitted for GLOBAL) and `metadata`; `ManualTriggerOutputSchema` type
updated to `{ payload?; metadata }`.
- **twenty-server**: `computeTriggerOutputSchemaFromAvailability`
mirrors the same nested shape for server-side validation.

The key is `metadata` (not `_metadata`) — custom fields can't start with
`_`, so collision risk was deemed acceptable.

## Test plan
- [x] `npx nx build twenty-shared`
- [x] `computeStepOutputSchema` unit tests pass (55)
- [x] Manual: create a manual-trigger workflow (GLOBAL / single-record /
bulk), confirm the picker shows `payload` and `metadata` as expandable
folders and that selecting a field yields `{{trigger.payload.<field>}}`
/ `{{trigger.metadata.workspaceMemberId}}`

> Note: server typecheck has pre-existing unrelated failures on main
(Stripe billing mocks, gmail mocks); none touch workflow files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21692?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-17 15:54:35 +00:00
martmull 102c530d0f Add limit on view widget (#21718)
<img width="1345" height="463" alt="image"
src="https://github.com/user-attachments/assets/a5d9ac2f-6375-4956-895d-3675aa9bebc1"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21718?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-17 14:43:31 +00:00
Thomas Trompette 8130fa1c45 feat(workflow): use workspace member as variable sender for emails (#21582)
## Summary

Lets the email workflow node sender be driven by a variable: the
connected-account field accepts a `{{variable}}`, and the backend
resolves it to a connected account at run time.

<img width="436" height="195" alt="Capture d’écran 2026-06-16 à 11 59
27"
src="https://github.com/user-attachments/assets/18eee21e-aed6-4447-9bf4-5cb0e2cfc371"
/>

### Email sender by variable
- The connected-account field now accepts a `{{variable}}` via the
variable picker (uses `FormSelectFieldInput` with
`WorkflowVariablePicker`), with a hint to pick a connected account or
set a workspace member as a variable.
- The email workflow action resolves the stored sender value explicitly:
if it is a `workspaceMemberId` (a UUID matching a workspace member), it
resolves that member's first connected account; otherwise the value is
used directly as a `connectedAccountId`.
- Resolution lives in `EmailWorkflowActionBase` and applies to both
`SEND_EMAIL` and `DRAFT_EMAIL`. If a matching member has no connected
account, the run fails fast with a clear message (no silent fallback).
- `DRAFT_EMAIL` also fails fast when the resolved connected account is
missing the required OAuth scopes (`gmail.compose` / `Mail.Send`), via a
server-side `getMissingDraftEmailScopes` util that mirrors the front-end
check.
- Existing workflows with a hardcoded `connectedAccountId` keep working
unchanged (no migration needed).

> Note: exposing the running workspace member as a manual-trigger
variable (`_metadata.workspaceMemberId`) is split into a follow-up PR.

## Test plan
- [x] Backend unit tests for `draft-email-tool`,
`get-missing-draft-email-scopes`, and the `send-email` / `draft-email`
workflow actions (incl. workspace-member sender resolution)
- [x] Lints clean on all changed files
- [x] Manual: configure an email node with a workspace-member variable
sender and confirm it resolves and drafts/sends
- [x] Manual: confirm a member lacking compose permission fails the run
with the permission message

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


---

### Update — scoped to Draft Email only

The sender variable picker is now exposed **only on the Draft Email
node**. The Send Email node keeps a plain account select (no variable
picker, no variable hint) until we enable it there in a follow-up.
Backend resolution still lives in `EmailWorkflowActionBase` and remains
generic, so enabling the picker for Send Email later requires no backend
change.
2026-06-17 13:57:22 +00:00
Thomas Trompette 1f9c4cf5b9 fix(front): home redirect honors first object of the navigation menu (#21626)
https://github.com/user-attachments/assets/db11f02c-4502-4adc-8cf7-55d3dc0211a6

## Summary

Fixes [#21166](https://github.com/twentyhq/twenty/issues/21166). Going
to the workspace root (`/`) always redirected to the
alphabetically-first object.

Following [the review
discussion](https://github.com/twentyhq/twenty/issues/21166#issuecomment-4611570420),
this redirects to the **first object of the navigation menu** instead of
the last-visited object. "Last-visited" had too many edge cases (which
page? records vs. index?), whereas the first item of the user's menu is
unambiguous and matches what they see at the top of the left sidebar.

## Changes

- New `getFirstObjectNavigationMenuItemLink` util walks the workspace
navigation menu items in display order (sorted by `position`) and
returns the link of the first object-backed item (`OBJECT`/`VIEW`) the
user can read.
- `useDefaultHomePagePath` uses it as the primary target. It stays on
`AppPath.Index` until navigation menu items have loaded (they load
*after* the minimal-metadata fast path, so resolving earlier would land
on the wrong object during the post-login window), then falls back to
the first readable object if the menu has no object item.

Scope is intentionally minimal: the last-visited tracking is left in
place and will be cleaned up separately now that the redirect no longer
reads it.

## Test plan

- `npx jest useDefaultHomePagePath
--config=packages/twenty-front/jest.config.mjs` — covers menu order
honored over alphabetical, `VIEW` item links, the loading deferrals
(object metadata + navigation menu items), the readable-object fallback,
and the no-readable-object profile-settings fallback.
- Manually: visit a non-first object, navigate to `/`, confirm the
redirect goes to the first object of the navigation menu.
2026-06-17 12:03:34 +00:00
Félix Malfait ece7a384df fix(front): wait for viewFields + fieldMetadataItems before opening the metadata gate (#21713)
## Problem

On twenty-main, loading a record-index/standalone page for the first
time renders the page chrome (title, view chip with record count) but
the table body stays blank. A subsequent reload fixes it. Regression
introduced by the cache-first `currentUser` bootstrap (#21532);
follow-up to #21592, which already mentioned the experiment "should be
reviewed."

## Root cause (concurrency)

The metadata loader runs in two phases and the gate opens between them:

1. **`loadMinimalMetadata`** fast-paths `objectMetadataItems` and
`views` to `status: 'up-to-date'` with only their *minimal* fields.
`viewFields` and `fieldMetadataItems` stay `'empty'`.
2. **`IsMinimalMetadataReadyEffect`** opens the gate as soon as those
two are `'up-to-date'` — before viewFields exist.
3. The page mounts. `viewsSelector` joins views with an empty
`viewFields` collection, so `view.viewFields = []`.
`RecordIndexLoadBaseOnContextStoreEffect` calls
`loadRecordIndexStates(view, …)` with the empty viewFields and pins
`loadedViewId === contextStoreCurrentViewId`.
4. `loadStaleMetadataEntities` later populates viewFields; the selector
recomputes, but the effect bails out on the `loadedViewId` guard.
`currentRecordFields` stays empty.
5. `visibleRecordFields` stays empty →
`RecordTableVirtualizedInitialDataLoadEffect` hits its
`isEmpty(visibleRecordFields)` guard and never fetches → empty body. The
"300" count visible in the screenshot comes from
`useGetRecordIndexTotalCount`'s separate aggregate query, which doesn't
depend on viewFields.

**Why the gate close/reopen self-heal doesn't work reliably:**
`replaceDraft → applyChanges` happen in the same microtask chain. React
18 automatic batching collapses both into a single render where status
goes `'empty' → 'up-to-date'` without an intermediate `'draft-pending'`
observable to React. The gate never closes, children never unmount,
`loadedViewId` is never reset.

**Why it surfaced after #21532:** Before, `currentUser` was loaded only
after `GetCurrentUser` returned — by which time
`loadStaleMetadataEntities` had typically also completed and viewFields
were populated when the gate opened. Now the cached `currentUser` lets
the gate open the moment `loadMinimalMetadata` finishes.

## Fix

Extend `IsMinimalMetadataReadyEffect` to also require
`fieldMetadataItems` and `viewFields` to be `'up-to-date'` before
opening the gate. Both are joined into the data the record-index page
reads on first paint (`objectMetadataItemsWithFieldsSelector` reads
fieldMetadataItems; `viewsSelector` reads viewFields), so the page can't
render correctly without them.

- **Warm cache** (all entities hydrated `up-to-date` from IndexedDB):
unaffected — gate opens immediately.
- **Cold cache and the first load post–IndexedDB-migration**: the gate
stays closed until `loadStaleMetadataEntities` + `applyChanges` finish,
then opens with full metadata. The page mounts once with a populated
view; no race.

## Tests

- [x] `nx typecheck twenty-front` clean (file change passes `oxlint` on
the touched file).
- [ ] Manual on twenty-main: cold reload + first navigation to a
record-index page renders the table body without needing a second
reload.

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

https://claude.ai/code/session_01DD3469JAWYURa2sKUTJ85e

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21713?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: Claude <noreply@anthropic.com>
2026-06-17 11:40:55 +00:00
Thomas des Francs ca7ffb97b9 Added page card box-shadow (#21688)
## Summary
- add a subtle left-side page card shadow in light and dark mode
- preserve the existing border-ring box shadow
- give the page card wrapper enough left padding for the shadow to
render



<img width="904" height="1116" alt="image"
src="https://github.com/user-attachments/assets/2948fe4a-81ff-4045-ad6f-c0bc9b58be26"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-17 12:45:31 +02:00
Raphaël Bosi 9c9c34fccf Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and
`twenty-front-component-renderer` from `twenty-ui-deprecated` to
`twenty-ui` (mechanical import swap — the packages have API parity) and
deletes the deprecated package along with its workspace/CI/config
wiring.

Also adds `@linaria/react`/`@linaria/core` as direct deps of
`twenty-front` (it used them transitively via the deprecated package).

Note: move the required status check from `ci-ui-status-check` to
`ci-new-ui-status-check`.

Argos: the Storybook box-model/button-reset baseline shift (the bulk of
the visual diffs) is isolated in #21665 — Storybook now loads
twenty-ui's global `reset.scss`, which the production app already ships.
Once #21665 merges and this branch is rebased, the remaining Argos diffs
are component-level visual-parity items only.
2026-06-17 09:41:11 +00:00
Thomas des Francs 079040f1c0 Fix selectable list arrow focus (#21679)
## Summary

Tested on all select fields one by one

- Keep searchable selectable-list inputs focused while ArrowUp/ArrowDown
moves the selected item.
- Remove the old global "grid focused" mode and blur/refocus recovery
path.
- Scroll the selected item into view with `block: 'nearest'`, which
restores keyboard scrolling in long relation pickers without forcing the
row to the top.
- Add focused regressions for command-menu input focus and selected-item
scrolling.

## Root Cause

`SelectableList` hotkeys blurred the active input before arrow
navigation and stored a global grid-focused state. That let ArrowDown
move selection, but focus could fall back to the underlying page/table
instead of remaining in the command menu input.

## Recording

### Before


https://github.com/user-attachments/assets/a802cbc3-4cfd-4466-bc22-274935a77715

### After


https://github.com/user-attachments/assets/9c315d2e-dcb1-424d-80c9-a5942eb1b6bd

## QA Note

I checked all inputs one by one with a 2h30 agent in goal mode.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21679?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-17 08:46:07 +00:00
Félix Malfait eeed998c9e Let users pick their workspace subdomain during sign-up (#21641)
## What & why

During onboarding the workspace subdomain was auto-generated at sign-up
and only editable later in Settings. This adds a subdomain picker to the
workspace-creation flow, with **live availability checking** and
**name-driven auto-fill**.

The subdomain is chosen **on the central sign-up domain, before the
redirect onto the workspace subdomain** — so there's no mid-onboarding
domain switch (which would otherwise force a re-auth, like the Settings
"this logs everyone out" flow). It works uniformly for credentials and
SSO, since workspace creation is a post-auth mutation.

## Flow

Authenticate → **Create a workspace** → new step (workspace name +
address with live availability + auto-fill, seeded from the work email)
→ workspace is created with the chosen subdomain → the single redirect
lands on the final subdomain → onboarding modal (name pre-filled).

## Changes

**twenty-shared**
- `getSubdomainSlugFromDisplayName` — friendly slug from a display name,
built on the existing `transliteration` package (also transliterates
non-Latin names, e.g. 日本語 → `ri-ben-yu`).

**twenty-server**
- `checkWorkspaceSubdomainAvailability(subdomain)` query
(workspace-agnostic, `UserAuthGuard`) → `{ isValid, available,
suggestedSubdomain }`.
- `SubdomainManagerService`: availability + suggestion logic with
friendly numbered suffixes (`acme`, `acme-2`, …) instead of random hex;
`generateSubdomain` reuses it.
- `signUpInNewWorkspace` accepts an optional `{ displayName, subdomain
}` input (validated; falls back to auto-generation when omitted —
backward compatible, so existing callers are unaffected). Concurrent
same-subdomain sign-ups return a clear "already taken" error instead of
a generic DB error.

**twenty-front**
- New `SignInUpStep.WorkspaceCreation` step +
`useWorkspaceSubdomainField` hook (debounced, stale-response-safe;
auto-fills from the name until the user edits it, with a one-click "use
suggested" when taken; ignores Enter during IME composition; surfaces a
clear error if the availability check fails).
- Onboarding modal name pre-filled from the chosen name.

## Testing

- Unit tests: shared slug util, the `useWorkspaceSubdomainField` hook
(real auto-fill/availability flows via `MockedProvider`), and the
workspace-creation component; existing sign-up tests still pass.
- Typecheck, lint, and format green across twenty-shared / twenty-server
/ twenty-front.

## Notes / out of scope

- No DB migration — the `subdomain` column already existed.
- Self-hosted single-workspace sign-up is unchanged; the step is gated
to multi-workspace (global scope).
- Low-priority follow-ups: length bounds on the subdomain / displayName
inputs, and an integration test for the availability query.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:52:51 +02:00
Raphaël Bosi 4b3f38f8f2 Show app logo and name in workflow step side panel header (#21689)
Show app logo and name in workflow step side panel header

## Before

<img width="798" height="106" alt="CleanShot 2026-06-16 at 17 59 39@2x"
src="https://github.com/user-attachments/assets/a8881d1c-355c-4e3a-9379-0c3a7cfbf42f"
/>

## After

<img width="800" height="102" alt="CleanShot 2026-06-16 at 18 05 27@2x"
src="https://github.com/user-attachments/assets/43820d1a-6445-42d8-8f45-963e8741cbb7"
/>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21689?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-17 08:41:03 +00:00
Félix Malfait 74b56ba66c fix(onboarding): show the connect step after workspace creation (#21701)
## Problem

Creating a new workspace as an existing user (e.g. someone who already
has other workspaces) skips the **Connect account / Sync emails** step
entirely — the connect modal never appears.

## Root cause (regression from #21640)

`#21640` moved the `CONNECTED_ACCOUNTS` permission gate onto the
`WORKSPACE_ACTIVATION → SYNC_EMAIL` transition in
`getNextOnboardingStatus`:

```ts
// before #21640: WORKSPACE_ACTIVATION → PROFILE_CREATION (no permission read)
// after  #21640:
if (WORKSPACE_ACTIVATION) {
  return isAccountSyncEnabled ? SYNC_EMAIL : PROFILE_CREATION;
}
```

That transition fires from `CreateWorkspace.tsx` immediately after
`activateWorkspace()` + `loadCurrentUser()`. But
`setNextOnboardingStatus` is a memoized callback that captured
`isAccountSyncEnabled` at render time — *before* activation, when the
brand-new workspace has no roles/permissions yet, so
`currentUserWorkspace.permissionFlags` is empty and the flag reads
`false`. `loadCurrentUser()` refreshes the atoms, but the executing
callback still holds the stale `false`. So it optimistically routes to
`PROFILE_CREATION`, skipping `SYNC_EMAIL` and overriding the backend
status (which correctly says `SYNC_EMAIL`). For an existing user the
profile step is also a no-op (name already set), so they sail straight
into the app.

Before #21640 the same permission gate lived on the `PROFILE_CREATION →
SYNC_EMAIL` transition, which fires from the profile step — long after
activation, when permissions are loaded — so it never misfired.

## Fix

The backend (`OnboardingService.getOnboardingStatus`) decides the
connect step purely from `ONBOARDING_CONNECT_ACCOUNT_PENDING` and never
consults the permission. Drop the permission gate from the frontend
transition so the two agree — `WORKSPACE_ACTIVATION` always advances to
`SYNC_EMAIL`. The `WORKSPACE_ACTIVATION` branch only ever runs for
workspace creators (who are admins with the permission), so the gate was
only ever reachable via the stale read.

Removes the now-unused `isAccountSyncEnabled` / `usePermissionFlagMap`
plumbing and the obsolete "skip SyncEmail when account sync is disabled"
unit test.

## Test plan

- [ ] Create a new workspace as an existing user (with other workspaces)
→ the Connect account / Sync emails step now appears
- [ ] Fresh signup → onboarding still flows `Workspace activation → Sync
emails → Create profile → Invite team`
- [x] `useSetNextOnboardingStatus` unit tests updated (the "after
workspace activation → SYNC_EMAIL" case is retained and now
unconditional)
- [x] `npx nx typecheck twenty-front` — clean for changed files (only
the pre-existing, unrelated `idb-keyval` module-resolution errors remain
in this environment)
- [x] `npx nx lint:diff-with-main twenty-front` — changed files clean

> Note: the unit test file couldn't be executed in my sandbox because
`idb-keyval` (a declared dependency, pulled in transitively via
`jotaiStore`) isn't installed here; it runs normally in CI.

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

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21701?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: Claude <noreply@anthropic.com>
2026-06-16 23:37:51 +02:00
Félix Malfait 35c2a24afb perf(onboarding): compute invite suggestions on-demand (#21696)
## Summary

Follow-up to #21640. In production, invite suggestions took ~1 minute to
appear because `FetchOnboardingInviteSuggestionsJob` ran on the shared
`calendarQueue` behind heavy calendar-sync jobs.

- **Drop the background job entirely.** `getInviteSuggestions` now
resolves the connected account from the authenticated
`@AuthUserWorkspaceId()` and computes suggestions on demand:
cache-first, with a bounded calendar fetch + cache write on a miss.
Removes the Google/Microsoft enqueues, the
`shouldComputeInviteSuggestions` threading through the auth controllers,
and the now-unused `shouldComputeInviteSuggestionsOnConnect` /
`isOnboardingConnectAccountPending` helpers.
- **Prefetch one step earlier.** New `usePrefetchInviteSuggestions` hook
fires the query from `CreateProfile` so the server cache is warm by the
time the invite step renders. `InviteTeam` switches from `network-only`
→ `cache-first`. If the profile step is skipped, the invite step still
computes on-demand (~1–3s, no queue) — no more minute-long waits.

No GraphQL schema change.

## Test plan

- [ ] Connect Google calendar in onboarding → invite step renders
prefilled teammates with no perceivable wait
- [ ] Connect Microsoft calendar in onboarding → same
- [ ] Onboard with workspace name already set so profile step is skipped
→ invite step still prefills (just with a brief on-demand fetch instead
of 1 min)
- [ ] Connect a non-work-email account → invite step renders empty form
(no suggestions)
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint:diff-with-main twenty-server` 
- [ ] `npx nx lint:diff-with-main twenty-front`  (changed files clean)
- [ ] `google-apis.service.spec.ts` + `microsoft-apis.service.spec.ts`
pass

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21696?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: Claude <noreply@anthropic.com>
2026-06-16 21:25:02 +02:00
Raphaël Bosi d4c1fc86e3 Stabilize flaky Argos stories in twenty-front storybook (#21691)
Three twenty-front stories rendered non-deterministically and
intermittently tripped Argos as false positives (flaky on `main`, not
caused by any recent change). Each is now deterministic:

- **SettingsDataModelFieldSettingsFormCard › WithRelationForm**: the
relation preview was screenshotted mid-settle — it briefly shows the
fallback object/record before the form default and the sample record
load. Added a `play` that waits for the settled state so Argos captures
it consistently.
- **MultiSelectInput › SingleSelection**: the final deselect click left
a transient hover/tooltip. The play now moves the pointer off the option
and waits for the tooltip to disappear.
- **Breadcrumb › Default**: ambiguous intrinsic width made the last
crumb flip between "New" and "N…". Gave the story a fixed container
width.

Verified the three stories pass in the Storybook vitest runner across
repeated runs.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21691?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-16 16:37:53 +00:00
neo773 1ad919955a Support variables file email attachment (#21613)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21613?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-16 18:11:37 +02:00
neo773 d8d5991977 fix(messaging): honor IMAP/SMTP encryption setting instead of inferring it from the port (#21562)
This pull request makes the IMAP and SMTP encryption setting actually
honor what the user selects.

As per spec there's 3 modes: SSL/TLS (implicit TLS from the start),
STARTTLS (it will attempt TLS but if the server doesn't support it, it
gracefully falls back to plaintext), NONE (plaintext)

Current implementation had a boolean flag for this, this replaces it
with the 3 modes

Upgrade command to migrate all existing accounts, to not risk breaking
anyone's existing account in production we map each account to the mode
that matches its current behavior, so nothing changes on the wire

/closes #21300

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21562?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>
2026-06-16 18:09:04 +02:00
Félix Malfait 61309c45e6 feat(onboarding): prefill the invite step with teammates from the connected calendar (#21640)
## What & why

Implements core-team-issues#1414: move the calendar/email connection
earlier in onboarding and use the freshly connected calendar to prefill
the **Invite your team** step with likely teammates, so users don't
start from an empty form.

## Approach

Everything is behind the feature flag
`IS_ONBOARDING_INVITE_SUGGESTIONS_ENABLED` (off by default).

**Reorder** — onboarding becomes `Workspace activation → Connect account
→ Create profile → Invite team`. Connecting before profile gives the
calendar sync a head start; connecting *before* the workspace exists
isn't possible (a connected account requires an activated workspace +
workspace member + OAuth transient token). Gated in both
`OnboardingService.getOnboardingStatus` (backend) and
`useSetNextOnboardingStatus` (frontend) so the two agree.

**Fast teammate lookup** — on Google/Microsoft connect *during
onboarding*, a background job (`FetchOnboardingInviteSuggestionsJob`)
runs a single bounded calendar fetch (recent events, attendees inline),
keeps same-work-email-domain colleagues (excludes self + aliases;
personal mailboxes yield nothing), ranks by meeting frequency, and
caches the top 5. The invite step reads the cache via a new
`getInviteSuggestions` query and prefills the form — polling briefly
while the cache warms, and never overwriting input the user has already
typed.

Providers: **Google** (Calendar `events.list`) and **Microsoft** (Graph
`calendarView`), routed by a `CalendarAttendeesService` dispatcher
(mirrors the existing `CalendarGetCalendarEventsService`). Any fetch
failure (missing scope, API error) degrades to today's empty form via
the orchestrator's best-effort catch.

## How to enable

Turn on `IS_ONBOARDING_INVITE_SUGGESTIONS_ENABLED` for a workspace
(admin panel).

## Notes

- New-workspace creators only (invitees never see the connect/invite
steps). Skipping the connect step, or signing up with a personal email,
falls back to the current empty form.
- The "We found teammates from your calendar" subtitle only shows once
suggestions are actually prefilled.
- i18n: the new `<Trans>` strings are extracted on merge to `main` by
the existing Crowdin workflow.

## Testing

- Frontend unit tests for the reorder state machine (both flag states).
- `npx nx typecheck` and `npx nx lint:diff-with-main` green for
`twenty-front` and `twenty-server`.
- Server boots with the new DI wiring (no circular dependency);
`getInviteSuggestions` / `InviteSuggestion` present in the live metadata
schema.
- Not exercised in CI: live Google/Microsoft OAuth end-to-end (requires
real accounts + calendar data).

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

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

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-06-16 15:45:11 +00:00
Raphaël Bosi 5b1cfa4cc0 Show app name on workflow logic function nodes (#21675)
Workflow logic-function nodes from installed integration apps now show
the owning app's name as the node label (e.g. "People Data Labs")
instead of the generic "Action". The descriptive title below it (e.g.
"Enrich Person") is unchanged.

This only applies to installed integration apps — plain custom-code
functions and standard functions keep showing "Action".

A new `useWorkflowNodeLabel` hook resolves the logic function's
`applicationId` and returns the app name for integration apps, falling
back to the node type otherwise. It's used by the editable, read-only,
and run node renderers.

## Before

<img width="572" height="766" alt="CleanShot 2026-06-16 at 16 26 42@2x"
src="https://github.com/user-attachments/assets/4ca9cf2f-5469-437d-a4dc-084057730589"
/>

## After

<img width="460" height="706" alt="CleanShot 2026-06-16 at 16 25 46@2x"
src="https://github.com/user-attachments/assets/504ed2ea-49a6-49c6-830e-2fdd214b8c61"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21675?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-16 14:30:23 +00:00
Raphaël Bosi 637ae65e9f Hide Input and Test tabs for app logic function nodes (#21671)
When opening a logic function node that comes from an app (e.g. "Enrich
Company" from People Data Labs), the node detail panel was showing
**Input** and **Test** tabs. App-sourced functions should only expose
their input fields, with no test surface.

This hides the tab list when the logic function has an `applicationId`,
rendering the input fields directly. The test tab is also never treated
as active for these nodes, which guards against a stale "test" tab state
leaking in from a previously-opened custom function (the tab list shares
one component instance id).

## Before

<img width="800" height="546" alt="CleanShot 2026-06-16 at 15 11 32@2x"
src="https://github.com/user-attachments/assets/c4582e27-9a41-4ebd-a436-49404cefb515"
/>

## After

<img width="800" height="454" alt="CleanShot 2026-06-16 at 15 11 09@2x"
src="https://github.com/user-attachments/assets/2756e07b-fa28-410a-be14-ad5f49e2d7ed"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21671?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-16 14:24:12 +00:00
martmull 306a1454aa Update Connection provider path (#21678)
## Before

After connecting to oAuth linear app connection:

<img width="1512" height="851" alt="image"
src="https://github.com/user-attachments/assets/39b94aaf-648f-46a6-8f4d-deb1cb7e22c5"
/>

## After

Redirects to Linear

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21678?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-16 13:53:13 +00:00
Etienne 16a92f52a4 feat(admin-panel) - add billing/usage section (#21672)
Add billing/usage section

<img width="740" height="763" alt="Screenshot 2026-06-16 at 14 39 51"
src="https://github.com/user-attachments/assets/42db4fe4-3158-4ab8-aee5-28121ea530cd"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21672?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-16 13:18:43 +00:00
Raphaël Bosi e515f174a4 Load twenty-ui global styles in twenty-front Storybook (#21665)
Loads `twenty-ui/style.css` in twenty-front's Storybook preview,
matching the real app — `src/index.tsx` already imports it, but
`.storybook/preview.tsx` only loaded the deprecated stylesheet, so
Storybook rendered without twenty-ui's global `reset.scss`
(`*{box-sizing:border-box}` + a native `button` reset) that the
production app has.

This isolates the resulting box-model/button-reset baseline shift ahead
of the `twenty-ui-deprecated` removal PR. The Argos diff here is exactly
that global shift (Storybook catching up to how the app already renders)
and is expected — bulk-approve. Once this lands, the removal PR's Argos
will show only genuine deprecated→twenty-ui component diffs instead of
being buried under ~240 reset-driven diffs.

Only the reset applies in this PR: twenty-ui's component CSS targets
hashed module classes that don't exist in the deprecated-rendered
Storybook, and `reset.scss` is the only global selector twenty-ui emits.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21665?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-16 12:12:49 +00:00
Thomas des Francs a67fdf2dac Clean up MCP Monaco editor (#21643)
Rebuild the MCP setup block on the shared Monaco editor with theme-driven colors, padding, and auto-height; add the settings description line-height token; revert the clipboard util to the shared navigator.clipboard implementation; and consolidate the code editor's auto-height to a single disposed, reactive effect.
2026-06-16 13:57:44 +02:00
nitin b7c68e082e Enable inline field editing in calendar event details (#21666)
before - 


https://github.com/user-attachments/assets/b6645bb5-0ffc-4281-b054-2035c865ba77




after - 


https://github.com/user-attachments/assets/a62a1134-c959-4281-aefe-b04e310b4926



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21666?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-16 10:52:54 +00:00
Charles Bochet 14d8105f22 fix(front): remove runtime default-view creation fallback (#21652)
## Problem

`useCreateDefaultViewForObject` was a temporary runtime fallback that
created a view + one view field per field (each with a fresh `v4()` id)
whenever `RecordIndexLoadBaseOnContextStoreEffect` found no view for the
current view id. Because the created view got a fresh id that never
matched the requested `contextStoreCurrentViewId`, the next load missed
again and re-created another duplicate — leaking `core.view` /
`core.viewField` rows without bound (notably during the 2.13.0
cache-first bootstrap window).

#21592 made the fallback idempotent as a stop-gap, but the mechanism is
no longer needed at all: standard/index views are created server-side at
object creation and during standard app installation, so the client
never needs to mint them.

## Change

Remove the fallback entirely:
- Delete `useCreateDefaultViewForObject`.
- In `RecordIndexLoadBaseOnContextStoreEffect`, when no view resolves
for the current id, do nothing and let the loaded views settle (the
effect re-runs once the view is present and loads it).

## Note

This removes the leak at the source for any client running the new
bundle. Clients still on old cached JS will keep creating duplicates
until they reload; the already-leaked rows are being cleaned up
separately via SQL.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21652?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-16 11:08:48 +02:00
Etienne ceb7698689 fix(ai) - workflow tool outputs optim + display fix (#21500)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21500?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-16 08:50:16 +00:00
Raphaël Bosi 504eaa5600 Fix relation-traversal filters showing no options on select fields (#21616)
Relation-traversal filters (e.g. *Company → Industry*) on
`SELECT`/`MULTI_SELECT` fields rendered an empty option picker, so the
filter couldn't be configured.

The value inputs resolved the select options from the **source relation
field** (which has no options) instead of the **relation target field**.

Fixed in both filter UIs:
- Dashboard chart filters and workflow "Find Records"
(`AdvancedFilterSidePanelValueFormInput`)
- Record-index simple & advanced filters and the role-permission filter
builder (`ObjectFilterDropdownOptionSelect`)

Both now resolve the value-input field and its options from
`relationTargetFieldMetadataId` when a filter traverses a relation,
falling back to the source field otherwise.

## Before

<img width="802" height="702" alt="CleanShot 2026-06-15 at 17 27 35@2x"
src="https://github.com/user-attachments/assets/032b0875-8a5a-4e09-a2ad-4c4a8a319f49"
/>

## After

<img width="804" height="796" alt="CleanShot 2026-06-15 at 17 27 05@2x"
src="https://github.com/user-attachments/assets/88248409-5781-49c7-aa9e-3502ff8c68e4"
/>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21616?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-15 16:00:49 +00:00
Weiko f0c3883fd1 fix(command-menu-item): persist overrides after save and add reset-to-default (#21623)
## Context
Command menu items are an overridable entity (like page-layout / FIELDS
widgets), but the override flow in layout-customization mode was broken:

- **Move / pin-unpin / hide-label didn't persist.**
`useSaveCommandMenuItemsDraft` fired the `updateCommandMenuItem`
mutations (backend persisted correctly) but never wrote the result back
into `metadataStoreState`, the source the live menu and edit panel read
from. So the UI reverted on exit and changes only showed after a hard
reload.
- **No true "reset to default".** Existing reset controls only reverted
the draft to the last-saved values (which still contained overrides).
there was no way to clear overrides back to the original values after a
save.

Notes
- removed the footer "Reset to default" button. This is not clear to me
how we want to build, let's re-implement better in the next version
- reset are done on click and not delayed on the save. This is similar
to other reset to default on page layouts where we actually usually
reload the component and this is because the FE has no idea what's
original VS override from the response itself

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21623?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-15 18:10:42 +02:00
Charles Bochet 9dd097e11e fix(front): set up Monaco workers for GraphQL playground (#21620)
## Problem

The GraphQL API playground (`/settings/playground/graphql/core`) crashes
with:

```
Uncaught Error: Cannot read properties of undefined (reading 'toUrl')
  at FileAccessImpl.toUri (monaco-editor)
  at WorkerManager.getLanguageServiceWorker (graphqlMode)
  at DiagnosticsAdapter._doValidate (graphqlMode)
```

## Root cause

GraphiQL 5 (adopted in the React 19 migration, #21531) renders its
editors with **Monaco** instead of CodeMirror. Monaco spawns web workers
for GraphQL validation/autocomplete and needs a
`globalThis.MonacoEnvironment.getWorker` factory. None was ever
configured, so Monaco fell back to a main-thread worker whose URL
resolves to `undefined` → the `toUrl` crash.

## Why not the official helper

GraphiQL ships `@graphiql/react/setup-workers/vite`, but its bundled
`?worker` imports are incompatible with our rolldown-based Vite setup:
- **pre-bundled** (in `optimizeDeps`): esbuild's optimizer can't process
`?worker` → the dep 504s and the page fails to load the chunk.
- **excluded** from `optimizeDeps`: rolldown tries to load
`editor.worker.js?worker` as a literal path → `UNLOADABLE_DEPENDENCY`,
crashing the dev server.

## Fix

- Register `MonacoEnvironment.getWorker` in **app source**
(`setupGraphiqlMonacoWorkers.ts`), where Vite's worker plugin handles
`?worker` reliably, and side-effect import it from
`GraphQLPlayground.tsx` before GraphiQL mounts.
- Align `monaco-editor` to `0.52.2` and add `monaco-graphql@1.8.0` as
direct deps so the workers run on the **same deduped Monaco instance**
GraphiQL uses on the main thread (a version mismatch would break the
worker protocol).

## Verification

Ran the playground locally against the dev server:
- Editor renders, syntax highlighting works, operation name parses
(GraphQL language service alive).
- All three worker files (`editor`, `json`, `graphql`) load `200` and
instantiate as module workers.
- Console is free of `toUrl` / `Cannot read` errors and the "must define
MonacoEnvironment.getWorker" warning.
- `oxlint`, `oxfmt`, and `nx typecheck twenty-front` pass.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21620?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-15 17:51:51 +02:00
Raphaël Bosi 03f9ace0bc Make flaky Storybook stories deterministic for Argos (animations + lazy-load) (#21609)
Follow-up to #21594. A set of stories were still flagged flaky by Argos
after the data/image/date fixes. The remaining causes are **animation**
and **lazy-load timing**, not data. Argos's screenshot stabilization
waits for fonts and images and disables CSS animations at capture, but
it does **not** stop JS-driven animations (react-spring) or wait for
`React.lazy` chunks to load.

Two global, test-only changes in the Storybook Vitest setup, plus an
`optimizeDeps` entry:

**1. Disable JS animations (charts).** GraphWidgetLineChart (incl.
`--catalog`) and the pie chart draw via `@react-spring/web`
(`useAnimatedPath`/`useArcsTransition`, used by nivo), so they get
captured mid-draw.
- `Globals.assign({ skipAnimation: true })` from `@react-spring/web`
(single hoisted instance → covers nivo), mirroring the existing
`MotionGlobalConfig.skipAnimations`.
- A global CSS rule zeroing animation/transition durations
(belt-and-suspenders for the pre-capture window).
- `@react-spring/web` added to `optimizeDeps.include` (next to
`@nivo/*`) so importing it in the setup file doesn't trigger a mid-run
Vite re-optimization.

**2. Pre-warm lazy chunks.** RichTextFieldInput (BlockNote editor),
AiChatMessage (`react-markdown`/`remark-gfm`), and AppNavigationDrawer
(favorites/workspace section dispatchers) lazy-load behind `Suspense`;
the snapshot could capture the skeleton fallback instead of the loaded
content. Pre-warming the chunks at setup time (same pattern already used
for the workflow-step mock) caches them before stories render.

The settings object-about form was already covered by the
framer-motion/CSS animation disable.
2026-06-15 13:54:28 +00:00
Félix Malfait 87c878b101 fix(twenty-front): stop unbounded default-view creation on record-index load (#21592)
## Problem

Since the 2.13.0 deploy, `core.viewField` and `core.view` rows are being
created without bound. From Sentry (`twenty-server`, prod), comparing
equal 24h windows before/after the deploy:

| INSERT (per day) | Before (Jun 11→12) | After (Jun 14→15) |
|---|---|---|
| `core.viewField` | 1,885 | 193,719 (**103×**) |
| `core.view` | 161 | 12,130 (**75×**) |

All under `POST /metadata`, via the `CreateManyViewFields` operation
(with frequent "Could not find view for given viewId" races). The
accumulating rows then feed a quadratic flat-map rebuild, ramping `POST
/metadata` tail latency (p99 0.67s → 7s → 11s and climbing) and server
CPU.

## Root cause

`useCreateDefaultViewForObject` is a temporary fallback that creates a
view + a view field per field, each with a fresh `v4()` id.
[`RecordIndexLoadBaseOnContextStoreEffect`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect.tsx)
calls it whenever the current view id has no match in the loaded views:

```ts
if (isDefined(view)) { loadRecordIndexStates(...) }
else { createDefaultViewForObject(objectMetadataItem); } // fires when the lookup misses
```

This is **non-convergent**: the created view gets a *fresh* id, never
equal to the requested `contextStoreCurrentViewId`, so the next load
misses again and creates another duplicate — every record-index load
mints a view + ~17 view fields forever.

**Why it started at 2.13.0:** the lookup now misses during normal loads
because of the cache-first bootstrap experiment (#21532, which is the
`v2.13.0` tag commit). It opens the app gate from cache before the
network revalidation, so the record-index effect runs while
`contextStoreCurrentViewId` is set but the views aren't settled — the
exact window that trips the fallback.

## Fix

Make the fallback idempotent: never auto-create a default view for an
object that already has one. During the cache-first load window the
object's views are present (just not the specifically-requested id), so
the guard short-circuits; and once any view exists, it can never
re-create. The legitimate case (an object genuinely without views) still
creates exactly one.

## Scope / follow-ups

- This is the **root-cause** fix for the leak.
- The quadratic amplification is mitigated separately by the O(N²)→O(N)
change in the flat-map builder (#21585).
- The cache-first experiment (#21532) should be reviewed — it's marked
"[Experiment] — not for merge as-is" yet shipped; reverting/gating it is
the fastest standalone stop-gap, and confirms the trigger if `viewField`
inserts drop.
- The already-leaked duplicate `core.view` / `core.viewField` rows need
a cleanup pass.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21592?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-15 16:01:11 +02:00
Charles Bochet 88b9294afd feat(front): persist metadata store cache in IndexedDB instead of localStorage (#21586)
## Problem

The metadata store cache (object/field metadata, views, page layouts,
command menu items, …) is persisted client-side to power **cache-first
boot**: the app renders instantly from the cache, then
`MinimalMetadataLoadEffect` revalidates per-collection hashes and only
refetches what's stale.

It was persisted to **localStorage**, which Safari/WebKit caps at **~5
MB per origin, counted in UTF-16 (2 bytes/char)** → an effective ceiling
of ~2.5 M characters. Measured on the seeded demo workspace (33 objects,
612 fields):

| Bucket | Safari quota (UTF-16) |
|---|---|
| `metadataStoreState__*` (26 keys) | **1.9 MB — 37%** |
| Whole origin | **2.47 MB — 48%** |

A workspace ~2.5× the demo's schema blows past 5 MB, and there is **no
`QuotaExceededError` handling** — `setItem` throws and breaks the app.
This is what large-workspace users on Safari have been hitting.

## Fix

Move **only the metadata store** to **IndexedDB** (multi-GB, disk-based
quota), keeping a **fully synchronous read path** so the ~24 consumers
that read these atoms with `useAtomValue` never suspend. The auth/UI
atoms (incl. the synchronously-read `tokenPair`) stay on localStorage —
intentionally scoped.

- **`createIndexedDbBackedJotaiStorage.ts`** — a synchronous Jotai
storage facade backed by an in-memory map, hydrated once from IndexedDB
at boot and written through on every set. IndexedDB access uses the
**`idb-keyval`** library (by the IndexedDB spec co-author, ~0.6 KB)
rather than a hand-rolled wrapper. Each cache gets its own database +
BroadcastChannel (`twenty-front-<cacheName>`), so it's safely reusable.
Swallowed errors are surfaced via `logError`. When IndexedDB is
unavailable the cache stays in memory only (re-fetched each boot).
- **`createAtomFamilyState`** — gains an optional `storage` param;
`metadataStoreState` uses the IndexedDB-backed storage.
- **`index.tsx`** — awaits hydration before mounting so atoms
(`getOnInit: true`) read the persisted snapshot synchronously →
cache-first boot preserved.
- **No migration**: the facade does not touch localStorage at all.
Pre-existing localStorage snapshots are ignored — on first boot of the
new code the IndexedDB cache is empty and atoms re-fetch from the
network (a one-time reconnect). Old `metadataStoreState__*` localStorage
keys are left in place (cleared by the existing logout/reset cleanup);
new writes only ever go to IndexedDB.
- **Cross-tab sync**: the old localStorage atoms synced across tabs for
free via `storage` events; the IndexedDB facade had no equivalent, so a
schema change in one tab left others stale until reload. Restored by
implementing the Jotai storage `subscribe` contract over a
**`BroadcastChannel`** — writes broadcast to other tabs, which update
their in-memory map and notify `atomWithStorage` subscribers so mounted
atoms re-render live. (BroadcastChannel doesn't echo to the sender, so
no feedback loop; guarded for environments without it.)

## Why a synchronous facade (not async `atomWithStorage`)

Consumers use `useAtomValue` directly; an async storage would make the
atoms resolve to Promises and **suspend** every reader. The in-memory
facade keeps reads synchronous (zero ripple on consumers) and confines
the async part to a single bulk read at boot, which the existing
`MinimalMetadataGater` loader already covers.

## Tests

### Automated
- Unit test (10 cases) for the storage facade: synchronous read/write,
IndexedDB write-through, hydration from IndexedDB, `removeItem`/`clear`,
per-cache DB namespacing, persist-failure logging, in-memory-only
behaviour when IndexedDB is unavailable, distinguishing a stored
`undefined` from a missing key, and cross-tab subscriber registration.
- Existing metadata-store tests (`useIsLayoutCustomizationDirty`,
`useDefaultHomePagePath`) still pass.
- `nx typecheck twenty-front` and `nx lint:diff-with-main twenty-front`
clean.

### Manual (local seeded workspace, two tabs, Playwright)
Storage:
- After login the metadata cache lives in **IndexedDB (24 keys, ~945
KB)** and **localStorage drops 48% → 11%** of the Safari quota (the
remainder is `currentUserState` + auth, out of scope).
- Reload boots from the cache (no heavy refetch).

Scenarios:

| Scenario | Result |
|---|---|
| **Sign out** | auth cleared, redirect to sign-in, no leftover
localStorage, no errors |
| **Sign back in** | metadata `up-to-date`, company table renders, token
restored |
| **Add object** (`Gadget`) | write-through to IndexedDB; survives
reload via cache-first hydration |
| **Add view** (`QA Cross Tab View`, TABLE) | persisted to the `views`
collection (`up-to-date`) |
| **Two tabs open** | second tab boots cleanly from the shared IndexedDB
— no lock/crash under concurrent access |
| **Cross-tab live sync** | creating an object in tab A makes it appear
in tab B's open settings object list **without a reload** |

Verified by design (no regression):
- Runtime sign-out (`clearSession`) clears session keys and does a full
`window.location.assign` reload; the metadata-clearing path
(`resetJotaiStore`) is test-only, so there's no
async-`clear()`-vs-sign-in race. Metadata persisting across sign-out is
unchanged from the old localStorage behavior (it's schema, revalidated
by hash on next login).

## Notes / follow-ups (not in this PR)

- **IndexedDB query capabilities** are not used yet: the cache stores
one blob per collection (as it did in localStorage), so this is still a
pure key-value use (`idb-keyval`). If we later want to query individual
metadata records — e.g. fields by `objectMetadataId` via an
index/cursor, or partial hydration — that means record-level storage and
a richer wrapper (`idb` for a thin near-native layer, or **Dexie** for a
full query API + reactive `liveQuery` that could also replace the
BroadcastChannel sync).
- IndexedDB still has a (large) quota and Safari ITP eviction applies to
both stores — the cache-first design already tolerates eviction by
revalidating.
- Complementary "load less" wins remain: the denormalized per-field
`relation` block (~700 chars/field of pure duplication) and persisting
`currentUser.workspaceMembers` (the ~0.5 MB still in localStorage).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21586?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-15 13:25:53 +00:00
Marie 018af36cfc Hide logic functions that are not exposed as tools (#21598)
each time we create a workflow node that is a logic function, it creates
a logic function that is then exposed in the options for future nodes.
Let's hide them by only showing here functions that were added by
applications that are not the workspace application. If a logic function
was meant to be exposed as a tool by an application, it will still show.
Decided to go with this as I see no case where we create a logic
function from the main app that we want exposed as a tool.

<img width="614" height="927" alt="image"
src="https://github.com/user-attachments/assets/e51d38a6-234f-4eee-a993-32d02ce6ad1e"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21598?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-15 13:24:56 +00:00
Marie 4be76e3fd1 Support morph relations in workflow record nodes (#21403)
## Support morph (polymorphic) relations in workflow record nodes

Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.

### What changed

**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.

**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: resolves the form
value (stored under the base field name, e.g. `owner`) into the correct
per-target join column (`ownerCompanyId`), nulling siblings to keep
exactly one target referenced.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).

### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.

### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.

<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>

Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
2026-06-15 12:56:17 +00:00
martmull 5207493cda Add useColorScheme hook to twenty-sdk (#21595)
Ability to update front compoonent design according to the dark or white
theme of the UI

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21595?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-15 12:15:28 +00:00
Charles Bochet 0a99f784eb fix(front): dedupe morph relation fields in view field pickers (#21580)
## Issue

Reported in quality-feedbacks: **"Issues with morph relation view
field"** — a morph relation column added to a view **disappears after
refresh** (and can be added several times).

## Root cause — the SSE metadata sync

A morph relation is stored as **one `fieldMetadata` row per target
object**, all sharing a `morphId`. Collapsing those rows into the single
field that represents the relation is a **read-time projection** in the
server's `objects.fieldsList` resolver — it is *not* a storage
invariant, and the rows are never merged.

The frontend metadata store is kept in sync with the raw rows **one row
at a time over SSE** (`MetadataStoreSSEEffect`): every metadata change
broadcasts a single created/updated record that's pushed straight into
the store. Creating a morph relation creates N rows (one per target), so
**N `create` events arrive and N raw sub-fields land in the store —
bypassing the `fieldsList` projection entirely.**

The view-field pickers read straight from that store, so they saw the
morph relation **once per target**. Each could be added as a column
referencing a different sub-field id; after a refresh the view reloads
from the projected (deduped) data, the non-survivor columns no longer
resolve, and they disappear.

## Fix & architecture note

Because the store deliberately mirrors raw rows (that's what the SSE
sync maintains), the fix applies the **same read-time projection on the
client** — deduping morph rows by `morphId` in
`useActiveFieldMetadataItems` — rather than filtering rows at each
insert path (SSE, optimistic create, …). This matches how the backend
already models morph fields and is robust regardless of which path
delivered the rows.

The survivor-selection rule (which sub-field id represents the relation)
now lives in `twenty-shared` (`pickMorphGroupSurvivor`) so client and
server can't drift.
2026-06-15 11:56:35 +00:00
Raphaël Bosi ecc7b38b75 Remove randomness from flaky twenty-front Storybook stories (#21594)
Several twenty-front Storybook stories were flagged flaky by Argos
because they render different pixels across runs. This removes the
non-determinism behind them.

**What changed**
- **Images** — replaced random `picsum.photos` URLs in the Logo and
TabList stories with the existing `AVATAR_URL_MOCK`, and added global
MSW handlers in `.storybook/preview.tsx` that serve a deterministic
image for every remote host (picsum, twenty-icons.com,
twentyhq.github.io, etc.) so no story depends on a network image load.
- **Numbers** — the line-chart story built its data with
`Math.random()`; now uses a deterministic formula.
- **Dates** — the terminal "long output" story stamped its lines with
`new Date()`; now uses a fixed base timestamp. The calendar-channel
date/time format previews and example event used render-time
`Date.now()`/`new Date()` in shared components; they now use a fixed
reference date (`DateTimeSettingsPreviewDate`).
- **Lazy-load timing** — the date-picker story now waits for the
lazily-loaded calendar before the snapshot.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21594?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-15 13:37:19 +02:00
martmull c848ac34fd Fix default view widget visibility (#21590)
Tim logged on the left, Phil on the right, Tim created view widget 

## Before

<img width="1512" height="938" alt="image"
src="https://github.com/user-attachments/assets/7f993f41-a244-42db-b56a-b17c15fb3409"
/>

## After Fix
Tim created a second view widget, Phil can see it

<img width="1512" height="982" alt="image"
src="https://github.com/user-attachments/assets/3908a70d-538b-4adf-95df-7373a2f6e269"
/>

## After slow migration

Phil can see first widget

<img width="1512" height="982" alt="image"
src="https://github.com/user-attachments/assets/6b30429e-1acf-4119-ba32-26db3155975e"
/>


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

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-06-15 10:36:07 +00:00
Charles Bochet fb4608e437 chore(deps): upgrade Tier-1 deps (googleapis 173, gaxios 7, express 5, jsdom 29, date-fns 4, stripe 20) (#21570)
## What

Security-driven upgrade of the biggest-drift Tier-1 dependencies
(staying on latest = staying patched). Bundled because they share the
lockfile and the googleapis/gaxios pair must move together.

| Package | From | To | Gap |
|---|---|---|---|
| googleapis | 105.0.0 | **173.0.0** | 68 majors |
| gaxios | 5.1.3 | **7.1.5** | 2 majors |
| express | 4.22.2 | **5.2.1** | 1 major |
| jsdom | 26.1.0 | **29.1.1** | 3 majors |
| date-fns | 2.30.0 | **4.4.0** | 2 majors |
| date-fns-tz | 2.0.0 | **3.2.0** | 1 major |
| stripe | 19.3.1 | **20.4.1** | 1 major |

`yarn npm audit` reports **0 high/critical** advisories before and
after.

## Code changes

- **gaxios v7** — `GaxiosError.code` is now `string | number` (guard the
calendar network-error check by `typeof`); `GaxiosError` config/response
use `URL` + `Headers`; and crucially the v7 constructor drops
`response.data` unless `bodyUsed` is set — updated the synthetic gmail
error mocks accordingly (production gaxios sets it, so real error
parsing is unaffected).
- **google-auth-library / gaxios dedup** — `googleapis-common@8.0.2`
exact-pins `google-auth-library@10.5.0` + `gaxios@7.1.3` while
`googleapis` pulls `^10.2.0`; the two copies made
`OAuth2Client`/`GaxiosError` type-identities diverge across every
gmail/calendar service. Added two singleton `resolutions` (documented
inline in root `package.json`).
- **express 5** — no source changes. `@nestjs/platform-express@11.1.24`
already resolves `express@5.2.1` internally; the old `4.22.2` pin was
the override.
- **jsdom 29** — no source changes, but it now pulls ESM-only transitive
deps (`@csstools/*` `.mjs`, `parse5`, `entities`, `tough-cookie`,
`@exodus/bytes`). Extended the server jest `transformIgnorePatterns`
allowlist and added `.mjs` to the transform/extensions so jest can load
jsdom.
- **stripe 20** — `Subscription` gained a required `customer_account`
field; added to mocks. No runtime changes.
- **date-fns v4** — `Locale` is no longer ambient (import explicitly in
5 files); per-locale entrypoints dropped the typed `default` export (the
locale loader now reads the single named export); fixed the default
locale import in `formatTimeZoneLabel`.

## Tests

- Full suites green locally: **twenty-server 5709 passed**,
**twenty-front 4937 passed**, twenty-ui / twenty-ui-deprecated green;
typecheck + builds (swc + vite) + lint all pass.
- Added regression tests for the two runtime behaviors these upgrades
touch and that had no coverage:
  - `getDateFnsLocale` — named-export locale resolution (date-fns v4).
- `sanitizeFile` — jsdom 29 + DOMPurify still strips `<script>`/event
handlers from uploaded SVGs (security guard).

## Deliberately deferred (not in this PR)

- **stripe → 21/22**: stripe **21** bundles a runtime `Decimal` type for
money fields **and** jumps the pinned API version to `2026-03-25.dahlia`
(changes webhook/billing payload behavior) — too risky to fold into a
deps bump on billing code. stripe **22** additionally drops the
node10-resolvable `types` entry, which would force a repo-wide
`moduleResolution` change. Capped at the latest clean **20.x**.
- **openid-client → 6**: v6 is a full functional rewrite and its
passport strategy manages the OAuth `state` internally, but our SSO flow
uses `state` to carry `identityProviderId` across the shared
`/auth/oidc/callback`. That needs an auth-flow redesign (session-carried
provider id) on Enterprise SSO code with no integration harness — it
deserves its own focused PR rather than riding along here.

## Tier-1 source

Originated from a dependency-drift audit; remaining Tier-1 items
(date-fns done here) plus Tier-2/3 follow-ups tracked separately.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21570?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-15 10:23:42 +02:00
Yash Singh d7d4b36d5e fix(front): keep the filename when a file has no extension (#21576)
Closes #21575.

## Summary

`getFileNameAndExtension` split filenames with `lastIndexOf('.')`. For
an extensionless name (`README`, `Makefile`), `lastIndexOf` returns `-1`
and `substring`'s negative-index clamping returned `{ name: '',
extension: 'README' }` — losing the whole name into the extension. In
the attachments UI this rendered an empty rename field and corrupted the
name on edit.

## Changes

- Early return `{ name, extension: '' }` when there is no dot
- Correct the existing test that asserted the buggy output; add no-dot +
trailing-dot cases

## Testing

Pure, dependency-free function — verified deterministically (17/17
assertions, red-green proven: reverting the fix fails the corrected
`README` assertion). Note: twenty's full nx lint/test wasn't run locally
(repo wants Node ^24.5.0; this box is on Node 26), so CI is the
authoritative check for lint/format.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21576?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-15 06:33:10 +00:00
dependabot[bot] 9d5561c96c chore(deps): bump @xyflow/react from 12.10.0 to 12.11.0 (#21561)
Bumps
[@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react)
from 12.10.0 to 12.11.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/xyflow/xyflow/releases">@​xyflow/react's
releases</a>.</em></p>
<blockquote>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.11.0</h2>
<h2>12.11.0</h2>
<h3>Minor Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/xyflow/xyflow/pull/5677">#5677</a> <a
href="https://github.com/xyflow/xyflow/commit/e6661de531212f9a209dba17dd63fbbd4ee16f62"><code>e6661de</code></a>
- Add <code>autoPanOnSelection</code> to auto-pan when user drags a
selection close to the edge of the viewport.</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5791">#5791</a> <a
href="https://github.com/xyflow/xyflow/commit/732c8eb8d5ff86ab1c057588724221e9b3b8553c"><code>732c8eb</code></a>
- Adds a type error when <code>handleId</code> is used without
<code>handleType</code> in <code>useNodeConnections</code></p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5793">#5793</a> <a
href="https://github.com/xyflow/xyflow/commit/c5c853d4a2f537caaea725ab9e7bd480e24b86fb"><code>c5c853d</code></a>
- Dev Warnings now use library-specific messaging with the correct
documentation links.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5776">#5776</a> <a
href="https://github.com/xyflow/xyflow/commit/0441e9f9471380b5ba057fc0a6a8cbdc6ff5ed7b"><code>0441e9f</code></a>
- Export <code>NodeHandle</code> type</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5755">#5755</a> <a
href="https://github.com/xyflow/xyflow/commit/88737f9713f3a6f99c6448e02b6518c7aeedae28"><code>88737f9</code></a>
- Add <code>@types/react</code> and <code>@types/react-dom</code> as
optional peer dependencies to prevent issues with pnpm strict mode
(<code>hoist: false</code>)</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5105">#5105</a> <a
href="https://github.com/xyflow/xyflow/commit/076ad3893725f654641f7b8c39e7a4e7935eb702"><code>076ad38</code></a>
- Fix type for event passed to onNodeDrag</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5784">#5784</a> <a
href="https://github.com/xyflow/xyflow/commit/7055140e66e4aebb08ce512bdff34add7e115472"><code>7055140</code></a>
- Fix node resizing possible beyond absolute extents</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5769">#5769</a> <a
href="https://github.com/xyflow/xyflow/commit/ad4d547724a1c2debf8eb7c6e117aabbfd601934"><code>ad4d547</code></a>
- Use <code>useEffect</code> for StoreUpdater to restore previous
behaviour</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/732c8eb8d5ff86ab1c057588724221e9b3b8553c"><code>732c8eb</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/c5c853d4a2f537caaea725ab9e7bd480e24b86fb"><code>c5c853d</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/e6661de531212f9a209dba17dd63fbbd4ee16f62"><code>e6661de</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/737194d571894dd84ce7cbab02f2a4d0b779d018"><code>737194d</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/40660cdb054fb1a110799a3ad7cecbf51371727f"><code>40660cd</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/4806e7cde6d69cd7570098ecca86523666b80175"><code>4806e7c</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/7055140e66e4aebb08ce512bdff34add7e115472"><code>7055140</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.77</li>
</ul>
</li>
</ul>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.10.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5735">#5735</a> <a
href="https://github.com/xyflow/xyflow/commit/a6c938fb2e5ed030512ef75d665ac80dc3a66bc6"><code>a6c938fb2</code></a>
Thanks <a href="https://github.com/nvie"><code>@​nvie</code></a>! -
Allow <code>type</code> field to be missing in <code>BuiltInNode</code>
(no <code>type</code> field is the same as <code>type:
&quot;default&quot;</code>)</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5722">#5722</a> <a
href="https://github.com/xyflow/xyflow/commit/8c9b7e726e0bb79871c85017dace0f1ccf1b478c"><code>8c9b7e726</code></a>
Thanks <a href="https://github.com/dfblhmm"><code>@​dfblhmm</code></a>!
- Add <code>snapGrid</code> to <code>screenToFlowPosition</code>
options</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5723">#5723</a> <a
href="https://github.com/xyflow/xyflow/commit/82249517a3338d7bd0d6d499abecfaa6bca8c339"><code>82249517a</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Pass options to useReactFlow/useSvelteFlow viewport helper functions
correctly</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md">@​xyflow/react's
changelog</a>.</em></p>
<blockquote>
<h2>12.11.0</h2>
<h3>Minor Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/xyflow/xyflow/pull/5677">#5677</a> <a
href="https://github.com/xyflow/xyflow/commit/e6661de531212f9a209dba17dd63fbbd4ee16f62"><code>e6661de</code></a>
- Add <code>autoPanOnSelection</code> to auto-pan when user drags a
selection close to the edge of the viewport.</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5791">#5791</a> <a
href="https://github.com/xyflow/xyflow/commit/732c8eb8d5ff86ab1c057588724221e9b3b8553c"><code>732c8eb</code></a>
- Adds a type error when <code>handleId</code> is used without
<code>handleType</code> in <code>useNodeConnections</code></p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5793">#5793</a> <a
href="https://github.com/xyflow/xyflow/commit/c5c853d4a2f537caaea725ab9e7bd480e24b86fb"><code>c5c853d</code></a>
- Dev Warnings now use library-specific messaging with the correct
documentation links.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5776">#5776</a> <a
href="https://github.com/xyflow/xyflow/commit/0441e9f9471380b5ba057fc0a6a8cbdc6ff5ed7b"><code>0441e9f</code></a>
- Export <code>NodeHandle</code> type</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5755">#5755</a> <a
href="https://github.com/xyflow/xyflow/commit/88737f9713f3a6f99c6448e02b6518c7aeedae28"><code>88737f9</code></a>
- Add <code>@types/react</code> and <code>@types/react-dom</code> as
optional peer dependencies to prevent issues with pnpm strict mode
(<code>hoist: false</code>)</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5105">#5105</a> <a
href="https://github.com/xyflow/xyflow/commit/076ad3893725f654641f7b8c39e7a4e7935eb702"><code>076ad38</code></a>
- Fix type for event passed to onNodeDrag</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5784">#5784</a> <a
href="https://github.com/xyflow/xyflow/commit/7055140e66e4aebb08ce512bdff34add7e115472"><code>7055140</code></a>
- Fix node resizing possible beyond absolute extents</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5769">#5769</a> <a
href="https://github.com/xyflow/xyflow/commit/ad4d547724a1c2debf8eb7c6e117aabbfd601934"><code>ad4d547</code></a>
- Use <code>useEffect</code> for StoreUpdater to restore previous
behaviour</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/732c8eb8d5ff86ab1c057588724221e9b3b8553c"><code>732c8eb</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/c5c853d4a2f537caaea725ab9e7bd480e24b86fb"><code>c5c853d</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/e6661de531212f9a209dba17dd63fbbd4ee16f62"><code>e6661de</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/737194d571894dd84ce7cbab02f2a4d0b779d018"><code>737194d</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/40660cdb054fb1a110799a3ad7cecbf51371727f"><code>40660cd</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/4806e7cde6d69cd7570098ecca86523666b80175"><code>4806e7c</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/7055140e66e4aebb08ce512bdff34add7e115472"><code>7055140</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.77</li>
</ul>
</li>
</ul>
<h2>12.10.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5735">#5735</a> <a
href="https://github.com/xyflow/xyflow/commit/a6c938fb2e5ed030512ef75d665ac80dc3a66bc6"><code>a6c938fb2</code></a>
Thanks <a href="https://github.com/nvie"><code>@​nvie</code></a>! -
Allow <code>type</code> field to be missing in <code>BuiltInNode</code>
(no <code>type</code> field is the same as <code>type:
&quot;default&quot;</code>)</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5722">#5722</a> <a
href="https://github.com/xyflow/xyflow/commit/8c9b7e726e0bb79871c85017dace0f1ccf1b478c"><code>8c9b7e726</code></a>
Thanks <a href="https://github.com/dfblhmm"><code>@​dfblhmm</code></a>!
- Add <code>snapGrid</code> to <code>screenToFlowPosition</code>
options</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5723">#5723</a> <a
href="https://github.com/xyflow/xyflow/commit/82249517a3338d7bd0d6d499abecfaa6bca8c339"><code>82249517a</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Pass options to useReactFlow/useSvelteFlow viewport helper functions
correctly</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/xyflow/xyflow/commit/6970ded32ff745e8fb6ecc97eb6b78956d7cc016"><code>6970ded</code></a>
chore(packages): bump</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/c9db70d050830fa1b06703ef775b12120a0662e2"><code>c9db70d</code></a>
Merge branch 'main' of <a
href="https://github.com/xyflow/xyflow">https://github.com/xyflow/xyflow</a>
into 5780-svelte-flow...</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/af23aef9de095f68fc893b8d18296398c54329bf"><code>af23aef</code></a>
chore: cleanup error messages</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/9d58ab9d2ff1bbf1dc79bb556cc9eed725d116d6"><code>9d58ab9</code></a>
fix: make error messages framework-specific</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/e52bb557888fbae0237432300142180aaca1774f"><code>e52bb55</code></a>
Merge pull request <a
href="https://github.com/xyflow/xyflow/tree/HEAD/packages/react/issues/5105">#5105</a>
from thedanchez/xydrag-type-generics</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/03e3dc0ae6ac64d77ab9281a3dbf629ee8b75d4e"><code>03e3dc0</code></a>
Merge pull request <a
href="https://github.com/xyflow/xyflow/tree/HEAD/packages/react/issues/5755">#5755</a>
from nielskaspers/fix/issue-5738-react-types-peer-dep</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/caebfd681b997020fe20444c181113d161ff7aa0"><code>caebfd6</code></a>
Merge pull request <a
href="https://github.com/xyflow/xyflow/tree/HEAD/packages/react/issues/5784">#5784</a>
from xyflow/fix-node-resizer-again</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/9dc7ec938bed30b59417bade64473cb8f795e039"><code>9dc7ec9</code></a>
chore(react): fix util function</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/c4e783308c2adb022a431a4eb0fb8e46706e27c7"><code>c4e7833</code></a>
Merge pull request <a
href="https://github.com/xyflow/xyflow/tree/HEAD/packages/react/issues/5785">#5785</a>
from xyflow/fix-useless-promises</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/01fb1f1524d6b514145c1c906e1e5b2b8bb359bb"><code>01fb1f1</code></a>
chore(system): add UseNodeConnectionsParams type</li>
<li>Additional commits viewable in <a
href="https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.0/packages/react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for <code>@​xyflow/react</code> since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@xyflow/react&package-manager=npm_and_yarn&previous-version=12.10.0&new-version=12.11.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21561?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: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-14 23:34:43 +02:00
Charles Bochet 965ff4337f fix(front): mass update targets explicitly selected records (#21548)
## Problem

Mass update silently does nothing for explicitly-selected records in a
filtered view (reported in
[quality-feedbacks](https://discord.com/channels/1130383047699738754/1515444299934728213)
— "Mass update does not work (for boolean?)", high severity).

When you select specific records (selection mode) and run **Update
records**, the action built its target filter with
`computeContextStoreFilters`, which intersects the selected record ids
with the **current view filters**:

```ts
// selection mode (before)
queryFilter = makeAndFilterVariables([
  anyFieldFilter,
  { id: { in: selectedRecordIds } },
  computeRecordGqlOperationFilter({ ...view filters... }), // ← intersect with view
]);
```

`useIncrementalUpdateManyRecords` first **fetches** the ids matching
that filter, then updates them:

```ts
if (firstPageRecordIds.length > 0) {
  await mutateRecordsBatch(...); // never runs when the fetch returns 0
}
```

So any selected record that doesn't match the view filter is silently
dropped. When *none* of the selected records match, the fetch returns 0
→ the `updateMany` mutation never fires → only the trailing
refetch/aggregate queries run, and nothing changes. The confirmation
modal still says "Update N records" (it reads
`selectedRecordIds.length`), and the side-panel header shows "0
selected" (it reads the find result) — the exact symptoms in the report.

This is especially easy to hit when updating the very field a view is
filtered on (e.g. a view filtered "QA Done = false" and you set "QA Done
= true" on the selected rows).

## Fix

In **selection mode**, target exactly the selected ids — the user picked
those records, so the action must act on them regardless of the active
view filter / any-field search:

```ts
// selection mode (after)
return { id: { in: contextStoreTargetedRecordsRule.selectedRecordIds } };
```

Exclusion mode (select-all) is unchanged — it still needs the view
filter to define "all matching except N". This also makes the side-panel
"N selected" count and other selection-mode actions (delete, export, …)
consistent with the explicit selection.

## Reproduction (deterministic)

1. Companies view filtered **QA Done = false**.
2. Select 3 rows individually.
3. Change those rows so they no longer match the filter (set `qaDone =
true`) without refetching the view — they stay selected.
4. Open **Update**, set **Employees = 100**, confirm "Update 3 records".

**Before:** `Employees` stays `NULL`; side panel shows "0 selected".
**After:** `Employees = 100` on all 3; side panel shows "3 selected".

## Tests

- Updated `computeContextStoreFilters` selection-mode test to the new
shape.
- Added a regression test asserting selection mode targets only the
selected ids and ignores active view filters / any-field search.

`nx lint:diff-with-main`, `nx typecheck twenty-front`, and the unit
tests pass.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21548?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-14 19:11:46 +00:00
Félix Malfait adfef5c830 fix(twenty-front): prevent stale transition page from blocking scroll (#21551)
## Problem

Switching from the app to Settings sometimes makes every settings page
unscrollable until a full page reload.

## Cause

`MainAppLayoutOutlet` cross-fades between the app and settings with
`AnimatePresence`, rendering both pages into the same grid cell
(`grid-area: 1 / 1`). When an exit animation doesn't clean up, the
outgoing page stays mounted on top of the active one. With the default
`pointer-events: auto`, that (now invisible) stale node intercepts
wheel/scroll events before they reach the page underneath — so the page
won't scroll even though its own scroll container is fine. The node
lives in the always-mounted layout, which is why it survives in-app
navigation and only a reload clears it.

A broken vs. working DOM snapshot differs only in the number of children
in the transition grid cell (2 vs 1) and which element sits under the
cursor; the scroll container, its CSS, and the whole flex/height chain
are identical.

## Fix

Set `pointer-events: none` on exiting pages so a stale exit node can't
capture input from the active page.

Tested by reproducing the stale-node-on-top state and confirming
`pointer-events: none` lets wheel/scroll reach the live page across the
content area, while the entering page stays interactive.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21551?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-14 20:46:22 +02:00
Sri Hari Haran Sharma 53de4c557b Fix record index sync when view fields arrive via SSE (#19069)
Fixes #19023
## What changed

This updates the record index/view field state flow so the current view
can react to late-arriving `viewFields` coming from SSE without
requiring a page refresh.

Changes:
- extracted a narrower `syncRecordIndexViewFields` path in
`useLoadRecordIndexStates`
- kept the initial full record-index load for first entry into a view
- added a follow-up sync in `RecordIndexLoadBaseOnContextStoreEffect`
when the same view receives updated `viewFields`
- updated `ViewBarRecordFieldEffect` so it re-syncs current record
fields when `currentView.viewFields` changes instead of only
initializing once

## Why

There is a race when a user navigates to a custom object while AI is
still creating metadata. In that case, the record index can initialize
from a partial view, and later SSE `viewFields` updates were not being
applied to the active view state. That could leave the table visually
empty or incomplete until a refresh.

## Impact

This should allow:
- record index columns to update live when view fields arrive via SSE
- view bar field state to update live as well
- the current view to stay usable without a refresh while AI-created
metadata is still streaming in

## Validation

Validated locally with:
- `npx prettier --check` on modified files
- `npx oxlint --type-aware` on modified files

Manual verification:
- confirmed live record creation appeared without refresh
- manual AI/SSE testing was partially limited by Groq TPM/token caps on
the selected model, but the state-sync path was verified in code and
local behavior checks

<img width="3024" height="1964" alt="image"
src="https://github.com/user-attachments/assets/f71c7490-bf57-4357-9d5f-087b2424b53b"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-14 19:25:06 +02:00
Arun f45c54679c [Fix] : fix: Allow label identifier system fields in view creation and fix resulting duplicate header columns (#19009)
fixes #18994 

After :
<img width="767" height="230" alt="Screenshot 2026-03-26 at 7 11 04 PM"
src="https://github.com/user-attachments/assets/73de0154-7da1-48fa-92fc-d51a3ef5b06e"
/>

Before : 
<img width="905" height="314" alt="Screenshot 2026-03-26 at 6 56 38 PM"
src="https://github.com/user-attachments/assets/89b1249b-8889-4034-a6c8-d41330154c1a"
/>

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-14 14:45:28 +00:00
Charles Bochet 7c0136b97b feat(deps): migrate frontend to React 19 (#21531)
## What

Migrates the frontend stack from **React 18.3 → 19.2**. The website,
sdk, companion and emails packages were already on React 19; this brings
the remaining holdouts (`twenty-front`, `twenty-ui`,
`twenty-ui-deprecated`, `twenty-front-component-renderer`) and
`twenty-server`'s email rendering onto 19, and pins a single React
version repo-wide.

## Why

React 18.x is now the legacy line. Staying current keeps us on the
patched/maintained branch and unblocks downstream library majors
(react-router 7, mantine 9, etc.) that require React 19 peers.

## Dependency bumps (required by React 19 peers / removed APIs)

| Package | From | To | Reason |
|---|---|---|---|
| react / react-dom | 18.3.1 | 19.2.3 | core |
| @hello-pangea/dnd | 16 | 18 | peer `^18 \|\| ^19` |
| react-datepicker | 6 | 9 | v<7 used removed `findDOMNode`; drops
`@types/react-datepicker` |
| react-data-grid | beta.13 | beta.59 | peer `^19.2`; new render API |
| graphiql (+ @graphiql/react, plugin-explorer) | 3 / 0.23 / 1 | 5 /
0.37 / 5.1 | peer `^18 \|\| ^19` |
| react-helmet-async | 1.3 | **@dr.pogodin/react-helmet** 3.2 | upstream
caps peer at `^18`; drop-in React 19 fork |

A `resolutions` pin enforces a single React (19.2.3) + `@types/react`
(19.2.14) across the monorepo to avoid duplicate copies / type-identity
splits. Versions are the aged lockfile patches (clears the
`npmMinimalAgeGate`).

## Code changes

- **Global `JSX` shim** (`react-jsx-global.d.ts` per package): React 19
moved the `JSX` namespace under `React.JSX`; several deps' published
types (notably `@linaria/react`'s `styled.d.ts`, which types every
`styled.x` via `keyof JSX.IntrinsicElements`) still reference the global
namespace. Without the shim, every styled component degrades to `any`
props.
- **Ref nullability**: `useRef<T>(null)` now returns `RefObject<T |
null>`; widened consumer prop/hook ref types accordingly (incl. the
shared `useListenClickOutside`).
- **react-datepicker v9**: `onChange`/`onSelect` accept `Date | null`,
`calendarStartDay` typing, `ReactDatePickerProps`→`DatePickerProps`,
relaxed the dynamic `selectsMultiple` discriminated union.
- **react-data-grid beta.59**: `formatter`→`renderCell`,
`editor`→`renderEditCell`, `headerRenderer`→`renderHeaderCell`,
`components`→`renderers`, `onRowClick`→`onCellClick`, object-shaped
`useRowSelection`, Set-based selection.
- **dnd style cast**: `@radix-ui/react-popper` augments `CSSProperties`
with a `--radix-*` index signature that dnd's closed `DraggingStyle`
doesn't satisfy → cast at the spread.

## Status / testing

-  `typecheck` green: twenty-front, twenty-ui, twenty-ui-deprecated,
twenty-front-component-renderer, twenty-server
-  build / lint / unit tests / storybook+argos / runtime smoke-test in
progress

Draft until local + CI verification completes. Notable behavior to QA
manually: spreadsheet import (data-grid), date pickers, drag-and-drop
boards/lists, GraphQL playground, page titles/favicon.

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