12518 Commits

Author SHA1 Message Date
Rashad Karanouh e721ebe300 chore(twenty-partners): bump app version to 0.3.3 (#21140)
Bumps the `twenty-partners` SDK app version 0.3.2 → 0.3.3 so `main`
tracks what's deployed to prod.

This is the deploy version for the partner-app changes that just landed:
marketplace `partnerScope` exposure (#21126), the
`submit-partner-application` endpoint + new Partner categories +
migration (#21040), the marketplace card rebind (#21127), and the signup
wizard (#21039).

No code changes — version bump only.
2026-06-02 11:42:06 +00:00
Prakhar Tripathi 2048efb75d fix(record-table): keep column header dropdown open after Move Left/Right (#21015)
Fixes #20999

## Summary

Fixes a UX issue where clicking **Move left** or **Move right** in the
column header
dropdown immediately closed the menu, forcing users to reopen it for
every single move.

## Problem

`handleColumnMoveLeft` and `handleColumnMoveRight` both called
`closeDropdownAndToggleScroll()` unconditionally at the top of their
handlers — before
even checking `canMoveLeft` / `canMoveRight`. This immediately set the
Jotai atom
`isDropdownOpenComponentState` to `false`, unmounting the dropdown.

Since move actions are **repeatable** — a user might want to shift a
column several
positions — they were forced into a frustrating loop: click header →
click move → click
header → click move → repeat for every step.

## Fix

Removed the two `closeDropdownAndToggleScroll()` calls from the move
handlers in
`RecordTableColumnHeadDropdownMenu.tsx`.

```diff
  const handleColumnMoveLeft = () => {
-   closeDropdownAndToggleScroll();
-
    if (!canMoveLeft) return;
    moveTableColumn('left', recordField.fieldMetadataItemId);
  };

  const handleColumnMoveRight = () => {
-   closeDropdownAndToggleScroll();
-
    if (!canMoveRight) return;
    moveTableColumn('right', recordField.fieldMetadataItemId);
  };
```

All other handlers — **Filter, Sort, Hide** — are untouched and still
close the dropdown
correctly, since those are one-shot or navigation actions.

## Changes

| File | Change |
|---|---|
| `RecordTableColumnHeadDropdownMenu.tsx` | Remove 2
`closeDropdownAndToggleScroll()` calls from move handlers |
| `RecordTable.stories.tsx` | Add `HeaderMenuStaysOpenAfterMoveRight`
regression story |

## Testing

**Storybook interaction test** — `HeaderMenuStaysOpenAfterMoveRight`:
clicks "Move right" then asserts the menu is still visible.

**Manual checklist:**
- [x] Move right → menu stays open
- [x] Move right again → column moves again, menu still open
- [x] Move left → menu stays open
- [x] Move rightmost column → "Move right" disappears, menu stays open
showing "Move left"
- [x] Filter → menu closes *(unchanged)*
- [x] Sort → menu closes *(unchanged)*
- [x] Hide → menu closes *(unchanged)*
- [x] Click outside → menu closes *(unchanged)*
- [x] Escape → menu closes *(unchanged)*
- [x] TypeScript: zero new errors (`tsc --noEmit`)

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-02 11:23:26 +00:00
Raphaël Bosi b2539f5b6a Prevent conditional availability variables from being used at runtime (#21110)
Fixes https://github.com/twentyhq/twenty/issues/21094

Conditional availability variables (`objectMetadataItem`,
`numberOfSelectedRecords`, `objectPermissions`, operators like
`everyEquals`/`none`, etc.) are compile-time-only constructs used in
`conditionalAvailabilityExpression`. They were previously exported from
`twenty-sdk/front-component`, which let developers mistakenly import
them into runtime component code where they have no value.

- Move conditional availability variables from
`twenty-sdk/front-component` to `twenty-sdk/define`.
- Add a build-time manifest validation
(validate-conditional-availability-usage) that fails the build if these
variables are imported/used outside of
`conditionalAvailabilityExpression`.
- Update the github-connector example app to register commands via
dedicated *.command-menu-item.ts files instead of inline command config
in front components.
- Update docs (all locales) and test mocks to reflect the new import
paths.
2026-06-02 11:22:38 +00:00
github-actions[bot] 2375c2f59c i18n - translations (#21141)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 13:35:06 +02:00
Charles Bochet 58907b733c feat(logic-function): add LIVE / PREBUILT execution modes (#20873)
## Summary

### Why

1. Sending the code to the lambda (~1Mb usually) is heavy on network and
results to a constant traffic of ~30Mb/s on AWS which results into TB of
network data every month
2. eval(1MB of code) is not that fast, it's heavy on memory and CPU on
lambda side

### High level

Adds two execution modes for logic functions, gated behind the new
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` workspace feature flag (off
everywhere by default):

- **LIVE** (current behavior, preserved bit-for-bit): the compiled
bundle is read from object storage and shipped in every Lambda invoke
payload. Used for fast iteration in the workflow editor / Settings test
runs.
- **PREBUILT** (new): the bundle is installed onto the per-function
Lambda alongside the unified executor, and invocations carry only `{
params, env, handlerName }` — saving JSON payload egress and warm-start
`import()` cost on every call.

### Key design choices

- **Unified Lambda handler** (`constants/executor/index.mjs`) dispatches
at runtime: `event.code` present ? LIVE (write to `/tmp`, dynamic
import) : `import('./prebuilt-logic-function.mjs')`. Both code paths
always coexist on the deployment package, so the same Lambda can serve
either mode without redeploying.
- **Install runs inside the `validateBuildAndRun` migration pipeline**,
not at execute time. `Create/UpdateLogicFunctionActionHandlerService`
calls `driver.installPrebuiltBundle` when `executionMode` flips
LIVE?PREBUILT or `checksum` changes while PREBUILT, gated on
`isBuildUpToDate=true` and a fresh checksum.
- **Strict execute, no reconciliation**:
`LogicFunctionExecutorService.execute` resolves `effectiveExecutionMode`
(caller override > feature flag > entity column). For PREBUILT it asks
the driver `getInstalledBundleChecksum` (Lambda `twenty:bundle-checksum`
tag for AWS, sidecar file locally) and throws
`LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED` on mismatch.
- **Feature flag gates every side effect**: with the flag off the
executor forces LIVE, the action-handler install hooks bail before AWS,
and workflow activation does not flip the mode. Rollback is just turning
the flag off.

### Lifecycle

- New workflow CODE step ? `LIVE`, no install.
- Workflow activated ? build + activation flips `executionMode=PREBUILT`
? action-handler installs the bundle + sets the Lambda tag.
- Draft from active version ? duplicated logic function reset to `LIVE`.
- App install ? manifest converter sets `PREBUILT`, create-action
handler installs.
- Test runs (`executeOneFromSource`, workflow editor) pass
`executionMode=LIVE` explicitly.

### Observability

`[lambda-timing]` log lines now include `effectiveExecutionMode` and
`payloadBytes`; the action handler logs `install_duration_ms` for each
install.

## Test plan

- [x] `npx nx typecheck twenty-server` ? passes
- [x] `npx oxlint --type-aware` on all changed files ? 0 warnings, 0
errors
- [x] `npx nx test twenty-server` ? 588 suites / 5009 tests pass (no
regressions vs main)
- [x] New unit suite `flat-logic-function-validator.service.spec.ts` ?
9/9
- [x] Existing
`workflow-version-step-operations.workspace-service.spec.ts` ? 8/8
(verified the new token-based DI avoids a circular-import regression)
- [x] Snapshot for
`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY` updated
to include `executionMode`
- [x] Integration suite `logic-function-execution.integration-spec.ts`
extended to assert `executionMode=LIVE` on newly-created functions and
continues to exercise the LIVE happy path
- [ ] Manual staging rollout: flip
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` per workspace, observe
`[lambda-timing]` `payloadBytes` drop + `install_duration_ms`, then ramp
in prod.
2026-06-02 11:14:39 +00:00
Rich Roberts 1ae00d6753 fix(ai): correct find-records tool description (top-level filter fields) (#21109)
## Problem

The AI `find_<object>` tool builds its input schema with
`generateFindToolInputSchema`, which **spreads field filters at the args
root** (alongside `limit`/`offset`/`orderBy`/`and`/`or`/`not`).
`tool-executor.service.ts` then maps the raw model args to a filter
with:

```ts
const { limit, offset, orderBy, ...filter } = args;
```

The zod schema is only used to generate the JSON schema *shown* to the
model (`z.toJSONSchema(...)`) — it is **never used to validate the args
coming back**. So when the model emits a bare operator where a field
name belongs, e.g. `{ ilike: "Foreman" }`, it passes straight to the
query runner, which throws and burns a retry mid-turn:

```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "eq" field.
```

Two contributing faults:
1. **The tool description actively misleads the model** — it says ``use
filter: { id: { eq: "record-id" } }``, a `filter` wrapper the
root-spread schema doesn't have, inviting the malformed shape.
2. **No server-side validation** — invalid root keys reach the query
runner instead of being rejected against the advertised contract.

## Fix

1. **`FindRecordsService` prunes invalid filter keys before querying.**
Using the same filter shape the tool schema advertises
(`generateRecordFilterSchema(...).filterShape`), it drops any key that
is neither a real field nor a logical operator (`and`/`or`/`not`),
recursing through `and`/`or`/`not`. A model that sends `{ ilike:
"Foreman" }` now gets a valid (empty) filter rather than an exception.
Extracted as a pure, unit-tested util `pruneFilterToAllowedKeys`.
2. **Corrected the `find_<object>` tool description** to describe the
real top-level-field shape and explicitly warn against a `filter`
wrapper and bare root operators.

## Test

`__tests__/prune-filter-to-allowed-keys.util.spec.ts` covers: valid
filters untouched, bare root operators dropped, valid siblings
preserved, `and`/`or`/`not` recursion, and non-object input.

## Notes

- Defensive for all `FindRecordsService` callers; `find_one` (`{ id: {
eq } }`) and workflow find-records pass valid filters and are
unaffected.
- Companion to #21106 (RICH_TEXT composite filters). Both surfaced from
the same `"Tom Foreman's notes"` AI-chat repro; this PR addresses the
root-level-operator half.

---------

Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
2026-06-02 10:58:13 +00:00
dev-kp-eloper e1a00ea42f fix(twenty-front): enable text selection for display-mode fields (#21068)
## Description
This PR resolves a usability issue where scalar field values (emails,
phone numbers, dates, IDs, text, etc.) rendered in display-mode or
read-only mode in the record detail side panel could not be highlighted,
selected, or copied natively with the cursor.

## Root Cause
Both `RecordInlineCellContainer` and
`RecordInlineCellHoveredPortalContent` wrapper elements had
`user-select: none;` hardcoded in their styled-component definitions.
This styling propagated down to all nested display widgets, locking
their content and preventing native text selection.

## Changes
- Updated `StyledInlineCellBaseContainer` in
`RecordInlineCellContainer.tsx` to use `user-select: text;` instead of
`none;`.
- Updated `StyledInlineCellBaseContainer` in
`RecordInlineCellHoveredPortalContent.tsx` to use `user-select: text;`
instead of `none;`.

These changes restore natural browser text selection capabilities for
record detail widgets without altering interactive edit-mode behaviors.

## Verification
- Verified styling changes.
- Tested locally to ensure that text highlighting and copy-pasting
function correctly when dragging over read-only fields.

Closes #21056

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-02 10:53:00 +00:00
Rashad Karanouh 4f47885054 feat(twenty-partners): submit-partner-application HTTP logic function (#21040)
## Summary

Adds a public `POST /partner-applications` HTTP logic function on the
twenty-partners SDK app that receives applications from the website
wizard and idempotently upserts the Partner / Person / Company graph in
the partners workspace. Also introduces the validated **Category**
taxonomy on `partnerScope` (additive, prod-safe) plus the legacy→new
migration tooling.

Companion PR (website side): #21039

### Logic function
- `defineLogicFunction({ httpRouteTriggerSettings: { path:
'/partner-applications', httpMethod: 'POST', isAuthRequired: false,
forwardedRequestHeaders: ['x-application-secret'] } })`.
- Authenticates via shared-secret header (`X-Application-Secret` ↔
`PARTNER_APPLICATION_SECRET` workspace variable). Twenty's
`isAuthRequired: true` only accepts user-session JWTs, so the handler
enforces auth itself.
- Idempotent upsert keyed on `Person.emails.primaryEmail`:
  - missing email → create Company → Person → Partner
  - existing Person, no Partner → create Company + Partner, link
- existing Person + Partner → update Partner fields; preserve
staff-owned columns (`validationStage`, `reviewed`, `ranking`,
`partnerTier`, `lastMatchAt`) by omitting them from the update
- Create-time defaults preserved on resubmit: `slug =
slugify(companyName)` ("YC Agency" → "yc-agency"), `reviewed = false`,
`partnerTier = 'NEW'`.
- Currency conversion to `{ amountMicros, currencyCode: 'USD' }` for
`hourlyRate` + `projectBudgetMin`.

### Categories (`partnerScope`) — additive, prod-safe
- Adds 5 validated category options — `ADVISORY`, `SOLUTIONING`,
`DEVELOPMENT`, `HOSTING`, `SUPPORT` — to the `partnerScope` MULTI_SELECT
**without removing** the legacy options (there is production data on
them). Field relabeled **"Categories"**. The website form only emits the
new values.
- **Migration tooling** (run deliberately, *not* in CI):
`scripts/migrate-partner-scope.ts` remaps existing records legacy→new —
dry-run by default, `MIGRATE_APPLY=1` to write, two-pass
(collect-then-apply, no mutate-while-paginating).
`scripts/partner-scope-map.ts` is the single mapping source;
`import-from-tft.ts` now routes imported scope through it so the TFT
import never re-introduces retired values. Removing the legacy options
is deferred until after the migration has run + been verified.

### applicationNotes
- New `applicationNotes` TEXT field holds the wizard's single free-text
"anything else" note (the handler passes it through directly).
`deploymentExpertise` was dropped from the handler
input/validation/builders (the column is retained for now, pending the
same migration cleanup).

### Application variable
- Declares `PARTNER_APPLICATION_SECRET` with `isSecret: true` so each
workspace sets the value via Settings → Apps → Twenty Partners →
Variables. Twenty encrypts at rest and merges the decrypted value into
the handler's `process.env` at execution time (workspace value wins over
container env).

### Code quality (from review)
- One shared `slugify` (`scripts/slugify.ts`, the import's algorithm)
used by both the handler and the import, so the `slug` identity key
can't diverge across paths.
- Unit-test tier: `vitest.unit.config.ts` (no `globalSetup`) + `yarn
test:unit`, so the pure `mapLegacyScope` test runs without a live server
(the integration suite stays server-backed).

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

### Tests
- Integration tests against a local Twenty workspace:
missing-/wrong-secret auth rejections, create flow (asserts slug +
`reviewed: false` + `partnerTier: 'NEW'`), update-on-resubmit +
staff-column preservation, new category values stored,
`applicationNotes` stored, bad-input shape.
- Pure `mapLegacyScope` unit test via `yarn test:unit` (no server).

## Test plan

- [ ] Install / upgrade the app on the target workspace; set
`PARTNER_APPLICATION_SECRET` in Settings → Apps → Twenty Partners →
Variables
- [ ] `curl -i -X POST <workspace-url>/s/partner-applications -H
'X-Application-Secret: <secret>' -H 'Content-Type: application/json' -d
'{"firstName":"Test","lastName":"User","email":"test@example.com","companyName":"YC
Agency","partnerScope":["ADVISORY"],"applicationNotes":"hi"}'` →
`HTTP/1.1 201` + `{"ok":true,"created":true,"partnerId":"..."}`
- [ ] Partner record shows `name: "YC Agency"`, `slug: "yc-agency"`,
`validationStage: APPLICATION`, `reviewed: false`, `partnerTier: 'NEW'`,
`partnerScope: ["ADVISORY"]`, `applicationNotes: "hi"`
- [ ] Re-curl same email with `city: "Paris"` → `created: false`,
`Partner.city` updated, staff-owned columns untouched
- [ ] Wrong / missing secret → `200` +
`{"ok":false,"reason":"unauthorized"}`
- [ ] `yarn test:unit` green (no server); `yarn migrate:partner-scope`
dry-run lists any legacy→new remaps without writing
2026-06-02 10:35:28 +00:00
Rashad Karanouh 7e034f711f feat(website): surface partner Categories (partnerScope) in marketplace, drop deploymentExpertise facet (#21127)
## What

Rebinds the marketplace's expertise facet from `deploymentExpertise`
(Cloud / Self-host) to **`partnerScope`** — the five partner Categories:
Advisory & Discovery · Solutioning · Custom Development · Hosting &
Infrastructure · Training & Adoption.

Moves the card chip, the profile facts row, the dropdown filter, the
`?categories=` URL param, and the API-boundary normalization onto
`partnerScope`. The standalone Cloud/Self-host facet is **dropped**
(hosting is now the `HOSTING` category), per the harmonization decision.

## Depends on

- The app exposing `partnerScope` — companion app PR #21126.
- The new `partnerScope` options + data migration — signup app PR
#21040.

## Tests

TDD red→green on: `filter-partners`, both API normalizers,
`filter-url-helpers`, `PartnerCard`, `use-filter-state`. 53/53 pass;
typecheck + lint + format clean.

## Merge order (we'll decide)

Independent diff. Suggested last of the four, after the signup PRs
(#21039 / #21040) and the app PR (#21126). Run `lingui:extract` once
after #21039 merges so the `.po` files don't conflict twice. Deploy the
app + migrate before the website ships.
2026-06-02 10:32:54 +00:00
github-actions[bot] 94d2e386e8 i18n - website translations (#21135)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 12:43:26 +02:00
Rashad Karanouh a3557373e6 feat(twenty-partners): expose partnerScope on list + by-slug endpoints (#21126)
## What

Adds `partnerScope` (the partner **Categories** multi-select) to the
output of the two public partner endpoints:
- `list-available-partners` (`/s/partners`)
- `get-partner-by-slug` (`/s/partner-by-slug`)

Additive only — `deploymentExpertise` is kept, so existing consumers
(the current live marketplace) are unaffected.

## Why

Part of the partner marketplace rework. The website marketplace
(companion branch `rk-rework-marketplace-cards`) consumes `partnerScope`
to show/filter partner Categories. The new options + migration live in
the signup app PR #21040.

## Merge order (we'll decide)

Independent diff — can merge in any order. Couplings to keep in mind:
- **Version line:** this branch and #21040 both bump the app
`package.json` version; whoever merges second re-bumps.
- **Deploy (not merge):** the partners app is deployed manually. Deploy
the final combined app (this + #21040) and run `yarn
migrate:partner-scope:prod` **before** the website is deployed.
2026-06-02 10:23:51 +00:00
Rashad Karanouh d0e0e27035 [Website] Partner application wizard + logic-function handover (#21039)
## Summary

Replaces the single-screen partner-application modal with a **4-step
wizard** on the public form, and points the route's upstream at the new
`submit-partner-application` HTTP logic function in the twenty-partners
SDK app.

After design review, the Expertise step landed on the validated
**Category + Skills** model: a small set of *stable* macro categories
the partner operates in, plus a *free, semi-structured* Skills field for
the concrete things that differentiate them (React, SAP, Shopify, …).

Companion PR (partners-app side): #21040

## ⚠️ Deployment notes

Before this can ship to prod, the website worker needs a new env var:

- **Add `PARTNER_APPLICATION_SECRET`** to the deploy config at
https://github.com/twentyhq/twenty-infra/tree/main/cloudflare/website.
Without it the route returns `503` ("Partner application endpoint is not
configured.").
- The value must **match** the `PARTNER_APPLICATION_SECRET` workspace
variable set in the partners workspace UI (Settings → Apps → Twenty
Partners → Variables) — that's how the handler authenticates the
incoming `X-Application-Secret` header.
- `PARTNER_APPLICATION_WEBHOOK_URL` also needs repointing from the TFT
webhook to the logic-function URL
(`https://partner.twenty.com/s/partner-applications` or equivalent) at
the same time.

## Wizard

- 4 steps inside `Modal.Root`: **Identity → Profile → Expertise →
Commercials**. Step-dot indicator, per-step required-field gating, reset
on close. The big serif hero shows **only on step 1**; later steps use
the compact `STEP n OF 4 · NAME` strip to reclaim vertical space.
- **Profile** captures Type of team (Solo/Agency), LinkedIn, City,
Country, Languages. Country uses the searchable Select (placeholder-only
label).
- **Expertise = Category + Skills + Notes:**
- **Category** — multi-select cards over 5 macro categories (`ADVISORY`,
`SOLUTIONING`, `DEVELOPMENT`, `HOSTING`, `SUPPORT`), each with a
one-line description + examples. (Replaces the old draft `partnerScope`
enum; the backend keeps the field name — see #21040.)
- **Skills** — free tag input with a clickable suggestion row + keyboard
autocomplete (↑/↓/Enter/Esc) and "add your own". Empty by default.
- **Notes** — one free textarea (merges the former `workspaceUrl` +
`customerReferences`), reviewed manually.
- `deploymentExpertise` removed from the form (covered by the Hosting
category).
- **In-modal success view** on submit ("Thanks, / we'll be in touch!")
with a Close button — replaces the old silent close.
- Removes the partners-page "Which partner program is right for you?"
three-cards section.

## Design-system primitives

- **`Form.Select`** — searchable popup whose dropdown is **portaled to
`<body>`** (fixed, anchored to the trigger, flips up, height-capped) so
the modal's `overflow`/`transform` can't clip it; pointer events are
stopped so clicking inside it doesn't dismiss the dialog.
- **`Form.TagInput`** — optional `suggestions` prop adds the suggestion
row + autocomplete menu (used by Skills); behaviour unchanged when no
suggestions are passed.
- **`CategoryCardSelect`** — compact multi-select cards.
- `Form.MultiSelect`, `Form.Currency`.

## Validation & payload

- **Single validation source:** client and server share Zod field
schemas (`partner-application-field-schemas.ts`). The reducer validates
via those instead of hand-rolled regexes, so client and server agree by
construction (e.g. both reject non-TLD URLs).
- **Typed request body:** `buildPartnerApplicationRequestBody(state)`
returns a typed `PartnerApplicationRequest` (unit-tested);
`handleSubmit` just serializes it.
- Payload is camelCase matching the logic-function input;
`applicationNotes` replaces `workspaceUrl`/`customerReferences`.
- Auth: the upstream call carries an `X-Application-Secret` header
backed by `PARTNER_APPLICATION_SECRET` (handler-enforced — the SDK's
`isAuthRequired` only accepts user-session JWTs, not workspace API
keys). The webhook-URL env uses `z.url()` (not `z.httpUrl()`) so
`http://localhost:2020/...` dev destinations parse.

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

## Tests

- **62 passing** across reducer, Zod schema, route, the new
payload-builder suite, and Form helper suites. `npx tsc` clean, `nx
lint:diff-with-main` clean, Lingui catalogs regenerated (French slots
are a follow-up).

## Test plan

- [ ] `/partners` → "Become a partner" → wizard opens on Step 1 (full
hero)
- [ ] Identity: name / work email / company → Next
- [ ] Profile: pick **Type of team**; search country ("fra" → France);
pick languages → Next (compact header from here on)
- [ ] Expertise: select 1+ **Category** cards; add **Skills** (click a
suggestion, type one + Enter, drive the ↑/↓ autocomplete); optionally
fill **Notes**
- [ ] Country dropdown opens without being clipped by the modal, and
clicking inside it does **not** close the wizard
- [ ] Commercials → Submit → **in-modal "Thanks, we'll be in touch!"**;
Network shows POST `/api/partner-application` `200`
- [ ] Partner record lands with the chosen categories in `partnerScope`,
plus `skills`, `applicationNotes`, `slug` from company, `reviewed:
false`, `partnerTier: 'NEW'`
- [ ] Re-submit same email + different city → Partner updates;
`validationStage`/`reviewed`/`partnerTier` preserved
- [ ] Back/Next preserves entered values; Reset on close; mobile
single-column / chips wrap

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 10:23:31 +00:00
Paul Rastoin 6a908b7876 Front component s3 redirect (#21116)
# Introduction
Unload the server of the file stream when possible
Also fix inconsistent pipeline exception management

Needs to highly be QA, not sure how the cors will behave here
2026-06-02 09:35:58 +00:00
Joseph Chiang 445c6fe9f6 feat: expose CURRENCY field settings (format/decimals) in shared types (#21090)
## What

Add a `CURRENCY` entry to `FieldMetadataSettingsMapping` (a
`FieldMetadataCurrencySettings` type of `{ format?: 'short' | 'full';
decimals?: number }`) so `FieldMetadataSettings<CURRENCY>` resolves to
the real settings shape instead of `null`.

## Why

The currency **format** (Short/Full) and **decimals** selectors already
ship in the field settings UI and persist through the generic `settings`
jsonb column — they render via
[`CurrencyDisplay.tsx`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx)
reading `settings.format` / `settings.decimals` (added in #12542 and
#16439).

But `twenty-shared` never got a `CURRENCY` entry in the settings
mapping, so `FieldMetadataSettings<CURRENCY>` is `null`. The SDK's
`defineField` derives its types from this mapping, so an app author
cannot set these from code — `universalSettings: { format: 'full',
decimals: 2 }` on a CURRENCY field is a type error, even though the
server stores and the frontend honours it. This aligns the type layer
with the already-shipped runtime behaviour.

## Changes

- `twenty-shared`: add `FieldMetadataCurrencySettings` +
`FieldCurrencyFormat`, wire the `CURRENCY` mapping entry, export
`FieldCurrencyFormat`.
- `twenty-server`: move `CurrencyFieldMetadata` from the
`NotDefinedSettings` assertions to a defined-settings assertion in the
field-metadata entity type test.

No runtime change — the server already accepts and stores these settings
via the generic jsonb column; this only makes them visible to the type
system and the SDK.

## Test plan

- [ ] `npx nx typecheck twenty-shared` / `twenty-server` pass
- [ ] In an app, `defineField({ type: FieldType.CURRENCY,
universalSettings: { format: 'full', decimals: 2 }, ... })` type-checks
and deploys
- [ ] Field renders with 2 decimals in full format, matching the
equivalent UI configuration

> Follow-up (not in this PR): the frontend keeps its own local
`fieldMetadataCurrencyFormat` / `FieldCurrencyFormat`; it could import
the shared `FieldCurrencyFormat` to de-duplicate.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:42:28 +02:00
neo773 1ba7a3fa54 fix: lowercase OAuth handle to prevent duplicate connected accounts (#21120)
Google and Microsoft connect flows stored the provider-returned email
verbatim, so a re-auth with different casing (Sam@ vs sam@) missed the
existing-account lookup and created a duplicate. Normalize the handle to
lowercase at extraction, matching the SSO controller.

Note: not doing backfill for now
2026-06-02 09:27:10 +00:00
Félix Malfait 7d7f32b243 docs: remove the self-host cloud providers page (#21134)
## What

Removes the community-maintained **"Other methods"** cloud-providers
page from the self-host docs (it covered Kubernetes/Terraform/Coolify
community deployments).

## Changes

- **Deleted** `developers/self-host/capabilities/cloud-providers.mdx`
and its 13 localized copies (ar, cs, de, es, fr, it, ja, ko, pt, ro, ru,
tr, zh).
- **Removed the slug** from `navigation/base-structure.json` (the source
of truth) and regenerated the derived files via the repo's own
generators (`yarn docs:generate`, `yarn docs:generate-paths`):
  - `docs.json` — nav entries dropped for every locale.
- `twenty-shared/.../DocumentationPaths.ts` —
`DEVELOPERS_SELF_HOST_CAPABILITIES_CLOUD_PROVIDERS` constant dropped
(was unused elsewhere).
- **Removed the "Cloud Providers" card** from the `self-host` overview
pages across all locales.
- **Dropped the dangling redirect**
`/developers/self-hosting/cloud-providers` (its destination no longer
exists).
- Cleared the matching entry from the unused `navigation-schema.json`
for consistency.

Net: 68 line deletions across config (pure removal); no insertions.

## Verification

- `grep` confirms **0** remaining references to `cloud-providers`
anywhere in the repo.
- All touched JSON files parse; `oxlint` on twenty-docs reports 0
errors.
- Generators (not hand edits) produced `docs.json` and
`DocumentationPaths.ts`.

> Note: `mintlify broken-links` can't run to completion on this branch
due to a **pre-existing** MDX parse error in the unrelated
`l/ar/.../contribute/contribute.mdx`; the grep above is the equivalent
guarantee that no link points at the removed page.
2026-06-02 11:17:37 +02:00
github-actions[bot] 5f0096c464 i18n - website translations (#21088)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 10:45:36 +02:00
Matt Van Horn 6ac797a69c fix: REST cursor encoding for nested order_by composite fields (#20974)
## Summary

fix: REST cursor encoding for nested order_by composite fields

Closes #20109

---
AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 07:10:20 +00:00
github-actions[bot] 3d6bcc3102 i18n - translations (#21128)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 07:30:02 +02:00
Félix Malfait 75df1f3997 chore(settings): address review comments from PR 21072 (#21121)
## Summary

Round through bosiraphael's 31 review threads on the merged PR #21072
(discovery hero + ephemeral playground token). The user asked to apply
each suggestion only where it adds value, so this PR is split into three
buckets.

### Comments (~17 threads)

- Tightened security-rationale / CSS-gotcha / API-doc comments to one or
two factual lines
- Kept (shortened) the comments above `RequireAccessTokenGuard` call
sites — without them a future reader could remove the guard and silently
reopen the escalation hole
- Kept (shortened) the in-memory-only rationale on
`playgroundApiKeyState` for the same reason
- Kept `flex: 1 + min-height: 0` CSS gotcha on `SubMenuTopBarContainer`
— non-obvious and easy to break

### Structure / extraction

- Move `WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS` to its own constants
file (one-export-per-file)
- Split `SettingsAgentToolsTab` and `SettingsAgentToolsTable` across
queries/, hooks/, types/, utils/:
  - `graphql/queries/findManyApplicationsForToolTable.ts`
  - `graphql/queries/findManyMarketplaceAppsForToolTable.ts`
  - `hooks/useSettingsAgentToolsTable.ts` (data loading + index merging)
  - `types/SettingsAgentToolItem|Application|MarketplaceApp`
  - `utils/getToolApplicationId|getToolLink`
- Extract `SettingsAiModelsTab` optimistic mutations into
`hooks/useSettingsAiModelsActions` (handleModelFieldChange,
handleUseRecommendedToggle, handleModelToggle,
handleToggleAllVisibleModels)
- Extract `SettingsAI.handleCreateTool` into `hooks/useCreateTool`
- Drop unnecessary `useMemo` wrappers on `heroTabs` arrays
(SettingsObjects, SettingsLayout)
- Simplify `MenuItemToggle` handler in SettingsAgentSkillsTab:
`onToggleChange={setShowDeactivated}` (no longer wrapping with arrow +
read of stale `!showDeactivated`)

### Hero assets

- Replace placeholder `customize-illustration` with per-page exports
- Rename `layout/customize-illustration-{light,dark}.png` →
`layout/cover-{light,dark}.png`
- Add `cover-{light,dark}.png` for **applications** and **members**
(they were both pointing at the layout placeholder as a TODO)
- Overwrite `data-model/cover-*.png`, `playground/cover-*.png`,
`ai/ai-tools-cover-*.png` with the new exports

## Test plan

- [ ] `npx nx typecheck twenty-front` 
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint twenty-front`  (oxlint + oxfmt, 0 warnings/errors)
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the new hero illustration (light + dark)
- [ ] AI tab: tool list still loads, search + Custom/Managed/Standard
filters still work, "New Tool" still navigates to detail
- [ ] AI tab: Models tab — smart/fast model select, "Use best models
only" toggle, per-model checkboxes, toggle-all all still
optimistic+revert on error
- [ ] Skills tab: "Deactivated" toggle still flips show/hide
- [ ] Webhooks table still uses the 1fr 28px grid
2026-06-02 07:23:14 +02:00
Paul Rastoin d6b3527552 Public assets server s3 redirection (#21108)
# Introduction
Avoid overloading the server on file streaming
Take profit of the different origin implied by the redirection to the s3
Only concern being the expiration date on a public file which is
acceptable

closes https://github.com/twentyhq/private-issues/issues/483
related https://github.com/twentyhq/private-issues/issues/491
2026-06-01 16:25:58 +00:00
Paul Rastoin 989b45db15 Strictly type encryption rotation key site maps constants through entity type derivation (#21085)
# Introduction
Followup https://github.com/twentyhq/twenty/pull/21001

Now that the typeorm entities provide grains over their
`encryptedString` value, we can strictly type the sitemaps of the
encrypted string to rotate in case of encryption key rotation and also
the integration tests tests cases
2026-06-01 15:25:58 +00:00
Weiko d86e827563 fix: return proper FORBIDDEN GraphQL errors from ApiKeyResolver (#21107)
## Context
CI is broken on main, regression introduced in
https://github.com/twentyhq/twenty/pull/21072

Guard-rejected ApiKey mutations returned malformed GraphQL responses.
RequireAccessTokenGuard (and SettingsPermissionGuard) throw plain
AuthException/PermissionException classes, which are not GraphQLErrors.
ApiKeyResolver had no @UseFilters, so these exceptions were never
translated, they surfaced as request-level errors with no data key
(data: undefined) and a non-FORBIDDEN code, instead of data: null +
FORBIDDEN.

This broke the `createApiKey › should reject a non-ACCESS token even
with API key permission` integration test
(expect(res.body.data).toBeNull() received undefined). The sibling
generateApiKeyToken test passed only because it lives on AuthResolver,
which already declares these filters.

## Fix
Add the standard exception filters to ApiKeyResolver, matching the idiom
used by other guard-protected resolvers
```ts
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
```
2026-06-01 18:08:02 +02:00
Paul Rastoin 6ad6fcce0f Bump playwright (#21113)
Playwright installation is infinite looping in the ci
seems like to be a global outage
2026-06-01 18:06:55 +02:00
github-actions[bot] ad47b2972c i18n - translations (#21112)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-01 17:42:34 +02:00
Parship Chowdhury ba493e5a23 fix: show relation field changes in the timeline (#21052)
## Summary
- Fixes #20970 
- When we changed only a relation field (e.g. company on a person):
- The diff builder skipped all RELATION fields -> empty diff -> no
timeline row.
- If we change company and something else, only the scalar field
appeared; the company change was missing.
- Even with a diff, the UI validated keys by field name (`company`)
while join columns (`companyId`) could be filtered out.

## Solution
- For `MANY_TO_ONE`, compare join column values (`companyId`) and store
the diff under the relation field name (`company`):
```
"company": {
  "before": { "id": "<old-id>" },
  "after": { "id": "<new-id>" }
}
```
- Frontend
- `filterOutInvalidTimelineActivities`: resolve diff keys by field name
or join column via `findFieldMetadataItemByDiffKey`, so relation diffs
are not stripped.
- `EventRelationFieldDiffValues`: resolve related record labels by id;
show only the new value in the row (same pattern as other fields:
Company -> airSlate).
- Tooltip (relations only): on hover, show before -> after with readable
names (e.g. Microsoft -> Apple). Scalar and composite fields (e.g.
Updated by) are unchanged and do not get this tooltip.
- `EventFieldDiff`: route RELATION diffs to the relation renderer; all
other field types keep the existing FieldDisplay behavior.

### What you’ll see
On a person (or opportunity) timeline after changing company:
```
You updated Company → airSlate
(tooltip: Microsoft → Apple)
```

## Test plan
- Change only company on a person -> timeline shows a company update
with names.
- Change company and name in one save -> both appear in the diff.
- Clear company -> row shows Empty; tooltip reflects previous -> empty
if applicable.
- Same we can do for the Opportunities also
- Scalar / ACTOR fields (e.g. Updated by) - no new tooltip; display
unchanged.

## Screenshots

### Before
<img width="527" height="124" alt="Screenshot 2026-05-29 155123"
src="https://github.com/user-attachments/assets/e067f19a-8184-4e50-9cd0-9135e06188b8"
/>
<br><br>
<img width="535" height="181" alt="Screenshot 2026-05-29 155149"
src="https://github.com/user-attachments/assets/3c513a1e-c5c5-4cff-ae1c-5fed62837798"
/>
<br><br>

### After
<img width="564" height="200" alt="Screenshot 2026-06-01 154539"
src="https://github.com/user-attachments/assets/459ad6b4-af4c-4f7a-b749-30762c979627"
/>
<img width="567" height="187" alt="Screenshot 2026-06-01 154555"
src="https://github.com/user-attachments/assets/6bc4711c-afca-43d8-b874-f51fc0f374df"
/>
<img width="563" height="188" alt="Screenshot 2026-06-01 154639"
src="https://github.com/user-attachments/assets/f275b9fd-a1c3-4c4b-9538-8042657eb593"
/>
<img width="556" height="180" alt="Screenshot 2026-06-01 154654"
src="https://github.com/user-attachments/assets/2e614368-8fd7-4c73-8876-2223f2f98e67"
/>
<img width="560" height="237" alt="Screenshot 2026-06-01 154802"
src="https://github.com/user-attachments/assets/0437aef4-7b2c-4d35-a2e9-f617b90a1beb"
/>

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 14:34:54 +00:00
Thomas Trompette 627b488556 Fix else branches not properly skipped in nested if/else workflows (#20938)
## Summary

- Extract `findParentSteps` utility that recognizes IF-ELSE steps as
parents of their branch children (via
`settings.input.branches[].nextStepIds`), used in all parent detection
sites (`shouldSkipStepExecution`, `shouldExecuteStep`,
`shouldFailSafely`, and their iterator variants)
- Centralize next-step resolution in `getNextStepIdsToExecute` via
extracted `getNextStepIdsForIterator` and `getNextStepIdsForIfElse`
utils — Iterator now properly returns loop children as
`nextStepIdsToSkip`/`nextStepIdsToFailSafely` when skipped
- Refactor `skipAndFailSafelyStepsThenContinue` to delegate to
`getNextStepIdsToExecute` instead of duplicating type-specific
propagation logic

Fixes #20934

## Test plan

- [x] New unit tests for `findParentSteps` (7 tests covering IF-ELSE
branch parent detection)
- [x] New IF-ELSE-specific tests added to `shouldSkipStepExecution`,
`shouldExecuteStep`, `shouldFailSafely` test suites
- [x] Updated Iterator skip/fail-safely tests in
`workflow-executor.workspace-service.spec.ts`
- [x] All 300 workflow executor tests pass
- [x] `lint:ci` passes
2026-06-01 17:03:50 +02:00
Weiko b9e5ff2065 fix: broadcast timeline activities to live SSE subscriptions (#21104)
## Context
Timeline activities never updated in real time. They were explicitly
excluded from the database-event pipeline
(formatTwentyOrmEventToDatabaseBatchEvent early-returned for the
timelineActivity object), so no SSE event was ever broadcast, and the
frontend timeline only refreshed on mount/manual refetch.

## Implementation
Backend
- Feat: Stop dropping timeline-activity events in
formatTwentyOrmEventToDatabaseBatchEvent.
Instead route them through EntityEventsToDbListener, which publishes
them directly to live subscriptions (but still skipping webhook/audit
handling).
- Fix: Harden ObjectRecordEventPublisher: wrap nested-relation
enrichment in try/catch so a failure broadcasts the event without
relations instead of dropping it (logs a warning).
- Fix: Skip unreadable relation targets in CommonSelectFieldsHelper when
the role lacks canReadObjectRecords, preventing errors while computing
selected fields.
- Fix: Support MORPH_RELATION alongside RELATION in RLS row-level
permission predicate matching (timeline activities use morph targets).

Frontend
- Feat: useTimelineActivities now registers the timeline query with the
SSE system via useListenToEventsForQuery and refetches on incoming
timeline-activity record operations.
- Feat: Add a skip option to useListenToEventsForQuery so the listener
isn't registered when the object has no timeline field.

## Test


https://github.com/user-attachments/assets/ed1d1c66-d6ea-434d-ac9c-9b83d2b78338


Note: "UpdatedBy" seems to be listen to and visible in the timeline
activity summary, this is probably a bug that we want to fix
2026-06-01 16:32:08 +02:00
Etienne 381ca32055 Billing - Fix credit upgrade invoice error (#21097)
In createImmediateUpgradeInvoice, the invoice is finalized with
auto_advance: true, which causes Stripe to automatically attempt payment
asynchronously. Then the explicit stripe.invoices.pay(invoice.id) call
races against that auto-payment — if Stripe already paid it, this throws
"Invoice is already paid".

The fix is to finalize with auto_advance: false and keep the explicit
pay call
2026-06-01 13:55:06 +00:00
nitin 6029847491 fix navigation item tree breadcrumb active state (#21101)
before - 



https://github.com/user-attachments/assets/9a35e07c-def3-47eb-aab4-0bdcaf302d38



after - 


https://github.com/user-attachments/assets/bf452b5a-7a31-4838-83ca-27cae598ef4d
2026-06-01 13:53:42 +00:00
Marie 66afd5a1de Fix array-typed parameters in code/logic-function action forms (#21102)
## Problem

`any[]` type prevented value input:
<img width="544" height="349" alt="Screenshot 2026-06-01 at 14 08 47"
src="https://github.com/user-attachments/assets/956238d0-6fea-4be1-b75a-ab0e6e6424ac"
/>

In the workflow Code action (and the Logic Function action), parameters
typed as any[], string[], etc. rendered as an empty grey box instead of
an "Enter value" text input. After any debounced save, even a properly
initialised array field would also collapse into an empty container.

The Array<T> / ReadonlyArray<T> generic form fell through to a generic
text input by accident (which "looked" right, but for the wrong reason —
no schema info downstream).

## Root causes
Three places treated arrays as plain objects via @sniptt/guards'
isObject (which is true for arrays):

1. WorkflowEditActionCodeFields.tsx — arrays went into the nested-fields
branch; Object.entries([]) is empty → empty container, no placeholder.
2. mergeDefaultFunctionInputAndFunctionInput.ts — recursed into arrays
during merge, turning [] into {}. Triggered on every debounced save, so
the bug surfaced after any edit.
3. get-function-input-schema.ts — only handled T[]
(SyntaxKind.ArrayType); Array<T> (SyntaxKind.TypeReference) was
unrecognised, so the form lost any item-type info.
2026-06-01 13:14:46 +00:00
github-actions[bot] da5e1152bb i18n - translations (#21103)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-01 14:24:47 +02:00
Félix Malfait b338a7a1d2 feat(settings): discovery hero rollout + ephemeral playground token (#21072)
## Summary

Two intertwined streams of work:

### UI — discovery hero pattern, settings shell, AI/API redesign
- **Generalize `SettingsDiscoveryHeroCard`** and use it on Layout, Data
Model, Apps, AI, API/Webhooks, Members. Drops 4 per-page wrapper files
(`SettingsObjectCoverImage`, `SettingsLayoutCoverImage`,
`SettingsLayoutCustomizeVideoModal`,
`SettingsDataModelVisualizeVideoModal`). Each page now supplies cover
src, modal id, and tab list.
- **Modal**: swap `<video>` placeholder for the Vimeo iframe pattern
from `twenty-docs`, per-tab `vimeoId`. Drop the parallel border-bottom
on the header (TabList draws its own baseline) and the grey background
behind the video. Note: Vimeo's embed allowlist applies — the iframes
load with the correct URL on `localhost` but the player itself requires
the video owner to allow the dev/staging domains in Vimeo settings.
- **AI page** rebuilt into a Cockpit pattern (Overview / Models / Skills
/ Tools / Usage). New `SettingsAiOverviewTab` with default Smart/Fast
pickers, at-a-glance stats, and an MCP signpost that deep-links to
`/settings/api-webhooks#mcp`. System Prompt link moved under Models.
Advanced tab removed.
- **API & Webhooks** now has 4 tabs (Playground / MCP / API Keys /
Webhooks). Hero card above tabs. Playground tab inverted to "Core API" /
"Metadata API" sections, each containing REST + GraphQL cards — schema
is the meaningful axis, protocol is secondary. Hash deep-link sync
delegated to the shared `TabListFromUrlOptionalEffect`.
- **Settings shell**: unified drawer outer padding (kill `isSettings`
branch), extract `CollapsibleNavigationDrawerSection`, add `iconColor`
on settings nav items, fix Exit Settings button alignment, 880px content
cap.

### Backend — strategy C: ephemeral playground token
The legacy paste-your-API-key flow is replaced by an on-demand
short-lived token scoped to the calling user's permissions. No shared
"Playground" API key to manage or revoke.

- New `JwtTokenTypeEnum.PLAYGROUND`. `PlaygroundTokenJwtPayload =
Omit<AccessTokenJwtPayload, 'type' | impersonation fields>` so any
future ACCESS claim flows through automatically.
- `AccessTokenService.generatePlaygroundToken` signs an access-shaped
JWT with `type: PLAYGROUND` and a configurable short TTL. A shared
private `resolveTokenSubject` helper parallelizes the user / workspace /
userWorkspace lookups for both generators.
- `JwtAuthStrategy.validateAccessToken` widened to accept
`AccessTokenJwtPayload | PlaygroundTokenJwtPayload`; impersonation gated
on `payload.type === ACCESS` so the union narrows without `as unknown
as` casts. The two branches in `validate()` collapse into one.
- New `PLAYGROUND_TOKEN_EXPIRES_IN` config var (default `2h`).
- New `generatePlaygroundToken` mutation (`WorkspaceAuthGuard`, no args,
returns `AuthToken`).
- Frontend `useOpenPlayground` hook centralizes mint → atom write →
navigate, with Apollo `onError` snackbar and a "use cached PLAYGROUND
token if still fresh" short-circuit (decodes via `jwt-decode`, checks
both `type` AND `exp`). Old API_KEY tokens left in localStorage from the
prior paste-form flow are rejected on `type` alone and force a re-mint —
this is what was causing the "This API Key is revoked" symptom on stale
browsers.

### Drive-by cleanups
- `PlaygroundToken` DTO removed (identical shape to `AuthToken` already
in use).
- 5 `customize-sidebar.webm` imports and the dead placeholder pipeline
removed.

## Test plan

### Discovery hero
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the discovery hero card with its
illustration + play button + tabbed modal
- [ ] Modal tabs show the correct Vimeo embed URL per tab; aspect ratio
stays at 1440/900; no parallel border-bottom jog at the tab baseline
- [ ] AI Overview tab shows Smart/Fast model pickers + stats grid + MCP
signpost card; the MCP card lands on `/settings/api-webhooks#mcp` with
the MCP tab active

### API playground (ephemeral token)
- [ ] With an empty `playgroundApiKeyState` in localStorage, clicking
REST or GraphQL playground card opens the playground and the cached
token has `type: "PLAYGROUND"` with ~2h exp
- [ ] Clicking the card again within the freshness window does **not**
re-mint (`iat` / fingerprint stable across visits)
- [ ] Planting a fake API_KEY-shaped JWT in localStorage and clicking
the card forces a fresh mint (old token rejected on `type`)
- [ ] `GET /rest/companies?limit=1` with the cached token returns 200 +
real data
- [ ] `POST /graphql { __typename }` returns 200

### Settings shell
- [ ] Settings nav matches main app drawer padding; sections collapse;
Exit Settings button aligns with the workspace links above
- [ ] Active nav items have a right-gap (cleaner active state)
- [ ] Content area capped at 880px

### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
2026-06-01 14:16:02 +02:00
nitin 6e00a122c6 fix(kanban): contain checkbox hover reveal within card bounds (#21100)
follow up to https://github.com/twentyhq/twenty/pull/20455

before - 


https://github.com/user-attachments/assets/e5a8a328-81ec-4dc4-8e54-1a54cf252135


after - 



https://github.com/user-attachments/assets/61cbb856-564c-487f-81e5-e27adc4a0d2d
2026-06-01 12:20:40 +02:00
Priyanshu Bartwal 4dff30f676 Twenty server:Fix REST pagination issues (#20980)
Fixes #20109 

The entry was repeating because in the database we store DateTime fields
with microsecond precision (timestamptz), but when JS parses timestamptz
into a Date object it only keeps millisecond precision.

### Example
If previous cursor was:
```
{
    name: "Quick Lead",
    createdAt: "2026-05-21T15:33:00.708Z",
}
```
The resulting query look something like:
```
...
WHERE (
  "workflow"."name" > "Quick Lead"
  OR (
    "workflow"."name" = "Quick Lead"
    AND "workflow"."createdAt" > "2026-05-21T15:33:00.708Z"
  )
  OR (
    "workflow"."name" = "Quick Lead"
    AND "workflow"."createdAt" = "2026-05-21T15:33:00.708Z"
    AND "workflow"."id" > "8b213cac-a68b-4ffe-817a-3ec994e9932d"
  )
)
```
So, when comparing the 2nd condition the `"workflow"."createdAt" >
"2026-05-21T15:33:00.708Z"` would always result to true because in db
the data for createdAt is `2026-05-21 21:03:00.708 +0530` which will
always be greater than `2026-05-21T15:33:00.708Z`
The second condition `"workflow"."createdAt" >
"2026-05-21T15:33:00.708Z"` always evaluates to true, because the value
actually stored in the DB for createdAt is something like `2026-05-21
21:03:00.708264 +0530`, which is always greater than
`2026-05-21T15:33:00.708Z` in the cursor. The row used to generate the
cursor therefore reappears on the next page.

### My solution
Truncate the column to milliseconds in the comparison so both sides have
the same precision: `date_trunc('milliseconds', ${fieldReference})`.

For the issue of nested sorting filters, when ordering by a composite
field (e.g. `createdBy.name`), `encodeCursor` stored the entire
composite object (`source`, `workspaceMemberId`, `name`, `context`). The
where-condition builder later iterated those sub-keys and threw "Invalid
cursor" because only name had an orderBy direction.

P.S: Duplicate of #20867 because last fork got polluted.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-01 08:50:14 +00:00
Nicolas Besnard 71c377484e fix(front): keep app variable cache in sync after update (#20861)
Updating an application variable in Workspace / Applications / <App> /
Settings persisted server-side but the Apollo cache kept the old value.
Switching tabs unmounted the settings tab, and remounting reseeded the
input from the stale cache — only a full refresh showed the new value.

Mutation now writes the new value into the ApplicationVariable entity
via cache.modify, so FindOneApplication reflects the change immediately.
Hook + table updated to pass the variable id through.

Adds a hook test that pre-seeds the cache and asserts the cached value
after the mutation.

NOTE: saving the plain value in Apollo's cache might not be the best
approach here

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:14:28 +00:00
8Maverik8 26906951b3 fix(twenty-sdk): minify front-component bundles & set NODE_ENV=production in deploy build (#20937)
## Summary

`twenty deploy` (and `twenty build`) currently ship front-component
`.mjs` bundles **unminified**, with `process.env.NODE_ENV` undefined at
build time. Two missing options in
`get-base-front-component-build-options.ts` — fix is two lines.

## Why this matters

Each front-component bundle includes React + ReactDOM + the design
system AOT (only `twenty-client-sdk/{core,metadata}` are listed in
`FRONT_COMPONENT_EXTERNAL_MODULES`). A trivial widget measures **~2 MB**
unminified. Because every widget mount spawns a fresh Web Worker that
re-fetches and re-parses the bundle (`FrontComponentWorkerEffect.tsx`),
that 2× size translates directly into 2× cold-start latency on **every
record-page navigation and every browser refresh**. On a CRM with even a
handful of custom widgets this dominates perceived UI latency.

## Why it bites every app, not one user

Reference apps in this repo
(`packages/twenty-apps/fixtures/{minimal,rich}-app`,
`community/github-connector`, `internal/twenty-for-twenty`) ship the
same way — verified by inspecting the released `twenty-sdk@2.5.0`
bundle. There's no CLI flag, env var, or config option to opt into a
production build. A search of issues/PRs for "minify", "bundle size",
"production build" surfaces nothing tracking this.

## Fix

Enable `minify: true` and `define: { 'process.env.NODE_ENV':
'"production"' }` in the base front-component build options. These flow
through `build-application.ts` (the orchestrator for `twenty deploy` and
`twenty build`).

**Watch mode (`twenty dev`) is intentionally untouched.**
`esbuild-watcher.ts` has its own configuration path that doesn't consume
`getBaseFrontComponentBuildOptions()`; it stays unminified so local
rebuilds remain fast and stack traces remain readable during
development.

## Measured impact

Two production extension apps using `twenty-sdk@2.5.0`:

| File | Before | After | Δ |
|---|---:|---:|---:|
| `oapps-deal-items` (single widget) | 2,114,953 B | 863,444 B |
**−59%** |
| `oapps-document-hub/documents-panel` | 2,120,341 B | 872,478 B |
**−59%** |
| `oapps-document-hub/hub-document-record` | 2,077,013 B | 852,544 B |
**−59%** |
| `oapps-document-hub/field-mapping-editor` | 1,168,941 B | 255,787 B |
**−78%** |

End-user effect: opening an Opportunity record with two custom-widget
tabs went from ~3-4s widget paint to under 1s on the same machine, same
browser, same record.

## Risk / scope

- **No behavior change.** Minification is a transparent transform;
`NODE_ENV=production` is the standard signal libraries already gate on.
No app code changes needed.
- **No effect on `twenty dev`** — separate code path.
- **No effect on logic functions** — they use their own build-options
object.
- One file touched.

## Test plan

- [ ] `yarn twenty deploy` on
`packages/twenty-apps/fixtures/minimal-app` → output `.mjs` is mangled
and `process.env.NODE_ENV` no longer appears literally inside the
bundle.
- [ ] `yarn twenty dev` on the same app → output `.mjs` remains
readable.
- [ ] Existing CI green.

Happy to add a feature-flag (`TWENTY_BUILD_MODE` env var or `twenty
deploy --no-minify` escape hatch) if maintainers prefer that over
unconditional minification.

---------

Co-authored-by: 8Maverik8 <8maverik8@users.noreply.github.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:13:55 +00:00
Nick 51202d5a32 fix(front): scroll long content in rich text editor (#20319)
## Summary

Fixes scroll behavior in the rich text editor.

Long content was unbounded — the popup grew off-screen, the
expand-to-side-panel button became unreachable, and the side panel
itself didn't scroll either.

Closes #20309

## Changes

- `RichTextFieldInput`: cap the popup at `min(60vh, 500px)` and wrap the
editor in a scrollable region. Collapse button stays at the top via
`align-items: flex-start`.

- `RecordInlineCellEditMode`: add `shift()` middleware to keep the popup
inside the viewport after `flip()` triggers (previously the popup could
extend above the viewport top with the collapse button out of reach).

- `SidePanelContainer` / `SidePanelRouter`: add `min-height: 0` to the
flex-column chain so the existing `overflow-y: auto` on the content area
can actually clip and scroll long children.

The previous attempt in #20310 added the same `min-height: 0` plus a
nested overflow wrapper inside the rich-text page; the nested wrapper
turned out to be the reason scrolling didn't work.

## Test plan

- [x] Open a record with a long `RICH_TEXT` field
- [x] Click the field — popup opens bounded; long content scrolls inside
- [x] Click the expand button (top-right) — side panel opens
- [x] Side panel: long content scrolls vertically
- [x] Popup near the bottom of the viewport flips upward and stays fully
inside the viewport (collapse button remains visible)
- [x] `lint:diff-with-main`, `typecheck`, prettier — green



https://github.com/user-attachments/assets/827be881-aadd-49ed-9ddc-7566c00cf4be

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:13:11 +00:00
Thomas Trompette f4380f89a8 fix: SSE event stream reconnection after idle connection death (#21061)
The SSE event stream could silently die from network partitions, NAT
table flushes, browser tab throttling, or server restarts. When this
happened:
1. The `error` callback only called `captureException` — no reconnection
was triggered
2. The `complete` callback was `() => {}` — a cleanly terminated stream
left the client permanently broken
3. No mechanism existed to detect a silently dead connection where no
FIN/RST was received

## Summary

- **Fix `error`/`complete` callbacks**: The `graphql-sse` subscription's
`error` callback only reported to Sentry, and `complete` was a no-op.
Both now set `shouldDestroyEventStreamState = true` to trigger the
destroy-recreate lifecycle, ensuring detected transport failures and
clean stream terminations lead to automatic reconnection.
- **Add server-side keepalive**: The existing heartbeat timer now runs
every 30s (instead of 6min) and publishes empty events through the Redis
pub/sub channel in addition to refreshing the Redis TTL (throttled to
~6min). Unlike GraphQL Yoga's opaque SSE comment pings, these are real
subscription events that flow through the client's `next`/`message`
handlers.
- **Add client-side keepalive monitor (`SSEKeepAliveEffect`)**: Tracks
the timestamp of the last received event. If no event arrives within 90
seconds (3x the keepalive interval), it clears query listeners and
triggers a stream destroy-recreate cycle.

## Test plan

- [x] Start the app, verify SSE events flow normally (workflow runs
update in real-time)
- [x] Leave the app idle for >90 seconds, then trigger a workflow run —
verify the stream auto-reconnects and events are delivered
- [x] Kill the server, restart it, verify the frontend recovers its
event stream
- [x] Verify keepalive events (empty
`objectRecordEventsWithQueryIds`/`metadataEvents`) appear in browser
network tab every ~30s
- [x] Verify no regressions in SSE-dependent features (record updates,
metadata changes, workflow run visualization)
2026-06-01 08:09:22 +00:00
nitin e430e4ea0a fix(ai): route xAI search through Responses API as native tools (#21037)
xAI deprecated Live Search, so the `searchParameters` provider option
now returns 410. This routes all xAI models through the Responses API
and binds web/X search as native agent tools, matching how
Anthropic/OpenAI expose search.

- xAI provider now uses `provider.responses()` — its
`webSearch()`/`xSearch()` tools only run against the Responses endpoint,
not chat completions
- web/X search migrated from the `provider-option` variant to `sdk-tool`
(`web_search`/`x_search`); deleted the dead `searchParameters` path, the
`provider-option` variant, and `providerOptions` on `NativeModelBinding`
- dropped a dead `rolePermissionConfig` param on `getAgentRoleId`, left
over from #20331

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-31 15:11:04 +02:00
Abdullah. b027e4bdb1 [Website] i18n module, page-local sections, translatable copy (#21082)
**i18n** — collapsed the ~22 scattered i18n files into a single module
and turned on Spanish alongside French.

**Sections** — dropped the old compound pattern (`Section.Root`,
`Section.Heading`, …). Reusable layout shells moved to `src/templates/`,
atomic bits stay in `design-system/`, and each page now owns its copy in
local `_components` blocks instead of pulling it out of shared sections.
Data files hold arrays only, no prose.

**Copy → `<Trans>`** — A lot of headings were split across several
`<HeadingPart>`s just for font styling, which meant each piece was a
separate translation string. A translator got "Build your Enterprise
CRM" and "at AI Speed" as two unrelated strings and had no way to
reorder them for their language. Those are now single `<Trans>` units
with placeholders. Same idea for the old `\n` + `white-space: pre-line`
line-break trick: replaced with a small `ResponsiveLineBreak` element so
the break is doesn't quietly rot, and did a dead-code pass.

The de-fragmentation changes the message IDs, so around 60 strings will
fall back to English in fr/es until Crowdin re-syncs.
2026-05-31 12:39:35 +00:00
github-actions[bot] fc90b4ba8b i18n - docs translations (#21064)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 19:30:26 +02:00
LazyBouy 88b77cb699 feat(server): opt-in FRONT_AUTO_BASE_URL for hostname-relative API URL (#20504)
## Problem

`generateFrontConfig()` writes `window._env_.REACT_APP_SERVER_BASE_URL =
process.env.SERVER_URL` unconditionally. The frontend then pins to that
absolute URL. For self-hosted deployments reachable from multiple
hostnames (Tailscale IP, LAN IP, internal DNS, SSH tunnel to localhost,
public DNS), only the one matching `SERVER_URL` works — others hit CORS
errors or unreachable hosts because the frontend tries to call the API
at the configured URL, not the one the user came in via.

The frontend already supports the right fallback:
`packages/twenty-front/src/config/index.ts:20-21` reads
`window._env_?.REACT_APP_SERVER_BASE_URL` and falls back to
`getDefaultUrl()` (which uses `window.location`) when the env var is
absent. But the server-side `generateFrontConfig` always populates
`_env_`, so the fallback never runs.

## Fix

One file: `packages/twenty-server/src/utils/generate-front-config.ts`.
Add a `FRONT_AUTO_BASE_URL=true` opt-in (also triggered when
`SERVER_URL` is unset entirely). When the toggle is on, inject
`window._env_ = {}` so the frontend's existing `getDefaultUrl()`
fallback resolves the origin from `window.location` at runtime.

## Backwards compatibility

When `SERVER_URL` is set AND `FRONT_AUTO_BASE_URL` is unset (or anything
other than `'true'`): unchanged — `REACT_APP_SERVER_BASE_URL:
process.env.SERVER_URL` is injected exactly as before.

The toggle is strictly additive. Existing single-hostname deployments
are not affected.

## Use case

Self-hosted Twenty reachable via:
- `http://100.115.12.29` over Tailscale
- `http://localhost:4440` over SSH tunnel
- `http://twenty.internal` over LAN DNS
- `http://crm.example.com` public

With `FRONT_AUTO_BASE_URL=true`, all four paths work without rebuilds or
per-hostname server processes.

## Test plan

- [ ] `SERVER_URL=http://x.com` (toggle unset) → `<script>window._env_ =
{"REACT_APP_SERVER_BASE_URL":"http://x.com"};</script>` (unchanged from
main)
- [ ] `SERVER_URL` unset → `<script>window._env_ = {};</script>` (new
fallback path)
- [ ] `SERVER_URL=http://x.com FRONT_AUTO_BASE_URL=true` →
`<script>window._env_ = {};</script>` (toggle wins)
- [ ] `FRONT_AUTO_BASE_URL=false SERVER_URL=http://x.com` → unchanged
(only `'true'` triggers the toggle)

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-29 16:26:43 +00:00
Raphaël Bosi 0ed2e9d82d Docs: clarify numberOfSelectedRecords usage for RECORD_SELECTION items (#21059)
Add a note to the command menu items docs explaining that
RECORD_SELECTION already guarantees a non-empty selection, so
numberOfSelectedRecords > 0 is redundant in
conditionalAvailabilityExpression.
2026-05-29 17:59:14 +02:00
github-actions[bot] 643cfe9b13 i18n - docs translations (#21062)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 17:29:40 +02:00
Thomas Trompette bc1b7f6fdf fix: resolve workflow form step auto-open race condition (#21053)
## Summary
- Fix intermittent failure where the Quick Lead workflow form step did
not auto-open
- Root cause: race conditions between SSE events, Apollo cache writes,
and the `runWorkflowVersion` mutation timing
- Add generic monotonicity guard in the SSE handler that drops stale
updates for all records (not just WorkflowRun)

## Changes
- **`useTriggerOptimisticEffectFromSseUpdateEvents.ts`**: Compare
incoming `updatedAt` with cached record before writing — skip if stale.
Moved `upsertRecordsInStore` after the guard so neither Apollo cache nor
Jotai store receive stale data.
- **`useRunWorkflowVersion.tsx`**: Await mutation before opening side
panel; register SSE listener eagerly before mutation
- **`useWorkflowRun.ts`**: Simplified back to plain `useFindOneRecord` +
schema parse (no extra state needed)
- **`generateWorkflowRunDiagram.ts`**: `shouldOpenStep` matches both
PENDING and RUNNING for form steps (backend RUNNING means "waiting for
user input")
- **`WorkflowRunVisualizerEffect.tsx`**: Pass `runStatus` directly
without status mapping
- **`WorkflowRunStepNodeDetail.tsx`**: Form is interactive when step is
PENDING or RUNNING
- **Deleted `latestWorkflowRunFamilyState.ts`**: No longer needed — the
generic SSE guard replaces it

## Test plan
- [x] Hard refresh, run Quick Lead workflow 10+ times — form should
always auto-open
- [x] Complete the form and verify all subsequent steps execute without
getting stuck
- [x] Verify the workflow diagram is always visible (never disappears)
- [x] Verify other record types still update correctly via SSE (e.g.
edit a person in another tab)
2026-05-29 14:18:24 +00:00
Raphaël Bosi 57118a868f Docs update: Calling a logic function from a front component (#21057)
Documents how a headless front component calls a server-side logic
function over HTTP via the /s/ route, so AI agents have a clear
reference for implementing this pattern.
2026-05-29 14:04:19 +00:00
github-actions[bot] 93f848fd2f i18n - translations (#21055)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 15:30:31 +02:00
Félix Malfait 667cb95730 fix(sso): accept HTTP-POST binding and surface descriptive parser errors (#21051)
Fixes https://github.com/twentyhq/twenty/issues/21044

## Summary
- Closes [#490](https://github.com/twentyhq/private-issues/issues/490) —
JumpCloud customers (and anyone else whose IdP only advertises
`HTTP-POST` for `SingleSignOnService`) could not upload their SAML
metadata; the parser silently rejected them with a generic `Invalid
file` toast.
- The SAML IdP metadata parser now falls back to `HTTP-POST` when
`HTTP-Redirect` is not advertised. Both are valid SAML 2.0 bindings.
- The parser now returns a descriptive `reason` string (Zod issues +
custom errors) instead of an opaque `error: unknown`, and the upload
snack bar surfaces it so the customer can self-diagnose (e.g. `entityID:
entityID is not a valid URL` if they forgot to fill in their IdP Entity
ID).
- Added unit tests for HTTP-POST-only metadata, HTTP-Redirect
preference, and each descriptive-error path.

## Test plan
- [x] `npx jest parseSAMLMetadataFromXMLFile
--config=packages/twenty-front/jest.config.mjs` — 8/8 pass
- [x] `npx oxlint -c packages/twenty-front/.oxlintrc.json` on changed
files — clean
- [x] `npx oxfmt --check` on changed files — clean
- [ ] Manual: upload the customer's JumpCloud metadata (HTTP-POST only,
placeholder `entityID`) and confirm the error now says `Invalid file:
entityID: entityID is not a valid URL` instead of `Invalid file`
- [ ] Manual: upload metadata with a real `entityID` and HTTP-POST-only
binding, confirm the form populates correctly
2026-05-29 15:22:50 +02:00
martmull 4e5d47168c 2439 improve command menu item display in right panel (#21020)
## Before

<img width="1512" height="389" alt="image"
src="https://github.com/user-attachments/assets/33274356-fb99-4a02-baa7-c324e6d151c6"
/>


## After

<img width="1512" height="357" alt="image"
src="https://github.com/user-attachments/assets/c0affb71-e920-4d64-b2f0-1bed53209ea5"
/>
2026-05-29 11:37:27 +00:00