Commit Graph

10661 Commits

Author SHA1 Message Date
Félix Malfait 573fd00ea7 feat(workflow): add Pick Record action (1/3 — random selection) (#21899)
## Overview

Adds a new workflow action, **Pick Record**, that selects **one** record
from a configured candidate pool and exposes the chosen record as the
step's output. Downstream steps can then reference it through the normal
variable picker — e.g. assign an owner in an _Update Record_ step by
setting **Account Owner = `{{step.<pickRecordId>.id}}`**.

This is the foundation for building **assignment workflows**
(round-robin / load-balanced owner assignment, reviewer rotation, etc.)
in Twenty.

## This is PR 1 of a 3-PR stack

| PR | Strategy | Adds |
|----|----------|------|
| **1 (this one)** | `RANDOM` | The whole `PICK_RECORD` action,
end-to-end, stateless |
| 2 | `ROUND_ROBIN` | A persistent, atomically-incremented per-step
cursor + the strategy selector UI |
| 3 | `LOAD_BALANCED` | "fewest related records wins" via an aggregate
count |

Each PR widens the `strategy` enum (a backward-compatible change), so no
data migration is needed between them.

## How it works

- **Editor**: pick an Object, then pick the candidate records (a
multi-record selector). A random record is selected from that pool at
run time.
- **Output**: a single record of the chosen object — the same output
shape as `CREATE_RECORD`/`UPDATE_RECORD` — so it drills into
`{{step.x.id}}`, `{{step.x.name}}`, … in the variable picker.
- **Execution**: reuses `FindRecordsService` to fetch the pool (`id IN
(recordIds)`, which also transparently drops any deleted candidates),
then returns one at random.

## Design decisions & tradeoffs

1. **Standalone step that outputs a variable, not an inline "random"
mode on the relation field.** This mirrors Attio's round-robin block.
The decisive reason is composition: the chosen record is almost always
reused (assign owner **and** create a follow-up task for them **and**
email them). A variable is chosen once and reused everywhere; an inline
per-field value would re-roll independently in each place. It also keeps
the (stateful) round-robin/load-balanced logic out of the field inputs.
Tradeoff: one extra step to wire up vs. an inline control — accepted for
the composability win. An inline "Assign automatically" entry point can
still be layered on later as sugar that inserts this step.

2. **Co-located in the `record-crud` action module and reuses
`FindRecordsService`.** Avoids duplicating module wiring (auth context,
permissions, object-metadata resolution) and the data-access path.
Tradeoff: "Pick" is a selection rather than a CRUD op, so the folder
name is slightly broad; chose reuse + low risk over a separate module.
Can be extracted if the family grows.

3. **`strategy` exists in the schema (defaulted `RANDOM`) but the
selector is hidden in this PR.** A dropdown with a single option would
be UX slop, and adding the field only in PR 2 would force a data
backfill for any `PICK_RECORD` steps created in between. Keeping the
field now (hidden) avoids both. PR 2 introduces the selector once
there's a real choice.

4. **Pool is an explicit static list (`recordIds`) for v1.** Matches the
most common assignment case ("rotate among these N people") and reuses
the existing `FormMultiRecordPicker`. A filter-based pool (reusing the
Find Records filter UI) and a list-from-a-previous-step pool are natural
follow-ups, intentionally out of scope here to keep the stack focused on
the three strategies.

5. **Output schema is computed on the frontend** (like `CREATE_RECORD`),
derived from `input.objectName` — so it is **not** added to
`PERSISTED_OUTPUT_SCHEMA_TYPES` and needs no server-side schema
computation.

6. **Validation**: `PICK_RECORD` is added to object-name metadata
validation (so a deleted/invalid target object is flagged) via a
dedicated `OBJECT_TARGETING_ACTION_TYPES` set — deliberately **not** to
`VARIABLE_CONSUMING_ACTION_TYPES`, because a static pool legitimately
references no upstream variable and would otherwise raise a spurious "no
variable reference" warning.

7. **Empty pool → step error** at run time (respecting the step's
error-handling options) rather than a silent no-op, since an empty pool
is a misconfiguration or fully-deleted set.

8. **`Math.random`** is used for selection — no cryptographic guarantee
is needed for assignment fairness.

## Testing

Per our testing convention (integration test over service/`.spec`
tests): added `pick-record-workflow.integration-spec.ts`, which builds a
workflow with a manual trigger + a `PICK_RECORD` step, configures a
known two-record pool, runs it, and asserts the run completes and the
picked record is **always** within the configured pool (verifying the
pool filter) across repeated runs.

Local verification (typecheck + lint for shared/server/front) is green;
running the integration suite and attaching editor screenshots in a
follow-up comment.

## Follow-ups

- PR 2: `ROUND_ROBIN` + persistent atomic cursor (Redis `incrBy` vs. a
Postgres counter table — tradeoff to be documented on that PR) +
strategy selector.
- PR 3: `LOAD_BALANCED`.
- Later (not in this stack): filter-based / variable-list pools, an
inline "Assign automatically" entry point on relation fields, OOO-skip /
weighting.

https://claude.ai/code/session_01MuPWZsqf2bmQSevk6fRbX8

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21899?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-21 21:51:16 +02:00
Charles Bochet f4219449db fix(front): prevent AI agent output field error message from overlapping the Type field (#21921)
## Problem

Follow-up to #21834, found during QA.

That PR added an inline validation error on the AI Agent **Output →
Variable Name** field. The error is rendered with `InputErrorHelper`,
which is `position: absolute`. When the message wraps to two lines
(which it does at the side-panel width), it is taken out of the layout
flow and **overlaps the "Type" selector** directly below it:

```
Variable Name
[ sdlfkj sdlkj          ]
Use only letters, numbers, underscores, dots or hyphens (max 64
Type   <-- overlapped by the error message
[ Text                ▾ ]
```

## Fix

Render the error with `InputHint danger` instead of `InputErrorHelper`,
matching how the sibling `FormNumberFieldInput` already shows its
errors. `InputHint` flows in the column (`margin-top`, not absolute), so
the error reserves its own space and pushes the following fields down
instead of overlapping them.

This is a one-line behaviour change in `FormTextFieldInput`; no new
component or styling is introduced.

## After

The `Type` field is pushed below the wrapped error message with correct
spacing:


![after](https://raw.githubusercontent.com/twentyhq/twenty/pr-21921-assets/repro-after.png)

## Tests

- Added a `WithError` story to `FormTextFieldInput` (mirrors the
existing `FormNumberFieldInput` `WithError` story) asserting the error
message is visible.

## QA

Reproduced and verified in Storybook against the real
`WorkflowOutputSchemaBuilder` (throwaway story, not committed): before
the fix the error overlapped `Type`; after the fix the `Type` field is
pushed below the wrapped message with correct spacing.
2026-06-21 18:13:40 +02:00
Charles Bochet 334e962ab5 fix: cannot create record from table view — empty morph to-many relation returns null (#21846)
## Problem

Creating a record from the table view (reproduced on **People**) crashes
the client even though the `createOne…` mutation succeeds server-side,
so the record never appears:

```
Cannot read properties of null (reading 'map')
  getRecordConnectionFromRecords → getRecordNodeFromRecord → optimistic cache effect → createOneRecord
```

## Root cause

An empty **morph** to-many relation comes back as `null`, while every
other to-many relation comes back as `{ edges: [] }`. The frontend then
runs `null.map` while building the optimistic cache node; the error
escapes the mutation `update`, the rollback evicts the record, and it
never lands in the table.

## Fix

**Server** — plain to-many relations are hydrated to `[]` and formatted
to `{ edges: [] }` by `ObjectRecordsToGraphqlConnectionHelper`; an empty
morph to-many was left undefined and the field was skipped (→ `null`).
Default an unset to-many value to `[]` so it goes through the **same
connection path as plain to-many relations**.

**Frontend** — defensive guard in `getRecordNodeFromRecord`: a to-many
relation whose value isn't an array is skipped instead of crashing,
mirroring the existing guard in `extractTargetRecordsFromRelation`.
Needed regardless, since cached data / SSE / older servers still send
`null`.

## Tests

- Unit: `getRecordNodeFromRecord` skips a null to-many (reproduces the
exact crash without the guard).
- Integration: an empty morph `ONE_TO_MANY` read returns `{ edges: []
}`, not null.
2026-06-21 18:05:04 +02:00
Félix Malfait a0689d1577 feat(workflow): condition filter on database-event triggers (#21868)
## Problem

Connecting a mailbox bulk-creates contacts via the email/calendar sync,
and each `person.upserted` fires the seeded **"Create company when
adding a new person"** workflow. The trigger enqueues one run per record
(no batching) and each run bills several `WORKFLOW_NODE_RUN` events — so
a single mailbox connect can rack up tens of thousands of runs and
exhaust credits on a brand-new workspace. The workflow is also redundant
on that path: the sync already creates the company from the email domain
and links the person to it.

## What this does

Adds an optional, user-defined **filter** to database-event (listener)
triggers, evaluated in the listener **before a run is enqueued**.
Non-matching events never create a run, so they consume zero execution
credits. This is the Filter node's capability, lifted to the trigger
level, and available for all event types (created / updated / upserted /
deleted).

The seeded "Create company when adding a new person" workflow now
carries a visible trigger filter — `Created by → Source is not Email`
**and** `is not Calendar` — so it no longer runs for sync-created
contacts, while still running for manually / API / CSV-added people.

## How (reuse)

- **Backend:** extracted `evaluateStepFilters()`, shared by the Filter
action and the trigger listener's new `eventMatchesRecordFilter` gate.
The record is exposed under the `trigger` key so filters reference it
exactly like steps do (`{{trigger.properties.after.…}}`).
- **Shared:** one optional `filter` added to the database-event trigger
zod schema; the front-end type derives from it (settings stay JSON — no
codegen).
- **Frontend:** extracted `WorkflowStepFilterBuilder` from the Filter
action's body; both the Filter action and the trigger editor render it.
The field picker needed no changes — at the trigger it already resolves
to the record's own fields via `TRIGGER_STEP_ID`.

## Scope / decisions

- **No migration for existing workspaces** (by request) — only newly
created workspaces get the filtered default; already-created workspaces
keep the always-on workflow.
- Deliberately did **not** add relation-enrichment to the upsert path
(it would add a DB lookup to the very bulk-sync path we're relieving).
Trigger filters work on the record's own scalar/composite fields (e.g.
`createdBy.source`); relation-based filters work on created/updated
where enrichment already runs.

## Verification

- Typecheck: `twenty-shared`, `twenty-server`, `twenty-front` all green.
- Lint (diff, autofix): 0 warnings / 0 errors across all three.
- Unit tests: a new `evaluate-step-filters` spec exercising the exact
`createdBy.source IS_NOT` seed mechanism, plus new listener specs
proving non-matching events are not enqueued. All backend
filter/listener suites pass.
- Not run here: integration tests (need a DB) and Storybook.

https://claude.ai/code/session_013k36vfekDppwRCgM6Ha7De

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21868?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-21 15:47:06 +00:00
Weiko f8db73598c Fix dangling relation fields crashing records after deleting a custom object (#21874)
Fixes https://github.com/twentyhq/twenty/issues/21706

## Context
Deleting a custom object that has relation/junction fields pointing to
it (e.g. a junction object linked from Person and Company) crashes
record pages with `Target object metadata item not found for <field>`.
The backend cascade correctly deletes the related relation fields, view
fields and page-layout widgets, but the frontend metadata store only
removed the deleted object itself, leaving dangling relation fields (and
stale UI-layer references) behind.

## Fix
After a successful deletion, `useDeleteOneObjectMetadataItem` now calls
`invalidateMetadataStore()`, triggering the existing reconcile path that
refetches objects, fields, indexes, views, view fields and page-layout
widgets. This removes
the dangling relations and cleans up the UI layers in one consistent
pass (also replacing the previous manual command-menu refetch).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21874?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-21 17:26:41 +02:00
Abdul Rahman 544c89119c fix: hide restricted objects and views nested in navigation folders (#21914)
Closes #20141 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21914?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-21 17:24:55 +02:00
Abdullah. 602acf7a16 fix(twenty-website): fill feature-card visual frame on wide viewports (#21876)
## Problem
On the home feature cards, the visual frame is capped at `max-width:
411px` (the scene's design width) and centered. Below ~411px-wide cards
this is invisible, but once a card grows past 411px (wider viewports)
the dark scene stops filling and the **card's light background shows on
both sides** of the visual.

At 1200px everything looks correct because the cards are narrower than
411px and the cap is never engaged; the issue only appears as the
viewport widens.

## Fix
`FeatureCard.tsx`, one file:
- **Remove the `max-width: 411px` cap** (and the now-dead `margin: 0
auto`) from `CardImageFrame` so the frame fills the card width at every
breakpoint. `useScaleToFit` then scales the 411×508 scene up to match —
it's a CSS transform on DOM, so it stays crisp; no raster upscaling.
- **Even out the card gutter** — `CardImage` padding `8px → 16px` (top +
sides) so the visual's inset matches the content's 16px inset instead of
stepping in. Bottom stays `0` (the content block's 16px provides the
bottom gutter).

The visual scenes themselves are untouched — this is purely the
frame/container.

## Before

<img width="1477" height="681" alt="image"
src="https://github.com/user-attachments/assets/731f1ef7-e761-468e-b7aa-a5a06f8ac790"
/>

## After

<img width="1473" height="705" alt="image"
src="https://github.com/user-attachments/assets/99d036a6-d3ad-4682-99c8-f283b5b95171"
/>
2026-06-21 15:05:51 +02:00
Abdullah. a99d380175 feat(website): spotlight visual on top and uniform tile background (#21906)
Two tweaks to the product-feature tiles section:

- **Spotlight visual moved to the top** — the spotlight tile now renders
its visual above its content, matching the layout of the regular grid
cells.
- **Uniform tile background** — every tile now uses the same neutral
gray background; removed the per-tile `TILE_MUTED` alternating toggle.
2026-06-21 15:05:35 +02:00
Abdullah. e90fb4b55c fix(security): bump dompurify to 3.4.11 (config/hook pollution) (#21905)
## fix(security): bump dompurify to 3.4.11 (config/hook pollution)

Resolves [Dependabot Alert
#1520](https://github.com/twentyhq/twenty/security/dependabot/1520) and
[#1509](https://github.com/twentyhq/twenty/security/dependabot/1509).

### What

`dompurify` is affected by:
- **Permanent `ALLOWED_ATTR` pollution via `setConfig()`**
([#1520](https://github.com/twentyhq/twenty/security/dependabot/1520),
Moderate, `<= 3.4.10`)
- **Trusted Types policy survives `clearConfig()`**
([#1509](https://github.com/twentyhq/twenty/security/dependabot/1509),
Low, `< 3.4.9`)

Both patched in `3.4.11`. Bumps the direct `twenty-server` dep `^3.4.0
-> ^3.4.11`.

### Compatibility

Both advisories are about config/hook state pollution via
`setConfig`/`clearConfig`/hooks. All four of our call sites use plain
`DOMPurify(window).sanitize(...)` with **default config** — no
`setConfig`, `clearConfig`, `addHook`, `ALLOWED_ATTR`, or
`RETURN_TRUSTED_TYPE` — so we are not on the affected path, and the fix
does not change default-`sanitize` behavior.

Verification: `typecheck twenty-server` passes; the
`prepare-file-for-storage`, `create-html-to-text-converter`, and
`email-composer` suites pass (28 tests).

### Verification

- `dompurify` resolves to `3.4.11` (no `<= 3.4.10` remains).
- Lockfile + single package.json pin change; `yarn install --immutable`
passes.
2026-06-21 15:05:17 +02:00
Abdullah. d74b6aeadf fix(security): bump nodemailer to 9.0.1 (raw-option SSRF / file read) (#21903)
## fix(security): bump nodemailer to 9.0.1 (raw-option SSRF / file read)

Resolves [Dependabot Alert
#1518](https://github.com/twentyhq/twenty/security/dependabot/1518) and
[#1519](https://github.com/twentyhq/twenty/security/dependabot/1519).

### What

`nodemailer` `<= 9.0.0` lets the message-level `raw` option bypass
`disableFileAccess`/`disableUrlAccess`, enabling **arbitrary file read**
and **full-response SSRF** in the delivered message ([GHSA
advisory](https://github.com/twentyhq/twenty/security/dependabot/1518),
High). Patched in `9.0.1`.

### How — direct bump, no resolution

- **twenty-server:** `nodemailer ^8.0.5 -> ^9.0.1` (major bump).
- **seed-dependencies:** the application-package template `nodemailer
^8.0.5 -> ^9.0.1`; both `DEFAULT_PACKAGE_JSON_CHECKSUM` and
`DEFAULT_YARN_LOCK_CHECKSUM` regenerated to match the recomputed seed
files (the deps-layer cache key).

### Compatibility — verified nothing breaks

It is a major upgrade, so the 9.0 breaking change was checked against
the current tree. The only behavior change is **stricter TLS validation
when nodemailer fetches remote content** (attachment `href`/`path` URLs,
built-in OAuth2 token endpoints, HTTP/HTTPS proxy `CONNECT`). None of
those paths are reachable here:
- Attachments are passed as **content buffers**, never `path`/`href`.
- Gmail OAuth uses **googleapis**, not nodemailer's built-in OAuth2.
- No proxy on any transport.
- The SMTP socket TLS is governed separately (unchanged).

Verification: `typecheck twenty-server` passes (with `@types/nodemailer
^7.0.3`), and the `email-sender`, `gmail-message-outbound`, and
`imap-smtp-caldav-connection` suites pass (10 tests).

### Not covered (follow-up)

Root alert **#1521** will stay open: `imapflow@1.3.6` exact-pins
`nodemailer@8.0.10`. The clean fix is `imapflow 1.4.2` (which pins
nodemailer `9.0.1`), but it published 2026-06-19 and is **age-gated
until ~2026-06-22** — it will land then as a parent-bump (no
resolution).

### Verification

- `nodemailer` resolves to `9.0.1` for twenty-server; seed lockfile has
`9.0.1`; both seed checksums match the canonical recompute.
- `yarn install --immutable` passes.
2026-06-21 15:05:00 +02:00
Félix Malfait 7eafbd91c6 test(server): make timeline integration test self-seed its data (#21896)
## Problem

`timeline-from-object-record.integration-spec.ts` is flaky depending on
Jest shard composition. Its `beforeAll` scans the dev-seeded people for
one with message threads and one with calendar events, and throws when
none is found:

```
Expected the seeded workspace to contain a person with message threads and calendar events
```

This was observed as a deterministic failure of `server-integration-test
(2)` (failed on re-run too), while the other 15 shards were green.

## Root cause

The suite depends on **mutable shared fixture state** under two fragile
assumptions:

1. **That no sibling suite wiped the seeded people.**
`deleteAllRecords('person')` is a common pattern across the REST/GraphQL
suites — `rest-api-core-find-many`, `rest-api-core-find-one`,
`all-people-resolvers`, `search-resolver`, etc. — each hard-deletes
every person (`DELETE FROM "...".person`) and leaves only its own
handful behind, without restoring the seed. Within a shard, Jest runs
files serially (`maxWorkers: 1`) ordered by file size descending (no
timing cache in CI). `rest-api-core-find-many` (~16 KB, runs 2nd)
executes **before** `timeline-from-object-record` (~12 KB, runs 5th), so
by the time the timeline `beforeAll` runs, only 4 company-linked test
people remain — none with threads or events.
2. **That the seeder's `Math.random` participant assignment** happened
to land a thread and an event on a company-linked person within the
first 100 results — itself non-deterministic across DB resets.

It surfaced now because an unrelated PR added a new integration test
file, which changed the total file set and therefore Jest's shard
distribution, moving `timeline-from-object-record` and
`rest-api-core-find-many` into the **same shard** for the first time. It
is a latent test-isolation issue, not a product regression.

### Reproduced locally

Against a DB where `rest-api-core-find-many` had already run (person
count = 4), the timeline suite fails with the exact CI error; on a
freshly seeded DB it passes. So the failure is purely order/seed
dependent.

## Fix

Make the suite self-contained: in `beforeAll` it now provisions its own
graph via the GraphQL API and tears it down in `afterAll`:

```
company → person → messageThread → message → messageParticipant(personId)
                 ↘ calendarEvent → calendarEventParticipant(personId)
```

The timeline resolvers count threads via `messageThread → messages →
messageParticipants.personId` and events via `calendarEvent →
calendarEventParticipants.personId`, so this graph is sufficient and
minimal. The suite no longer reads any ambient seeded data, making it
independent of execution order and seeding randomness.

## Validation

- Self-seeding suite passes against the **polluted** DB (4 people, no
seeded threads/events) — the exact CI failure condition.
- Idempotent across repeated runs and leaves **no residue** (all
fixtures destroyed in `afterAll`).
- Full `--shard=2/16` run green except a pre-existing environmental
failure (`successful-save-imap-smtp-caldav-account`, fails locally with
no mail server, identical before/after this change).

https://claude.ai/code/session_013k36vfekDppwRCgM6Ha7De

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21896?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-20 14:29:28 +02:00
Félix Malfait b8ea742a88 fix(front): respect user number format for counts and aggregates (#21894)
## Problem

Several user-facing numbers were rendered raw (e.g. `153909`) instead of
honoring the workspace member's **Number format** preference (e.g. `153
909` with `Spaces and comma`). The formatting utilities already existed
(`formatNumber` / `useNumberFormat`) but were not applied on these
surfaces.

## Root cause

`transformAggregateRawValueIntoAggregateDisplayValue` — the shared
helper behind every table/board/chart aggregate — returned the `COUNT`
branch as a raw string and never threaded the user's locale format into
`formatNumber` for the other branches (so they silently fell back to
`COMMAS_AND_DOT`).

Its existing `numberFormat` param actually held the chart `SHORT`/`FULL`
abbreviation setting, so it is renamed to `chartNumberFormat`, and a new
`numberFormat: NumberFormat` now carries the locale separators.

## Surfaces fixed

- Record table footer aggregates, including the raw **"Count all"**
total
- Record board column / group-section aggregates
- Aggregate chart and pie-chart center metric (including their raw
`COUNT` early-returns)
- View picker `<view> · <count>` total
- Record show breadcrumb pagination `(x/y)`
- Record index header and side panel `N selected` counts

The board-column header needs no change — it now receives an
already-formatted string from the transform.

## Out of scope (intentionally left raw)

The editable `SettingsCounter` input (formatting would break parsing),
the advanced-filter pill, the `+N` overflow badge, and the AI routing
debug display.

## Testing

- New + existing unit tests pass
(`transformAggregateRawValueIntoAggregateDisplayValue`, `formatNumber`,
`useNumberFormat`), with added locale-aware coverage (`SPACES_AND_COMMA`
→ `153 909`, `DOTS_AND_COMMA` → `153.909`).
- `nx typecheck twenty-front`, oxlint and oxfmt on the diff all pass.

> Note: two i18n strings change placeholder shape (`{count} selected` →
`{0} selected`); a `lingui:extract` will refresh the catalogs (runtime
falls back to source text meanwhile).

https://claude.ai/code/session_013XNL2Xa11Bw7fsnPFQgsGX

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

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

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-20 00:40:19 +02:00
martmull 6423c4cd3c Add recall io webhook endpoint (#21879)
## Context

Bot-recording integrations (e.g. the Recall.ai meeting bot) receive
webhooks from a third-party provider that delivers **every
tenant's events to a single URL**. Our existing `route-trigger` (`/s/…`)
resolves the workspace from the request host, which can't
work for one shared multi-tenant webhook URL. We need an instance-scoped
ingress that identifies the target workspace from the payload
  instead.

  ## Strategy

Add a new **`ingress-trigger`** logic-function trigger, mirroring
`route-trigger`:

  - A public endpoint keyed by the app's identifiers: `POST

/webhooks/ingress/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier`.
- The logic function declares an `ingressTriggerSettings` block in its
manifest describing how to find the workspace in the payload
  (`workspaceId: { source: 'body' | 'query' | 'header', path }`).
- Core only **resolves the workspace** (declarative, fail-closed,
prototype-safe path getter), verifies the app is installed in that
workspace, then runs the function **synchronously** so the provider sees
the response (status codes / retries).
- **Signature verification stays in the logic function** (it gets
`rawBody` + forwarded headers), keeping core provider-agnostic.
- Shared execution logic (`build event → execute → map response`)
extracted into `LogicFunctionTriggerService`, now reused by both
  `route-trigger` and `ingress-trigger`.

  ## Major changes

- **twenty-shared**: new `ingressTriggerSettings` on
`LogicFunctionManifest` (`IngressTriggerSettings` type).
- **twenty-server**: new `ingress-trigger` module (controller, service,
exception + filter, workspace-id resolver util).
- **twenty-server**: extracted `LogicFunctionTriggerService` +
`route-trigger-response.util` (response builder + sender); refactored
  `RouteTriggerService` and both controllers to reuse them.
- **twenty-docs**: documented the ingress trigger (endpoint, workspace
resolution, signature responsibility, provider HMAC examples).
  - Unit tests for the resolver and the ingress service.
2026-06-19 23:34:43 +02:00
nitin dd6fbbe854 Rename meeting bot app variables from RECALL_BOT_* to MEETING_BOT_* (#21878)
sorry recall :)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21878?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-20 00:32:49 +05:30
nitin c2e3296d85 Add transcript tab to calendar record page (#21792)
Adds a read-only "Transcript" tab to the CalendarEvent record page,
contributed by the Twenty Meeting Bot app. Renders the diarized
transcript stored on `CallRecording.transcript`, with placeholder states
for pending and failed transcription.

Front-end only — the transcription pipeline (request/download/reconcile
+ PENDING/FAILED markers) already landed on main.

Deferred: live-mount verification via `yarn twenty dev`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21792?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-19 23:35:35 +05:30
Raphaël Bosi f9084fd208 Clear stale parent-view filters on front-component cross-object navigation (#21869) 2026-06-19 19:21:54 +02:00
Abdullah. 8899360ebe fix(security): refresh undici across lockfiles (6.x → 6.27.0, 7.x → 7.28.0) (#21870) 2026-06-19 19:15:47 +02:00
github-actions[bot] 1578cc2562 i18n - docs translations (#21873)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 19:10:58 +02:00
Raphaël Bosi fecf699bc5 Fix broken CSV import grid layout (#21867)
## What

Import `react-data-grid/lib/styles.css` in `SpreadsheetImportTable`, the
single component that renders the import grid (used by the Validate Data
and Select Header steps).

## Why

The React 19 migration (#21531) bumped `react-data-grid` from
`7.0.0-beta.13` to `7.0.0-beta.59`. The old beta auto-injected its
layout CSS; beta.59 ships it as a separate
`react-data-grid/lib/styles.css` export that must be imported manually.
It was never imported, so the grid lost its base layout (grid template,
row heights, cell positioning): rows stacked at full height and columns
no longer aligned. The library scopes its styles under `@layer rdg`, so
the existing Linaria theme overrides still take precedence.

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


## Before
<img width="2540" height="1448" alt="CleanShot 2026-06-19 at 17 43
58@2x"
src="https://github.com/user-attachments/assets/a208b518-9088-4988-8245-4fdc4f8bc8de"
/>


## After
<img width="2454" height="1392" alt="CleanShot 2026-06-19 at 18 02
36@2x"
src="https://github.com/user-attachments/assets/e0b30d71-8244-4363-86aa-60b12a2dfdd9"
/>
2026-06-19 18:10:12 +02:00
Paul Rastoin ee5b3a65f4 Workspace migration post transaction commit side effect (#21845)
# Introduction
Avoid side effect in transaction to reduce lock duration
As discussed we're going to introduce in migration runner side effect
later through jobs and metadata boolean state tracker in db

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21845?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-19 15:50:13 +00:00
github-actions[bot] a98e3a6df9 i18n - website translations (#21866)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 17:26:11 +02:00
github-actions[bot] 4e64a38e1e i18n - docs translations (#21865)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 17:23:24 +02:00
github-actions[bot] 9695d77252 i18n - website translations (#21864)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 15:22:32 +00:00
martmull a870e034a6 Add twenty slack to internal application ci (#21849)
- adds `twenty-slack` to internal application ci 
- unify config with twenty-last-contact app
- add base oxlint config to show error twenty-shared is used in internal
app

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21849?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-19 14:39:42 +00:00
Thomas Trompette 15fd236ad0 fix(workflow): add tooltip explaining why the variable picker is disabled (#21862)
## Context

Closes #21773
<img width="448" height="301" alt="Capture d’écran 2026-06-19 à 16 33
56"
src="https://github.com/user-attachments/assets/4efc637e-3361-4108-86b6-92ffc2e84252"
/>


When a workflow's variable picker (the `+` button next to a field) is
disabled — e.g. on a step whose only trigger is a global manual trigger
that produces no record variables — the button just shows a
`not-allowed` cursor with no explanation of *why*.

## Change

Add an `AppTooltip` to the disabled state of `WorkflowVariablesDropdown`
explaining the reason:

> No variables are available yet. Variables come from the workflow
trigger and previous steps.

The disabled state is reached via `disabled === true ||
noAvailableVariables`. In practice the callers hide the picker entirely
in read-only mode (it's rendered only when `!disabled`/`!readonly`), so
the meaningful trigger is **no available variables** — hence a single
message rather than separate copy per reason.

The tooltip is anchored with a `data-*` attribute selector instead of an
`#id`, because the picker's `instanceId` comes from React's `useId()`
(values like `:r1:`) which are invalid in a CSS `#id` selector that
`AppTooltip` runs through `querySelectorAll`.

## Testing

- `nx lint:diff-with-main twenty-front` — passes (lint + format).
- Verified the component resolves/renders on a local instance running
this branch.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21862?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-19 16:37:07 +02:00
nitin 973b35989e Add standard record page layout for calendar events (#21857)
Moves calendar event details from the bespoke side-panel page to the
standard record page layout system.

- Adds standard calendar event record page metadata, fields view,
widgets, tests, snapshots, and upgrade command for existing workspaces.
- Opens calendar events through the generic ViewRecord side-panel path.
- Adds participants and call recordings as standard field widgets.
- Removes the old custom calendar event side-panel page and related
side-panel enum/config entry.

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




https://github.com/user-attachments/assets/c1f88cac-1615-478c-a3dd-87d0c61ab9a8

<img width="3024" height="1658" alt="CleanShot 2026-06-19 at 19 01
27@2x"
src="https://github.com/user-attachments/assets/c3df5705-ff08-446e-ac3c-6ccb11cf21ec"
/>

<img width="3024" height="1658" alt="CleanShot 2026-06-19 at 19 01
19@2x"
src="https://github.com/user-attachments/assets/633525db-310c-4462-8458-a72068cc1432"
/>
2026-06-19 20:06:33 +05:30
Félix Malfait 23f5ba9ebf feat: add resizable kanban column width (#21828)
## What & why

Lets users resize the columns of a Kanban (record board) view. Requested
by a user; the design avoids the "ragged board" problem by making the
width a **single shared value**.

## Behaviour

- A drag handle appears on the right edge of every column header.
- Because all columns read **one** width value, dragging any handle
resizes **every** column together — they can never end up mismatched.
- Width is clamped between **150px** and **400px** (default **200px**).
- The width is **persisted per view** and restored on reload.

## Approach

**Backend** — a new nullable `View.kanbanColumnWidth` field, threaded
through the existing view-level setting pattern (the same one
`kanbanAggregateOperation` / `shouldHideEmptyGroups` use), so it gets
create/update/manifest/override support for free:
- entity column + `ViewOverrides` + `@WasIntroducedInUpgrade`
- `CreateViewInput` / `UpdateViewInput` (`Int`, `@Min(150)`/`@Max(400)`)
+ `ViewDTO`
- flat-view editable properties, entity-properties config, compare-type,
standard-view + manifest converters
- a fast instance command adding the `core.view` column

**Frontend** — the value hydrates into a view-scoped atom and drives a
single CSS variable set on the board container, which both column
headers and bodies read. Live dragging only writes that CSS variable (no
per-move React re-render); the final width is committed to the atom and
persisted via `updateView` on pointer-up.

## Nullability / defaults

`kanbanColumnWidth` is nullable — `null` means "never resized" and the
UI falls back to the 200px default, so existing rows need no backfill.

## Validation

- `nx typecheck twenty-server`  and `nx typecheck twenty-front` 
- `nx lint:diff-with-main twenty-server` ; frontend lint fixes applied
(split constants to one-per-file, removed `useRef`-for-state in favour
of `useState`).
- Draft pending a final green CI run (the dev container reclaimed
`node_modules` mid-session; re-running locally).

## Test plan

- [ ] Drag a kanban column edge → all columns resize together, clamped
150–400px
- [ ] Reload → width persists for that view; other views unaffected
- [ ] A view that was never resized still renders at 200px

https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21828?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-19 15:58:13 +02:00
Raphaël Bosi 1f6c2b89fd Accessibility guardrails and component hardening for twenty-ui (#21848)
Builds on twenty-ui's existing runtime axe gate by adding a static
enforcement layer and fixing accessibility gaps in shared components.
Color contrast is intentionally out of scope (still deferred via
`A11Y_DEFER_COLOR_CONTRAST`).

## What changed
- **Static guardrails:** enabled oxlint's `jsx-a11y` plugin
(keyboard-operability rules at `error`), and added a custom
`twenty/no-storybook-a11y-disable` rule that blocks `a11y: { test: 'off'
| 'todo' }` so the axe gate can't be silently disabled again.
- **Focus visibility:** wired the existing `focus-ring` mixin into all
buttons for real `:focus-visible` rings (was `outline: none`).
- **Decorative icons:** `aria-hidden` on icons inside labeled buttons
(added to `IconComponentProps` + render sites).
- **Inputs:** accessible-name support on `SearchInput` and `Checkbox`.
- **Interactive components:** `Tag` renders a real `<button>` when
clickable; the non-semantic clickable `div`s (`Avatar`, `Status`,
`ColorSchemeCard`, `NavigationBarItem`, etc.) are now keyboard-operable
via a shared `handleClickableElementKeyDown` helper, role and accessible
name.

## Notes for reviewers
- Two `oxlint-disable` lines remain on genuine non-interactive capture
wrappers (`CodeEditor`, `OverflowingTextWithTooltip`).
- 8 lint warnings remain by design: conditional-interactivity
`no-static-element-interactions` and legitimate `autoFocus` on
`SearchInput`.
- `NavigationBarItem` gained a required `ariaLabel`; its only consumer
(`MobileNavigationBar`) is updated with translated labels.

## Follow-ups (separate PRs)
- Enforced accessible names on icon-only buttons
(`IconButton`/`LightIconButton`) — breaking, ~128 call sites.
- `aria-activedescendant` wiring for the dropdown/listbox keyboard
layer.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21848?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-19 15:57:23 +02:00
Abdullah. 3473410d64 fix(website): restore deploymentId, security headers and redirects in next config (#21851)
The rebuilt-site cutover dropped several `next.config.ts` blocks the
previous twenty-website had:

- **`deploymentId`** — required by `open-next.config` skew protection;
deploys were failing with *"Deployment ID should be set in the Next
config when skew protection is enabled"*.
- **Security headers** (HSTS, CSP `frame-ancestors`, X-Frame-Options, …)
+ immutable asset `Cache-Control`.
- **Redirects** — www→apex canonicalization and content redirects (docs,
legal, case-studies, partners, why-twenty).

Legacy raw locale-code redirects were intentionally not restored (the
site only serves en/fr/es; those URLs never existed for other locales).

Pairs with twentyhq/twenty-infra#741, which builds twenty-ui before the
worker build.
2026-06-19 18:47:40 +05:00
github-actions[bot] 8712ae754d i18n - docs translations (#21859)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 15:43:40 +02:00
Thomas Trompette 34a2037b65 fix(front): keep record table footer visible below information banner (#21852)
## Context

Fixes #21765.

<img width="1439" height="961" alt="Capture d’écran 2026-06-19 à 15 03
46"
src="https://github.com/user-attachments/assets/07104099-19de-45f9-9cca-eaaacbc326af"
/>

When a page-level information banner is visible on a record table (e.g.
the mailbox **"Sync lost with mailbox … Please reconnect"** banner), the
table footer / bottom edge was hidden behind the card boundary. As a
side effect, drag-select **auto-scroll never triggered** near the
bottom, because the cursor could not reach the scroll wrapper's real
bottom edge.

## Root cause

In `PageCardLayout`, the `InformationBannerWrapper` and the page
children are siblings in a flex column. The record index child
(`StyledIndexContainer`) used `height: 100%`, so it demanded the
**full** body height regardless of the banner. With a banner present,
banner height + 100% exceeded the card, and since the container's
content (the table) has a large min-content height it would not shrink —
so the bottom (the footer) was pushed past `StyledCard`'s `overflow:
hidden` and clipped.

`useDragSelectWithAutoScroll` only scrolls when the cursor is within
`AUTO_SCROLL_EDGE_THRESHOLD_PX` (20px) of `containerRect.bottom`. With
the bottom edge clipped off-screen, that zone was unreachable, so
auto-scroll appeared broken.

## Fix

Replace `height: 100%` with `flex: 1; min-height: 0;` so the container
takes the space **remaining** after the banner — the same flex idiom its
parent `StyledBodyContent` already uses. When no banner is shown, the
banner wrapper collapses to `height: 0`, so the table fills the full
height exactly as before (no behaviour change in the common case).

## Testing

- Verified locally: with the mailbox reconnect banner forced visible on
the Companies table, the footer (aggregate row) stays visible and
drag-select auto-scroll reaches the bottom.
- No change when no banner is present.

This is a layout fix, not a drag-select threshold change — as suggested
in the issue, raising the threshold would only mask the layout problem.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21852?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-19 15:39:22 +02:00
Abdul Rahman 0758a4fcef reset filter search input on field select (#21850)
### Before



https://github.com/user-attachments/assets/3e5d2193-c638-4898-a11b-a9a1b9607206





### After



https://github.com/user-attachments/assets/99eab3e2-8475-46dd-915b-0324ada53e5a





<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21850?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-19 18:56:00 +05:30
Paul Rastoin 98c35ea804 App uninstall lambda, layers cleanup (#21749)
# Introduction
On a logic function deletion also remove the driver entry
On a app uninstall also remove the sdk layer ( keeps the dep one as it
can be shared across several lambda )

| Resource | Scope | Before this PR | After |
|---|---|---|---|
| DB metadata (functions, objects, fields…) | per-app | deleted |
deleted |
| Source folder (`FileFolder.Source`) | per-function | deleted | deleted
|
| Built handler file (`FileFolder.BuiltLogicFunction`) | per-function |
deleted | deleted |
| **Lambda function** | per-function | **leaked** | **deleted** (driver
`delete`) |
| **SDK layer** `sdk-<wsId>-<appId>` (all versions) | per-app |
**leaked** | **deleted** (driver `deleteApplicationResources` →
`deleteSdkLayer`) |
| Deps layer `deps-<checksum>` | shared across apps/workspaces | not
deleted | **intentionally not deleted** (content-addressed, GC'd) |

## What I don't like about all that
Right now there's some non reversible side effect inside the workspace
migration transaction
- If the transaction fails we're facing data loss
- It also slows down everything

I'm about to create a new PR that allow population post transaction
commit side effect / cleanup to be run later

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21749?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-19 13:10:39 +00:00
Thomas Trompette a59ed80422 fix(front): allow null subfields in Phones default value so Save enables (#21847)
## Problem

Closes #21780.

When editing a **Phones** field in Settings → Data Model, changing the
**Default Country Code** does not enable the Save button — the form
becomes dirty but never valid, so the change can't be saved.

## Root cause

The settings form validates `defaultValue` with the record-value
`phonesFieldValueSchema`, which requires non-null strings:

```ts
primaryPhoneNumber: z.string(),
primaryPhoneCountryCode: z.string(),
```

But a Phones default value can legitimately have **null** subfields — a
default country code with no default number. The backend normalizes
empty subfields to `null`
(`nullify-empty-phones-default-value.util.ts`), and the shared contract
`FieldMetadataDefaultValuePhones` is `string | null`. So an existing
field whose stored default has `primaryPhoneNumber: null` makes the form
**permanently invalid**: changing the country code preserves the null
number → `isValid` stays `false` → `canSave = isDirty && isValid` keeps
Save disabled.

The sibling **address** field doesn't have this bug because
`addressFieldValueSchema` already makes every subfield `.nullable()`.
Phones was simply inconsistent.

## Fix

Add a dedicated `phonesFieldDefaultValueSchema` with nullable subfields
(mirroring the address pattern and matching
`FieldMetadataDefaultValuePhones`) and use it in the Phones settings
form. The stricter record-value `phonesFieldValueSchema` is left
untouched, so record input/persistence/empty-checks are unaffected.

## Test plan

- [x] Unit test covering the partial-null default value (and asserting
the record-value schema still rejects it)
- [x] `nx typecheck twenty-front` clean
- [x] `nx lint:diff-with-main twenty-front` clean
- Manual: open a Phones field, set a Default Country Code and save,
re-open, change the country code → Save now enables.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21847?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 Opus 4.8 <noreply@anthropic.com>
2026-06-19 13:07:37 +00:00
Félix Malfait 705caab2b0 fix(onboarding): refresh stale workspace in currentWorkspace field resolver (#21839)
## What & why

After sign-up, users are redirected to `/sync/emails` and the page loads
forever; a full refresh fixes it. This blocks production deploy.

**Root cause** — a GraphQL field resolver returns an unrefreshed (stale)
workspace:

- `@AuthWorkspace()` (`request.workspace`) is read from the per-instance
core entity cache and can still be `PENDING_CREATION` /
`ONGOING_CREATION` right after `activateWorkspace`.
- The `currentUser` query resolver and the `onboardingStatus` field
already guard against this by calling
`refreshWorkspaceIfPendingOrOngoingCreation(...)`.
- But the `currentWorkspace` `@ResolveField` returned the raw
`@AuthWorkspace()` workspace. Because a field resolver takes precedence
over any value the query resolver attaches to its returned object, the
client receives that stale workspace.

So right after activation the client got an inconsistent payload:
- `onboardingStatus: SYNC_EMAIL` (fresh — computed from a direct DB
read)
- `currentWorkspace.activationStatus: ONGOING/PENDING_CREATION` (stale)

On the frontend, metadata loading is gated on
`isWorkspaceActiveOrSuspended(currentWorkspace)`, and
`MinimalMetadataGater` does **not** exclude `/sync/emails`. So the
workspace looked inactive → metadata never loaded (no metadata GraphQL
request was even issued) → the gater's loader showed indefinitely. A
full refresh worked because the cache had since refreshed to `ACTIVE`.

## Why it surfaces on staging but isn't caught by tests

The stale window only opens on a real fresh sign-up followed by
immediate activation, against a workspace cache that hasn't refreshed
yet (multi-instance / cache TTL). Single-instance local dev and the
existing `successful-user-and-workspace-creation` integration test
exercise `activateWorkspace` + `getCurrentUser` against one consistent
cache, so `currentWorkspace` already looks `ACTIVE` and they pass —
which is why this reproduces on staging/production but not locally, and
why a manual refresh recovers.

## How

Refresh the workspace in the `currentWorkspace` field resolver too, so
it is consistent with `onboardingStatus`. For active workspaces this is
a no-op (no extra DB read).

```ts
async currentWorkspace(@AuthWorkspace({ allowUndefined: true }) workspace) {
  if (!isDefined(workspace)) return workspace;
  return this.userService.refreshWorkspaceIfPendingOrOngoingCreation(workspace);
}
```

This is preferred over the frontend alternative (excluding
`/sync/emails` from `MinimalMetadataGater`), which would only hide the
symptom while every other consumer still received a wrong
`activationStatus`.

## Verification
- `nx typecheck twenty-server` and `nx lint:diff-with-main
twenty-server` (oxlint + oxfmt) are green.

https://claude.ai/code/session_018c1X6CwDgttMXA5tB797yS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-19 14:45:12 +02:00
Raphaël Bosi 88967a6e47 Add Update fields select to People Data Labs enrichment functions (#21801)
## What

The People Data Labs enrichment logic functions (`enrich-person`,
`enrich-company`, `enrich-people`, `enrich-companies`) now expose an
**`Update fields`** select instead of the `overrideExistingValues`
boolean, and always return the enriched data in their output.

`Update fields` options:
- **Yes and overwrite**: persist, overwriting existing standard fields
- **Yes and don't overwrite** (default): persist, filling standard
fields only when empty
- **No**: write nothing to the record (no CRM fields, no PDL metadata,
no company creation)

## Why

The functions previously only persisted data. With `No`, they can now
fetch from PDL and return the result without modifying the record, so
downstream workflow steps can consume it. Every matched result now
carries a `data` object with the mapped record fields (standard + `pdl*`
values), and the bulk functions also declare their `results[]` array in
the output schema.

Billing is unchanged: a successful PDL match is still charged in all
modes, since the API cost is incurred regardless of persistence.

## Notes

- Default behavior is preserved (unset input means fill-empty +
persist).
- Typecheck, lint, and the full unit suite (368 tests) pass.
2026-06-19 14:39:49 +02:00
Abdul Rahman d19b7f8485 Enable getting started translations (#21842)
## Summary

The Getting Started pages on the docs site (docs.twenty.com) were only
ever available in English, never translated into the other supported
languages.

**Root cause:** The Getting Started section (added in #19728) was never
added to the Crowdin source config (`crowdin-docs.yml`), so its `.mdx`
files were never uploaded for translation. Only `user-guide`,
`developers`, and `twenty-ui` were configured.

This also surfaced a related bug: because the pages had no translations,
the navigation generator fell back to the English page path for every
language, duplicating paths like `getting-started/introduction` across
all 14 language navs. Mintlify treats duplicate cross-language paths as
undefined behavior, which broke the language switcher (it always
redirected to `/getting-started/introduction`).

## Changes

- `.github/crowdin-docs.yml` — add `getting-started/**/*.mdx` as a
translation source so the pages get sent to Crowdin.
- `packages/twenty-docs/scripts/fix-translated-links.sh` — add
`getting-started` link-rewriting rules to match the other sections.
- `packages/twenty-docs/scripts/generate-docs-json.ts` — only include a
page in a non-default language when its translated file exists; drop
empty groups/tabs (removes the duplicate cross-language paths that broke
the switcher).
- `packages/twenty-docs/docs.json` — regenerated.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21842?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: Cursor <cursoragent@cursor.com>
2026-06-19 14:16:17 +02:00
github-actions[bot] f61522b56a i18n - docs translations (#21841)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 13:52:47 +02:00
Charles Bochet 0064ff6741 fix(ai): validate AI agent output field names against schema-key constraint (#21834)
## Problem

On a self-hosted instance, an AI Agent workflow action fails at run time
with an opaque model error:

```
The model returned the following errors: tools.0.custom.input_schema.properties:
Property keys should match pattern '^[a-zA-Z0-9_.-]{1,64}$'
```

This is Anthropic's validation on tool `input_schema` **property keys**.
An AI Agent's structured **Output** fields are turned into a JSON schema
and passed to the model as a tool; each output **variable name** becomes
a property key. Anthropic rejects any key that does not match
`^[a-zA-Z0-9_.-]{1,64}$` — most commonly a name containing a **space**
(e.g. `meetings brief`), but also names over 64 characters or with other
symbols.

Until now nothing validated this: `fieldsToSchema` writes
`properties[field.name]` verbatim, so a bad name only failed once the
workflow executed, with an error that gives the user no idea what to
fix. It doesn't reproduce on every instance — it depends purely on how
the workflow's output variables happen to be named.

## Fix

Introduce a single shared check,
`isValidAgentResponseSchemaPropertyKey`, and enforce it in two places:

- **Backend** — `validateAgentResponseFormat` now rejects invalid output
field names at agent **save time** with a clear `userFriendlyMessage`,
instead of letting the broken schema reach the model. This also gates
agents created via the API and re-saves of existing bad data.
- **Frontend** — the output schema builder shows an inline error on the
Variable Name field as soon as an invalid name is entered.

## Tests

- Unit test for the shared validity check (valid + invalid cases:
spaces, leading space, empty, > 64 chars, symbols, unicode).
- Unit test for `validateAgentResponseFormat` covering text/json
formats, valid names, a space in a name, an over-length name, and
reporting multiple invalid names at once.

## Notes for the reporter

The immediate unblock for an affected workflow is to rename the output
variable to remove the space (e.g. `meetings brief` → `meetings_brief`)
and retry the run. With this change the bad name is caught up front with
an explanation rather than failing mid-run.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21834?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-19 13:50:44 +02:00
martmull 576f88b5c5 Update ci internal applications (#21837)
Fix `twenty-last-contact` ci

- add tests
- add tsgo
- update package commands

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21837?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-19 13:42:15 +02:00
Dilan Melvin T 1bd7be36e0 fix(front): recompute ExpandableList visible chips on resize (#21139)
## Summary

Relation field cells in the record table render their chips through
`ExpandableList`, which measured how many chips fit only once (during
the
children ref pass) and cached the cutoff. It recomputed on item-count
and
hover changes, but never when the cell's available width changed — so a
cell
measured while narrow stayed stuck on that count even after the column
grew
wider. This is the "only ~3 items shown even when the cell is larger"
bug.

This PR adds a `ResizeObserver` on the outer container that resets the
first
hidden child index whenever the available width changes, so the list
reveals
as many chips as fit (and re-trims when narrowed). The outer container
is
observed because its width tracks the available width independently of
how
many chips are currently rendered, which avoids a
measure → trim → shrink → re-measure feedback loop. The observer is
cleaned
up on unmount.

## Test plan

- [x] Added a Storybook interaction test
(`RecomputesVisibleChipsOnResize`)
that renders the list in a narrow container, widens it, and asserts more
      chips become visible.
- [x] Verified the test fails without the fix and passes with it.
- [x] `oxfmt` and `oxlint` pass on the changed files.
- Manual: open a record table with a to-many relation field that has
several
  linked records, widen the column, and confirm more chips appear.

Fixes #12039

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 13:38:39 +02:00
Paul Rastoin 26db3f5735 Deprecate legacy encryption (#21831)
# Introduction
Still preserving the cross-upgrade flow

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


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21831?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-19 11:32:39 +00:00
Parship Chowdhury 4de9f45015 fix: Edit Layout keeping the command menu open (#21161)
## Summary
- Fixes https://github.com/twentyhq/core-team-issues/issues/2460
- Engine/headless commands always skipped closing the menu
(`closeSidePanelOnCommandMenuListExecution: false`), even when the item
was not pinned. Edit Layout is `isPinned: false`, so the menu should
close like other list-only actions.

## Approach
- Option 1 (I chose this one): Derive close behavior from
`item.isPinned` -> pinned commands keep the menu open; non-pinned ones
close it.
- Option 2 (not chosen): remove the engine command override totally and
use the default close behavior for all commands.

Option 1 is more targeted: it fixed Edit Layout without changing pinned
commands (e.g. Export progress in the menu list). Option 2 is simpler
but widens the blast radius to every engine command clicked from the
side panel list.

## Screenshots
### Before

https://github.com/user-attachments/assets/70b8dc75-af00-4917-81a1-646381f571d5

### After

https://github.com/user-attachments/assets/f733d696-9330-4e6c-993b-8a8133c53e0d

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 11:21:50 +00:00
Thomas Trompette 1ec59beb8b docs(user-guide): remove unsupported Between operator for Number filters (#21838)
## What

The [Filters & Sorting user
guide](https://docs.twenty.com/user-guide/views-pipelines/capabilities/filters-and-sorting)
lists a **Between** operator for **Number** fields, but it does not
exist in the product and we do not plan to implement it. This removes it
from the operators table.

| Field Type | Before | After |
|---|---|---|
| Number | Equals, Greater than, Less than, **Between**, Is empty |
Equals, Greater than, Less than, Is empty |

The **Date** row is intentionally left untouched.

## Why

Avoids documenting a capability that does not exist (the same gap that
prompted #20932). Closing the external PR that attempted to add it for
numbers; the docs should reflect what we actually ship.

## Notes

- Only the canonical English source is edited; localized copies under
`packages/twenty-docs/l/*` are regenerated by the existing i18n
docs-translation workflow.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21838?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-19 12:58:50 +02:00
Félix Malfait 5674f693d7 fix: prevent Create Workspace redirect from being cancelled (#21835)
## Summary

Clicking **Create Workspace** in the multi-workspace dropdown did
nothing.

The handler closed the dropdown right before redirecting:

```ts
const createWorkspace = () => {
  closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); // unmounts this component
  redirectToDefaultDomain({ ... });           // schedules window.open ~1ms later
};
```

`redirectToDefaultDomain` → `useRedirect` wraps the navigation in
`useDebouncedCallback(..., 1)`. `closeDropdown` flips the dropdown
content to `{isDropdownOpen && ...}` → `false`, unmounting
`MultiWorkspaceDropdownDefaultComponents` — the component that owns that
debounced callback. `use-debounce` drops the pending call on unmount, so
the queued `window.open` never fires. React commits the unmount before
the 1ms timer, so it loses every time. Regression from #21723, which
added the `closeDropdown` call.

## Fix

Remove the `closeDropdown` call. The redirect navigates the whole page
away, so closing the dropdown first is unnecessary — and it mirrors the
sibling "switch workspace" handler, which already redirects without
closing.

## Why not reorder, or drop the debounce?

The 1ms debounce in `useRedirect` is intentional (#9079, "sleep before
redirect"). Callers set cookie-backed state immediately before
redirecting — e.g. `redirectToDefaultDomain` clears the
`lastAuthenticateWorkspaceDomain` cookie via `useCookieStorage`.
Deferring the hard navigation by one macrotask lets that cookie write
flush before the page tears down; removing it risks dropping the write.
Reordering wouldn't help either, since the unmount still beats the
timer. So the debounce is left untouched.

## Logout is not affected

`signOut` → `clearSession` navigates with `window.location.assign(...)`
directly (synchronous, not debounced) and never calls `closeDropdown`,
so it can't hit this race.

## Testing

- Before: clicking Create Workspace → `window.open` called 0 times, page
unchanged.
- After: navigates to
`<defaultDomain>/welcome?action=create-new-workspace` and renders the
"Create your workspace" form.
- Switch-workspace and Log out both still work.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21835?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-19 12:57:06 +02:00
Thomas Trompette 4075018834 fix(workflow): label manual trigger record output as Record/Records (#21832)
## What

The manual trigger output schema exposed the triggering record(s) under
a node labeled **Payload**. Relabels it to match what the node actually
contains:

- **Single-record** availability → **Record**
- **Bulk-records** availability → **Records**

## Why

"Payload" was a misnomer — the node holds the record(s) that triggered
the workflow. This is a display-label-only change.

## Notes for reviewers

- **No migration.** The persisted output schema key stays `payload`, so
existing variable references (`{{trigger.payload.x}}`) are unaffected.
- The front recomputes the output schema on the fly
(`computeStepOutputSchema`), so the variable picker shows the new labels
immediately, including for existing triggers.
- The backend (`workflow-schema.workspace-service`) is updated to match
for newly persisted/re-saved schemas. Previously persisted schemas keep
"Payload" until re-saved.
- Added `WORKFLOW_TRIGGER_RECORD_LABEL` /
`WORKFLOW_TRIGGER_RECORDS_LABEL` and removed the now-unused
`WORKFLOW_TRIGGER_PAYLOAD_LABEL`.
- Unit tests updated for both single and bulk cases (55/55 passing).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21832?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-19 12:36:14 +02:00
martmull 8f7d7101ff Add application to test input in workflow (#21830)
https://github.com/twentyhq/twenty/pull/21791 follow up
2026-06-19 11:56:56 +02:00
Thomas Trompette ceecae30db fix(workflow): show fields for system objects in record-updated trigger (#21826)
## Problem

On the **Record is updated** (and **upserted**) workflow trigger,
picking a record type under the **Advanced** submenu (i.e. a *system*
object) showed an empty "Fields (Optional)" list — you couldn't select
any field to watch.

## Root cause

The trigger's field picker (`WorkflowFieldsMultiSelect`) was called with
`actionType="UPDATE_RECORD"`, which runs each field through
`shouldDisplayFormField`. For `UPDATE_RECORD` that predicate requires
`(isUIEditable ?? true)` — correct for the *Update Record action* (you
can't write to a read-only field), but wrong for a *trigger*, where
you're choosing which fields to **watch for changes** and editability is
irrelevant.

System objects define their fields with `isUIEditable: false`, so every
field failed the gate and the list rendered empty.

## Fix

Add DATABASE_EVENT trigger type to separated from action type.

The `UPDATE_RECORD` / `UPSERT_RECORD` action paths are untouched.
2026-06-19 09:48:14 +00:00
Abdullah. 8ff494e5e8 fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file smuggling) (#21829)
## fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file
smuggling)

Resolves [Dependabot Alert
#1500](https://github.com/twentyhq/twenty/security/dependabot/1500).

### What

`tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to
intermediary GNU long-name/long-link headers, causing a tar-parser
interpretation differential (file smuggling) —
[GHSA-vmf3-w455-68vh](https://github.com/advisories/GHSA-vmf3-w455-68vh).
Patched in `7.5.16`.

This is the **seed-dependencies holdout** deferred from the main tar PR
(#21813): that lockfile + its checksum constants were also touched by
the form-data PR, so it was carved out to avoid a conflict. The
form-data PR has since merged, unblocking it.

### How

- Refreshed `tar` `7.5.13 -> 7.5.16` in `seed-dependencies/yarn.lock`
(transitive via `^7.5.4`, which already permits it) — an in-range
refresh, no override.
- Regenerated `DEFAULT_YARN_LOCK_CHECKSUM` in
`get-default-application-package-fields.util.ts` so the row-stored
checksum matches the value recomputed from file content in
`application.service.ts` (the deps-layer cache key;
`logicFunctionCreateHash` = SHA-512, first 32 hex). `package.json` is
unchanged, so `DEFAULT_PACKAGE_JSON_CHECKSUM` is unaffected.

### Verification

- No `tar <= 7.5.15` remains in the seed lockfile.
- Both checksum constants verified to match the canonical recompute of
the current seed files.
- Lint + format pass on the changed `.ts` file.
2026-06-19 11:46:28 +02:00