main
12518 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
2375c2f59c |
i18n - translations (#21141)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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.
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
94d2e386e8 |
i18n - website translations (#21135)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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 |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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. |
||
|
|
5f0096c464 |
i18n - website translations (#21088)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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> |
||
|
|
3d6bcc3102 |
i18n - translations (#21128)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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) ``` |
||
|
|
6ad6fcce0f |
Bump playwright (#21113)
Playwright installation is infinite looping in the ci seems like to be a global outage |
||
|
|
ad47b2972c |
i18n - translations (#21112)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
da5e1152bb |
i18n - translations (#21103)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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)
|
||
|
|
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> |
||
|
|
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. |
||
|
|
fc90b4ba8b |
i18n - docs translations (#21064)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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>
|
||
|
|
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. |
||
|
|
643cfe9b13 |
i18n - docs translations (#21062)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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) |
||
|
|
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. |
||
|
|
93f848fd2f |
i18n - translations (#21055)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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 |
||
|
|
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" /> |