Commit Graph

616 Commits

Author SHA1 Message Date
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 02aa086866 fix(twenty-front): new layout fast-follows (#21360)
Fast-follows for the new layout / flat redesign (master:
twentyhq/core-team-issues#2478).

## Changes
- **Main navbar 48px** (twentyhq/core-team-issues#2479) —
`SIDE_PANEL_TOP_BAR_HEIGHT` 40 → 48, so `PageCardHeader` matches the
Figma target. The side panel top bar shares this constant and stays
aligned.
- **Content panel 12px radius** (twentyhq/core-team-issues#2480) —
`PageCardLayout` card gets a full border + 12px radius and an 8px inset
(`spacing[2]`) so it floats on the shell instead of square full-bleed.
- **Square three-dots button** (twentyhq/core-team-issues#2481) — added
a `square` option to `AnimatedButton`; the page-header side-panel toggle
now renders a 24×24 square icon button instead of a 32×24 pill.
- **Table checkbox sizing** (twentyhq/core-team-issues#2482) — restored
`box-sizing: content-box` on the checkbox box. Its border is declared
outside the label size, so the global `border-box` reset (#21349) was
shrinking it (14px → 12px). Same fix pattern as #21349.
- **Tertiary navbar background** (twentyhq/core-team-issues#2483) — left
navbar / app shell use `background/tertiary` instead of the noisy
surface (`DefaultLayout`, `UserOrMetadataLoader`).
- **Skeleton loading** (twentyhq/core-team-issues#2484) — metadata +
content loading now match the new layout: tertiary shell, 12px rounded
content panel, 48px navbar, sparse bars, empty body (removed the dense
full-width rows). Left-panel skeleton bars use `quaternary` so they stay
visible on the tertiary shell.

## Verification
- typecheck (tsgo) + oxlint + oxfmt pass for all changed files.
- Verified live against a running workspace: measured navbar = 48px,
three-dots = 24×24, checkbox box-sizing = content-box (14px), card
radius = 12px, shell background = tertiary (no noisy image).
Content-loading skeleton matches the target.
2026-06-09 16:10:36 +02:00
Raphaël Bosi 7606dd75a8 Fix: pinned command-menu actions run with empty selection (#21366)
## Cause
PR #21308 ("generalize the page primary/secondary bars") swapped the old
`PageHeader` for the new `PageCardHeader` on the record-index,
record-show, and standalone pages. The old header set
`data-click-outside-id="page-action-container"` on its action container
— an id that the record table/board/calendar click-outside listeners
exclude so header clicks don't clear the current selection. The new
`PageCardHeader` dropped that attribute.

## Implications
With the attribute gone, clicking a pinned command-menu item registered
as a click *outside* the table/board, which reset the selected records
before the action read them. As a result, pinned actions and workflows
triggered from the top bar ran with an empty selection.

## Fix
Re-add `data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}`
to `PageCardHeader`'s action container. Since all three migrated headers
route their buttons through this shared component, the single change
covers every affected page.
2026-06-09 15:32:41 +02:00
Félix Malfait 8a3e6e645a fix(ui): restore content-box sizing for components broken by the global border-box reset (#21349)
## Problem

Since the `twenty-ui` → `twenty-ui-deprecated` / `twenty-new-ui` →
`twenty-ui` rename (#21315), many deprecated components render with
**compacted height** — e.g. dropdown menu items collapse from 32px to
16px, and chips from ~24px to 16px.

## Root cause

The new `twenty-ui` (formerly `twenty-new-ui`) ships a global reset in
`packages/twenty-ui/src/styles/base/reset.scss`:

```css
*, *::before, *::after {
  box-sizing: border-box;
}
```

This is bundled into `twenty-ui/style.css`, which the app imports in
`index.tsx`. #21315 did not change the `import 'twenty-ui/style.css'`
line, but it changed what `twenty-ui` resolves to (old → new), so this
**global `border-box` reset now applies app-wide**.

Several deprecated components were authored against the **content box**,
e.g. `StyledMenuItemBase`:

```css
height: calc(32px - 2 * var(--vertical-padding));
padding: var(--vertical-padding) var(--horizontal-padding);
```

With `content-box` the padding sits *outside* the declared height → 32px
total. Under the new `border-box` reset the padding is folded *inside* →
16px total. (`Chip` uses `height: spacing[4]` + outside padding — same
failure mode.)

Verified in the running app: the collapsed menu item computes
`box-sizing: border-box`, matched by the rule `*, ::before, ::after {
box-sizing: border-box }`; `height` resolves to `calc(32px - 2 * 8px) =
16px`.

## Fix

Add `box-sizing: content-box` to the affected deprecated components. A
class selector outranks the universal `*` reset, so this restores their
intended sizing **without touching the global reset** (which the new
`twenty-ui` components rely on).

Affected: `StyledMenuItemBase` (and its hoverable variant),
`MenuItemSelect`, `MenuItemSuggestion`, `Chip`.
2026-06-09 08:52:32 +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 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
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
nitin 4ba9c0ca0b [Navigation Drawer] Multiple fixes in settings and app drawer (#20634)
closes -
https://discord.com/channels/1130383047699738754/1487720717192527942



https://github.com/user-attachments/assets/6db2df8b-be01-4b5f-a958-575d87b41559

~~waiting on @Bonapara 's feedback!~~
2026-05-18 15:22:03 +00:00
Etienne 695a374efd fix - nav drawer expansion (#20545)
[PR](https://github.com/twentyhq/twenty/pull/20505), I merge, does not
fix the whole issue

Closes https://github.com/twentyhq/twenty/issues/20502
2026-05-13 16:30:37 +00:00
Charles Bochet ee6c0ef904 Replace sign-in mocked metadata with hardcoded BackgroundMock (#20308)
## Summary

When the user is logged out, we render the auth modal on top of a sample
table to make the empty page feel alive. So far this was achieved by
**loading a full set of mocked object / field / view / navigation-menu
metadata into the runtime metadata store** and then mounting the real
`RecordTable` and `AppNavigationDrawer` behind the modal. This had a few
downsides:

- Significant bundle weight pulled in for unauthenticated users (mocked
GraphQL fixtures + the real `RecordTable` virtualization stack).
- Plenty of code paths that had to know about the "showAuthModal" case
(`useRecordIndexTableQuery`, `useTriggerInitialRecordTableDataLoad`,
`MainContextStoreProvider`, `IsMinimalMetadataReadyEffect`...).
- Any change to metadata-store internals or to the record-table runtime
risked breaking the logged-out background.

This PR replaces the entire flow with a small, self-contained
`BackgroundMock` component tree that **does not consume any metadata**
and **does not load any mocked metadata at runtime**.

### What changed

- New module under `sign-in-background-mock`:
- `BackgroundMockPage` + `BackgroundMockViewBar` + `BackgroundMockTable`
+ `BackgroundMockTableRow` render a hardcoded "Companies" table that
visually mirrors the real one.
- `BackgroundMockNavigationDrawer` renders a hardcoded sidebar with
People / Companies / Opportunities / Tasks / Notes (with their standard
colors).
- Hardcoded constants in `BackgroundMockCompanies.ts`,
`BackgroundMockColumns.ts`, `BackgroundMockNavigationItems.ts`.
- `MinimalMetadataLoadEffect` no longer calls `loadMockedMetadataAtomic`
for unauthenticated users — it just doesn't load anything.
- `IsMinimalMetadataReadyEffect` now reports ready immediately when
there is no access token pair, so the skeleton loader doesn't hang
waiting for metadata that will never come.
- `MainContextStoreProvider`, `useRecordIndexTableQuery`, and
`useTriggerInitialRecordTableDataLoad` drop their `showAuthModal`
branches — the real `RecordTable` is no longer mounted behind the modal.
- `DefaultLayout` and `NotFound` now lazily load `BackgroundMockPage` /
`BackgroundMockNavigationDrawer` instead of the deleted
`SignInBackgroundMockPage` / `SignInAppNavigationDrawerMock`.
- Removed: `SignInBackgroundMockPage`, `SignInBackgroundMockContainer`,
`SignInBackgroundMockContainerEffect`, `SignInAppNavigationDrawerMock`,
`SignInBackgroundMockColumnDefinitions`,
`SignInBackgroundMockCompanies`, `SignInBackgroundMockViewFields`.

`useLoadMockedMetadata` and `preloadMockedMetadata` are kept on purpose:
Storybook decorators (`ObjectMetadataItemsDecorator`,
`WorkflowStepDecorator`) still rely on the mocked metadata fixtures, but
**production** unauthenticated runtime no longer touches them.

### Visual parity

Side-by-side at 1440×900 on `/sign-in`:

**Before** (loads mocked metadata + real RecordTable):

![before](https://github.com/user-attachments/assets/before-placeholder)

**After** (purely hardcoded BackgroundMock):

![after](https://github.com/user-attachments/assets/after-placeholder)

## Test plan

- [ ] `npx nx typecheck twenty-front`  (passes locally)
- [ ] `npx nx lint:diff-with-main twenty-front`  (oxlint + prettier
clean)
- [ ] `npx jest useRecordIndexTableQuery` 
- [ ] Manually verify `/sign-in` renders the table + nav drawer behind
the modal
- [ ] Manually verify `/not-found` still renders the background
- [ ] Verify CI: storybook, unit tests, e2e tests
2026-05-06 11:49:24 +02:00
Abdul Rahman f19b3bfd38 Fix left/right arrow keys not working in dropdown search inputs (#19759)
Closes #12847 

Implements a two-mode focus management pattern for `SelectableList`
components with search inputs, resolving the conflict between text input
cursor movement and grid/list navigation.

### How it works

**Input mode** (search input focused):
- Left/right arrow keys move the text cursor normally
(`enableOnFormTags: false` on ArrowLeft/ArrowRight hotkeys)
- Up/down arrow keys blur the input and transfer focus to the grid,
entering grid mode

**Grid mode** (search input blurred):
- All arrow keys navigate the selectable list grid
- Pressing up arrow from the top row clears the grid selection and
refocuses the search input, returning to input mode
- Typing any printable character refocuses the search input (wildcard
hotkey with `enableOnFormTags: false`)

### Demo 


https://github.com/user-attachments/assets/825ad603-a5f8-4863-8269-3ecf35965847


https://github.com/user-attachments/assets/9d07346d-18a0-40fa-8874-21040c11f03d
2026-04-21 18:25:44 +00:00
Charles Bochet 96fc98e710 Fix Apps UI: replace 'Managed' label with actual app name and unify app icons (#19897)
## Summary

- The Data Model table was labeling core Twenty objects (e.g. Person,
Company) as **Managed** even though they are part of the standard
application. This PR teaches the frontend to resolve an `applicationId`
back to its real application name (`Standard`, `Custom`, or any
installed app), and removes the misleading **Managed** label entirely.
- Introduces a single, consistent way to render an "app badge" across
the settings UI:
- new `Avatar` variant `type="app"` (rounded 4px corners + 1px
deterministic border derived from `placeholderColorSeed`)
- new `AppChip` component (icon + name) backed by a new
`useApplicationChipData` hook
- new `useApplicationsByIdMap` hook + `CurrentApplicationContext` so the
chip can render **This app** when shown inside the matching app's detail
page
- Reuses these primitives on:
- the application detail page header (`SettingsApplicationDetailTitle`)
  - the Installed / My apps tables (`SettingsApplicationTableRow`)
  - the NPM packages list (`SettingsApplicationsDeveloperTab`)
- Backend: exposes a minimal `installedApplications { id name
universalIdentifier }` field on `Workspace` (resolved from the workspace
cache, soft-deleted entries filtered out) so the frontend can resolve
`applicationId` -> name without N+1 fetches.
- Cleanup: deletes `getItemTagInfo` and inlines its tiny
responsibilities into the components that need them, matching the
`RecordChip` pattern.
2026-04-21 00:44:14 +02:00
martmull 381f3ba7d9 Fix app design 1/2 (#19735)
comply with
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=96977-349627&m=dev

## After
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40
37"
src="https://github.com/user-attachments/assets/6d80191a-79a9-4f0f-aa4f-0e447fff4f6d"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40
22"
src="https://github.com/user-attachments/assets/4f763272-027e-4246-b455-7d46babf7d8c"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39
11"
src="https://github.com/user-attachments/assets/b9b35e18-8068-447e-821d-5ec28bb5bd16"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39
05"
src="https://github.com/user-attachments/assets/57d9318a-902f-4fd7-a2a3-5795ebe0b9dc"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39
02"
src="https://github.com/user-attachments/assets/78a33fa8-6bdd-484e-a82d-bd0f7592a623"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38
58"
src="https://github.com/user-attachments/assets/f7987aed-c6e1-4032-a611-86817655137d"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38
55"
src="https://github.com/user-attachments/assets/d1c451ab-1d2d-41e4-a059-cf4303ecabe7"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38
48"
src="https://github.com/user-attachments/assets/593cae36-2320-443f-a955-93b211a6ee3f"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37
40"
src="https://github.com/user-attachments/assets/c9f602b1-8de3-4e82-a3a6-344594a0c153"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37
34"
src="https://github.com/user-attachments/assets/b54ddddf-5dda-46c8-ace3-cffe6015825a"
/>

## before

<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
18"
src="https://github.com/user-attachments/assets/c0976a0a-0124-48ec-8e7c-78627cea7063"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
16"
src="https://github.com/user-attachments/assets/d2db926c-4040-411d-9091-8b60e7c519e6"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
13"
src="https://github.com/user-attachments/assets/2d69f2ff-f26e-4249-91a3-2cf3d261e840"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
07"
src="https://github.com/user-attachments/assets/1028aabc-77ac-4c51-a8c3-9a194faba87f"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
01"
src="https://github.com/user-attachments/assets/1caa9f5e-3eaa-433c-9d3b-e0f094f16e8e"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
56"
src="https://github.com/user-attachments/assets/f42b6976-3a8f-4591-9283-bda79bdb424b"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
53"
src="https://github.com/user-attachments/assets/93d00df8-0091-4dfa-9ac0-f6f376be5962"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
43"
src="https://github.com/user-attachments/assets/9deae7e5-39c1-4518-a463-6d79bc5bf132"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
37"
src="https://github.com/user-attachments/assets/3e21b521-c47d-482c-ad41-66abfe973772"
/>
2026-04-16 09:47:44 +00:00
Raphaël Bosi 3b85747d3f Go back to the original command K button (#19727)
## Before


https://github.com/user-attachments/assets/868e5293-8843-44c8-8011-b7130e97fa95


## After


https://github.com/user-attachments/assets/89cf46cc-1fb1-4e78-bfef-53e1d04e8ebf
2026-04-15 15:53:26 +00:00
Raphaël Bosi 4b3a46d953 Refactor command menu items deprecated code (#19508)
- Removes the intermediate `CommandMenuItemConfig` /
`CommandConfigContext` / `CommandMenuItemDisplay` abstraction layers,
replacing them with a single `CommandMenuItemRenderer` that renders
directly from the command menu items from the backend
- Eliminates the server-items/ subdirectory by moving its contents
(hooks/, contexts/, states/, display/, edit/) up into the parent
command-menu-item/ module, removing an unnecessary nesting level.
2026-04-10 09:28:27 +00:00
nitin 50c5a1a8de [AI] new model tab design (#19384)
closes
https://discord.com/channels/1130383047699738754/1480979892610007121

<img width="1839" height="1316" alt="CleanShot 2026-04-07 at 15 22 38"
src="https://github.com/user-attachments/assets/d8f6048c-4e0f-425f-b7ea-4913d116020e"
/>
2026-04-09 14:15:43 +00:00
Félix Malfait 83d30f8b76 feat: Send email from UI — inline reply composer & SendEmail mutation (#19363)
## Summary

- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.

### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`

### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links

### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order

## Test plan

- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account


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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 08:43:48 +02:00
martmull f8c3960cf2 Disable setting tab when empty (#19315)
## before
<img width="1063" height="520" alt="image"
src="https://github.com/user-attachments/assets/db92432b-a30c-49e8-9246-a09d0815e6c9"
/>


## after
<img width="1049" height="490" alt="image"
src="https://github.com/user-attachments/assets/e926b065-6a65-417b-b802-0e1df6ca638f"
/>
2026-04-03 15:27:17 +00:00
Baptiste Devessier 291792f864 Repair Edit Layout command menu item and add a button in settings to start edition too (#19208)
https://github.com/user-attachments/assets/f23f0777-abf3-4ff4-8fab-ec2004df60bc
2026-04-01 15:24:05 +00:00
Félix Malfait a51b5ed589 fix: resolve settings/usage chart crash and add ClickHouse usage event seeds (#19039)
## Summary

- **Fix settings/usage page crash**: The `GraphWidgetLineChart`
component used on `settings/usage` was crashing with "Instance id is not
provided and cannot be found in context" because it requires
`WidgetComponentInstanceContext` (for tooltip/crosshair component
states) which is only provided inside the widget system. Wraps the
standalone chart usages with the required context provider.
- **Avoid mounting `GraphWidgetLegend` when hidden**: The legend
component calls `useIsPageLayoutInEditMode()` which requires
`PageLayoutEditModeProviderContext` — another context only available
inside the widget system. Since the settings page passes
`showLegend={false}`, the fix conditionally unmounts the legend instead
of always mounting it with a `show` prop. Applied consistently across
all four chart types (line, bar, pie, gauge).
- **Add ClickHouse usage event seeds**: Generates ~400 realistic
`usageEvent` rows spanning the past 35 days with weighted user activity,
weekday/weekend patterns, and gradual ramp-up. Enables developers to see
the usage analytics page with data locally.

## Test plan

- [ ] Navigate to `settings/usage` — page should render without errors
- [ ] Verify the daily usage line chart displays correctly
- [ ] Navigate to a user detail page from the usage list
- [ ] Verify the user detail chart renders without errors
- [ ] Run `npx nx clickhouse:seed twenty-server` and confirm usage
events are seeded
- [ ] Verify chart legend still works correctly on dashboard widgets (no
regression)


Made with [Cursor](https://cursor.com)
2026-03-29 07:31:36 +02:00
nitin afeef8e04a feat: add command menu item to layout customization (#18764)
figma
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=93630-365000&t=QesAkY3JOyI9D3UU-0


https://github.com/user-attachments/assets/cf3b354d-0b08-41ae-91ae-386054b5cb2e



https://github.com/user-attachments/assets/73bfd676-f823-44c9-a70d-107c20523664

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-26 16:04:03 +01:00
Baptiste Devessier cfefe9273b Use record page layouts in the merge records feature (#18961)
There was a design issue. It revealed that we weren't using record page
layouts in the merge records feature.

## Before

<img width="802" height="1283" alt="image"
src="https://github.com/user-attachments/assets/e4238144-e10e-47a6-83e6-7cc03ca89b15"
/>

## After


https://github.com/user-attachments/assets/c0fa9cf5-2b28-4696-bf20-8271dce9e62c
2026-03-26 09:54:33 +00:00
Samuel Arbibe cec23e89fa Fix batch update optimistic and prevent accidental mass-update (#17213)
## Problem
- ⚠️ Multi-edit could silently update ALL records of an object with no
undo, no ctrl-z
- After a batch update the table showed stale data —
`useIncrementalUpdateManyRecords` had no explicit `findMany` refetch and
`skipOptimisticEffect` was missing
- Clicking inside the side panel deselected all kanban cards —
`RecordBoardClickOutsideEffect` uses `refs:[]` and the side panel had no
`data-click-outside-id`
- Clicking a currency/select dropdown inside the panel also triggered
deselection — `FloatingPortal` renders outside the side panel DOM,
bypassing the click-outside-id exclusion
- An empty selection silently matched every record —
`computeContextStoreFilters` returned `undefined` filter when
`selectedRecordIds` was `[]`

## Fix
- Table refreshes correctly after batch update —
`useRefetchFindManyRecords` explicitly refetches `FindMany<Object>`
queries; `useIncrementalUpdateManyRecords` adds `skipOptimisticEffect:
true` and calls it in `finally`
- Clicking the side panel no longer deselects kanban cards —
`SidePanelForDesktop` carries `data-click-outside-id`;
`RecordBoardClickOutsideEffect` +
`RecordTableBodyFocusClickOutsideEffect` exclude it
- Clicking dropdowns inside the panel no longer deselects either —
`ParentClickOutsideIdContext` propagates the side panel ID into
`FloatingPortal` content via `DropdownInternalContainer`
- Empty selection can no longer match all records —
`computeContextStoreFilters` returns `{ id: { in: [] } }` instead of
`undefined`
- Apply is disabled with no selection; a confirmation modal shows the
count + no-undo warning before executing —
`UpdateMultipleRecordsContainer`

## Not included
- Undo / snapshot restore — requires backend changes, out of scope

## Blast radius
- `ParentClickOutsideIdContext` touches `DropdownInternalContainer` (207
`<Dropdown>` usages). `parentClickOutsideId` is `undefined` everywhere
outside the side panel → attribute not rendered → zero behavioral change
for existing consumers.

---------

Co-authored-by: Samuel Arbibe <samuelarbibe@Samuels-MacBook-Pro.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-03-24 17:14:30 +01:00
Thomas Trompette c6d4162b73 Fix: Background color selected row + border bottom settings tab (#18870)
<img width="1285" height="73" alt="Capture d’écran 2026-03-23 à 18 58
09"
src="https://github.com/user-attachments/assets/a01cfc8e-6492-4ebe-a47a-b504a73e616c"
/>
<img width="574" height="59" alt="Capture d’écran 2026-03-23 à 18 58
39"
src="https://github.com/user-attachments/assets/4947ec02-e151-48fb-87e9-86dbc0ed4fe9"
/>
2026-03-23 19:22:28 +00:00
Félix Malfait 77d4bd9158 Add billing usage analytics dashboard with ClickHouse integration (#18592)
## Summary
This PR adds a comprehensive billing usage analytics feature that
provides detailed breakdowns of credit consumption across execution
types, users, resources, and time periods. The implementation includes a
new ClickHouse-backed analytics service, GraphQL API endpoint, and a
frontend dashboard component.

## Key Changes

### Backend
- **New BillingAnalyticsService**: Queries ClickHouse for usage
breakdowns by user, resource, execution type, and time series data
- **BillingEventWriterService**: Writes billing events to ClickHouse for
analytics while maintaining best-effort semantics (never blocks Stripe
billing)
- **ClickHouse Schema**: Added `billingEvent` table with 3-year TTL for
storing detailed billing event data
- **GraphQL Resolver**: New `getBillingAnalytics` query that aggregates
usage data for the current billing period, protected by feature flag and
billing permissions
- **Enhanced BillingUsageEvent**: Added `userWorkspaceId` field to track
per-user credit consumption
- **AI Billing Integration**: Updated AI billing service to pass
`userWorkspaceId` when recording usage events

### Frontend
- **SettingsBillingAnalyticsSection**: New component displaying:
  - Usage breakdown by execution type with progress bars
  - Daily usage time series chart (28-day view)
  - Per-user credit consumption breakdown
  - Per-resource (agent/workflow) credit consumption breakdown
- **SettingsUsage Page**: Dedicated page for viewing usage analytics
- **GraphQL Query**: `GetBillingAnalytics` query with generated hooks
- **Navigation**: Added Usage menu item in settings (feature-flagged)
- **Mock Data**: Included screenshot mock data for preview/testing

### Feature Flag
- Added `IS_USAGE_ANALYTICS_ENABLED` feature flag to control visibility
and access to analytics features

## Implementation Details
- Analytics data is queried in parallel for performance
- ClickHouse writes are non-blocking to ensure billing operations never
fail
- Progress bars use dynamic coloring from a predefined palette
- Time series visualization normalizes bar heights relative to max value
- Empty state handling when no analytics data is available
- Responsive UI with proper text truncation for long names

https://claude.ai/code/session_01Y1EqrX6PFq3EJxJq89h7DF

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 10:28:23 +01:00
Charles Bochet 9f7c29bce8 refactor(twenty-front): unify Favorites and Workspace navigation menu item code (#18697)
## Summary

- Consolidate duplicated Favorites and Workspace navigation menu item
frontend code into a single unified codebase within
`navigation-menu-item/`
- Move all DnD-related code from `navigation/` module into
`navigation-menu-item/display/dnd/`, renaming `workspaceDndKit*` files
to `navigationMenuItemDndKit*`
- Unify duplicated components (DnD providers, DnD hooks, folder
components, orphan items, section shell) using a `NavigationSections`
enum to parameterize section-specific behavior
- Rename residual workspace-prefixed symbols
(`WorkspaceDndKitSortableItem`, `WorkspaceDndKitDroppableSlot`,
`useWorkspaceSectionItems`, etc.) to `navigationMenuItem`-prefixed
equivalents
- Clean `navigation/` module to only contain app-level concerns (drawer
layout, settings, routing)

### Key changes

| Before (duplicated) | After (unified) |
|---|---|
| `FavoritesDndKitProvider` + `WorkspaceDndKitProvider` |
`NavigationMenuItemDndKitProvider` with `section` prop |
| `useFavoritesDndKit` + `useWorkspaceDndKit` |
`useNavigationMenuItemDndKit(section)` |
| `FavoritesFolderItem` + `WorkspaceNavigationMenuItemsFolder` |
`NavigationMenuItemFolder` with `section` prop |
| `FavoritesOrphanItems` | `NavigationMenuItemOrphanItems` with
`section` prop |
| Separate section shells | Shared `NavigationMenuItemSection` with thin
wrappers |
| `WorkspaceDndKitSortableItem` | `NavigationMenuItemSortableItem` |
| `WorkspaceDndKitDroppableSlot` | `NavigationMenuItemDroppableSlot` |
| `useWorkspaceSectionItems` | `useNavigationMenuItemSectionItems` |
| `useWorkspaceFolderOpenState` | `useNavigationMenuItemFolderOpenState`
|

Net result: **~1650 lines deleted** across 51 files.

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint twenty-front` passes (0 errors)
- [x] `npx nx test twenty-front` passes (763 suites, 4467 tests)
- [ ] Smoke test: Favorites section renders, DnD reorder works, folder
create/rename/delete works
- [ ] Smoke test: Workspace section renders, edit mode works, DnD
reorder works
- [ ] Smoke test: Add-to-navigation from side panel works for both
sections

Made with [Cursor](https://cursor.com)
2026-03-17 19:01:09 +01:00
nitin ab881350b2 refactor: unify layout customization mode (record pages + navigation) (#18640)
### What

Unifies record page layout editing and navigation menu editing into a
single global "layout customization" session. Dashboard editing stays
separate.

  ### How it works

  **Two edit mode systems, one context-based read:**
- `isLayoutCustomizationModeEnabledState` -- global atom for record
pages + navigation
- `isDashboardInEditModeComponentState` -- dashboard-only, independent
per-component atom
- `PageLayoutEditModeProvider` -- context that dispatches to
`RecordPageLayoutEditModeProvider` (reads global atom) or
`DashboardPageLayoutEditModeProvider` (reads component atom), one
component per file

  **Session registry + independent atoms:**
- `activeCustomizationPageLayoutIdsState` -- accumulates page layout IDs
as user navigates during customization (`string[]`)
- Save/cancel iterate the ID list and read each layout's draft/persisted
atoms independently
- Follows the same pattern as `settingsRoleIdsState` +
`settingsDraftRoleFamilyState`

  **Unified UI:**
- `LayoutCustomizationBar` replaces the old `NavigationMenuEditModeBar`
- Enter once -- edit record layouts + navigation -- save/cancel
everything together
- `useSaveLayoutCustomization` orchestrates sequential save: navigation
draft -- page layouts -- field widget groups
- Error snackbar on partial save failure (with TODO for future atomic
server mutation)

  **Draft protection during customization:**
- `PageLayoutRelationWidgetsSyncEffect` guarded -- only updates
persisted state from server, skips draft/currentLayouts while
customization is active
- `useExecuteTasksOnAnyLocationChange` skips draft reset when
customization mode is enabled
  - Command execution blocked during layout customization

  ### Cleanup
- Deleted `NavigationMenuEditModeBar`,
`isNavigationMenuInEditModeState`,
`isPageLayoutInEditModeComponentState`,
`useIsGlobalLayoutCustomizationActive`
- `DraftPageLayout` type changed from `Omit` to `Pick` (explicit fields)
- Removed save/cancel from `DefaultRecordCommandMenuItemsConfig` (bar
handles it now)
  - Extracted `useSaveFieldsWidgetGroups` from save orchestration
- Split `PageLayoutEditModeProvider` into 3 separate files (one
component per file, Twenty convention)

  ### Known issues
- **Stale deleted widget after save (pre-existing on `main`)**: Delete
widget -- save -- exit customization -- Apollo cache stale -- sync
effect overwrites Jotai from stale data -- widget reappears until
refresh. Separate PR needed, likely tied to the planned server-side
`saveLayoutCustomization` atomic endpoint.

  ### Open questions
- **Module location**: Layout customization hooks/states live in `/app`
-- should they move to their own `modules/layout-customization/`?
- **Atomic server mutation**: All save mutations are on metadata schema
(`createNavigationMenuItem`, `deleteNavigationMenuItem`,
`updateNavigationMenuItem`, `updatePageLayoutWithTabsAndWidgets`,
`upsertFieldsWidget`). A single `saveLayoutCustomization` endpoint could
make saves truly atomic.


https://github.com/user-attachments/assets/036ef542-97f3-485b-a68f-3726002c81fb
2026-03-17 16:24:09 +01:00
Charles Bochet f1dfcfd163 refactor(twenty-front): reorganize NavigationMenuItem module into common/display/edit subfolders (#18691)
## Summary

- Reorganizes the `navigation-menu-item` frontend module from a flat
structure into `common/`, `display/`, and `edit/` subfolders with
type-specific subdirectories (`link/`, `folder/`, `object/`, `view/`,
`record/`)
- Every file is now in a leaf folder describing its type: `components/`,
`hooks/`, `utils/`, `types/`, or `constants/`
- Moves 14 NavigationMenuItem-related components out of
`object-metadata/` and `side-panel/pages/` into the
`navigation-menu-item` module where they belong
- Creates type-specific display utility functions (e.g.,
`getLinkNavigationMenuItemLabel`,
`getObjectNavigationMenuItemComputedLink`) to replace generic
switch-based functions
- Unifies the Favorites section drag-and-drop from `@hello-pangea/dnd`
to `@dnd-kit/react`, matching the Workspace section's DnD library
- Renames Favorites section components from `CurrentWorkspaceMember*` to
`Favorites*` for clarity
- Deletes unused `FavoritesDragDropProviderContent` and
`NavbarDragProvider`

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes (0 warnings, 0
errors)
- [x] `npx nx test twenty-front` passes (763 suites, 4467 tests)
- [ ] Verify favorites drag-and-drop still works in the UI (reorder
items, move between folders)
- [ ] Verify workspace edit mode drag-and-drop still works
- [ ] Verify "add to navigation" drag from command menu/side panel still
works
2026-03-17 06:46:50 +01:00
Marie c1da7be6d7 Billing for self-hosts (#18075)
## Summary

Implements enterprise licensing and per-seat billing for self-hosted
environments, with Stripe as the single source of truth for subscription
data.

### Components

- **twenty-website** hosts the private key to sign `ENTERPRISE_KEY` and
`ENTERPRISE_VALIDITY_TOKEN`. It communicates with Stripe to emit the
daily `ENTERPRISE_VALIDITY_TOKEN` if the subscription is active, based
on the user's Stripe subscription ID stored in `ENTERPRISE_KEY`.
- **Stripe** is the single source of truth for subscription data
(status, seats, billing).
- **The client** (twenty-server + DB + workers) saves `ENTERPRISE_KEY`
in the `keyValuePair` table (or `.env` if
`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false) and the daily-renewed
`ENTERPRISE_VALIDITY_TOKEN` in the `appToken` table.
`ENTERPRISE_VALIDITY_TOKEN` is verified client-side using a public key
to grant access to enterprise features (RLS, SSO, audit logs, etc.).

### Flow

1. When requesting an upgrade to an enterprise plan (from **Enterprise**
in settings), the user is shown a modal to choose monthly/yearly
billing, then redirected to Stripe to enter payment details. After
checkout, they land on twenty-website where they are exposed to their
`ENTERPRISE_KEY`, which they paste in the UI. It is saved in the
`keyValuePair` table. On activation, a first `ENTERPRISE_VALIDITY_TOKEN`
with 30-day validity is stored in the `appToken` table.

2. **Every day**, a cron job runs and does two things:
- **Refreshes the validity token**: communicates with twenty-website to
get a new `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity if the Stripe
subscription is still active. If the subscription is in cancellation,
the emitted token has a validity equal to the cancellation date. If it's
no longer valid, the token is not replaced. The cron only needs to run
every 30 days in practice, but runs daily so it's resilient to
occasional failures.
- **Reports seat count**: counts active (non-soft-deleted)
`UserWorkspace` entries and sends the count to twenty-website, which
updates the Stripe subscription quantity with proration. Seats are also
reported on first activation. If the subscription is canceled or
scheduled for cancellation, the seat update is skipped.

3. `ENTERPRISE_VALIDITY_TOKEN` is verified server-side via a public key
to grant access to enterprise features.

### Key concepts

Three distinct checks are exposed as GraphQL fields on `Workspace`:

| Field | Meaning |
|---|---|
| `hasValidEnterpriseKey` | Has any valid enterprise key (signed JWT
**or** legacy plain string) |
| `hasValidSignedEnterpriseKey` | `ENTERPRISE_KEY` is a properly signed
JWT (billing portal makes sense) |
| `hasValidEnterpriseValidityToken` | `ENTERPRISE_VALIDITY_TOKEN` is
present and not expired (expiration depends on signed token payload, not
on "expiresAt" on appToken table which is only indicative) |

Feature access is gated by `isValid()` =
`hasValidEnterpriseValidityToken || hasValidEnterpriseKey` (to support
both new and legacy keys during transition). After transition isValid()
= hasValidEnterpriseValidityToken

### Frontend states

The Enterprise settings page handles multiple states:
- **No key**: show "Get Enterprise" with checkout modal
- **Orphaned validity token** (token valid but no signed key): prompt
user to set a valid enterprise key
- **Active/trialing but no validity token**: show subscription status
with a "Reload validity token" action
- **Active/trialing**: show full subscription info, billing portal
access, cancel option
- **Cancellation scheduled**: show cancellation date, billing portal
- **Canceled**: show billing history link and option to start a new
subscription
- **Past due / Incomplete**: prompt to update payment or restart

### Temporary retro-compatibility: legacy plain-text keys

Previously, enterprise features were gated by a simple check: any
non-empty string in `ENTERPRISE_KEY` granted access. With this PR, we
transition to a controlled system relying on signed JWTs.

To avoid breaking existing self-hosted users:
- **Legacy plain-text keys still grant access** to enterprise features.
`hasValidEnterpriseKey` returns `true` for both signed JWTs and plain
strings, and `isValid()` checks `hasValidEnterpriseKey` as a fallback
when no validity token is present.
- **A deprecation banner** is shown at the top of the app when
`hasValidEnterpriseKey` is `true` but `hasValidSignedEnterpriseKey` is
`false`, informing the user that their key format is deprecated and they
should activate a new signed key.
- **No billing portal or subscription management** is available for
legacy keys since there is no Stripe subscription to manage.

This retro-compatibility will be removed in a future version. At that
point, `isValid()` will only check `hasValidEnterpriseValidityToken`.

### Edge cases

- **Air-gapped / production environments**: for self-hosted clients that
block external traffic (or for our own production), provide a long-lived
`ENTERPRISE_VALIDITY_TOKEN` (e.g. 99 years) directly in the `appToken`
table, with no `ENTERPRISE_KEY`. The daily cron will skip the refresh
(no enterprise key to authenticate with), but the pre-seeded validity
token will be used to grant feature access. No billing or seat reporting
occurs in this mode.
- **`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false**: if the user tries to
activate an enterprise key but DB config writes are disabled, the
backend returns a clear error asking them to add `ENTERPRISE_KEY` to
their `.env` file manually.
- **Canceled subscriptions**: the `/seats` endpoint skips Stripe updates
for canceled or cancellation-scheduled subscriptions to avoid Stripe API
errors.

### How to test
- launch twenty-website on a different url (eg localhost:1002)
- add ENTERPRISE_API_URL=http://localhost:3002/api/enterprise (or else)
in your server .env
- ask me for twenty-website's .env file content (STRIPE_SECRET_KEY;
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID;STRIPE_ENTERPRISE_YEARLY_PRICE_ID;
ENTERPRISE_JWT_PRIVATE_KEY; ENTERPRISE_JWT_PUBLIC_KEY;
NEXT_PUBLIC_WEBSITE_URL)
- visit Admin panel / enterprise
2026-03-12 15:07:53 +01:00
Thomas Trompette 6e628bb751 Fix add more tab bottom separator (#18529)
Adding a full height prop

Before 
<img width="409" height="139" alt="Capture d’écran 2026-03-10 à 14 06
13"
src="https://github.com/user-attachments/assets/6f9bb8d1-8834-44b9-a0cf-1253dffc9ea0"
/>

After
<img width="409" height="139" alt="Capture d’écran 2026-03-10 à 14 06
33"
src="https://github.com/user-attachments/assets/1d3551fe-0b4b-4bbf-88e4-fd466c9bb704"
/>
2026-03-10 18:08:51 +01:00
neo773 dd58eb6814 Fix linaria css regressions (#18492)
before
<img width="310" height="60" alt="SCR-20260309-ctul"
src="https://github.com/user-attachments/assets/7141d495-b8f2-4fd6-bf3b-36bb2b11d1aa"
/>

after (fixed icon alignment)
<img width="235" height="67" alt="SCR-20260309-ctel"
src="https://github.com/user-attachments/assets/36078039-93dc-4c2c-b553-0bbcde4cb81c"
/>

before
<img width="637" height="318" alt="SCR-20260309-ctnp"
src="https://github.com/user-attachments/assets/34b66129-d619-43a2-8896-aa92b511644e"
/>

after (fixed chart colors)
<img width="650" height="317" alt="SCR-20260309-cthj"
src="https://github.com/user-attachments/assets/82c095b1-34bb-4ae4-a8f2-7a3746a31b0a"
/>


before



<img width="909" height="650" alt="image"
src="https://github.com/user-attachments/assets/14649aed-bfa8-4b9d-aa35-f4de2bfaddd6"
/>


after (fixed buttons text color)
<img width="930" height="646" alt="SCR-20260309-csob"
src="https://github.com/user-attachments/assets/c724a849-dabe-406c-8258-0674211374f2"
/>

before

<img width="544" height="141" alt="image"
src="https://github.com/user-attachments/assets/815c3b70-2f7c-42ca-8a32-3fbd5fe4c556"
/>


after (fixed missing border on :active state)

<img width="554" height="145" alt="image"
src="https://github.com/user-attachments/assets/845b1afd-36b6-4ae4-b6ef-c49ccbd89c10"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-10 16:26:53 +01:00
BugIsGod 033d297695 feat: add visual time picker to DateTimePicker (#15057) (#17952)
## Summary
Added a visual time picker dropdown for DateTime fields, replacing the
previous text input. Users can now select hours and minutes through an
intuitive scrollable interface. (Fixes #15057

 ## Changes
- **New component**: Add a `TimePickerDropdown` - Visual picker with
scrollable hour/minute columns
- **Updated**: `DateTimePickerHeader` - Implemented time picker dropdown
in `DateTimePickerHeader` and Move month/year picker to right side
  ## Snapshots
  
<img width="493" height="421" alt="image"
src="https://github.com/user-attachments/assets/3bd1f0a0-0ac2-473d-935e-d9f28b0e40e2"
/>


https://github.com/user-attachments/assets/daa5cba5-c86c-46aa-a634-0f5c04523af1



**If there is no enough place at right, auto-move month/year selector to
the left side**

Hi, @Bonapara I followed the Figma you shared to complete this feature.
Could you please review it for me? Thanks a lot.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-03-10 13:39:46 +00:00
Arun 1656bb5568 [Feat] : add source to actor fields (#18118)
fixes #18099 

Simple implementation of the matchingSourceValues to be searched in the
ACTOR case in turnRecordFilterIntoRecordGqlOperationFilter

<img width="1239" height="494" alt="Screenshot 2026-02-20 at 5 21 14 PM"
src="https://github.com/user-attachments/assets/20ee076e-dccd-4747-a1ab-38d649f6591e"
/>

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-03-10 13:08:18 +00:00
Charles Bochet ef499b6d47 Re-enable disabled lint rules and right-size CI runners (#18461)
## Summary

- Re-enable one lint rule that was temporarily disabled during the
ESLint-to-Oxlint migration:
- **`twenty/sort-css-properties-alphabetically`** in twenty-front — 578
violations auto-fixed across 390 files
- Document why **`typescript/consistent-type-imports`** cannot be
auto-fixed in twenty-server: NestJS relies on `emitDecoratorMetadata`
for DI, so converting constructor parameter imports to `import type`
erases them at compile time and breaks dependency injection at runtime
- Right-size CI runners, reducing 8-core usage from 18 jobs to 3:

| Change | Jobs | Rationale |
|--------|------|-----------|
| **Keep 8-core** | `ci-merge-queue/e2e-test`,
`ci-front/front-sb-build`, `ci-front/front-build` | Heavy builds needing
max CPU + memory (10GB NODE_OPTIONS, full Storybook webpack bundling) |
| **8-core → 4-core** | `ci-server` (build, lint-typecheck, validation,
test, integration-test), `ci-front/front-sb-test`,
`ci-zapier/server-setup`, `ci-sdk/sdk-e2e-test` | Already sharded into
10-12 parallel instances, I/O-bound (DB/Redis), or moderate single
builds |
| **8-core → 2-core** | `ci-emails/emails-test` | Trivially lightweight
(build + curl health check) |
| **Removed** | `ci-front/front-chromatic-deployment` | Dead code —
permanently disabled with `if: false` |

- Fix merge queue CI issues:
- **Concurrency**: Use `merge_group.base_ref` instead of unique merge
group ref so new queue entries cancel previous runs
- **Required status checks**: Add `merge_group` trigger to all 6
required CI workflows (front, server, shared, website, docker-compose,
sdk) with `changed-files-check` auto-skipped for merge_group events —
status check jobs auto-pass without re-running full CI
- **Build caching**: Add Nx build cache restore/save to E2E test job
with fallback to `main` branch cache for faster frontend and server
builds

## Test plan

- [ ] CI passes on this PR (verifies lint rule auto-fix works)
- [ ] Verify 4-core runner jobs complete within their 30-minute timeouts
- [ ] Verify merge queue status checks auto-pass (ci-front-status-check,
ci-server-status-check, etc.)
- [ ] Verify merge queue E2E concurrency cancels previous runs when a
new PR enters the queue
2026-03-06 13:33:02 +00:00
Charles Bochet 364c944ca6 Improve build performance 2x (#18449)
## Summary

Front Before:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/b978f67c-c0a6-49fc-bedd-a443f11c365d"
/>

Front After:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/a4939dbb-a8b4-4c74-978c-daa7f27d00f3"
/>


Server Before:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/da53e97f-ec65-4224-a656-ca41040aef6e"
/>


Server After:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/8cdf3885-f515-4d6c-989f-a421a4e8206c"
/>


### CI Server Pipeline Restructuring
- Split monolithic `server-setup` job into three parallel jobs:
`server-build`, `server-lint-typecheck`, and `server-validation`
- `server-build` only handles build + Nx cache save (~1m vs old 3.5m),
unblocking downstream jobs faster
- `server-lint-typecheck` runs in parallel with no DB dependency
- `server-validation` handles DB setup, migration checks, and GraphQL
generation checks in parallel with tests
- Make `server-test` (unit tests) fully independent — no longer waits
for server-setup, builds its own artifacts
- Increase integration test shards from 8 to 10 for better parallelism
- Expected critical path reduction: ~10m → ~7m (~30% faster)

### CI Front Pipeline Improvements
- Use artifact upload/download for storybook build instead of rebuilding
in test shards
- Serve pre-built storybook via `http-server` in test jobs, with
`STORYBOOK_URL` env var
- Update `vitest.config.ts` to use `storybookUrl` when `STORYBOOK_URL`
is set
- Remove redundant `twenty-shared`, `twenty-ui`, `twenty-sdk` builds
from storybook test shards

### Vite Build Optimizations
- Conditionally enable `rollup-plugin-visualizer` behind `ANALYZE=true`
env var (not loaded by default)
- Broaden Istanbul coverage exclusions to skip test files, stories,
mocks, and decorators
- Remove `@tabler/icons-react` alias from twenty-front and storybook
configs
- Bundle `@tabler/icons-react` into twenty-ui instead of treating it as
an external dependency
- Add lazy loading with `React.lazy` + `Suspense` for all page-level
route components in `useCreateAppRouter`
2026-03-06 11:02:26 +01:00
Charles Bochet 9d57bc39e5 Migrate from ESLint to OxLint (#18443)
## Summary

Fully replaces ESLint with OxLint across the entire monorepo:

- **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint
configs (`.oxlintrc.json`) for every package: `twenty-front`,
`twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`,
`twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`,
`twenty-apps/*`, `create-twenty-app`
- **Migrated custom lint rules** from ESLint plugin format to OxLint JS
plugin system (`@oxlint/plugins`), including
`styled-components-prefixed-with-styled`, `no-hardcoded-colors`,
`sort-css-properties-alphabetically`,
`graphql-resolvers-should-be-guarded`,
`rest-api-methods-should-be-guarded`, `max-consts-per-file`, and
Jotai-related rules
- **Migrated custom rule tests** from ESLint `RuleTester` + Jest to
`oxlint/plugins-dev` `RuleTester` + Vitest
- **Removed all ESLint dependencies** from `package.json` files and
regenerated lockfiles
- **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in
`nx.json` and per-project `project.json` to use `oxlint` commands with
proper `dependsOn` for plugin builds
- **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more
ESLint executor
- **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with
`oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and
format-on-save with Prettier
- **Replaced all `eslint-disable` comments** with `oxlint-disable`
equivalents across the codebase
- **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint
- **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules`

### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`)

| Rule | Package | Violations | Auto-fixable |
|------|---------|-----------|-------------|
| `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes
|
| `typescript/consistent-type-imports` | twenty-server | 3814 | Yes |
| `twenty/max-consts-per-file` | twenty-server | 94 | No |

### Dropped plugins (no OxLint equivalent)

`eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`,
`import/order`, `prefer-arrow/prefer-arrow-functions`,
`eslint-plugin-mdx`, `@next/eslint-plugin-next`,
`eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial
coverage for `jsx-a11y` and `unused-imports`.

### Additional fixes (pre-existing issues exposed by merge)

- Fixed `EmailThreadPreview.tsx` broken import from main rename
(`useOpenEmailThreadInSidePanel`)
- Restored truthiness guard in `getActivityTargetObjectRecords.ts`
- Fixed `AgentTurnResolver` return types to match entity (virtual
`fileMediaType`/`fileUrl` are resolved via `@ResolveField()`)

## Test plan

- [x] `npx nx lint twenty-front` passes
- [x] `npx nx lint twenty-server` passes
- [x] `npx nx lint twenty-docs` passes
- [x] Custom oxlint rules validated with Vitest: `npx nx test
twenty-oxlint-rules`
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx typecheck twenty-server` passes
- [x] CI workflows trigger correctly with `dependsOn:
["twenty-oxlint-rules:build"]`
- [x] IDE linting works with `oxc.oxc-vscode` extension
2026-03-06 01:03:50 +01:00
Charles Bochet 4797f97a95 fix: vertical alignment of +N More tab overflow button (#18446)
## Summary
- Add `align-items: center` to tab list `StyledContainer` so the
overflow button aligns vertically with tabs
- Remove ineffective `> * { height }` hack from `TabMoreButton` (was
being reset by `all: unset` in `StyledTabButton`)

## Test plan
- Open a record detail page with enough tabs to trigger the "+N More"
overflow
- Verify the overflow button is vertically centered with the visible
tabs
2026-03-05 22:20:14 +01:00
Charles Bochet c53a13417e Remove all styled(Component) patterns in favor of parent wrappers and props (#18430)
## Summary

Eliminates all ~350 `styled(Component)` usages across `twenty-front` and
`twenty-ui` (212 files changed). Each was replaced following these
rules:

- **Margin/layout CSS** (margin, padding, flex, align-self, width) →
wrapped in a `styled.div`/`styled.span` parent container
- **Third-party components** (Link, TextareaAutosize,
ReactPhoneNumberInput, Handle, etc.) → parent container with child CSS
selectors (`> a`, `> textarea`, `> input`, etc.)
- **Intrinsic behavior via existing props** (TableRow
`gridTemplateColumns`, TableCell `color`/`align`) → replaced
`styled(TableRow)` / `styled(TableCell)` with direct prop usage
- **Other visual overrides on twenty-ui components** (Card, Section,
TabList, Button, MenuItem, ScrollWrapper, etc.) → parent wrappers with
`> div` / `> *` child selectors
- **Extending styled.div/span** → merged all CSS into a single
`styled.div`/`styled.span`

Also adds `overflow: hidden` to parent containers wrapping
`ScrollWrapper` so scroll activates correctly with the new wrapper
structure.

### Migration patterns

| Before | After |
|--------|-------|
| `styled(Avatar)` with `margin-right` | `<StyledAvatarContainer><Avatar
/></StyledAvatarContainer>` |
| `styled(Link)` with `text-decoration: none` |
`<StyledLinkContainer><Link /></StyledLinkContainer>` with `> a { ... }`
|
| `styled(TableRow)` with `grid-template-columns` | `<TableRow
gridTemplateColumns="..." />` |
| `styled(TableCell)` with `color` / `align` | `<TableCell color={...}
align="right" />` |
| `styled(Card)` with `margin-top` | `<StyledCardContainer><Card
/></StyledCardContainer>` |
| `styled(TabList)` with `background` |
`<StyledTabListContainer><TabList /></StyledTabListContainer>` with `>
div { ... }` |
| `styled(StyledBase)` extending a `styled.div` | Single merged
`styled.div` with all styles inlined |
2026-03-05 18:16:25 +01:00
nitin 5853891b02 refactor!: rename Command Menu page/navigation layer to Side Panel (#18393) 2026-03-05 15:46:31 +01:00
Charles Bochet 647c32ff3e Deprecate runtime theme objects in favor of CSS variables (#18402)
## Summary

- **Eliminate `ICON_SIZES` / `ICON_STROKES` constants**: all icon
dimensions are now resolved at runtime via
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.X)`, ensuring
values always come from computed CSS variables
- **No more consumer imports from `twenty-ui/theme`**: moved
`ColorSchemeContext`, `ColorSchemeProvider`, `ThemeColor`,
`MAIN_COLOR_NAMES`, `getNextThemeColor`, `AnimationDuration` to
`twenty-ui/theme-constants`
- **Remove `ThemeContext` / `ThemeContextProvider` / `ThemeProvider` /
`ThemeType`**: replaced across ~300 files with `themeCssVariables` (for
CSS contexts) or `resolveThemeVariable` / `resolveThemeVariableAsNumber`
(for JS runtime values)
- **Simplify provider chain**: only `ColorSchemeProvider` remains — it
toggles `light`/`dark` class on `document.documentElement` and provides
`colorScheme` via React context
- **Fix pre-existing test failures**: `useIcons.test.ts`
(non-configurable ES module spy) and
`turnRecordFilterGroupIntoGqlOperationFilter.test.ts`
(`Omit<RecordFilter, 'id'>` type mismatch)

### Theme access pattern (before → after)

| Context | Before | After |
|---------|--------|-------|
| CSS (Linaria) | `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| JS runtime (icon size, animation) | `theme.icon.size.md` /
`ICON_SIZES.md` |
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.md)` |
| Color scheme check | `theme.name === 'dark'` |
`useContext(ColorSchemeContext).colorScheme === 'dark'` |
2026-03-05 14:39:01 +01:00
Charles Bochet eda905f271 [DevXP] Improve Linaria pre-build speed (#18382)
## Summary

This PR improves Linaria/WYW pre-build speed and continues the migration
of `twenty-ui` components away from runtime `ThemeContext` reads toward
static CSS variables and theme constants.

### Linaria/WYW profiling plugin improvements (`twenty-shared`)

- **Babel JIT warmup**: added a `buildStart` warmup step that triggers
WYW's Babel JIT compilation before the real build starts, so the first
real file doesn't pay the cold-start penalty
- **`configResolved` hook**: detects dev vs prod mode and resolves the
correct warmup file path relative to `config.root`
- **Dev-only per-file logging**: slow file warnings are now gated behind
`isDevMode`, keeping production/CI build output clean
- **`closeBundle` summary**: moved the final top-slow-files report to
`closeBundle` for accurate end-of-build reporting
- **Removed noisy progress interval logging** in favor of the warmup log
+ final summary

### Migration from `ThemeContext` to static CSS variables / constants

Across `twenty-ui`, replaced runtime `useTheme()` reads with:
- `themeCssVariables` CSS custom properties (colors, spacing)
- Hard-coded design-system constants (`ICON.size.md` → `16`,
`ICON.stroke.sm` → `1.6`) so components no longer need a React context
at render time — enabling Linaria static extraction

**Components migrated:**
- `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`,
`AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon`
- `ProgressBar` (Framer Motion width animation → CSS `transition`)
- `Info`, `HorizontalSeparator`, `LinkChip`
- `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`,
`NavigationBarItem`
- `JsonArrow`, `JsonNestedNode`
- `ModalHeader`

### Other
- Added `aria-valuenow` to `ProgressBar` for accessibility
- `VisibilityHidden` component updated to inline accessibility styles
2026-03-04 17:04:16 +01:00
Raphaël Bosi b11f77df2a [FRONT COMPONENTS] Introduce conditionalAvailabilityExpression to command menu items (#18319)
## PR Description

- Uses `expr-eval` to enable front components (SDK plugins) to define
conditional availability as declarative expressions.
- Moves shared types and constants to `twenty-shared`
- Introduces a `conditionalAvailabilityExpression` field on
`CommandMenuItemEntity`, allowing command menu items to store an
`expr-eval` compatible expression string that is evaluated against a
CommandMenuContext to determine if the item should be shown.
- Creates an esbuild transform plugin
`conditional-availability-transform-plugin` in `twenty-sdk` that
converts TypeScript conditional availability expressions into
`expr-eval` compatible syntax at build time, so SDK developers can write
natural TS expressions that get transformed to evaluable strings.
- Removes deprecated `forceRegisteredActionsByKey` state and its usage.
- Creates `useCommandMenuContext` hook that builds the full
`CommandMenuContext` object from React state, which is then passed to
`useCommandMenuItemFrontComponentActions` for evaluating conditional
availability expressions.
2026-03-04 16:33:58 +01:00
Charles Bochet 3b2bf39565 Refactor modal (#18377)
## Summary

- Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`,
`ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as
stateless, reusable components
- Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai
state (`isModalOpenedComponentState`) to the stateless `Modal` via an
`isOpen` prop
- Rename `modalVariant` prop to `overlay` with clearer values: `'dark'`
(default), `'light'` (in-container), `'transparent'` (invisible panel).
Remove unused `'medium'` overlay
- Rename `modalId` to `modalInstanceId` across the entire modal zone
(~30 consumer files)
- Extract `ModalProps` to its own file in
`twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to
its own file using `Pick<ModalProps, ...>` for shared props
- Extract `ModalBackdrop` to its own file and export from `twenty-ui`;
use it in `UserOrMetadataLoader` instead of a local styled component
- Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in
`SpreadsheetImportStepperContainer` instead of duplicated `styled.div`
definitions
- Remove unused `onClose` prop from stateless `Modal`; fix `typeof
document` guard in `ModalStatefulWrapper`
- Split shared types into individual files: `ModalSize.ts`,
`ModalPadding.ts`, `ModalOverlay.ts`
- Extract wyw profiling instrumentation from `vite.config.ts` into
reusable `createWywProfilingPlugin` with parametrized threshold and
improved logging
- Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`,
`ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front`
- Add comprehensive Storybook stories in `twenty-ui` covering Default,
Confirmation, Small, ExtraLarge, Closed, and Interactive variants
2026-03-04 13:22:31 +01:00
Charles Bochet 7a2e397ad1 Complete linaria migration (#18361)
## Summary

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

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

## Changes

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

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

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

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

## How it works

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

- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
2026-03-04 00:50:06 +01:00
Charles Bochet 3bfdc2c83f chore(twenty-front): migrate command-menu, workflow, page-layout and UI modules from Emotion to Linaria (PR 4-6/10) (#18342)
## Summary

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

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

### Migration patterns applied

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

### After this PR

- **Remaining files**: ~400 (object-record: ~160, settings: ~200, UI:
~44)
- **No breaking changes**: CSS variables resolve identically to the
previous Emotion theme values
2026-03-03 16:42:03 +01:00
Charles Bochet 802a5b0af6 chore(twenty-front): migrate auth, activities, AI, pages and small modules from Emotion to Linaria (PR 2-3/10) (#18328)
## Summary

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

## Test plan

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


Made with [Cursor](https://cursor.com)
2026-03-03 11:17:47 +01:00
Charles Bochet c4140f85df chore(twenty-front): migrate small modules from Emotion to Linaria (PR 1/10) (#18314)
## Emotion → Linaria migration — PR 1 of 10

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

### Modules migrated

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

### Migration pattern

Every file follows the same mechanical transformation:

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

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

### Gotchas encountered & fixed

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