Commit Graph

995 Commits

Author SHA1 Message Date
Charles Bochet 7c0136b97b feat(deps): migrate frontend to React 19 (#21531)
## What

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

## Why

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

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

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

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

## Code changes

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

## Status / testing

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

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21531?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-14 15:42:22 +02:00
Thomas des Francs d06e687b77 Fix settings UI polish pass 2 (#21540)
## Summary

This PR groups the requested second UI polish pass across the app shell,
settings pages, data model, community, AI tools, and developer setup
surfaces.

### Shell and navigation polish
- Lets the app content card fill the available viewport and removes the
bottom-left radius on the main container.
- Uses an outside-looking container border treatment so the rounded
top-left edge matches the Figma frame without a double border.
- Restores the separator between the AI chat side panel and the record
container.
- Updates navigation drawer background/skeleton tones to use gray/3
consistently.
- Simplifies the full-screen backend error fallback so the white screen
fills the viewport.

### Settings page polish
- Moves the layout customization action from the settings navbar into a
settings card under the hero.
- Tightens layout page copy now that the customization surfaces are not
directly manageable yet.
- Aligns role detail title sizing and hover treatment with other
settings headers.
- Supports settings card brand icon colors, then restores Discord and X
brand logos on the Community page.
- Adds the Community discovery cover, moves community/social content to
the top, and reorganizes Partners and Features.
- Keeps setting card subtitle and separator behavior available for the
updated settings cards.

### Data model and object settings polish
- Adds a shared data-model table body wrapper so object, field, and
relation tables all keep the expected bottom border.
- Displays the object icon in the object settings navbar.
- Shares the new-field wizard parent object icon wrapper and keeps the
object icon at 64% opacity in wizard headers.
- Updates the new-field type selector icon treatment to match the wizard
header opacity behavior.

### Integrations, tools, and API polish
- Reworks the MCP setup config card with light syntax coloring and a
top-right copy icon button instead of the bottom copy section.
- Fixes email import icons to match the Figma asset shape.
- Ensures AI tool table icons and chevrons never fall back to black.

## Validation

- `npx nx lint:diff-with-main twenty-front`
- `npx tsc -p packages/twenty-front/tsconfig.json --noEmit`
- `git diff --check`
- Manual local visual pass on `apple.localhost:3001` for companies,
settings/community, data model, object settings, new-field wizard,
layout settings, account import, MCP setup, AI tools, and role detail
pages.

## Visual QA

### Shell and settings frame

<img width="1308" height="1397" alt="Shell and settings frame
before/after board"
src="https://raw.githubusercontent.com/twentyhq/twenty/pr-assets/ui-fixes-2/visual-qa/ui-fixes-2/shell-settings-frame.png"
/>

### Community and data model

<img width="1308" height="1806" alt="Community and data model
before/after board"
src="https://raw.githubusercontent.com/twentyhq/twenty/pr-assets/ui-fixes-2/visual-qa/ui-fixes-2/community-data-model.png"
/>

### Integrations and tools

<img width="1308" height="1397" alt="Integrations and tools before/after
board"
src="https://raw.githubusercontent.com/twentyhq/twenty/pr-assets/ui-fixes-2/visual-qa/ui-fixes-2/integrations-tools.png"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21540?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-14 06:15:49 +02:00
neo773 5d892bdfd0 [WIP] Feat/marketing emails (#21173)
Marketing/campaign emails on top of the emailing-domain (SES) feature:
send a broadcast to a hand-picked list, with per-customer-domain
unsubscribe links and opt-out-only **unsubscribe topics**.

## Model

Standard objects (workspace schema, flat-metadata):
- `messageCampaign` — a campaign send (subject, body template, from
address, status, list, optional unsubscribe topic).
- `messageList` + `messageListMember` — the hand-picked audience (person
↔ list join). A campaign's recipients are its list's members; everyone
is sendable unless suppressed.

Core entities (`core` schema, workspace-scoped — readable by the public
unsubscribe flow without a workspace context):
- `unsubscribeTopic` — an opt-out-only category (name, description,
visibility). There is no opt-in subscription state.
- `messageSuppression` — the single consent store: a row with
`unsubscribeTopicId` NULL is a global block; a row with an
`unsubscribeTopicId` and reason `UNSUBSCRIBE` is a per-topic opt-out.
Two partial unique indexes dedupe global vs per-topic rows (Postgres
treats NULLs as distinct).
- `emailingDomain` — the workspace's SES sending domain,
auto-provisioned when an email channel is added (and cleaned up when its
last channel is removed), with verification status + DNS records.

Campaign messages reuse the existing `message` / `messageThread` /
`messageParticipant` model — one outbound `message` per recipient with a
`deliveryStatus` state machine.

## Sending

- `sendMessageCampaign` resolves the audience **under the caller's
permissions**, creates the campaign, and enqueues a single fan-out job
(the request never materializes per-recipient rows or jobs).
- The fan-out job materializes one QUEUED message per recipient
(deterministic ids → idempotent re-runs, reconciles crash-orphaned rows)
and fans out per-recipient send jobs carrying **only ids**.
- Each send job renders per-recipient `{{variable}}` merge fields and
sends via `EmailingDomainSenderService`, which applies suppression
(global + per-topic) and the unsubscribe footer/headers. Suppressed
recipients are recorded `SKIPPED`.
- The campaign finalizes `SENT`, or `SENT_WITH_ERRORS` if any recipient
terminally failed.
- `previewMessageCampaignAudience` returns a pre-send breakdown (total /
without-email / duplicate / globally-unsubscribed / topic-unsubscribed /
sendable), shown as a hint under the composer pickers.

## Unsubscribe

- Encrypted (AES-256-GCM) token carrying workspaceId, address, optional
`unsubscribeTopicId`, `issuedAt`, and a `preview` flag.
- One-click POST (RFC 8058) + `mailto:` — topic-scoped when the token
carries a topic, global otherwise.
- Preferences page: a checkbox per visible topic (checked = still
receiving); submitting creates per-topic opt-outs for unchecked topics
and lifts re-checked ones (UNSUBSCRIBE only — never
`BOUNCE`/`COMPLAINT`, never a global block).
- A **Preview** action in settings opens the live page via a
preview-claim token; opt-out POSTs are no-ops for preview tokens, so
previewing never mutates state.
- SES webhooks: inbound unsubscribe + outbound bounce/complaint →
suppression (race-safe against at-least-once delivery, with reason
escalation that never downgrades).
- Per-customer unsubscribe hostname (Cloudflare DNS); sends are gated on
it being active, except in LOG/demo mode.

## Architecture

Campaign orchestration, suppression, the sender, the unsubscribe
controller, and the SES webhook handlers live in `src/modules/emailing`
+ `src/modules/messaging-webhooks` (the workspace-feature layer).
`core-modules/emailing-domain` keeps the SES driver, domain
provisioning, the `unsubscribeTopic` / `messageSuppression` core
entities, and the unsubscribe token/hostname plumbing. Domain creation
is validated (`CreateEmailingDomainInput` — domain-format regex,
lowercased) before any value reaches SES or the unsubscribe hostname.

## Frontend

- Campaign composer side panel (from / list / unsubscribe topic /
subject / body) with a live audience-preview hint.
- Email settings: email channels each showing their auto-provisioned
sending domain in a single section (status + DNS records + a "Check
verification" action), plus an **Unsubscribe Topics** section to
create/manage topics and preview the recipient page. A demo-mode banner
is shown when the LOG driver is active.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-06-13 18:37:39 +02:00
Félix Malfait 76f69efb43 Keep synced messages and events when removing a workspace member (#21443)
## Context

Removing a workspace member deletes their connected accounts, which
cascades into deleting every message and calendar event those accounts
synced. For a CRM, losing the email history of departed teammates is a
big deal.

## What this does

Connected accounts are now kept and reassigned instead of deleted when a
member is removed:

- Ownership moves to the acting user (whoever removed the member). When
members remove themselves (leave workspace, account deletion), it falls
back to the oldest admin.
- OAuth tokens are revoked, credentials wiped, message/calendar channels
get `isSyncEnabled = false`, and the account is stamped with a new
`archivedAt` column (fast instance command included).
- Synced messages, threads and calendar events stay in the workspace.
Channel visibility settings keep applying as before, since channels and
associations survive.
- The reassigned account appears in the new owner's Settings → Accounts,
where it can still be deleted (with its data) like any other account.

The transfer happens synchronously during removal, while the member's
userWorkspace row still exists. This also removes
`DeleteWorkspaceMemberConnectedAccountsCleanupJob` and its listener: the
async job had to reconstruct the account-owner link from rows the
removal flow had just deleted, which was race-prone (see 2181fb541e).

Archived accounts are excluded from the workflow send-email default
account resolution, and both removal confirmation modals now mention
what happens to synced data.

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-06-13 15:52:13 +02:00
Thomas des Francs b14195428a Fix navigation and settings UI polish (#21523)
## Summary

This PR groups the requested UI polish pass across navigation, settings,
AI settings, data model, community/lab, and dashboard chips.

### Navigation and drawer polish
- Smooths app/settings route switching with a 300ms content transition.
- Smooths app menu/settings menu drawer swaps with a fade transition
while preserving the existing drawer dimensions.
- Keeps navigation and AI chat history panes mounted to avoid flicker
when switching tabs.
- Aligns the first app/settings section with the AI chat history section
title.
- Removes the main app navigation "Other" section.
- Aligns the settings exit header with the workspace switcher header.
- Sets the settings exit icon gap to 8px, uses the small icon stroke
token, and keeps the icon color on text-secondary.
- Makes the workspace switcher button 28px high inside its 32px
container, moves the dropdown up so the workspace name does not jump,
and keeps a 2px gap between header icon buttons.
- Moves Settings below Support in the workspace switcher menu.
- Pins the Advanced toggle to the bottom of the settings drawer and
aligns its right padding with nav items.

### Settings surface polish
- Fixes vertically cropped dropdown menu headers.
- Makes wizard parent titles tertiary when a nested wizard title is
visible.
- Places danger-zone buttons side by side.
- Uses a 32px Visualize button.
- Restores 8px top/bottom padding on settings tables.
- Separates AI overview counts into three columns like layout overview.
- Adds separators inside setting cards, including the Smart Model / Fast
Model card.
- Groups lab/early-access toggles into one card with separators and
full-width row backgrounds.
- Replaces deprecated Enterprise adornment tags with the Organization
adornment on gated AI/security/settings surfaces.
- Uses the Discord brand icon in Community while keeping the standard
icon component rendering pattern.
- Tightens Skills/Tools search-to-table spacing so switching tabs does
not shake the table top.
- Fixes the small gap in nested navigation breadcrumbs between Emails
and Calendars.

### Dashboard/data table polish
- Fixes vertically cropped "Not shared" chips on dashboards.
- Keeps table row/header spacing stable after the settings table padding
restoration.

## Validation

- `npx nx lint:diff-with-main twenty-front`
- `npx tsc -p packages/twenty-front/tsconfig.json --noEmit`
- Manual Chrome pass on `apple.localhost:3001` for profile, app
navigation, workspace switcher, AI overview/models/skills/tools/usage,
community/lab, data model, new-field wizard, and dashboards.

## Visual QA

### Settings surface fixes

<img width="1324" height="2668" alt="Settings surface fixes before/after
board"
src="https://github.com/user-attachments/assets/312131fa-3922-4724-9460-46fd373754d1"
/>

### Navigation drawer fixes

<img width="1324" height="1816" alt="Navigation drawer fixes
before/after board"
src="https://github.com/user-attachments/assets/ca0b29e5-e393-46c0-81f7-e6bed2557b59"
/>

### Tables, wizards, gates and chips

<img width="1324" height="2242" alt="Tables, wizards, gates and chips
before/after board"
src="https://github.com/user-attachments/assets/5f358240-c31c-4c15-8ca8-04ed761fbf85"
/>
2026-06-13 13:13:55 +02:00
Félix Malfait 1efa3567ef Rename isUIReadOnly to isUIEditable, add isUICreatable, expose both to app developers (#21504)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# UI capability flags: `isUIEditable` + `isUICreatable`

## Per-verb capability model

This PR replaces the negative `isUIReadOnly` metadata flag with
positive, per-verb capability flags (à la Salesforce
`createable`/`updateable`):

- **`isUIEditable: boolean`, default `true`** — rename of `isUIReadOnly`
with inverted polarity, on **both** `objectMetadata` and
`fieldMetadata`. It is one concept ("can the user edit this through the
generic UI?") at two altitudes, so it carries one name at both levels.
- **`isUICreatable: boolean`, default `true`** — new, **object-level
only** (fields have no create verb). When `false`, no generic UI
affordance to create a record of this object appears anywhere (table "+"
buttons, board column add, calendar add, relation-section "Add new",
record picker "Add new", command-menu create action and its keyboard
shortcut).

Both flags are **UI-affordance flags only**: the server does not block
create/edit mutations based on them, so the system, API, and workflows
continue to mutate these records freely. They are orthogonal statements
about the object's nature with no implication rule in the data model.
Because today's inline creation UX creates a blank record the user must
then edit, the frontend create predicate currently requires both
`isUICreatable` and effective editability.

There is no CREATE permission in `ObjectPermissions`; the frontend keeps
gating creation on `canUpdateObjectRecords` as a proxy, ANDed with the
new flags.

## Unified create predicate

All generic creation entry points now flow through one predicate,
`canCreateRecordsForObjectMetadataItem` (`isUICreatable` && not
`isSystem` && not effectively read-only, where effective read-only
covers `isUIEditable`, `isRemote`, and the `canUpdateObjectRecords`
proxy via `isObjectMetadataReadOnly`). This deletes the previously
hardcoded suppression lists:

- `isRecordTableCreateDisabled.ts` and its hardcoded
`WorkflowRun`/`WorkflowVersion` list — deleted; those objects (plus
`workspaceMember`) now declare `isUICreatable: false` in the standard
application instead.
- The hardcoded `workspaceMember` guard inside
`useAddNewRecordAndOpenSidePanel.ts` — deleted.
- The `CREATE_NEW_RECORD` command menu item's availability expression
now checks `objectMetadataItem.isUICreatable`, `isUIEditable`,
`isSystem`, and `isRemote`; a workspace upgrade command re-syncs the
expression in existing workspaces.

Component-local conditions (soft-delete filter active, layout
customization mode) stay in their components.

## GraphQL compatibility and removal plan

The schema delta versus main is **purely additive plus deprecations —
zero breaking changes**:

- `isUIReadOnly` remains on both the ObjectMetadata and FieldMetadata
GraphQL output types for **one release** as a deprecated field computed
as `!isUIEditable` (`deprecationReason: 'Use isUIEditable'`). The Twenty
frontend no longer queries it.
- `isUIReadOnly` also remains on the **input side** for one release
(`CreateFieldInput`, `UpdateFieldInput`, `FieldFilter`, `ObjectFilter`),
keeping the schema shape identical to main for those members. On create
it acts as a legacy alias mapped to `!isUIReadOnly` (`isUIEditable` wins
when both are provided); on update it is ignored, exactly as on main (it
was never an editable property). Filtering on the deprecated member
keeps working until the column is dropped at upgrade time; after that it
is a deprecated no-op surface kept only for schema compatibility.

**Removal plan for next release: drop `isUIReadOnly` from the output
DTOs (and resolvers' `@ResolveField`s), from the input/filter types,
from the create-input mapping, and the `@WasRemovedInUpgrade`-retained
entity columns and decorators.**

## ⚠️ Webhook / database-event payload shape change

The `database-event-payload` type in `twenty-shared` got a clean rename
(no alias): metadata snapshots in webhook and database-event payloads
now carry `isUIEditable` (and `isUICreatable` at object level) **instead
of** `isUIReadOnly`, with inverted polarity. Consumers of these payloads
that read `isUIReadOnly` must switch to `isUIEditable`.

## New manifest properties (app-developer DX)

Application developers can now set these flags in their app manifests
(purely additive — existing manifests and older `twenty-sdk` versions
are unaffected, defaults apply when omitted):

- `objects[].isUICreatable?: boolean` (default `true`)
- `objects[].isUIEditable?: boolean` (default `true`)
- `fields[].isUIEditable?: boolean` (default `true`)

The manifest converters previously hardcoded `isUIReadOnly: false`; they
now read the manifest values with `?? true` defaults. The types are
re-exported through `twenty-sdk` from `twenty-shared`.

## Migration & backfill

- One fast instance command: adds `isUIEditable` (NOT NULL default
`true`) on `core."objectMetadata"` and `core."fieldMetadata"`, backfills
`isUIEditable = false` exactly where `isUIReadOnly = true`, drops
`isUIReadOnly`, and adds `isUICreatable` (default `true`) on
`objectMetadata`. The `down` is the exact inverse. Uses `ADD/DROP COLUMN
IF (NOT) EXISTS`, matching the 2-12 drop-`isCustom` precedent. Verified
up and down in separate transactions against a dev database with exact
backfill counts.
- **Cross-version upgrade safety (multi-version self-hosted jumps):**
the upgrade sequence interleaves per version (instance → workspace
commands), so pre-2.13 workspace commands run **before** the 2.13 rename
when an old instance jumps several versions. Following the `isCustom`
precedent: `isUIEditable`/`isUICreatable` are marked
`@WasIntroducedInUpgrade` and `isUIReadOnly` stays on both entities as
`@WasRemovedInUpgrade`, so the upgrade-aware entity metadata adapter
hides the not-yet-existing columns (and keeps the legacy column live) at
pre-2.13 cursors. **No committed upgrade command outside the 2-13
directory is modified**: the old 1-21/2-8/2-9 commands keep their
original `isUIReadOnly: true` inputs, which still compile (entity
property retained, deprecated create-input alias mapped) and still
produce the correct legacy column writes pre-rename.
- A 2-13 workspace command (`sync-standard-ui-capability-flags`)
re-syncs `isUICreatable` **and** `isUIEditable` on standard objects and
`isUIEditable` on standard fields from the standard-application
definitions. This backfills `isUICreatable: false` on
`workflowRun`/`workflowVersion`/`workspaceMember` and heals fields
created mid-cross-upgrade by pre-2.13 commands (whose hidden
`isUIEditable` value cannot reach the insert). Both 2-13 sync commands
pass `isSystemBuild: true` — the flat metadata validator otherwise
rejects direct updates to system objects (verified against a
deliberately drifted dev database; the run is idempotent).
- A second 2-13 workspace command re-syncs the create-record command
availability expression.

## Testing

- Unit tests for `canCreateRecordsForObjectMetadataItem`
(flag/permission/system combinations) and for the manifest converters
(flags set / omitted → defaults).
- Full `upgrade --dry-run` boots the sequence (107 steps) and validates
the upgrade-aware decorator references; both 2-13 sync commands verified
end to end against real drift and re-run idempotently.
- Schema verified by live introspection after the input-alias restore:
all four input/filter members match main, output deprecations intact;
frontend metadata types and `twenty-client-sdk` schema regenerated from
the running server.
- Read-only-related and touched jest suites pass on both packages;
typecheck and lint pass on `twenty-server` and `twenty-front`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>



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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-13 07:10:22 +02:00
martmull 84a8504473 Add Enter HotKey to aAuth authroize screen (#21512)
as title

<img width="1263" height="834" alt="image"
src="https://github.com/user-attachments/assets/73dbfca2-8c7f-469d-8273-6adec5472abd"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21512?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-12 19:10:38 +00:00
Raphaël Bosi c4453923f0 Update CI: Argos visual regression for twenty-front storybook (#21454)
## What

Adds Argos visual regression for `twenty-front`, reusing the storybook
CI already builds and the existing sharded test matrix. Stories in the
`modules` and `pages` scopes are captured as PNGs during
`front-sb-test`, merged into one artifact, and pixel-diffed against
`main` on the self-hosted Argos with results posted as a PR comment —
same pipeline as `twenty-ui` (#21210 / #21262).

## How

- **Capture**: `@argos-ci/storybook` vitest plugin, same setup as
`twenty-ui`. Skipped for `performance` stories (nondeterministic
profiling reports). Freezes framer-motion to avoid flaky diffs (#21412).
- **Sharding**: each modules/pages shard uploads a partial artifact; a
new `front-sb-screenshots` job merges them into
`argos-screenshots-twenty-front` (`overwrite: true` so re-runs work).
- **Baselines**: `CI Front` now runs on `push: main` — Argos resolves
base builds by exact merge-base commit, so every main commit needs a
build (#21217/#21222 pattern). Main pushes get a per-SHA concurrency
group so back-to-back merges can't cancel queued runs and leave baseline
gaps; the `performance` scope is dropped on push.
- **Dispatch**: `visual-regression-dispatch.yaml` watches `CI Front` →
`project=twenty-front`.

## Rollout

-  Prod Argos project `twenty-front` created (id 68) +
`ARGOS_TOKEN_FRONT` secret set
-  Merge the twentyhq/ci-privileged companion PR **before** this one
- First PR builds show as *orphan* until the first main push creates a
baseline
  (expected, same as the twenty-ui rollout)
2026-06-12 13:36:16 +00:00
Charles Bochet 184c4948d6 security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context

AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.

## Changes

### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).

**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.

### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:

- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).

**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.

## typeorm status (pulled out)

typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.

## Verification

- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
2026-06-11 15:11:29 +02:00
Félix Malfait adba66caea fix(twenty-front): new layout fast-follows — settings drawer, loading & command menu (#21389)
Second batch of new-layout fast-follows (master:
twentyhq/core-team-issues#2478). All changes verified live against a
running workspace.

## Settings drawer & header
- **twentyhq/core-team-issues#2489** — sidebar icons render as plain
16px icons, no background tiles.
- **twentyhq/core-team-issues#2488** — Advanced toggle spans the full
drawer width; yellow dot removed.
- **twentyhq/core-team-issues#2497** — page title stays centered in the
settings header (breadcrumb stays left).
- **twentyhq/core-team-issues#2490** — Exit Settings control aligned to
the workspace switcher (24px, matching padding/gap).
- **twentyhq/core-team-issues#2499** — 2px vertical gap restored between
collapsible drawer section items.
- **twentyhq/core-team-issues#2491** — settings drawer rhythm now
matches the main app (28px items, 2px gaps, 28px section headers).
- **twentyhq/core-team-issues#2492** — Home/Chat tab switch no longer
flickers: both tab subtrees stay mounted (a shared
`NavigationDrawerTabbedContent` toggles visibility instead of remounting
+ flashing the chat skeleton).

## Loading states
- **twentyhq/core-team-issues#2486** — metadata loading shows an empty
body (no dense skeleton rows).
- **twentyhq/core-team-issues#2487** — settings table keeps its layout
while loading, with the shimmer localized to the first row's first cell.

## Command menu & navigation
- **twentyhq/core-team-issues#2501** — navigation section header height
matches the nav item rhythm (28px).
- **twentyhq/core-team-issues#2502 (part 1)** — the page side-panel
toggle stays as the dots glyph while the command menu is open, instead
of morphing into a second close control.

## New-field flow
- **twentyhq/core-team-issues#2494** — the new-field stepper moved from
a breadcrumb dropdown into a centered secondary wizard bar (back chevron
+ Save on the configure step); breadcrumb stays clean and the object
label is the centered title.

## Descoped (substantive bugs already fixed)
- **twentyhq/core-team-issues#2500** — command-menu highlight right
gutter: the menu-item base measures full-width, so it's likely a
scrollbar gutter on the list, not the shared component. Left for a
focused follow-up.
- **twentyhq/core-team-issues#2502 part 2** — moving the command-menu
close from left to right is cosmetic (the duplicate-control bug is fixed
by part 1) and would touch the shared `SidePanelTopBar` used by
search/AI panels.

## Verification
typecheck (tsgo) + oxlint + oxfmt green for all changed files; each
change DOM-measured / screenshotted in the running app.
2026-06-10 11:02:36 +02:00
Félix Malfait ce2d77be2a feat(server): in-app server-level admin management (#19785) (#21321)
## Closes #19785

In-app management of **server-level admin rights**
(`canAccessFullAdminPanel`, `canImpersonate`) so self-hosters no longer
need raw SQL + a Redis flush + restart to grant access.

> **Draft** — feature complete; `/code-review` + `/security-review` run
and addressed.

### Background
`AdminPanelGuard` / `ServerLevelImpersonateGuard` read
`request.user.{canAccessFullAdminPanel,canImpersonate}`, hydrated each
request from `CoreEntityCacheService.get('user', …)` (local 30-min +
Redis no-TTL). The cache was only invalidated on soft-delete, so a raw
`UPDATE core."user"` never took effect. The **first** signup auto-gets
both flags; every subsequent admin previously needed raw SQL.

### UX
- **Admin Panel → General → Administrators**: a read-only overview of
every user with server-level access; each row links to that user's admin
page.
- **Find anyone** via the user search (Recent Users) — available to full
admins and impersonators — then open their **admin user page**.
- On the user page, an **"Administrator access"** card (gated on
`canAccessFullAdminPanel`) has two toggles — *Full admin panel access*
and *Impersonation* — that work for **any** user (a user with no access
shows both off). Mirrors how **Impersonate** already works (find user →
user page → act). Each change opens a confirm dialog with a **2FA code**
field; the last full admin's toggle is disabled.

### Backend / security
- **Cache fix** — invalidate the user entity cache on committed user
updates (not just soft-delete) so privilege changes propagate (~100 ms,
cluster-wide) with no restart.
- `getServerAdmins` query + `updateServerAdminAccess` mutation (any
`targetUserId`), gated on `canAccessFullAdminPanel`.
- `NoImpersonationGuard` on both — an impersonated full-admin session
can't be used to escalate an impersonator.
- Fresh **2FA TOTP step-up** (enrolled+verified method **and** a fresh
code; genuine 2FA errors surface; dev-skip on trusted `NODE_ENV`).
- **Last-admin lockout** in a transaction with a pessimistic row lock
(no TOCTOU).
- **Email-to-all-admins + affected user** (rendered once per locale),
structured log, audit event-log emit.
- **Authorization**: the read-only `userLookupAdminPanel` +
`adminPanelRecentUsers` lookups now accept `canAccessFullAdminPanel OR
canImpersonate` (new `AdminPanelOrImpersonateGuard`), so a full admin
without impersonate can still find users to manage.
Workspace/impersonation queries stay impersonate-gated.

### Reviews
- `/code-review` (max effort): 3 security findings
(impersonation-escalation sink, lockout TOCTOU, step-up accepting
PENDING 2FA) — **all fixed**. `/simplify`: applied. `/security-review`:
**no high/medium vulnerabilities**.

### Follow-ups (not in this PR)
- Unit tests for `AdminPanelServerAdminService` + a frontend test.
- Point the self-host troubleshooting docs at the new UI.
- OTP retry UX: `ConfirmationModal` closes on confirm, so a wrong code
needs a reopen (kept to reuse the existing modal; no new pattern).

### Notes for reviewers
- `generated-admin/graphql.ts` entries were hand-added to match codegen
output (admin codegen needs a running server); re-run `nx
graphql:generate twenty-front --configuration=admin` to confirm parity.
- First-admin bootstrap (first signup) is unchanged.

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-06-10 06:50:25 +02:00
Félix Malfait bfefcd3755 feat(twenty-front): generalize the page primary/secondary bars (flat redesign) (#21308)
Replaces #21279 and #21282 with one clean PR from `main`.

Generalizes the settings primary-bar / secondary-bar card chrome to the
record index, record show and standalone pages via a shared
`PageCardLayout` + `PageCardHeader` (the side panel sits as a sibling of
the content card), and applies the new flat design direction: square
corners on the card, side panel and loading skeletons.

Iterating toward the new design (Figma node 102282-221623); the
confirmed direction and the explicit "remove rounded corners" change are
in, remaining designer specifics to follow.
2026-06-08 22:47:05 +02:00
Raphaël Bosi c596a5e342 Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)
## Description

Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.

## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
2026-06-08 18:12:28 +02:00
Félix Malfait dfb3da1f8d feat(emailing-domain): add LOG driver for local development (#21286)
## What

Adds a `LOG` driver to the emailing-domain feature, selected via a new
`EMAILING_DOMAIN_DRIVER` config variable (defaults to `AWS_SES`, so
production behavior is unchanged).

The LOG driver:
- resolves domains to `VERIFIED` instantly (no DNS / SES setup)
- logs each `sendEmail` and returns a synthetic `messageId` instead of
calling SES

It also dev-seeds a pre-verified domain per workspace
(`<workspaceId>.dev.twenty.local`) so the feature works out of the box.

## Why

The emailing-domain feature currently ships only the AWS SES driver, so
the verify → send flow can't be exercised locally (or in CI) without
real AWS credentials. This unblocks local development and review of
anything built on emailing domains.

## Usage

```
EMAILING_DOMAIN_DRIVER=LOG
```

The seeded `*.dev.twenty.local` domain is already verified; sends are
logged (`[log-driver] sendEmail ...`).

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-06-08 14:59:11 +02:00
Félix Malfait a2fa941ce3 chore(twenty-front): remove dead SettingsAiMCP component and covers (#21281)
## What

`SettingsAiMCP` is dead code — nothing imports or renders it. The
redesign moved MCP setup to the **APIs & Webhooks** page
(`SettingsMcpSetup`, on the MCP tab), and the AI settings page now
**deep-links** there (`ApiWebhooks#mcp`) instead of rendering this
component.

Removes the orphaned component and its two cover SVGs:
- `src/pages/settings/ai/components/SettingsAiMCP.tsx`
- `public/images/ai/ai-mcp-cover-{light,dark}.svg`

## Notes

- Verified nothing references the component or the SVGs in source (only
stale `.po` source-reference comments remain, which `lingui` extraction
reconciles separately — not hand-edited here).
- The hero on the APIs & Webhooks page (incl. its MCP tab) is
unaffected; that's the `playground/cover` image.
2026-06-06 12:23:42 +02:00
Félix Malfait 4658d44d8b fix(settings): ship borderless hero cover images (#21277)
## What

The settings discovery hero images (AI, Applications, Page Layouts,
Members, Data Model, APIs & Webhooks) baked the rounded border into the
pixels — transparent rounded corners plus a 1px edge stroke. Rendered
inside `Card rounded` — which already draws a 1px border + border-radius
and clips children with `overflow: hidden` — this produced a doubled,
slightly misaligned border.

This replaces all 12 files (light + dark per section) with clean
full-bleed exports (opaque square corners), so the border and rounding
come entirely from CSS.

## Notes

- Pure asset swap, no component changes.
- The MCP section's `.svg` cover is untouched (no new export provided).
Billing's unused cover is left as-is.

## Verification

- Each new image confirmed 1388×300, opaque square corners (no baked
border), correct light/dark variant.
- `Card` (twenty-ui) provides `border` + `border-radius` + `overflow:
hidden`, so the square images are clipped to the rounded card.
2026-06-06 11:21:06 +02:00
Félix Malfait 91f2f08995 feat(server): unify workspace-event ingestion behind one EventSink pipeline (#21197)
## Why

The five event-log streams (`workspaceEvent`, `pageview`, `objectEvent`,
`usageEvent`, `applicationLog`) each wrote to ClickHouse through their
own fire-and-forget writer (`AuditService`, `UsageEventWriterService`,
and the `application-logs` driver), with the per-type knowledge (table
names, normalization, access rules) spread across several modules. Three
of them reimplemented the same ClickHouse insert, and the read side, the
live stream, and the producers lived in different modules under two
different names.

This consolidates them into one `core-modules/event-logs/` subsystem
(emit, write, live, read), with the per-type config in a single registry
so adding an event type is roughly one file.

The base Logs settings tab and free application logs shipped separately
in #21180 (merged). This PR adds the unified backend, the registry, and
the viewer's live mode and entitlement gating.

## Pipeline

```mermaid
flowchart TB
    subgraph PROD["Producers"]
      A["auth, billing, impersonation,<br/>webhook, custom-domain"]
      U["usage listener"]
      F["logic-function executor (app logs)"]
      R["record CRUD (entity events)"]
    end
    EM["EventLogEmitterService<br/>createContext().insert* / dispatch()"]
    EQ(["entityEventsToDbQueue<br/>(existing, shared with timeline)"])
    CIE["CreateEventLogFromInternalEvent"]
    SINK["WorkspaceEventSinkService.ingest()"]
    C1["ClickHouseEventSink"]
    C2["ConsoleEventSink"]
    LIVE["EventLogLiveService.publishWatched()<br/>(presence-gated)"]
    CH[("ClickHouse, 5 tables, async_insert")]
    CHAN(["WORKSPACE_EVENTS_CHANNEL"])
    RS["EventLogsService (registry-driven read)"]
    LR["EventLogsLiveResolver"]
    UI["Settings > Logs"]

    A --> EM
    U --> EM
    F --> EM
    EM -->|direct| SINK
    R --> EQ --> CIE -->|ingest| SINK
    SINK --> C1 --> CH
    SINK --> C2
    SINK --> LIVE -.->|if a viewer is watching| CHAN --> LR --> UI
    CH --> RS --> UI
```

## What it does

- Producers call `EventLogEmitterService.createContext().insert*()`,
which builds a typed `WorkspaceEventEnvelope` and writes it through
`WorkspaceEventSinkService` to the configured sinks (ClickHouse,
Console) plus a presence-gated live fan-out. Record/CRUD events reach
the same sink through the existing `entityEventsToDbQueue`. There is no
dedicated queue; ClickHouse `async_insert` batches server-side. Writes
are best-effort, as on main today.
- `EVENT_LOG_TYPES[table]` is the per-type source of truth: the
ClickHouse table, the required entitlement, the free-text filter column,
and the row-to-GraphQL mapping. Read row shapes derive from the write
rows.
- Four modules along their dependency boundaries:
`EventLogEmitterModule` (producer API), `EventLogIngestionModule` (sink
layer), `EventLogLiveModule` (fan-out), and `EventLogsViewerModule` (the
entitlement-gated GraphQL read, which is where
billing/enterprise/permissions stay so producers stay light).
- Logs viewer: per-table columns, filters (text, date, record), live
mode, and an upgrade card that points to Billing on Cloud or the Admin
Panel on self-hosted. Application logs are free on every plan; the other
four require the `AUDIT_LOGS` entitlement (with a `NO_ENTITLEMENT`
fallback to the upgrade card).
- Renames `AuditService` to `EventLogEmitterService`, and the generic
`Monitoring` event to a typed `Impersonation` event (`level` +
`action`).
- Removes `UsageEventWriterService`, the `application-logs`
driver/module, and `AuditService`'s direct inserts.

## Durability

Writes are best-effort, the same as main today (the old writers were
fire-and-forget). A dedicated queue was tried mid-PR and removed:
`async_insert` already batches server-side, so the queue only added
durability, which isn't a requirement right now. The `EventSink` seam
keeps a durable transport (e.g. a Redis-Streams buffer) easy to add
later without touching producers.

## Out of scope

S3 peer sink (seam only), Postgres or any second read path,
`ReplicatedMergeTree`, ClickHouse table-schema changes, and the
record-data `EVENT_STREAM_CHANNEL` (unchanged, separate concern).

## Testing

Unit tests cover the registry definitions and row normalization, the
entitlement gating, the envelope builders, and the producers.
Integration tests cover the write paths (record create produces an
`objectEvent`; the track mutation produces a `workspaceEvent`) and the
read/query path across all five tables. Verified with typecheck, lint, a
server boot, and GraphQL/SDK codegen.
2026-06-06 10:32:56 +02:00
Félix Malfait c3dd6b25a6 fix: use canonical oxlint rule id in lint-disable directives (#21253)
## What

Many `oxlint-disable` / `eslint-disable` directives across the repo
carry a corrupted rule id — `@typescripttypescript/<rule>` — most likely
a find-and-replace accident that mangled the eslint-era
`@typescript-eslint/` prefix.

oxlint matches disable directives **loosely by rule name**, so these
still suppress in practice (not a silent no-op), but the id is malformed
and misleading.

## Change

Replace them with the **canonical oxlint id** `typescript/<rule>` —
matching the plugin name and rule keys declared in `.oxlintrc.json` —
**127 files, 262 directives**:

| rule | count |
| --- | ----- |
| `typescript/no-explicit-any` | 250 |
| `typescript/ban-ts-comment` | 6 |
| `typescript/no-misused-promises` | 4 |
| `typescript/no-empty-object-type` | 2 |

- `twenty-server`: 122 files
- `twenty-front`: 5 files

Comment-only — no code or runtime changes.

## Verification

`oxlint --type-aware -c .oxlintrc.json` reports **0 warnings / 0
errors** for both `twenty-server` and `twenty-front`. Every changed line
is exactly the id correction inside a disable directive (262 insertions
/ 262 deletions, no collateral edits).

> Addresses the cubic review, which flagged that the canonical oxlint id
is `typescript/...` (no `@`). Worth noting the original
`@typescripttypescript/` was not actually a silent no-op — oxlint
matches these directives loosely by rule name — but `typescript/` is the
correct, config-aligned id.
2026-06-05 13:52:32 +02:00
Félix Malfait a3a44c8315 fix(front): settings skeleton, app-detail header & empty favorites (#21209)
Three small post-redesign UI fixes. Each is an independent commit, so
they can be split into separate PRs if preferred.

## 1. Settings loading skeleton — match the rounded-card layout

The redesign (#21131) moved settings chrome into a rounded card
(`SettingsPageLayout`: bordered header with breadcrumb + centered title,
optional secondary bar, 760px body), but `SettingsSkeletonLoader` still
rendered the old flat `PageHeader` + `PageBody` — so pages painted as a
full-width flat bar then snapped into the card.

- `SettingsSkeletonLoader` now reproduces the card and **reuses the real
`SettingsPageHeader` + `SettingsPageContainer`**, so the frame aligns by
construction; the card CSS is replicated (not `SettingsPageLayout`) to
avoid the layout's side effects (hotkeys, side panel, info banner).
- It's **composed with `SettingsSectionSkeletonLoader`** so the loading
body is identical whether or not chrome is present. Rule: no chrome on
screen yet → full-page skeleton; chrome already on screen → body-only
`SettingsSectionSkeletonLoader` (the admin Enterprise tab now uses it,
matching its sibling tabs). A short comment on each component documents
this.

## 2. Application detail header — pass a plain title

`SettingsApplicationDetails` / `SettingsAvailableApplicationDetails`
passed a custom `SettingsApplicationDetailTitle` (avatar + name +
multi-line description, fixed width) into `SettingsPageLayout`'s
**centered single-line title slot**, which broke the header. They now
pass the app's display name like every other page. The available-app
"unlisted" notice moves into the body as a reusable `InlineBanner`; the
now-unused `SettingsApplicationDetailTitle` is removed.

## 3. Navigation — hide Favorites when empty

Always rendering the Favorites section (#21087) left a stray "Favorites"
title above Workspace for users with no favorites. It now renders only
when at least one favorite exists (redundant per-child guards dropped).
Note: the "+ add favorite" entry point therefore appears once you have
≥1 favorite; the first favorite is created from a record/view as before.

## Verification
- `nx typecheck twenty-front`  · `oxlint` + `oxfmt --check` on changed
files 
- i18n catalogs intentionally untouched — handled by the repo's separate
i18n pipeline.
2026-06-04 09:32:36 +02:00
Félix Malfait 2ac515894b feat(settings): add Logs as a dedicated tab in General settings (#21180)
## What & why

The audit-log viewer lived as a full-screen page reachable only via a
"View Logs" button buried in the **Security** tab. This surfaces it as
the **third tab in General settings** (`General | Security | Logs`),
consistent with the other tabs.

## Changes

- **Relocated** the event-logs module
`pages/settings/security/event-logs/` → `modules/settings/event-logs/`
and render it as tab content instead of a `FullScreenContainer` page.
Dropped `SettingsPath.EventLogs`, its route, and the fullscreen handling
in favor of the `general#logs` hash tab.
- **Security tab:** removed the "View Logs" entry; kept the
log-retention setting there.
- **In-tab gating** (shown to users with the Security permission):
Enterprise upgrade card when not entitled, a clear "ClickHouse not
configured" placeholder otherwise (derived from client config), and the
query is skipped when disabled. Replaces a bespoke error component that
string-matched error messages with the shared `SettingsEmptyPlaceholder`
/ `SettingsEnterpriseFeatureGateCard`.
- **Layout:** boxed content column with the table selector + filters
grouped in a `Card` and the results table below, matching settings
conventions. Kept the existing fixed filters (page/event name, member,
period) rather than recreating the record-view filter chips (those are
tightly coupled to record/view context).

Frontend + `twenty-shared` only — no changes to the log query or data.

## Test plan

- [x] `npx nx typecheck twenty-front` and `npx nx lint twenty-front`
pass
- [x] Settings → General shows three tabs; Logs is the third; breadcrumb
stays "Workspace / General"
- [x] With Enterprise + ClickHouse: table selector, filters, refresh,
and the paginated table work
- [x] Non-Enterprise: Enterprise upgrade card shown; no failing query
fires
- [ ] Enterprise without ClickHouse: shows the "ClickHouse not
configured" placeholder
- [ ] Security tab still shows the log-retention setting and the "View
Logs" button is gone
- [ ] A user without the Security permission sees neither the Security
nor Logs tab
2026-06-04 08:47:23 +02:00
Parship Chowdhury 120793f69f fix: block self-impersonation in admin panel (#21130)
## Issue
- From Settings -> Admin Panel -> Workspace -> Members, impersonating
the currently logged-in user still issued an impersonation login token.
Token exchange produced invalid impersonation JWTs
(`impersonatorUserWorkspaceId === impersonatedUserWorkspaceId`). JWT
validation then failed with `User cannot impersonate themselves`,
leaving the app in an endless loading state until cookies were cleared.
- Closes #21086

## Approach
I was first thinking of to only hide the impersonate button for the
logged-in user in the admin, since they can not click what isn’t shown
(as I thought it was just a frontend issue).

But that was not enough:
- The `impersonate` mutation can still be called directly (GraphQL
client, scripts, devtools).
- Before this fix, the mutation could succeed and only fail later at JWT
validation, which led to invalid tokens and a broken session.

So the PR does both:
- Frontend: hide/disable self-impersonation in the UI and avoid
reloading on failed token exchange (UX).
- Backend: reject self-impersonation in `ImpersonationService` and at
token exchange (enforcement, fail fast before bad tokens).

Hiding the button is the right product behavior; the backend change is
what makes the rule real and safe.

## How to test
Manual:
- Log in as a user with admin impersonation.
- Go to Settings -> Admin panel -> Workspace -> open your workspace ->
members.
- Confirm your row has no Impersonate button; other members still do.
- Open Admin Panel -> User for yourself -> confirm no impersonate
button.
- Open Settings -> Members -> your own member profile -> confirm no
Impersonate action.
- Impersonate another member -> should work as before

Automated: `npx jest impersonation.service.spec`

### Before:
<img width="830" height="413" alt="Screenshot 2026-06-02 122224"
src="https://github.com/user-attachments/assets/46f38a74-8bd6-4ffa-b749-500ce18314f1"
/>

### After:
<img width="795" height="369" alt="Screenshot 2026-06-02 122333"
src="https://github.com/user-attachments/assets/62ece4a8-d38b-4f91-817f-792ff49b146b"
/>

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 13:15:44 +00:00
Félix Malfait 431f6ae98f feat(settings): move settings chrome into a single rounded card (#21131)
## What

Replaces `SubMenuTopBarContainer` with a settings-specific
`SettingsPageLayout` that puts the whole page chrome — breadcrumb,
centered title, actions, an optional secondary bar (tabs or wizard
step), and the 760px body — inside **one rounded card**, with
`SidePanelForDesktop` as a sibling. Title, tabs and body content share
one centered vertical axis at every card width.

Supersedes #21122. One PR, no feature flag.

## New components (`@/settings/components/layout/`)

- **SettingsPageLayout** — owns the rounded card + side-panel sibling,
`useCommandMenuHotKeys`, mobile command menu
- **SettingsPageHeader** — breadcrumb · centered title · actions in a
symmetric `1fr auto 1fr` grid (symmetric padding throughout)
- **SettingsSecondaryBar** — the secondary row, bracketed by top +
bottom borders
- **SettingsTabBar** — centered tabs reusing `activeTabIdComponentState`
+ `TabListFromUrlOptionalEffect` for URL-hash sync (does not touch the
shared `TabList`)
- **SettingsWizardStepBar** — back arrow · "N. Label" · optional
trailing slot

## Migrations

- Bulk rename across ~80 call sites (`SubMenuTopBarContainer` →
`SettingsPageLayout`); old component deleted.
- 5 tab pages (AI, APIs & Webhooks, Applications, Members, Role) + the
Data Model object-detail page render their tabs in `secondaryBar`
(object-detail keeps "See records" / "New Field" in the header actions).
- The 2 role object-level steps render the wizard step bar with working
back navigation.
- Accounts consolidated into **General / Emails / Calendars** tabs;
standalone `SettingsAccountsEmails` / `SettingsAccountsCalendars` pages
+ routes + stories removed. `SettingsPath.AccountsEmails` /
`AccountsCalendars` now resolve to `accounts#emails` /
`accounts#calendars`, so existing `getSettingsPath()` links deep-link to
the right tab via the existing hash sync — no call-site changes.

## Verification

- `nx typecheck twenty-front` and `nx lint twenty-front` both clean.
- Browser (logged-in workspace): title / tab / body / card centers align
on a single axis at multiple widths — width-invariant, so alignment
holds when the AI side panel (a sibling) shrinks the card. Rounded card
with even gaps on all four sides; tab row bracketed by two 1px lines;
no-tab pages render header → body with no lines; wizard back navigation
works; `…/accounts#emails` opens the Emails tab.

The shared `PageHeader` and `TabList` are untouched. The settings side
panel itself isn't wired to open yet — that's a follow-up PR.
2026-06-02 14:22:57 +02:00
Félix Malfait 75df1f3997 chore(settings): address review comments from PR 21072 (#21121)
## Summary

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

### Comments (~17 threads)

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

### Structure / extraction

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

### Hero assets

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

## Test plan

- [ ] `npx nx typecheck twenty-front` 
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint twenty-front`  (oxlint + oxfmt, 0 warnings/errors)
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the new hero illustration (light + dark)
- [ ] AI tab: tool list still loads, search + Custom/Managed/Standard
filters still work, "New Tool" still navigates to detail
- [ ] AI tab: Models tab — smart/fast model select, "Use best models
only" toggle, per-model checkboxes, toggle-all all still
optimistic+revert on error
- [ ] Skills tab: "Deactivated" toggle still flips show/hide
- [ ] Webhooks table still uses the 1fr 28px grid
2026-06-02 07:23:14 +02:00
Félix Malfait b338a7a1d2 feat(settings): discovery hero rollout + ephemeral playground token (#21072)
## Summary

Two intertwined streams of work:

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

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

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

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

## Test plan

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

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

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

### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
2026-06-01 14:16:02 +02:00
Nicolas Besnard 71c377484e fix(front): keep app variable cache in sync after update (#20861)
Updating an application variable in Workspace / Applications / <App> /
Settings persisted server-side but the Apollo cache kept the old value.
Switching tabs unmounted the settings tab, and remounting reseeded the
input from the stale cache — only a full refresh showed the new value.

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

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

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

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:14:28 +00:00
martmull c2df39405c Fix admin pannel server variable config tab (#21017)
## Before

<img width="1046" height="490" alt="image"
src="https://github.com/user-attachments/assets/450557de-fcf5-4b51-afdb-36c0c36e43d8"
/>


## After

<img width="1040" height="414" alt="image"
src="https://github.com/user-attachments/assets/4a5fe2ab-85d6-4431-9397-6f81ae24055d"
/>
2026-05-29 09:55:11 +00:00
martmull 6fb6ef4e7a Add darkmode for oAuth screen (#21005)
## after

<img width="1512" height="828" alt="image"
src="https://github.com/user-attachments/assets/71664eab-c921-45da-ac67-7a660c976d5c"
/>
2026-05-28 15:48:57 +02:00
Félix Malfait 7d9f9605a2 feat(settings): move email handles and emailing domains to dedicated Email page (#21008)
## Summary

Both **Email Handles** and **Emailing Domains** were rendered on the
General workspace settings page, but they're workspace-level *email
infrastructure* (inbound shared addresses + outbound sender
authentication) and don't belong with the workspace name, picture, and
domain config.

- New `SettingsWorkspaceEmail` page at `/settings/email`
- Nav item under **Workspace**, hidden when `IS_EMAIL_GROUP_ENABLED` is
off (and gated by `WORKSPACE` permission)
- Related sub-routes (`email-group/:messageChannelId`,
`emailing-domain/:domainId`, etc.) moved from `general/` to `email/` so
the URL space stays consistent with the page
- General page now only contains name, picture, workspace domain, and
the delete-workspace section

No behavior changes to the underlying section components — they're
imported as-is into the new page.

## Test plan

- [ ] With `IS_EMAIL_GROUP_ENABLED` enabled: **Email** appears in the
Workspace nav and the page renders both sections
- [ ] With the flag disabled: **Email** is hidden from nav; navigating
to `/settings/email` directly renders nothing
- [ ] General page no longer shows Email Handles / Emailing Domains
- [ ] Clicking a shared inbox row navigates to
`/settings/email/email-group/:id` (was `general/...`)
- [ ] "Add emailing domain" navigates to
`/settings/email/emailing-domain/new`

## Notes

- Pre-existing `twenty-front` typecheck error in
`FrontComponentRendererProvider.tsx` (React types mismatch between
sibling packages) reproduces on `main` and is unrelated to this PR.
2026-05-28 13:22:55 +02:00
Félix Malfait 7065441972 fix: refresh admin panel feature flags after toggling (#21007)
## Summary

- Add `refetchQueries` to the `updateWorkspaceFeatureFlag` mutation in
the admin workspace detail page so the toggle reflects the new value
after toggling.
- Rename `useFeatureFlagState` → `useAdminUpdateFeatureFlag` since the
hook lives under `admin-panel` and is only consumed by the admin
workspace detail page.

## Bug

In the admin panel, toggling a feature flag for a workspace other than
the admin's own workspace sent the backend mutation successfully, but
the toggle in the UI remained unchanged.

The displayed value is derived from:
```tsx
const currentWorkspaceValue =
  currentWorkspace?.id === workspaceId
    ? currentWorkspace?.featureFlags?.find((f) => f.key === flag.key)?.value
    : undefined;
const displayedValue = currentWorkspaceValue ?? flag.value;
```

When viewing a different workspace, `currentWorkspaceValue` is
`undefined` so the toggle reads `flag.value` from the
`WORKSPACE_LOOKUP_ADMIN_PANEL` query. That query was never refetched
after the mutation, so the displayed value stayed stale.

The existing optimistic Jotai update on `currentWorkspaceState` still
runs — it is needed so the rest of the app (anything consuming
`useIsFeatureEnabled`) reacts immediately when an admin toggles a flag
on their own workspace.

## Test plan

- [ ] Open the admin panel → pick a workspace that is not your own →
Feature Flags tab → toggle a flag → toggle visually flips after the
mutation completes.
- [ ] Same flow on your own workspace → toggle flips, and any UI gated
on that flag also reacts.
- [ ] If the mutation fails, the toggle reverts (existing `onError`
rollback path).
2026-05-28 12:23:26 +02:00
neo773 c5606212f2 Ses outbound followup (#20610)
This pull request unifies outbound with inbound under the new feature
and the new email groups feature.

These are workspace level shared inboxes that are shared between all
workspace members.

outbound sending with SES works, we only listen for tenant status
events, rest is managed by AWS

PR refactors old code and webhook to be split for outbound and inbound
for proper separation


| Area | Change |
|---|---|
| AWS SES driver | Split into `AwsSesRegisterDomainService` (tenant +
identity + DKIM + MAIL FROM + configuration-set + EventBridge dest +
contact list) and `AwsSesSendEmailService` (SendEmail). |
| Reputation webhook | New `/webhooks/messaging/ses/outbound` route. SES
→ EventBridge (`Sending Status Enabled/Disabled` on default bus) → SNS →
router → `SesOutboundSendingStateHandlerService` updates
`emailing_domain.tenantStatus`. |
| Inbound webhook | Refactored into `SesInboundWebhookRouterService` +
`SesInboundMailHandlerService`. Shared `SnsSignatureVerifierService` +
`SnsSubscriptionConfirmerService` across both routes. |
| Global uniqueness | New migration + instance command:
`emailing_domain.domain` is now globally unique (one tenant per domain
across workspaces). |
| Tenant status | New `emailing_domain.tenantStatus` column (`ACTIVE` /
`PAUSED`) + `EmailingDomainTenantStatusService`. |
| Send-email mutation | New `sendEmailViaDomain` GraphQL mutation +
DTOs. |
| Cleanup | `EmailingDomainWorkspaceCleanupJob` wired into
`WorkspaceService.deleteWorkspace` — tears down SES tenant association +
identity on workspace delete. |
| Settings UI | Rewritten around reusable `SettingsTableListSection`.
"Email Group" → "Email Handle" rename. New cells for
status/source/forwarding. Outbound domains surfaced on workspace
settings page. |

### Env vars (new)

All in `config-variables.ts`, group `AWS_SES_SETTINGS`, all optional:

- `AWS_SES_REGION` — `@IsAWSRegion`, consumed by `AwsSesClientProvider`
+ driver factory
- `AWS_SES_ACCOUNT_ID` — used for ARN construction in driver factory
- `SES_SNS_TOPIC_ARN_ALLOWLIST` — **shared** by inbound + outbound
webhook routers, comma-separated list of accepted SNS topic ARNs
(verified via `sns-payload-validator`)

### Migrations

- `1778862608620-add-emailing-domain-tenant-status` (fast) — adds
`tenantStatus` column.
- `1778865501791-unique-emailing-domain-globally` (slow, idempotent) —
enforces global uniqueness on `domain`.
- Instance commands bumped to `2.5`.

### Infra dependency

Two coupled twenty-infra PRs:

- `ses-inbound-email` — receipt-rule + inbound SNS topic + S3 bucket
policy + KMS grant + `email_group_*` outputs.
- `ses-outbound-tf` — EventBridge rule + outbound SNS topic + SES IAM
policy + outbound `webhook_url` subscription. **Based on
`ses-inbound-email`.**

Merge order: inbound first, then outbound. Outbound PR's chart edit owns
the comma-joined `SES_SNS_TOPIC_ARN_ALLOWLIST` value (both ARNs).


Features lives under `/settings/general`

<img width="1496" height="845" alt="SCR-20260519-ofhi-2"
src="https://github.com/user-attachments/assets/a025485a-09f7-4131-91cd-0067690ff18d"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-27 19:38:44 +02:00
fujiwara 951ead5a1e fix(front): ignore IME composition Enter in input hotkeys (#20958)
## What

Pressing Enter to confirm an IME (CJK) composition no longer submits the
input. The Enter / Escape / Tab handlers now ignore key events fired
while a composition is in progress (`isComposing`, or the legacy
`keyCode === 229`).

Fixes #20954

## Why

`isComposing` was not checked anywhere in `twenty-front`, so the Enter
that confirms a Japanese / Chinese / Korean conversion was also consumed
as a submit / escape / tab hotkey — making it very hard to type CJK text
into any input that submits on Enter.

## Changes

- `useHotkeysOnFocusedElement` — central guard; covers every input wired
through `useRegisterInputEvents` (~13 components) and all hotkeys routed
through this hook.
- Direct `onKeyDown` Enter handlers: `CreateWorkspace`,
`SettingsDevelopersApiKeysNew`, `SettingsAccountsBlocklistInput`.

## Notes

- No effect on non-IME (Latin) typing — `isComposing` is only true
during an active composition. It also improves accented / dead-key input
on Latin layouts.
- `react-hotkeys-hook@4` does not handle IME composition on its own, so
the guard is explicit.

## Testing

Manually verified with a Japanese IME on Chrome (macOS) against the
v2.8.3 self-hosted image: romaji + Enter now only confirms the
conversion; a second Enter on committed text submits as expected. The
GIF in #20954 shows the original buggy behavior.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:09:10 +00:00
Marie 986b9dcb3d Deprecate dummy enterprise key 1/2 (#20890)
Remove usage of hasValidEnterpriseKey in FE (replaced by
hasValidSignedEnterpriseKey)

To avoid breaking change at deploy time, we will wait until after this
has been deployed in prod, to remove hasValidEnterpriseKey in the BE.
2026-05-27 12:34:48 +00:00
martmull a051490ec9 Basic app logo fixes (#20919)
as title, took the quick win fixes from
https://github.com/twentyhq/twenty/pull/20909/changes#diff-3367344412b2f44f0273d8019c1bc36396198244b9558d02921b135f62522baaR180
and leave the main fix for later as it requires an architectural update
2026-05-27 08:21:35 +00:00
Weiko 08682fb3f5 Various fixes - some AI findings (#20921)
App permissions tab:
- The fallback uuidv4() for a marketplace field was generated twice, so
id and universalIdentifier could diverge; it's now computed once and
reused as it seemed to be the intention (even though I don't really
think it's a good idea)
- Renamed buildobjectMetadataItemsFromMarketplaceApp →
buildObjectMetadataItemsFromMarketplaceApp to follow camelCase.

Morph relation validation:
- Fixed the user-facing message "At least one relation is require" →
"...is required"
- Typos in the related test descriptions (Morh → Morph, samefield → same
field) and their snapshots.

Docs
- The UUID field-type row in views.mdx only listed IS; updated to the
full set supported by FILTER_OPERANDS_MAP (IS, IS_NOT, IS_EMPTY,
IS_NOT_EMPTY).
2026-05-26 14:43:50 +00:00
martmull 7ca9081efa Add application installation validation modale (#20907)
## After

<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/231d4f0d-6052-4c4e-9a2a-0244d2b3832e"
/>
2026-05-26 09:48:31 +00:00
Félix Malfait d602f35cbd feat(data-model): custom-indexes management UI and mutations (#20846)
## Summary

Brings indexes management into the per-object Settings tab as a section
under Search (no feature flag, advanced mode only). Admins can create /
delete non-unique indexes with the UI; apps can declare indexes in code
with `defineIndex`. Composite-typed fields are now indexable by picking
a specific sub-column (e.g. `Address > City`).

A few related polish items also land here (invite-user dropdown lands on
the Invite tab; standard warning callout above the new-index form).

## What ships

### UI — custom indexes on per-object Settings
- New section directly under Search, wrapped in
`AdvancedSettingsWrapper`.
- Filter dropdown on the search bar toggles system-index visibility
(shown by default since advanced mode).
- **+ Add Index** button (disabled with tooltip once the per-object cap
is reached) navigates to a dedicated `SettingsObjectNewIndex` page
(matches the field-creation pattern, not a modal):
- Field picker mirrors the webhook event-form layout (rows of dropdowns,
implicit trailing empty row).
- Composite fields surface their sub-properties (`Address > City`,
`Currency > Amount`, …).
  - BTREE / GIN type selector.
- Standard warning Callout: "Use indexes sparingly — each one speeds
reads but slows writes."
- Trash icon on `isCustom: true` rows → confirmation modal →
`deleteOneIndex`.

### Server — `createOneIndex` / `deleteOneIndex` mutations
- Gated by `SettingsPermissionGuard(DATA_MODEL)`.
- `IndexMetadataService` wraps the existing migration runner via
`WorkspaceMigrationValidateBuildAndRunService` so the metadata row and
the SQL index land atomically.
- Validation: rejects empty fields, duplicate `(fieldMetadataId,
subFieldName)` pairs, fields not on the object, requires `subFieldName`
for composite parents, forbids `subFieldName` on scalar/relation,
enforces `MAX_CUSTOM_INDEXES_PER_OBJECT = 10`.
- Delete refuses on `isCustom: false` rows so system indexes can't be
removed via this API.
- Dedicated GraphQL exception handler maps each typed error to the right
transport error class.

### Composite sub-field indexing
- Adds `subFieldName: string | null` column to
`IndexFieldMetadataEntity` (fast instance command).
- The flat-entity flow (`UniversalFlatIndexFieldMetadata`,
`FlatIndexFieldMetadata`, `from-universal-flat-index-to-flat-index`,
runner column resolution) all carry `subFieldName` through.
- For composite parents, the runner uses
`computeCompositeColumnName({...}, property)` for the picked sub-column;
for non-composite parents, behavior is unchanged.
- The `'::'` separator encodes `(fieldMetadataId, subFieldName)` for
dedup on the wire; the frontend uses the same separator inside the
Select component's string value.

### Apps can declare indexes in code (`defineIndex`)
- New `IndexManifest` + `IndexFieldManifest` types in
`twenty-shared/application` wired into the `Manifest` type.
- `defineIndex` SDK helper + `IndexConfig`. CLI manifest builder +
extractor recognize `defineIndex` / `ManifestEntityKey.Indexes`.
- Server: `from-index-manifest-to-universal-flat-index` converter
resolves field IDs, validates composite/scalar `subFieldName` rules, and
delegates to `generateFlatIndexMetadataWithNameOrThrow` for the
deterministic name.
- Orchestrator wires the loop after the field-resolution pass;
per-object cap enforced inline against the manifest.
- Cascade on uninstall is automatic — when an app disappears its indexes
drop with it (universal-flat-entity diff handles it).
- Rich-app fixture ships a real `defineIndex` on `PostCard.status`,
exercising the full manifest → install path in CI.

### Closed for now (open later if needed)
- Apps cannot declare `isUnique` indexes — unique constraints stay with
the field-creation flow.
- Apps cannot use a partial-`indexWhereClause` — the UI surface keeps
the framework's hardcoded allowlist.
- UI cannot create unique or partial indexes either; same reasons.

### Cleanups along the way
- Reused the existing `getCompositeSubFieldLabel` +
`COMPOSITE_FIELD_SUB_FIELD_LABELS` (deleted the duplicates I'd created
early in the PR).
- Moved `MAX_CUSTOM_INDEXES_PER_OBJECT` to `twenty-shared/constants`
(single source for FE + BE).
- Replaced inline `isDefined(x) && x !== ''` with `isNonEmptyString`
(from `@sniptt/guards`).
- Hoisted the per-object fields Map + inlined the cap counter into the
indexes orchestrator loop (drops the install scan from O(indexes ×
totalFields) to O(totalFields + indexes)).
- Per design-feedback: page-based create flow (not a modal), filter
dropdown on the SearchInput (not a separate toggle), webhook-style
picker, field icons.

### Unrelated polish that lands here
- "Invite user" link in the multi-workspace dropdown now lands on the
Invite tab directly (`#invite`) instead of the first tab of the members
page.

## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
2026-05-25 17:47:09 +02:00
Weiko 3bda05ea57 [Breaking change] Prepare non-system permission flags (#20847)
# Summary

Replaces the enum-keyed `permissionFlags: PermissionFlag[]` on roles
with `permissionFlagUniversalIdentifiers: string[]`

This unlocks mixing system flags (`SystemPermissionFlag.*`) with
app-defined flags in a role config.

This is a breaking change. Existing app source must switch to the new
field.

# Breaking changes

- `RoleManifest.permissionFlags` removed. Use
`RoleManifest.permissionFlagUniversalIdentifiers: string[]`.
- `RoleConfig.permissionFlags` removed (was `PermissionFlagType[]`). Use
`RoleConfig.permissionFlagUniversalIdentifiers: string[]`.
- `PermissionFlagManifest` type removed from
`twenty-shared/application`.
- `PermissionFlag` re-export removed from `twenty-sdk/define`.
`SystemPermissionFlag` is re-exported in its place.
- Retargeting a permission flag between roles is now classified as
delete + create instead of update

 ### Not in this PR
- definePermissionFlag SDK function and top-level
Manifest.permissionFlags catalog (apps defining their own custom flags).
Until those land, permissionFlagUniversalIdentifiers only accepts
SystemPermissionFlag.* UUIDs; arbitrary UUIDs fail validation.
2026-05-22 21:22:28 +00:00
nitin 05f58b2dba Fix onboarding modals spacings (#20682)
closes
https://discord.com/channels/1130383047699738754/1496415056085389422
2026-05-22 10:06:18 +00:00
Priyanshu Bartwal e2ee4ffdff Fix: Added description below application variable (#20781)
Fixes #20757 

Before: 
<img width="655" height="777" alt="Screenshot 2026-05-21 at 12 51 25 AM"
src="https://github.com/user-attachments/assets/d164d2b8-22e6-43b4-9079-a4c33324d7dc"
/>

After:
<img width="655" height="777" alt="Screenshot 2026-05-21 at 12 50 52 AM"
src="https://github.com/user-attachments/assets/0f83062f-7703-43b9-a049-0164f5e7d9cd"
/>

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-05-21 21:14:42 +00:00
martmull 9a7f50fcb7 Fix breaking change in install app command (#20825)
add backward compatibility for twenty-sdk install command
2026-05-21 20:58:34 +00:00
Thomas des Francs c721fa8502 Add editor mode for text field widgets (#20779)
## Summary

Tested ↓

<img width="3456" height="1990" alt="CleanShot 2026-05-20 at 21 14
04@2x"
src="https://github.com/user-attachments/assets/b4e0d3d3-715f-4ad7-bd03-e8e1922b3c6c"
/>


- Enable `FieldDisplayMode.EDITOR` for plain `TEXT` field widgets while
keeping `FIELD` as the default display mode.
- Add a plain multiline text editor renderer for `TEXT + EDITOR` field
widgets with optimistic record-store/cache updates and debounced
persistence.
- Reuse the shared `TextArea` component through a transparent, uncapped
variant so the editor has no input chrome and lets the widget grow.
- Add unit coverage for text display-mode config and a Storybook
scenario for a text field widget in editor mode.

## useEffect cleanup note

`FieldWidgetTextEditor` flushes the debounced persist callback in a
`useEffect` cleanup:

```ts
useEffect(() => () => persistTextDebounced.flush(), [persistTextDebounced]);
```

This follows the existing debounced autosave cleanup pattern already
used in `WorkflowEditActionHttpRequest`. It ensures pending text changes
are persisted when the widget unmounts, while `onBlur` still flushes
immediately for normal editor exits.

## Validation

- `npx nx test twenty-front --testPathPattern=page-layout`
- `npx nx typecheck twenty-front`
- `npx nx lint twenty-front`
- Browser check on
`http://apple.localhost:3001/object/company/20202020-a305-41e7-8c72-ba44072a4c58`
for transparent textarea, no internal max-height/scroll, equal padding,
and widget growth.

Note: lint passes with two unrelated existing warnings in
`NavigationDrawerItem.tsx` and `ConfigVariableEdit.tsx`.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-21 22:29:29 +02:00
Félix Malfait 0bfcd9a701 fix(api-keys): refresh API keys list after key creation (#20806)
New API keys are created successfully but the API keys table can keep
showing a stale pre-create result, so users think the key vanished. This
blocks key management from the expected UI flow.

Fix: Updated the API key creation mutation to explicitly synchronize
Apollo cache for the API keys list:

- In `SettingsDevelopersApiKeysNew.tsx`, imported `GetApiKeysDocument`.
- Changed `useMutation(CreateApiKeyDocument)` to:
  - `refetchQueries: [GetApiKeysDocument]`
  - `awaitRefetchQueries: true`

Why: the list page (`SettingsApiKeysTable`) reads from
`GetApiKeysDocument`, and creation previously did not invalidate/refetch
that query. With this change, successful creation refreshes the list
query so the new key appears when the user returns to APIs & Webhooks.

Validation attempted:
- `npx nx lint:diff-with-main twenty-front` → failed due missing Nx
modules in this environment.
- `npx nx typecheck twenty-front` → failed due missing Nx modules in
this environment.

Authored by Sonarly by autonomous analysis (run 44851).

Co-authored-by: sonarly-bot <sonarly@sonarly.com>
2026-05-21 16:40:48 +00:00
martmull a9ff1a9d3c Fix server variable not shown (#20799)
## Before

<img width="1502" height="675" alt="image"
src="https://github.com/user-attachments/assets/b64b24b4-11a5-4f6b-a3aa-c77108f22e9d"
/>

## After

<img width="1262" height="593" alt="image"
src="https://github.com/user-attachments/assets/42d662b4-2ec6-4ad4-9d19-58f25f52475c"
/>

---------

Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-05-21 13:43:05 +00:00
Paul Rastoin 9b9c97a049 Deprecate and backfill delete ConnectedAccount twenty standard object (#20752)
# Introduction
Following connected account permissions refactor and encryption
Removing the old workspace schema twenty standard application
connectedAccount objects and related standard fields and index

- a lot of deadcode
- instance command backfill cleaning the connected account object from
workspaces
2026-05-21 13:42:09 +02:00
martmull 2963fa9324 Navigate to installed page after app install (#20797)
as title
2026-05-21 11:19:18 +00:00
martmull 237a943947 Update twenty sdk commands (#20735)
Performs twenty-sdk cli command migration:

Summary

``` ┌─────┬──────────────────────────┬────────────────────────────┬───────────────────────┐
 │  #  │       Old command        │        New command         │        Status         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 1   │ twenty dev [appPath]     │ twenty dev [appPath]       │ Unchanged (now also   │
 │     │                          │                            │ DEFAULT)              │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 2   │ twenty dev --once        │ twenty dev --once          │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 3   │ twenty dev --watch       │ twenty dev [appPath]       │ --watch flag removed  │
 │     │ [appPath]                │                            │ (was default)         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 4   │ twenty dev --verbose     │ twenty dev --verbose       │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 5   │ twenty dev --debug       │ twenty dev --debug         │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 6   │ twenty dev --debounceMs  │ twenty dev --debounceMs    │ Unchanged             │
 │     │ <ms> [appPath]           │ <ms> [appPath]             │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 7   │ twenty build [appPath]   │ twenty dev:build [appPath] │ Deprecated → colon    │
 │     │                          │                            │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 8   │ twenty build --tarball   │ twenty dev:build --tarball │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 9   │ twenty typecheck         │ twenty dev:typecheck       │ Deprecated → colon    │
 │     │ [appPath]                │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 10  │ twenty logs [appPath]    │ twenty dev:fn-logs         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 11  │ twenty logs -n <name>    │ twenty dev:fn-logs -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 12  │ twenty logs -u <id>      │ twenty dev:fn-logs -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 13  │ twenty exec [appPath]    │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 14  │ twenty exec -n <name>    │ twenty dev:fn-exec -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 15  │ twenty exec -u <id>      │ twenty dev:fn-exec -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 16  │ twenty exec -p <json>    │ twenty dev:fn-exec -p      │ Deprecated → colon    │
 │     │ [appPath]                │ <json> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 17  │ twenty exec              │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │ --postInstall [appPath]  │ --postInstall [appPath]    │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 18  │ twenty exec --preInstall │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │  [appPath]               │ --preInstall [appPath]     │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 19  │ twenty add [entityType]  │ twenty dev:add             │ Deprecated → colon    │
 │     │                          │ [entityType]               │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 20  │ twenty add --path <path> │ twenty dev:add --path      │ Deprecated → colon    │
 │     │  [entityType]            │ <path> [entityType]        │ command               │
 └─────┴──────────────────────────┴────────────────────────────┴───────────────────────┘

 App lifecycle commands

 ┌─────┬────────────────────────┬────────────────────────────┬─────────────────────────┐
 │  #  │      Old command       │        New command         │         Status          │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 21  │ twenty publish         │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 22  │ twenty publish --tag   │ twenty app:publish --tag   │ Deprecated → colon      │
 │     │ <tag> [appPath]        │ <tag> [appPath]            │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 23  │ twenty deploy          │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ --private [appPath]        │ command + --private     │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 24  │ twenty install         │ twenty app:install         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 25  │ twenty uninstall       │ twenty app:uninstall       │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 26  │ twenty uninstall -y    │ twenty app:uninstall -y    │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 └─────┴────────────────────────┴────────────────────────────┴─────────────────────────┘

 Server commands

 ┌─────┬─────────────────────────┬─────────────────────────────┬──────────────────────┐
 │  #  │       Old command       │         New command         │        Status        │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 27  │ twenty server start     │ twenty docker:start         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 28  │ twenty server start -p  │ twenty docker:start -p      │ Deprecated → colon   │
 │     │ <port>                  │ <port>                      │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 29  │ twenty server start     │ twenty docker:start --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 30  │ twenty server stop      │ twenty docker:stop          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 31  │ twenty server stop      │ twenty docker:stop --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 32  │ twenty server status    │ twenty docker:status        │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 33  │ twenty server status    │ twenty docker:status --test │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 34  │ twenty server logs      │ twenty docker:logs          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 35  │ twenty server logs -n   │ twenty docker:logs -n       │ Deprecated → colon   │
 │     │ <lines>                 │ <lines>                     │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 36  │ twenty server logs      │ twenty docker:logs --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 37  │ twenty server reset     │ twenty docker:reset         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 38  │ twenty server reset     │ twenty docker:reset --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 39  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ [version]               │ [version]                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 40  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ --test [version]        │ --test [version]            │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 41  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync            │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 42  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync -r <name>  │ -r <name>                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 43  │ twenty catalog-sync     │ (removed)                   │ Removed (was already │
 │     │                         │                             │  deprecated)         │
 └─────┴─────────────────────────┴─────────────────────────────┴──────────────────────┘

 Remote commands

 ┌─────┬────────────────────────┬──────────────────────────┬──────────────────────────┐
 │  #  │      Old command       │       New command        │          Status          │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 44  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 45  │ twenty remote add --as │ twenty remote:add --as   │ Deprecated → colon       │
 │     │  <name>                │ <name>                   │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 46  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-key <key>        │ --api-key <key>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 47  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-url <url>        │ --api-url <url>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 48  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --local                │ --local                  │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 49  │ twenty remote add      │ twenty remote:add --test │ Deprecated → colon       │
 │     │ --test                 │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 50  │ twenty remote list     │ twenty remote:list       │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 51  │ twenty remote switch   │ twenty remote:use [name] │ Deprecated → colon       │
 │     │ [name]                 │                          │ syntax + renamed         │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 52  │ twenty remote status   │ twenty remote:status     │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 53  │ twenty remote remove   │ twenty remote:remove     │ Deprecated → colon       │
 │     │ <name>                 │ <name>                   │ syntax                   │
 └─────┴────────────────────────┴──────────────────────────┴──────────────────────────┘
```

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-20 15:12:39 +00:00
Paul Rastoin 57f13c9b92 [CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction
Prevent any cross user `connectedAccount` `connectionParamaters` leak
Also encrypt in db all `connectionParameters` password
Never return any password through `DTO` anymore
The settings now allow update mutation without providing the password in
edition mode

Verified all `connectionParameters.password` interaction

## Integration tests
- Added more coverage for both failing and successful paths
- Introduced a new env var that allow bypass the provider connection
test

## Legacy connected Account decryption support
Stop allowing non encrypted decryption on `accessToken` and
`refreshToken`, only allow legacy decryption on refactored
`connectionParameters`

## Upsert ownership
Completely got rid of the connected workspace schema context which is
legacy
Also now a user can only upsert a connected account for him only..

## New UI
<img width="1770" height="1852" alt="image"
src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b"
/>
If in edition the password is by default disabled
It needs to be selected as being edited to be enabled

## Next
- Refactor tool permissions flag not to include connected accounts
- Remove the legacy connected standard object
- Refactor and improve connected account resolver auth
2026-05-19 12:56:44 +00:00
nitin db0547f503 [1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)
Split of #20377.

## Summary

This PR separates available permission flags from per-role permission
flag grants.

Previously, `core.permissionFlag` stored the role assignment directly:
`roleId + flag`. This PR renames that legacy grant table to
`core.rolePermissionFlag`, then recreates `core.permissionFlag` as the
catalog of available permission flags.

## What changed

- Rename the existing `core.permissionFlag` grant table to
`core.rolePermissionFlag`.
- Add the new syncable `core.permissionFlag` catalog entity with key,
label, description, icon, permission type, relevance flags, and
custom/standard metadata.
- Add stable `SystemPermissionFlag` universal identifiers for the
built-in `PermissionFlagType` values.
- Seed the standard permission flags for every workspace under the
Twenty standard application.
- Backfill existing role grants:
  - create missing catalog rows for existing grant keys,
  - add `rolePermissionFlag.permissionFlagId`,
- migrate grants from the old string `flag` column to the new catalog
FK,
- replace the old `(flag, roleId)` uniqueness with `(permissionFlagId,
roleId)`.
- Rewire role permission flag caches, permission checks, role DTO
mapping, and `upsertPermissionFlags` to resolve through the catalog.
- Keep the existing public role permission API shape: product/app
surfaces still talk about `permissionFlags` and return `{ id, roleId,
flag }`.
- Update metadata flat-entity machinery, migration builders, validators,
action handlers, snapshots, generated schemas, docs, and app fixtures
for the new `permissionFlag` / `rolePermissionFlag` split.

## Behavior after this PR

- Existing permission flag grants keep working.
- Existing GraphQL role permission flows keep the same public naming.
- Standard permission flags are represented as catalog rows.
- Permission checks now compare grants through catalog universal
identifiers instead of the legacy `flag` column.
- Workspace deletion cleanup now verifies both `permissionFlag` and
`rolePermissionFlag`.

## What is not in this PR

- Public GraphQL CRUD for custom permission flags.
- App manifest support for declaring new custom permission flags.
- Frontend UI for creating or assigning custom permission flags beyond
the existing role permission flow.

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-05-18 11:57:47 +00:00
Félix Malfait 6b49a14b9f feat(auth): set 50-character maximum length on passwords (#20655)
## Summary
- Cap password length at 50 characters in the shared regex used by
sign-up, password reset, and password change (both `twenty-front` and
`twenty-server`).
- Update the user-facing validation message on sign-up and password
reset to mention both the 8 min and 50 max bounds.
- Extend the `PASSWORD_REGEX` unit test to cover the new upper bound.

The cap also prevents unbounded inputs from reaching bcrypt, which
silently truncates passwords above 72 bytes and can mask user-visible
bugs.

## Test plan
- [x] `npx jest src/modules/auth/utils/__tests__/passwordRegex.test.ts`
passes (8-char min and 50-char max).
- [ ] Sign up with a 51-character password — form rejects with "Password
must be between 8 and 50 characters".
- [ ] Sign up with an 8–50 character password — succeeds.
- [ ] Password reset rejects a 51-character password with the same
message.
- [ ] Existing users with longer passwords (if any pre-exist) can still
sign in (the regex only gates write paths: sign-up, change, reset).
2026-05-18 10:12:19 +02:00
Thomas des Francs 8dd0aa46e3 Update OAuth consent modal design (#20540)
## Summary

Updates the OAuth consent screen to match the provided modal treatment,
including the header artwork, app-to-Twenty logo layout, scope icons,
and a content-hugging title that stays on one line until its max width.

Also preserves `returnToPath` through Google and Microsoft social SSO so
users who sign in from `/authorize` return to the OAuth consent flow
with the original OAuth query parameters intact.

## Reference

![OAuth consent modal
reference](https://raw.githubusercontent.com/twentyhq/twenty/046e02d9b3916e286c6bb4ef1c8045b0df53c074/oauth-modal-reference.png)

## Validation

- `yarn nx test twenty-front
--testFile=src/modules/auth/hooks/__tests__/useAuth.test.tsx
--coverage=false`
- `yarn nx test twenty-server
--testFile=src/engine/core-modules/auth/services/auth.service.spec.ts
--coverage=false`
- `yarn nx typecheck twenty-front`
- `yarn nx typecheck twenty-server`
- `git diff --check`

## Notes

The reference screenshot is linked from a separate branch-hosted image
commit so it renders in the PR body without adding that screenshot to
the product diff.

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-14 07:15:11 +00:00