Commit Graph
10569 Commits
Author SHA1 Message Date
Charles BochetandGitHub 3b2bf39565 Refactor modal (#18377)
## Summary

- Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`,
`ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as
stateless, reusable components
- Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai
state (`isModalOpenedComponentState`) to the stateless `Modal` via an
`isOpen` prop
- Rename `modalVariant` prop to `overlay` with clearer values: `'dark'`
(default), `'light'` (in-container), `'transparent'` (invisible panel).
Remove unused `'medium'` overlay
- Rename `modalId` to `modalInstanceId` across the entire modal zone
(~30 consumer files)
- Extract `ModalProps` to its own file in
`twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to
its own file using `Pick<ModalProps, ...>` for shared props
- Extract `ModalBackdrop` to its own file and export from `twenty-ui`;
use it in `UserOrMetadataLoader` instead of a local styled component
- Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in
`SpreadsheetImportStepperContainer` instead of duplicated `styled.div`
definitions
- Remove unused `onClose` prop from stateless `Modal`; fix `typeof
document` guard in `ModalStatefulWrapper`
- Split shared types into individual files: `ModalSize.ts`,
`ModalPadding.ts`, `ModalOverlay.ts`
- Extract wyw profiling instrumentation from `vite.config.ts` into
reusable `createWywProfilingPlugin` with parametrized threshold and
improved logging
- Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`,
`ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front`
- Add comprehensive Storybook stories in `twenty-ui` covering Default,
Confirmation, Small, ExtraLarge, Closed, and Interactive variants
2026-03-04 13:22:31 +01:00
Paul RastoinandGitHub 995793c0ac [CREATE_APP] Integration testing scaffold (#18345)
# Introduction
Adding integration test scaffold to the create twenty app and an example
to the hello world app
This PR also fixes all the sdk e2e tests in local

## `HELLO_WORLD`
Removed the legacy implem in the `twenty-apps` folder, replacing it by
an exhaustive app generation

## Next step
Will in another PR add workflows for CI testing

## Open question
- Should we still add vitest config and dep even if the user did not ask
for the integration test example ? -> currently we don't
- That's the perfect timing to identify if we're ok to handle seed
workspace authentication with the known api key
2026-03-04 13:12:13 +01:00
Abdullah.andGitHub 225f185278 fix: fast-xml-parser has stack overflow in XMLBuilder with preserve order (#18375)
Resolves [Dependabot Alert
551](https://github.com/twentyhq/twenty/security/dependabot/551).
2026-03-04 10:36:58 +01:00
Abdullah.andGitHub ca1d49c6cd fix: rollup 4 has arbitrary file write via path traversal (#18373)
Resolves [Dependabot Alert
508](https://github.com/twentyhq/twenty/security/dependabot/508).
2026-03-04 10:35:10 +01:00
Abdullah.andGitHub 2f9c94d9a0 fix: upgrade nestjs dependencies to upgrade multer transitive import (#18374)
Resolves [Dependabot Alert
549](https://github.com/twentyhq/twenty/security/dependabot/549) and
[Dependabot Alert
550](https://github.com/twentyhq/twenty/security/dependabot/550).
2026-03-04 10:34:44 +01:00
a9c4920fcc i18n - docs translations (#18370)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 09:16:33 +01:00
07dd27f6c4 i18n - translations (#18368)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 01:29:35 +01:00
Charles BochetandGitHub 7a2e397ad1 Complete linaria migration (#18361)
## Summary

Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.

This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.

## Changes

### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals

### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support

### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection

### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales

## How it works

Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.

- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
2026-03-04 00:50:06 +01:00
Paul RastoinandGitHub 8a3b96d911 Remove files (#18360) 2026-03-03 19:01:27 +01:00
Paul RastoinandGitHub 132a19f688 [SDK] Execute logic function e2e test (#18351)
# Introduction
Creating an e2e test covering the execute logic function public
operation
2026-03-03 17:44:19 +01:00
Charles BochetandGitHub 3bfdc2c83f chore(twenty-front): migrate command-menu, workflow, page-layout and UI modules from Emotion to Linaria (PR 4-6/10) (#18342)
## Summary

Continues the Emotion → Linaria migration (PR 4-6 from the [migration
plan](docs/emotion-to-linaria-migration-plan.md)). Migrates **311
files** across four module groups:

| Module | Files |
|---|---|
| command-menu | 53 |
| workflow | 84 |
| page-layout | 84 |
| UI (partial - first ~80 files) | ~80 |
| twenty-ui (TEXT_INPUT_STYLE) | 1 |
| misc (hooks, keyboard-shortcut-menu, file-upload) | ~9 |

### Migration patterns applied

- `import styled from '@emotion/styled'` → `import { styled } from
'@linaria/react'`
- `import { useTheme } from '@emotion/react'` → `import { useContext }
from 'react'` + `import { ThemeContext } from 'twenty-ui/theme'`
- `${({ theme }) => theme.X.Y.Z}` → `${themeCssVariables.X.Y.Z}` (static
CSS variables)
- `theme.spacing(N)` → `themeCssVariables.spacing[N]`
- `styled(motion.div)` → `motion.create(StyledBase)` (11 components)
- `styled(Component)<TypeParams>` → wrapper div approach for non-HTML
elements
- Multi-declaration interpolations split into one CSS property per
interpolation
- Interpolation return types fixed (`&&` → ternary `? : ''`)
- `TEXT_INPUT_STYLE` converted from function to static string constant
(backward compatible)
- Emotion `<Global>` replaced with `useEffect` style injection
- Complex runtime-dependent styles use CSS custom properties via
`style={}` prop

### After this PR

- **Remaining files**: ~400 (object-record: ~160, settings: ~200, UI:
~44)
- **No breaking changes**: CSS variables resolve identically to the
previous Emotion theme values
2026-03-03 16:42:03 +01:00
martmullandGitHub 8b26020a0b Fix missing omit in application config (#18354)
as title
2026-03-03 16:33:49 +01:00
b1107c823a Apollo enrich (#18277)
- apollo enrich application (via OAuth 2)
- add applicationId to var env in logic function executor
- update `getDefaultUrl` logic

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-03 14:43:10 +01:00
martmullandGitHub 5c4a1f931a Fix trigger missing (#18348)
## After

<img width="1170" height="448" alt="image"
src="https://github.com/user-attachments/assets/3a8dd77c-06d2-438e-b2d3-07d272c08588"
/>

<img width="977" height="441" alt="image"
src="https://github.com/user-attachments/assets/3e16ec94-63ce-4d09-af54-38f330bfa0c9"
/>

<img width="802" height="333" alt="image"
src="https://github.com/user-attachments/assets/0975f199-b89a-4a05-a8e4-2101f5556445"
/>
2026-03-03 14:37:17 +01:00
4266f4022a i18n - translations (#18349)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-03 14:24:43 +01:00
Thomas TrompetteandGitHub b2b3a3f860 Workflow iterator continues on faillure (#18325)
<img width="450" height="212" alt="Capture d’écran 2026-03-03 à 11 41
54"
src="https://github.com/user-attachments/assets/b2c29a48-7dc0-4b16-a085-8f305d21f7ca"
/>

New status `FAIL_SAFE` added. This status propagates to the following
nodes until reaching the iterator, that will start the new iteration.

The difference with `SKIP` is that, when the parent nodes have at least
one `FAIL_SAFE`, it becomes `FAIL_SAFE` too. While a parent 1 `SKIP` +
parent 2 `SUCCESS` => to be executed.

I also thought about just going back to the iterator as a break would,
but since we have branches, it may lead to inconsistent statuses with
parallel updates.
2026-03-03 14:18:57 +01:00
Charles BochetandGitHub d48c58640c Migrate CI runners from Depot back to GitHub-hosted runners (#18347)
## Summary
- Replaces all `depot-ubuntu-24.04` runners with `ubuntu-latest`
- Replaces all `depot-ubuntu-24.04-8` runners with
`ubuntu-latest-8-cores`
- Updates storybook build cache keys in ci-front.yaml to reflect the
runner name change

Reverts the temporary Depot migration introduced in #18163 / #18179
across all 23 workflow files.
2026-03-03 14:14:27 +01:00
Paul RastoinandGitHub 005223de8c [SDK] Make public-operations non throw (#18343)
Followup https://github.com/twentyhq/twenty/pull/18320
2026-03-03 14:04:13 +01:00
Paul RastoinandGitHub 2f09fb8c04 SDK Split command and cli logic (#18320)
# Introduction

Allow a consumer call the commands programmatically instead of passing
by the exec
To do so extract from the command definition all the core logic, created
a new error api that allow keeping same error logs granularity than
before

## Usage
```ts
import { authLogin, appUninstall, functionExecute } from 'twenty-sdk/cli';

const result = await authLogin({
  apiKey: 'my-key',
  apiUrl: 'https://my-twenty.com',
});

if (!result.success) {
  throw new Error(result.error);
}
```

## `app:build`
Introduced a new command that will allow building the whole project
without any watch setup
- Build and validate manifest
- Get or create app
- Synchronize manifest with twenty-sdk stub and no typecheck
- generate client
- Run typecheck
- Synchronize manifest again
2026-03-03 12:24:49 +01:00
neo773andGitHub 083df3e7ca OAuth Edge case crash + cleanup (#18326)
Fixes Sentry issue https://twenty-v7.sentry.io/issues/6603377117/

Also cleaned up the code with proper types removing `//
eslint-disable-next-line @typescript-eslint/no-explicit-any`
2026-03-03 12:22:12 +01:00
martmullandGitHub 2e9624858c Fix name singular updates in dev mode (#18339)
as title
2026-03-03 11:40:17 +01:00
Paul RastoinandGitHub 58e37a118c Builder runs delete update and then create (#18272)
# Introduction
We need to build and validate the flat entity operation in the following
order delete update and create
For example if not, if a created field has the same name than a deleted
one than it will fail whereas it should not
2026-03-03 11:26:06 +01:00
Paul RastoinandGitHub 0b766464e4 Composite action: Spawn twenty instance (#18317)
# Introduction

## Runs:

Public personal repo:
-
[latest](https://github.com/prastoin/twenty-app/actions/runs/22568051680/job/65368592903)
2026-03-03 11:19:08 +01:00
Charles BochetandGitHub 802a5b0af6 chore(twenty-front): migrate auth, activities, AI, pages and small modules from Emotion to Linaria (PR 2-3/10) (#18328)
## Summary

- Migrate ~200 files from `@emotion/styled` / `@emotion/react` to
`@linaria/react` + `themeCssVariables`, continuing the zero-runtime
CSS-in-JS migration (PR 2-3 of the [migration
plan](docs/emotion-to-linaria-migration-plan.md))
- Modules covered: **auth** (19), **activities** (53), **ai** (30),
**pages** (70), **action-menu** (3), **object-metadata** (4),
**onboarding** (2), **workspace** (2), **file** (3), **error-handler**
(2), **front-components** (1), **geo-map** (1), **loading** (5),
**testing** (5), plus a `style` prop addition to `TableRow`
- Handles `styled(FunctionComponent)<Props>` incompatibility with
Linaria by using CSS custom properties via `style` + `var()` references

## Test plan

- [x] `npx nx lint:diff-with-main twenty-front` passes
- [x] `npx nx typecheck twenty-front` passes
- [ ] Visual spot-check of auth, onboarding, settings, activities, and
AI chat screens
- [ ] No remaining `@emotion/styled` or `@emotion/react` imports in
migrated files


Made with [Cursor](https://cursor.com)
2026-03-03 11:17:47 +01:00
Abdul RahmanandGitHub ae291c99ba fix: record does not open in side panel after returning from fullscreen (#17131)
Closes #17089 

### 1. Can't reopen record after having navigated to its show page
After opening a record in the show page from the command menu and going
back to the index, clicking the same record again did nothing. The
command menu navigation stack was not cleared when opening in the show
page, so the "already open" check skipped reopening. We now clear the
command menu navigation stack before navigating to the show page (in
`RecordShowRightDrawerOpenRecordButton`), so the same record can be
reopened from the index.

### 2. Row doesn't highlight when opening command menu after return from
show page
After returning from the record show page to the index, the first row
click opened the command menu but the row did not highlight. The "side
panel close" event was emitted not only when the panel actually closed,
but also when opening the command menu (cleanup ran with
`isCommandMenuClosing` and always emitted the event). Listeners like
`RecordTableDeactivateRecordTableRowEffect` then deactivated the row. We
now emit the side panel close event only when the close animation
actually completes (`CommandMenuSidePanelForDesktop`), and skip emitting
it when cleanup is run from the open path (`useNavigateCommandMenu`
passes `emitSidePanelCloseEvent: false`). The table still deactivates
the row when the user closes the panel, but no longer when they open the
command menu by clicking a row.
2026-03-02 23:25:11 +00:00
7809f83e72 fix: [Note] Title not filled by default #13838 (#18297)
Fixes #13838 
When creating a note from the command menu side panel (e.g. clicking
"Add Note" in a related notes section on an Opportunity/company/people
page), the title field was not auto-focused — focus point went to body
instead.

## Root Cause

When a record opens in the side panel, there is no page navigation, so
`PageChangeEffect` (which handles title auto-focus for full-page views)
never runs. `openNewRecordTitleCell()` was simply never called for the
side-panel path.

## Fix

`openRecordInCommandMenu` is the single entry point for all side-panel
record opens, so title auto-focus is handled there once for all callers.
Previously, `useCreateNewIndexRecord` called `openRecordInCommandMenu`
and then called `openNewRecordTitleCell` separately, which would have
caused a double invocation after this fix. The redundant call has been
removed.

## Before


https://github.com/user-attachments/assets/df0d9e4f-dc25-4a0d-a49e-898a14f9c0a0

## After


https://github.com/user-attachments/assets/1a5044f7-6bb7-4333-8934-c1081b935e97

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 20:09:36 +00:00
27847f6ac6 i18n - translations (#18330)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-02 21:00:18 +01:00
Félix MalfaitandGitHub 6351c6c1c6 feat: remember original URL and redirect after login (#18308)
## Summary

- Implement a return-to-path mechanism that preserves the user's
intended destination across authentication flows (login, magic link,
cross-domain redirects)
- Uses layered persistence: Jotai atom (in-memory), sessionStorage with
TTL (tab-switch resilience), URL query parameter (cross-domain
propagation)
- Includes path validation to prevent open redirects, automatic cleanup
after successful login, and comprehensive test coverage
- Replaces the unused `previousUrlState` with a robust
`returnToPathState` system

## Test plan

- [ ] Visit a deep link (e.g. `/objects/tasks`) while logged out —
should redirect to login, then back to `/objects/tasks` after logging in
- [ ] Visit an OAuth authorize link while logged out — should redirect
to login, then to the authorize page
- [ ] Test magic link flow: click sign-in link that opens new tab —
should still redirect to original destination
- [ ] Test cross-domain: visit `app.twenty.com/objects/tasks` — should
preserve path through workspace domain redirect
- [ ] Verify auth/onboarding paths are excluded from being saved as
return paths
- [ ] Verify return-to-path is cleared after successful navigation
- [ ] All 215 existing `usePageChangeEffectNavigateLocation` tests pass


Made with [Cursor](https://cursor.com)
2026-03-02 19:00:48 +01:00
Abdullah.andGitHub 20a2c3836e feat: introduce role selector when inviting members to a workspace (#18085)
This PR adds an explicit role selector to the "Invite by email" flow,
requires a role choice before sending, and stores the selected role with
each invitation. The backend now accepts and persists `roleId` on
invitations and applies it when the invite is accepted, while keeping it
optional to avoid breaking existing clients and legacy invites.

---

### Frontend

- **Settings → Members → Invite by email**
- New **Role** dropdown (same `Select` pattern as member/API key role
selectors) between the email input and Invite button.
- Roles are loaded via `SettingsRolesQueryEffect` and
`settingsAllRolesSelector`; only roles with `canBeAssignedToUsers` are
shown.
- Role is **required**: form validates `roleId` (e.g.
`z.string().min(1)`) and the Invite button is disabled until a role is
selected and emails are valid.
- `WorkspaceInviteTeam` receives `roles` as a prop from the parent;
layout is responsive (e.g. stacked on small viewports).
- **Pending invitations table**
- New **Role** column showing the invitation’s role label (or "Unknown
role" for legacy invites without `roleId`), using the same roles source
for lookup.
- **Onboarding invite step**
- When sending invites during onboarding, the workspace **default role**
is used when available (`currentWorkspace?.defaultRole?.id`), so no role
selector is added there.
- **GraphQL**
- `sendInvitations` mutation accepts optional `roleId`;
`findWorkspaceInvitations` and resend mutation responses include
`roleId` on `WorkspaceInvitation`. Frontend types (e.g.
`WorkspaceInvitation`, hook variables) updated accordingly.

---

### Backend

- **API**
- `SendInvitationsInput` has an **optional** `roleId` (UUID, nullable).
The resolver normalises `null` to `undefined` so existing callers and
legacy flows are not broken.
- **Validation (when `roleId` is provided)**
- Role checks are centralised in **RoleValidationService**
(`RoleValidationModule`, in `metadata-modules/role-validation/`). It
validates that the role exists in the workspace and has
`canBeAssignedToUsers`, and throws a permissions-style error otherwise.
This avoids circular dependencies (e.g. `RoleModule` imports
`UserWorkspaceModule`, so invite/accept flows cannot depend on
`RoleModule`).
- **Send flow:** `WorkspaceInvitationResolver` and
`WorkspaceInvitationService.sendInvitations` both call
`RoleValidationService.validateRoleAssignableToUsersOrThrow` when
`roleId` is present (resolver before calling the service; service again
before creating tokens so that **resend** also validates the stored role
and fails fast if the role was deleted or made unassignable).
- **Accept flow:**
`UserWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace` uses the
same service in `resolveRoleIdForNewMember` when an invitation provides
a `roleId`, then falls back to `workspace.defaultRoleId` when not.
Role/default is resolved and validated before any user/workspace/member
creation.
- **Persistence**
- Invitation app tokens store `roleId` in `context` next to `email`
(`context: { email, roleId? }`). `generateInvitationToken` and
`createWorkspaceInvitation` accept an optional `roleId` and only add it
to `context` when defined.
- **Resend**
- Resend passes the existing invitation’s `context.roleId` into
`sendInvitations`. The service validates that role (when present) before
creating the new token, so if the role was deleted or made unassignable,
resend fails with a clear error instead of sending a broken link.
- **Response shape**
- `SendInvitationsOutput.result` remains `WorkspaceInvitation[]`. When
`usePersonalInvitation` is false we only push full invitation records
(from `castAppTokenToWorkspaceInvitationUtil`), so the result always
matches the GraphQL type (`id`, `email`, `roleId`, `expiresAt`).
- **Modules**
- `WorkspaceInvitationModule` and `UserWorkspaceModule` import
**RoleValidationModule** (not `RoleModule`) and inject
**RoleValidationService** for validation. `RoleModule` imports
`RoleValidationModule` and `RoleService` delegates to
`RoleValidationService` for the same validation where the module graph
allows.

---

### Backward compatibility

- **Optional `roleId`**: Clients that don’t send `roleId` (or send
`null`) are unchanged; invitations are created without a role and the
accept flow uses the workspace default role.
- **Legacy invitations**: App tokens with only `context.email` still
work; `context.roleId` is optional and the UI can show e.g. "Unknown
role" for those in the pending-invitations table.
2026-03-02 18:58:32 +01:00
nitinandGitHub 1eb284c87f Fix command menu text/number inputs to commit on blur and cancel cleanly on Escape (#18283)
closes https://github.com/twentyhq/twenty/issues/18264




https://github.com/user-attachments/assets/7b576a00-78bc-46a2-9528-d8b3bcbdd530




https://github.com/user-attachments/assets/4102468e-e85f-46a0-8b23-e7abd77bfc95



### PR description -
This fixes flaky persistence in command menu text and number inputs.

- moved commit logic to onBlur (single commit path)
- Enter now blurs, so it uses the same commit path
- Escape now cancels edit (restores draft + exits) without persisting
- removed dependency on input click-outside commit timing

### Outcome -

- clicking anywhere outside the input now reliably persists edits
- Escape consistently discards edits
2026-03-02 15:30:51 +00:00
Charles BochetandGitHub c4140f85df chore(twenty-front): migrate small modules from Emotion to Linaria (PR 1/10) (#18314)
## Emotion → Linaria migration — PR 1 of 10

First batch of the `twenty-front` migration from Emotion (runtime
CSS-in-JS) to Linaria (zero-runtime, build-time extraction via
wyw-in-js). Covers **100 files** across 10 small standalone modules —
chosen as the lowest-risk starting point.

### Modules migrated

spreadsheet-import (28) · navigation-menu-item (17) · views (14) ·
billing (10) · blocknote-editor (7) · advanced-text-editor (7) ·
favorites (7) · navigation (4) · information-banner (3) ·
sign-in-background-mock (3)

### Migration pattern

Every file follows the same mechanical transformation:

| Emotion | Linaria |
|---|---|
| `import styled from '@emotion/styled'` | `import { styled } from
'@linaria/react'` |
| `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| `${({ theme }) => theme.spacing(4)}` |
`${themeCssVariables.spacing[4]}` |
| `const theme = useTheme()` | `const { theme } =
useContext(ThemeContext)` |
| `import { type Theme } from '@emotion/react'` | `import { type
ThemeType } from 'twenty-ui/theme'` |

`themeCssVariables` is a build-time object where every leaf is a
`var(--t-xxx)` CSS custom property reference, evaluated statically by
wyw-in-js. Runtime theme access (icon sizes, colors passed as props)
uses `useContext(ThemeContext)`.

### Gotchas encountered & fixed

- **Interpolation return types** — wyw-in-js requires `string | number`,
never `false`/`undefined`. Replaced `condition && 'css'` with `condition
? 'css' : ''`.
- **`css` tag inside `styled` templates** — Linaria `css` returns a
class name, not CSS text. Replaced with plain template strings.
- **`styled(Component)` needs `className`** — added `className` prop to
`NavigationDrawerSection`, `DropdownMenuItemsContainer`, and `Heading`.
- **`shouldForwardProp` not supported** — Linaria filters invalid DOM
props automatically for HTML elements. For custom components, used
wrapper divs where needed.
- **`FormFieldPlaceholderStyles`** — converted from Emotion `css`
function to a static string using `themeCssVariables`.
2026-03-02 16:33:40 +01:00
Charles BochetandGitHub 9c4b0f526c Refactor chip component hierarchy: AvatarChip → AvatarOrIcon (#18313)
## Summary

Cleans up the chip component hierarchy in `twenty-ui`:

- **Fix twenty-ui Storybook** — The `wyw-in-js` Vite plugin crashed on
`/@react-refresh` virtual module. Fixed by setting `enforce: 'pre'` so
it runs before the React refresh plugin injects virtual imports.
- **Rename `AvatarChip` → `AvatarOrIcon`** — The old name was
misleading. This component is not a chip — it's a polymorphic renderer
that displays either an `Avatar` (image/initials) or an `Icon` (plain or
with colored background). It's typically slotted into `Chip`/`LinkChip`
as `leftComponent`.
- **Move `rightComponentDivider` to `Chip`/`LinkChip`** — The vertical
separator between chip content and a right action (e.g. a close button)
is a chip layout concern, not an avatar concern. Added
`rightComponentDivider` boolean prop to `Chip` and `LinkChip`.
- **Remove `MultipleAvatarChip`** — Zero consumers in the codebase. The
command menu implements its own overlapping avatar layout.
- **Migrate raw icon usages** — `CalendarEventDetails` and `FileIcon`
(small size) now use `AvatarOrIcon` for consistent Chip icon rendering.
- **Enhance stories** — Full `CatalogDecorator` coverage for `Chip` and
`LinkChip` showing all variants, sizes, accents, and states.

## Component hierarchy

```
AvatarOrIcon (twenty-ui)
  ├── No Icon → renders Avatar (image or initials)
  ├── Icon + background → renders icon in colored square
  └── Icon only → renders plain icon
  Used as leftComponent/rightComponent in Chip or standalone

Chip (twenty-ui)
  ├── leftComponent (typically AvatarOrIcon)
  ├── label (with overflow tooltip)
  ├── rightComponentDivider (optional vertical separator)
  └── rightComponent (e.g. close icon via AvatarOrIcon)

LinkChip (twenty-ui)
  └── Wraps Chip inside a react-router <Link>

RecordChip (twenty-front)
  └── Composes Chip/LinkChip + AvatarOrIcon with record data
```

## `Chip` API additions

| Prop | Type | Description |
|------|------|-------------|
| `rightComponentDivider` | `boolean` | Renders a vertical separator
before `rightComponent` |

## Stories

<img width="1032" height="576" alt="image"
src="https://github.com/user-attachments/assets/fe7c7666-9b16-4545-b87e-1b53e22d462d"
/>
2026-03-02 15:48:49 +01:00
WeikoandGitHub 37bcb35391 Migrate pagelayout position frontend (#18229)
## Context
Part 1 of migrating gridPosition in favor of typed position
FE should now always send both values to the BE and use both.

Next steps: 
- Update the backend to enforce and validate the new position field + DB
migrations gridPositon -> position (type: GRID)
- Cleanup frontend usage
- Cleanup backend
2026-03-02 14:42:30 +01:00
Thomas TrompetteandGitHub 78a0197643 Prevent deletion of il-else branches (#18294)
If-else branches cannot be recreated once deleted. Only else-if branches
can. On if-else branches removal, we now remplace the node by an empty
node instead of only deleting

Also fixing nested if-else.
2026-03-02 13:52:32 +01:00
ff3326a53b i18n - translations (#18323)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-02 13:46:36 +01:00
martmullandGitHub 5e92fb4fc6 Do not console.log while consoleListener (#18322)
It can occur infinite loops

see
https://twenty-v7.sentry.io/issues/7269592888/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20!issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=freq
2026-03-02 12:04:12 +00:00
Félix MalfaitandGitHub 1a8be234de OAuth security hardening: RFC compliance, PKCE binding, rate limiting (#18305)
## Summary

Follow-up to #18267. Hardens the OAuth implementation with security
fixes identified during audit:

**P0 — Critical:**
- Bind authorization codes to `client_id` in context to prevent auth
code injection (RFC 6749 §4.1.3)
- Store PKCE `code_challenge` directly in auth code context instead of a
separate `CodeChallenge` token — cryptographically binds the challenge
to its code
- Enforce `code_verifier` when `code_challenge` was used during
authorization
- Hash authorization codes (SHA-256) before storage to prevent exposure
if DB is compromised
- Add `Cache-Control: no-store` + `Pragma: no-cache` headers on token
responses (RFC 6749 §5.1)
- Add rate limiting on `/oauth/token` endpoint (20 req/min per client
via existing `ThrottlerService`)

**P1 — High:**
- Return HTTP 401 for `invalid_client` errors instead of 400 (RFC 6749
§5.2)
- Verify refresh tokens belong to the presenting client (cross-client
token theft prevention)
- Limit fields exposed by public `findApplicationRegistrationByClientId`
query to only what the frontend needs (`id`, `name`, `logoUrl`,
`websiteUrl`, `oAuthScopes`)
- Require `API_KEYS_AND_WEBHOOKS` permission for
`createApplicationRegistration` mutation

**P2/P3 — Medium/Low:**
- Add error handling and loading states to frontend Authorize page
- Rename redirect URL param from `authorizationCode` to `code` (RFC
standard)
- Add unit tests for `validateRedirectUri` utility (8 test cases)

## Test plan

- [ ] Existing OAuth integration tests updated for all changes (hashed
codes, context-based PKCE, client binding, 401 status codes, cache
headers)
- [ ] New test: auth code rejected when presented by a different client
- [ ] New test: refresh token rejected when presented by a different
client
- [ ] New test: `code_verifier` required when PKCE was used in
authorization
- [ ] New test: `Cache-Control: no-store` header present on responses
- [ ] New unit tests for `validateRedirectUri` (HTTPS, localhost,
fragments, invalid URIs)
- [ ] Verify frontend authorize page shows errors gracefully


Made with [Cursor](https://cursor.com)
2026-03-02 12:21:26 +01:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
d021f7e369 Fix self host application (#18292)
- Fixes self host application
- add new telemetry information
- add serverId to identify a server instance
- remove .twenty from git tracking
- tree-shake "twenty-sdk" usage in built logic functions and front
components
- fix "twenty-sdk" version usage
- fix twenty-zapier cli

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-02 12:06:05 +01:00
neo773andGitHub 1b67ba6a75 Draft emails fix onblur on text input and callout banner component overflow (#18310)
Before

<img width="396" height="759" alt="SCR-20260301-daft"
src="https://github.com/user-attachments/assets/c3fb3a19-3456-424d-9fd2-dd13ed0d2ad5"
/>

After

<img width="391" height="752" alt="SCR-20260301-daio"
src="https://github.com/user-attachments/assets/80a64991-6e69-4ddf-b968-bdc788af02cd"
/>
2026-03-02 12:04:30 +01:00
5afc46ebd3 i18n - translations (#18321)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-02 12:03:13 +01:00
a06abb1d60 Fields widget rename group (#18169)
## Rename


https://github.com/user-attachments/assets/b151683a-d1ae-447f-9d9f-95a14b50608b

## Delete



https://github.com/user-attachments/assets/8da73a33-1c57-4771-b712-527b8080117d

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-03-02 10:32:31 +00:00
Félix Malfait 2a5b2746c9 Fix preview-env-dispatch: repository_dispatch requires contents:write
The `repository_dispatch` API endpoint requires `contents: write`
permission on the GITHUB_TOKEN, not `actions: write`. Our security
hardening PR inadvertently changed this to `contents: read`, breaking
the self-dispatch to the keepalive workflow.

Made-with: Cursor
2026-03-02 11:25:14 +01:00
Félix MalfaitandGitHub 0223975bbd Harden GitHub Actions: fix injections, isolate privileged operations to ci-privileged repo (#18318)
## Summary

- Fix expression injection vulnerabilities in composite actions
(`restore-cache`, `nx-affected`) and workflow files (`claude.yml`)
- Reduce overly broad permissions in `ci-utils.yaml` (Danger.js) and
`ci-breaking-changes.yaml`
- Restructure `preview-env-dispatch.yaml`: auto-trigger for members,
opt-in for contributor PRs via `preview-app` label (safe because
keepalive has no write tokens)
- Isolate all write-access operations (PR comments, cross-repo posting)
to a new dedicated
[`twentyhq/ci-privileged`](https://github.com/twentyhq/ci-privileged)
repo via `repository_dispatch`, so that workflows in twenty that execute
contributor code never have write tokens
- Create `post-ci-comments.yaml` (`workflow_run` bridge) to dispatch
breaking changes results to ci-privileged, solving the [fork PR comment
issue](https://github.com/twentyhq/twenty/pull/13713#issuecomment-3168999083)
- Delete 5 unused secrets and broken `i18n-qa-report` workflow
- Remove `TWENTY_DISPATCH_TOKEN` from twenty (moved to ci-privileged as
`CORE_TEAM_ISSUES_COMMENT_TOKEN`)
- Use `toJSON()` for all `client-payload` values to prevent JSON
injection

## Security model after this PR

| Workflow | Executes fork code? | Write tokens available? |
|----------|---------------------|------------------------|
| preview-env-keepalive | Yes | None (contents: read only) |
| preview-env-dispatch | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN
only |
| ci-breaking-changes | Yes | None (contents: read only) |
| post-ci-comments (workflow_run) | No (default branch) |
CI_PRIVILEGED_DISPATCH_TOKEN only |
| claude.yml | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN,
CLAUDE_CODE_OAUTH_TOKEN |
| ci-utils (Danger.js) | No (base branch) | GITHUB_TOKEN (scoped) |

All actual write tokens (`TWENTY_PR_COMMENT_TOKEN`,
`CORE_TEAM_ISSUES_COMMENT_TOKEN`) live in `twentyhq/ci-privileged` with
strict CODEOWNERS review and branch protection.

## Test plan

- [ ] Verify preview environment comments still appear on member PRs
- [ ] Verify adding `preview-app` label triggers preview for contributor
PRs
- [ ] Verify breaking changes reports still post on PRs (including fork
PRs)
- [ ] Verify Claude cross-repo responses still post on core-team-issues
- [ ] Confirm ci-privileged branch protection is enforced
2026-03-02 10:57:14 +01:00
Félix MalfaitandGitHub 8d47d8ae38 Fix E2E tests broken by redesigned navigation menu (#18315)
## Summary
- **Settings selector**: The Settings navigation item is now rendered as
a `<button>` (via `NavigationDrawerItem` with `onClick`) instead of an
`<a>` link (with `to`). Updated `leftMenu.ts` POM and
`create-kanban-view.spec.ts` to use `getByRole('button', { name:
'Settings' })`.
- **create-record URL field**: The Linkedin field interaction was
missing an initial label click to trigger the hover portal rendering.
Added `recordFieldList.getByText('Linkedin').first().click()` before the
value click, matching the pattern used by the working Emails field.

## Test plan
- [ ] E2E `signup_invite_email.spec.ts` passes (uses
`leftMenu.goToSettings()`)
- [ ] E2E `create-kanban-view.spec.ts` passes (uses Settings click
directly)
- [ ] E2E `create-record.spec.ts` passes (Linkedin URL field
interaction)
- [ ] Existing passing E2E tests remain green


Made with [Cursor](https://cursor.com)
2026-03-02 10:52:10 +01:00
Félix MalfaitandGitHub 68d2297338 Fix expression injection in cross-repo GitHub Actions workflow (#18316)
## Summary

- Fixes a script injection vulnerability in the `claude-cross-repo`
job's `actions/github-script` step where `${{ steps.prompt.outputs.repo
}}` and `${{ steps.prompt.outputs.issue_number }}` were interpolated
directly into JavaScript string literals. A crafted dispatch payload
could inject arbitrary JavaScript with access to
`secrets.TWENTY_DISPATCH_TOKEN`.
- Values are now passed via `env:` and accessed through `process.env`,
which treats them as data rather than code.

## Context

Motivated by the [hackerbot-claw
campaign](https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation)
which exploited similar `${{ }}` expression injection patterns in
workflows at Microsoft, DataDog, and CNCF projects.

The broader analysis found that our workflow is **not vulnerable** to
the primary attack vector (Pwn Request via `pull_request_target` +
untrusted checkout), and `claude-code-action` already gates on write
access internally. This expression injection in the cross-repo dispatch
job was the only concrete vulnerability identified.

## Test plan

- [ ] Verify the `claude-cross-repo` job still posts comments back to
the source issue after a dispatch run
- [ ] Confirm `TARGET_REPO` and `TARGET_ISSUE` env vars are correctly
resolved from step outputs


Made with [Cursor](https://cursor.com)
2026-03-02 09:20:03 +01:00
Charles BochetandGitHub 1db2a40961 Migrate twenty ui to linaria (#18307)
## Migrate twenty-ui from Emotion to Linaria

Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).

- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing

**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};

// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```

### Theme architecture

Two build-time utilities produce the theme system:

- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`

Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.

### Spacing cleanup

Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.

### Framer Motion integration

Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:

```tsx
const StyledBarBase = styled.div`
  background-color: ${themeCssVariables.font.color.primary};
  height: 100%;
`;

const StyledBar = motion.create(StyledBarBase);
```

### Block interpolations

Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:

```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
  const border = `1px solid ${theme.border.color.light}`;
  return divider ? `border-${divider}: ${border}` : '';
}}

// Linaria — one interpolation per property
border-left: ${({ divider }) =>
  divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
  divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```

### Dynamic styles via CSS variables

When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:

```tsx
const StyledButton = styled.button`
  background: var(--btn-bg);
  border-color: var(--btn-border-color);
  &:hover { background: var(--btn-hover-bg); }
`;

const dynamicStyles = useMemo(() => {
  const s = computeButtonDynamicStyles(variant, accent, ...);
  return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);

return <StyledButton style={dynamicStyles} />;
```

### CSS var + unit concatenation

CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:

```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;

// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
2026-03-01 15:13:42 +01:00
Charles BochetandGitHub 159bb9d70a A few fixes on table performance (#18304)
## RecordTable Performance Investigation & Optimization (WIP)

Investigates what makes the RecordTable slow (14 components, ~12 hooks
per cell) and starts applying fixes.

### Key Findings (2,000 cells benchmark)

![Cell Render
Benchmark](https://github.com/twentyhq/twenty/blob/table-perf/packages/twenty-front/src/modules/object-record/record-table/__perf__/cell-render-benchmark.png?raw=true)

![State Access
Benchmark](https://github.com/twentyhq/twenty/blob/table-perf/packages/twenty-front/src/modules/object-record/record-table/__perf__/state-access-benchmark.png?raw=true)

- **Jotai atoms are the dominant cost**: 10 atom reads/cell = +312%.
Full sim with atoms = +476%.
- **Derived atoms are 3x cheaper** than individual reads (12 sources:
+93% vs +294%).
- **Component depth is expensive**: 4-level nesting = +82%, 14 wrappers
= +109%.
- **Styling engines are comparable**: Linaria vs Emotion is within
noise.
- **Context reads and useState are nearly free** vs baseline.

### Optimizations Applied

1. **Static focus providers** — replaced per-cell `useState(false)` with
static context. Eliminates 400 useState instances.
2. **Delegated onMouseMove** — single handler on table body instead of
400 per-cell handlers.
3. **Hoisted `useObjectMetadataItems()`** — moved from per-cell to
table-level context. Eliminates 400 global atom reads.

### Tooling

- Perf page at `/__perf__/table`: 17 cell render + 13 state access
benchmarks
- Render profiler: `window.__RECORD_TABLE_PROFILE = true`
- Full plan in `__perf__/PERFORMANCE_PLAN.md`

### Remaining Phases (not high priority to-be-honest)

| Phase | What | Status |
|-------|------|--------|
| 1 | Separate display from interaction | Partial |
| 2 | Flatten hierarchy (14 → ~5 components/cell) | TODO |
| 3 | Reduce atom reads per cell | Partial |
| 4 | CSS-only hover/focus | TODO |
| 5 | Event delegation, lazy Draggable | TODO |
2026-02-28 15:12:44 +01:00
b341704a0e i18n - translations (#18306)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-28 14:24:11 +01:00
d5d0f5d994 i18n - translations (#18302)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-28 14:11:52 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
012d819557 OAuth Client — Unified ApplicationRegistration, OAuth server, and frontend (#18267)
## Summary

Consolidates three separate PRs (#18260, #18261, #18262) into a single
unified branch with all review feedback addressed:

### New features
- **ApplicationRegistration entity** — server-level registration for
OAuth apps with encrypted server variables
- **OAuth 2.0 server** — authorization code, client credentials, refresh
token grants with PKCE support
- **OAuth discovery endpoint** —
`.well-known/oauth-authorization-server` metadata
- **Frontend UI** — app registration details page with credential
management, redirect URI editing, and server variable configuration
- **CLI integration** — `twenty dev` auto-registers apps and stores
OAuth credentials locally
- **Authorize consent screen** — OAuth consent page at `/authorize`
showing requested scopes

### Review feedback addressed

**Renames (PR #18260):**
- `appRegistration` → `applicationRegistration` (entity, tables, files,
imports, GraphQL types)
- `appRegistrationVariable` → `applicationRegistrationVariable`
- `clientId` → `oAuthClientId`, `clientSecretHash` →
`oAuthClientSecretHash`, `redirectUris` → `oAuthRedirectUris`, `scopes`
→ `oAuthScopes`

**Security fixes (PR #18261):**
- Fixed redirect URI validation bypass when `oAuthRedirectUris` is an
empty array
- Fixed workspace isolation in `clientCredentialsGrant` — now uses
`find()` with explicit handling for multiple installations
- Added error logging in refresh token `catch` block instead of silently
swallowing

**Code quality (PR #18262):**
- Split `VersionDistributionEntry` into its own file (one export per
file)
- Split GraphQL queries and mutations into individual files with a
shared fragment
- Removed unused `OAuth` entry from `AuthProviderEnum`
- Added loading state to `handleRotateSecret`
- Removed 27 narration-style comments from test files
- Added proper guards (`PublicEndpointGuard`, `NoPermissionGuard`) to
controllers and resolvers

## Test plan

- [ ] Verify `twenty dev` registers an app and stores OAuth credentials
- [ ] Test OAuth authorization code flow end-to-end (authorize → token →
API call)
- [ ] Test client credentials grant
- [ ] Verify redirect URI validation rejects requests when no URIs are
registered
- [ ] Verify app registration detail page renders correctly
- [ ] Test secret rotation with loading state
- [ ] Verify server variable editing and saving
- [ ] Run `npx nx database:reset twenty-server` to validate migration

Closes #18260, #18261, #18262


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-28 14:07:49 +01:00