Commit Graph

473 Commits

Author SHA1 Message Date
Shinu Cherian b81ca99162 fix(twenty-front): hide layout editor UI when SystemPermissionFlag.LAYOUTS is missing (#23303) (#23343)
## Description
Fixes #23303.

This PR ensures that the layout editor UI and customization entry points
are hidden and protected when a user lacks the
`SystemPermissionFlag.LAYOUTS` permission flag.

### Changes Made:
1. **`useEnterLayoutCustomizationMode.ts`**: Added
`useHasPermissionFlag(PermissionFlagType.LAYOUTS)` check inside
`enterLayoutCustomizationMode` to return `false` and prevent entering
customization mode if the user lacks permission.
2. **`WorkspaceSection.tsx`**: Updated the sidebar `WorkspaceSection`
component to render the layout edit button (`IconTool`) only when
`hasLayoutsPermission` is `true`.
3. **`ObjectLayout.tsx`**: Disabled customize and reset layout controls
in the Data Model Object Details settings page if the user lacks
`LAYOUTS` permission.
4. **`useEnterLayoutCustomizationMode.test.tsx`**: Added unit tests to
verify that `useEnterLayoutCustomizationMode` correctly guards layout
customization initialization based on permission.

## Testing
- Added unit tests for `useEnterLayoutCustomizationMode` permission
checks.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23343?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-07-27 12:57:24 +00:00
Guillaume Flambard 710d4da4b1 fix(emails): bump @react-email/render to ^2.0.6 to fix empty transactional email bodies (#23323)
## Problem

Fixes #23307. Every transactional email (workspace invite, password
reset,
email verification, etc.) is delivered with an **empty body** — no
title, text,
or CTA.

## Root cause

`twenty-server` pins `@react-email/render` directly at `^1.2.3`:

```jsonc
// packages/twenty-server/package.json
"@react-email/render": "^1.2.3",
```

In 1.2.3, `render()` reads `renderToReadableStream` **before** the email
template's async Suspense boundary (i18n/locale load) has resolved. The
result
is the Suspense fallback marker instead of the real markup:

```html
<!DOCTYPE html ...><!--$!--><template></template><!--/$-->
```

This was fixed upstream in `@react-email/render@2.0.6`
(*"await stream.allReady before reading renderToReadableStream
output"*).
`twenty-emails` already resolves a 2.x render via `react-email@6.5.0`,
so the
server's direct pin was simply stale — the two were out of sync.

## Fix

Bump the direct pin to `^2.0.6` (resolves to `2.1.0`) and regenerate the
lockfile. The server's `render()` imports now use the fixed 2.x.

> Note: a `1.2.3` entry remains in `yarn.lock` — it is an internal
transitive
> pin of `@react-email/components@0.5.3`, not the server render path, so
it is
> expected and harmless.

## Verification

Rendering `SendInviteLinkEmail` through the real `render()` (Node 24)
now
returns full markup (5.7 kB) with no Suspense marker and the resolved
invite
link + workspace content, instead of the empty fallback.

A jest unit test was intentionally not added: `@react-email/render` 2.x
uses a
dynamic import that jest's CJS runtime rejects ("A dynamic import
callback was
invoked without --experimental-vm-modules") — which is exactly why the
existing
email specs mock `render`. The fix was verified with a standalone Node
script.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23323?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: Félix Malfait <felix.malfait@gmail.com>
2026-07-27 11:37:09 +02:00
Félix Malfait 3a8f086d15 Converge drag and drop on shared dnd-kit primitives, remove @hello-pangea/dnd (#23211)
Follow-ups recorded in #23023, done in one pass.

## Shared primitives

- Folded `PageLayoutWidgetSortableItem` and `PageLayoutWidgetDropLine`
into the shared `DragDropItemSortableCell` / new `DragDropItemDropLine`
(new `data`, `dropLine`, `highlightWhileDragging`, `hasTransition`
props).
- Added generic `DragDropProviderDragStartEvent` (and
DragMove/DragOver/DragEnd/DropTarget) helpers and deleted the 7 copied
`Parameters<...>` extractions across the dnd hooks.
- Replaced the `useMovePageLayoutWidgetUp/Down` implementations (~140
lines) with `moveWidgetWithinTabInDraft`.
- Migrated the remaining page-layout test suites onto
`pageLayoutDraftFixtures`.

## Tab reordering off Pangea

- Tabs are sortable cells on the same provider as widget drags,
segregated by dnd type, so widget drops on tab buttons keep working
while tabs reorder.
- Reordering is ID based (`reorderTabInDraft`: insert before the hovered
tab), which keeps the pinned first tab in place without index
arithmetic.
- Preserved overflow behaviors: the dropdown stays open while a tab drag
is in flight, dropping a tab on the "+N More" button appends it and
opens the dropdown, and both the visible strip and the overflow list
have end drop zones.

## Fields configuration editors off Pangea

- Group reorder, field reorder and cross-group field moves now run on
the shared cells (same drop line and end-zone patterns).

## DraggableList off Pangea

- `DraggableList` / `DraggableItem` keep their consumer-facing API — the
~9 consumers now type their handlers with a local
`DraggableListDropResult` instead of pangea's `DropResult` — but run on
the shared sortable cells; each list's uuid group doubles as its dnd
type so nested lists stay isolated from page-level providers.
- Items register their index in a list-scoped registry so the end drop
zone can resolve the append index at drop time (with insert-before
semantics an item could otherwise never reach the last position).
- Deleted three dead files that only existed for pangea plumbing (the
side panel navigation placeholder, `getCssCompatibleDraggableProps`, the
orphaned `recordGroupPendingDragEndReorderState`).

## Record table row drag off Pangea

- Rows register through `useSortable` directly on the row element — no
wrapper div, so row CSS, sticky cells and virtualization stay untouched
— with the grip cell wired as the drag handle via the shared sortable
handle ref context.
- Both table modes (virtualized flat list and record groups) share a
`DragOverlay` clone that replaces pangea's virtual-mode `renderClone`,
and end drop zones per record group (and after the virtualized list)
allow dropping after the last row or into an empty group.
- The drop handlers keep their pangea-shaped result object, retyped as a
local `RecordDragDropResult`, so the position computation logic is
untouched.

## Pangea removed

`@hello-pangea/dnd` is gone from `package.json` and the lockfile, along
with its orphaned transitive entries (`css-box-model`, `raf-schd`,
`react-redux`, `redux`). Nothing in the repo imports it anymore.

## Dashboards: cross-tab widget drag for grids

react-grid-layout drags never enter dnd-kit, so the bridge hit-tests the
pointer against the tab buttons' `data-page-layout-tab-drop-target-id`
rects during grid drags, highlights the hovered tab through state, and
on drop moves the widget to the destination grid below its existing
content (`moveWidgetToGridTabInDraft`, `buildTabWidgetLayouts`). The
grid's own post-drag layout commit is suppressed once so it does not
overwrite the cross-tab move.

## Fixes found while testing

- With `feedback: 'clone'`, the drag source is its own initial drop
target and its placeholder is a DOM clone taken at drag start, so the
drop line rendered into the source got baked into the placeholder and
stuck there for the whole drag. The line is now hidden on the source
cell, leaving a single indicator at the actual target.
- Reorderable tabs collapsed to text height and sat top-aligned next to
"+ New Tab" because the sortable cell wrapper defaults to `display:
block; height: auto`, breaking the tab height chain — the tab list now
uses the cell's `fill` mode so tabs stretch to the strip height again.

## Testing

Playwright against the dev app:
- Record page: widget reorder up and down in the pinned column (single
blue drop line at the target), drag to another tab via its tab button
(highlight + move), drag back into content at a specific position,
chained cross-tab moves, tab reorder with vertical drop line, new tab
creation.
- Overflow (narrow viewport): drop a tab on "+N More" (appends last,
dropdown opens), reorder inside the dropdown (stays open), drag a tab
from the dropdown back to the visible strip.
- Dashboard: grid drag within a tab, cross-tab drag onto a tab button
(hover highlight, widget lands below destination content, remaining
widgets keep their positions), save and reload persistence in both
directions.
- Fields editor: field reorder, group reorder, field move across groups,
plus the Move Up / Move Down widget actions.

Since the pangea-removal commits:
- Typecheck, oxlint and oxfmt green over the full front source; unit
suites green including the migrated `useStartRecordDrag` test (jest
needed a scoped transform exemption for `@preact/signals-core` once
dnd-kit reached the side-panel suites).
- Storybook visual regression unchanged across ~700 stories — expected,
since the migrated surfaces render identical DOM at rest (drop lines and
drag overlays only exist mid-drag).
- The tab strip fix reverses the exact regression mechanism: the
sortable cell wrapper defaulted to `display: block; height: auto`,
collapsing the tab height chain next to the full-height "+ New Tab"
button; `fill` restores the stretch.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XKRCzzu8oGyocXZtFp7VEG)_


<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23211?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-07-24 14:04:13 +02:00
Félix Malfait 5e27e04c0a Add mostly-empty field hints to data model settings (#22962)
## What

Fields that are empty in almost all records now show a subtle `Mostly
empty` hint next to their name in the object's Fields settings table
(same visual treatment as `Deactivated`), with a tooltip explaining the
signal and a matching **Mostly empty** toggle in the search filter
dropdown. The goal is to nudge admins to clean up and deactivate fields
nobody uses, while keeping the page untouched when the data model is
healthy.

## How

**No table scans.** Emptiness is read from Postgres planner statistics,
so the cost is a catalog lookup regardless of table size:

- `pg_class.reltuples` gates the feature on an approximate row count (≥
100 records; never-analyzed tables mean no hints). Reuses the shared
helper extracted from `ObjectRecordCountService`.
- `pg_stats.null_frac` plus the sampled frequency of the column type's
empty sentinel (`''` for text columns, `'{}'` for arrays, `'{}'`/`'[]'`
for json — matched per physical column type) gives a per-column empty
fraction. A value dominating ≥ 95% of a column is guaranteed to appear
in the most-common-values list, so the approximation is reliable exactly
at the threshold we care about.

**Decision rules** (pure util, unit-tested):

- Flag when every relevant column is ≥ 95% empty and the object has ≥
100 records.
- Skip system fields, the label identifier, relations, booleans, and
actor fields (exhaustive switch — a new `FieldMetadataType` fails to
compile until classified).
- Composite fields must have all their columns empty, with column sets
derived from `compositeTypeDefinitions`; only default-bearing code
columns (`currencyCode`, phone country/calling codes) are excluded so
stamped defaults don't mask emptiness.
- Anything unknown (missing stats, new column since last ANALYZE)
degrades to silence — no hint is ever shown on missing data.

**API:** one `mostlyEmptyFieldMetadataIds(objectMetadataId)` query on
the metadata schema, guarded by the `DATA_MODEL` settings permission,
fetched lazily when the fields page opens.

**UI:** exception-based — no new columns, no persistent controls. The
badge and the filter toggle only materialize when at least one field
qualifies, and disappear once things are cleaned up.

## Test

- Unit tests for the decision util (threshold,
system/label-identifier/inactive exclusion, missing statistics,
composite all-columns rule, links label/secondary data, currency
narrowing, excluded types).
- Catalog SQL validated against Postgres 16 with a table mimicking
Twenty's column shapes (text `''` defaults, enums, arrays, jsonb,
currency pairs), including the type-aware sentinel matching (a text
column full of literal `"{}"` strings does not count as empty).
- End-to-end on a seeded dev instance: 899 companies with a mix of
filled/empty fields — the API returned exactly the five fields predicted
by the raw statistics (`annualRevenue`, `employees`, `introVideo`,
`tagline`, `workPolicy`) and correctly excluded `address` (city 33%
filled), actor/system fields, and the label identifier.
- UI driven with Playwright: badge, tooltip copy, filter toggle, and
filtered table all verified visually.
- `lint:diff-with-main`, `typecheck` (server + front), and all three
`graphql:generate` configurations + SDK metadata client regenerated and
committed.
2026-07-17 12:03:11 +00:00
Abdul Rahman f4ff234db8 feat: make record avatar/icon resolution data-driven via a configurable image identifier field (#22644)
## Summary

Today the avatar/icon shown for a record is hardcoded per object —
Company pulls a favicon from its domain link, Person uses `avatarUrl`,
etc. This PR replaces that hardcoding with a generic, data-driven
abstraction based on a configurable **image identifier field** on each
object's metadata (mirroring the existing **label identifier** concept).

An object's image identifier can point to:
- a **`FILES`** field → the uploaded image is used directly (rounded
avatar), or
- a **`LINKS`** field → a favicon is derived from the primary URL via
the Twenty icons service (squared avatar), gated by
`ALLOW_REQUESTS_TO_TWENTY_ICONS`.

This lets any object type (Opportunity, a custom "Listing", etc.) define
its own avatar/icon without code changes, and makes the field
configurable/overridable for standard objects.


##  Open question: also allow `TEXT` → direct image URL?
Right now the image identifier is restricted to `FILES` (uploaded file)
and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image
URL** (e.g. an imported/synced photo URL stored in a text field).
There's precedent for it — Person's avatar was originally a `TEXT`
`avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a
`TEXT` field has no favicon-vs-image ambiguity, and selecting it as the
image identifier is itself the declaration of intent). It's a small,
clean extension:
- add `TEXT` to the allowed image-identifier types,
- add an explicit `TEXT → raw URL` case
- `getAvatarType`: `TEXT → rounded`.
Caveats: it relies on admin assertion that the text values are image
URLs (no data-level guarantee), and external image URLs load third-party
content in the browser (IP-leak/hotlinking, same as favicons — a
proxy/cache would be the more robust long-term answer).

###  Resolution
Decision: **we will not support `TEXT` as an image identifier.** Image
identifiers stay restricted to `FILES` and `LINKS`, and any other type
fails closed (returns no avatar) on both the frontend and backend.
Instead, the legacy items that still rely on a `TEXT` avatar — Person's
deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be
migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember
remains an exception (its `avatarUrl` still resolves through the
existing CorePicture path), and legacy Person `avatarUrl` values that
haven't been migrated will show initials placeholders.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?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-07-15 19:15:47 +05:30
Thomas des Francs e609320666 Squirclesssss 🟦🔵 (#22535) 2026-07-04 07:07:29 +02:00
Félix Malfait 86e4685781 fix: format numbers according to user preferences in settings views (#22501)
## Context

Record and field counts in several settings views were rendered as raw
numbers (e.g. `158355`), ignoring the workspace member's number format
preference. This PR routes them through the existing `useNumberFormat`
hook (which wraps `formatNumber` with the user's `numberFormat`
preference), so they render as `158,355` / `158 355` / `158.355` /
`158'355` depending on the preference.

## Changes

Starting point (from the screenshot): the Settings → Data model objects
table.

- **`SettingsObjectItemTableRow`** — Fields and Records columns in
Settings → Data model
- **`SettingsAvailableStandardObjectItemTableRow`** — Fields column in
Settings → Data model → New object
- **`SettingsDataModelOverviewObject`** — record count next to the
object name in the data model graph overview (guards against `undefined`
while the count query loads)
- **`SettingsLogs`** — "X of Y" record counts above the event logs table
- **`SettingsAdminGeneral`** — Users column in the admin panel top
workspaces table
- **`SettingsAdminWorkspaceContent`** — Members value in the admin panel
workspace info card
- **`NoteList`** — total notes count in the record page Notes tab

## Test coverage

- `npx nx lint:diff-with-main twenty-front` (oxlint + oxfmt) passes
- `npx nx typecheck twenty-front` passes
- No behavioral change beyond formatting; `formatNumber` defaults to 0
decimals so integer counts stay integers

https://claude.ai/code/session_019pXXZSaza8TXPYK8NefQiS

---
_Generated by [Claude
Code](https://claude.ai/code/session_019pXXZSaza8TXPYK8NefQiS)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22501?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-07-03 08:28:51 +02:00
Thomas des Francs d8cc81cb91 Improve billing settings UI (#22377)
## Summary

- Refresh the settings billing subscription and credits cards with
clearer status, usage, and action states.
- Add the credit-package picker flow and route past-due/cancellation
actions to billing management instead of credit modals.
- Clean up related billing UI helpers and formatting.

## Screens

### regular
<img width="1007" height="781" alt="image"
src="https://github.com/user-attachments/assets/dc9c0c59-8cda-422a-a0d3-56292421a744"
/>

### downgrading
<img width="1049" height="818" alt="image"
src="https://github.com/user-attachments/assets/0dc3dea8-5b55-4cf2-bc92-997cf6e9bebc"
/>
<img width="1048" height="901" alt="image"
src="https://github.com/user-attachments/assets/fba805ad-f523-444b-93a0-2011b6b43443"
/>

### Trialing

without card
<img width="1008" height="903" alt="image"
src="https://github.com/user-attachments/assets/d5dd11b6-4c92-4102-ac22-ad1ad4e9fbfb"
/>

with card
<img width="1008" height="806" alt="image"
src="https://github.com/user-attachments/assets/0dbeb00e-890e-4448-94fe-0cc0ef411e8d"
/>


### Past due & Unpaid

<img width="1052" height="860" alt="Past due"
src="https://github.com/user-attachments/assets/25ba53ef-6e74-4b4c-bfe1-d6c74e165917"
/>
<img width="1138" height="860" alt="Unpaid"
src="https://github.com/user-attachments/assets/bb16fe65-84e4-40a7-8951-90b01030aded"
/>

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-07-01 17:02:54 +00:00
Paul Rastoin b0d7516951 Deprecate asExpression from field metadata search_vector (#22287)
## Summary

Fully deprecates the cached `asExpression` / `generatedType` settings on
`TS_VECTOR` (searchVector) fields. Previously the generated-column
expression was stored in `FieldMetadataSettings` and kept in sync via
imperative recompute side-effects. It is now **derived at DDL time**
from the `searchFieldMetadata` rows that describe which fields feed the
search vector, making `searchFieldMetadata` the single source of truth
and removing a whole class of cache-drift bugs.

This is delivered across the milestones tracked in #2587 and coordinates
with the frontend migration (#1428).

## Why

- The searchVector expression lived in two places (stored
`settings.asExpression` + the actual generated column), kept consistent
by bespoke side-effects (`recompute-search-vector-on-field-rename`,
label-identifier recompute, etc.).
- The frontend reconstructed the searchable-fields list by
**regex-parsing** the stored `asExpression`.
- Both are brittle. Deriving the expression from `searchFieldMetadata`
rows at build/run time removes the cache and the parsing.

## What changed

### Server - data model & derivation
- Introduce the `tsVectorFieldMetadata` relation on
`searchFieldMetadata` (`tsVectorFieldMetadataId` / universal identifier)
linking each searchable-field row to its target `TS_VECTOR` field.
- New runtime derivation
`deriveSearchVectorAsExpressionForTsVectorField`
(`flat-search-field-metadata/utils/...`) used by the create-object and
update-field handlers to generate the column expression from
`searchFieldMetadata` rows.
- Remove `asExpression` / `generatedType` from stored settings:
`FieldMetadataSettings.TS_VECTOR` is now `null`; the column builder
(`generate-column-definitions.util.ts`) hardcodes `generatedType:
'STORED'` and requires the derived expression.
- Delete the imperative recompute side-effects and the
`compute-search-vector-universal-settings-from-object-manifest` path;
drop the `settings` block from all 28 standard
`compute-*-standard-flat-field-metadata` utils.

### Server - migration runner
- New `rebuildSearchVector` marker on `update-field` actions: the
orchestrator synthesizes targeted column rebuilds
(`compute-search-vector-rebuild-target-universal-identifiers.util.ts` +
the deprioritize aggregator) only when a searchFieldMetadata change or
indexed-field rename actually requires it - instead of rebuilding on
every settings touch.
- Deferrable FKs + in-flight ID resolution so a `searchFieldMetadata`
row and its `TS_VECTOR` field can be created in the same transaction
(deterministic UUIDs).

### Frontend (contract change, #1428)
- New `SearchFieldMetadataDTO` + dataloader exposing
`searchFieldMetadataList` on object metadata.
- `SettingsObjectSearchSection` now reads
`objectMetadataItem.searchFieldMetadatas` instead of parsing
`asExpression`; new `SearchFieldMetadataItem` type, fragment, and
mapping updates.

### Upgrade commands (2.18)
-
`2-18-instance-command-fast-...-add-ts-vector-field-metadata-id-to-search-field-metadata`
-
`2-18-instance-command-fast-...-make-search-field-metadata-fks-deferrable`
-
`2-18-instance-command-slow-...-backfill-ts-vector-field-metadata-id-on-search-field-metadata`

(These were relocated from 2.16 to 2.18 and re-timestamped into an
ordered block - add column -> make FK deferrable -> backfill data -
since 2.16/2.17 are released.)

### Tests
- Updated search-vector side-effect integration specs to assert behavior
(search works) rather than the now-removed `asExpression`; removed the
obsolete expression-validation specs; refreshed the application-sync
snapshot (`universalSettings: null`).

## Upgrade / compatibility notes
- Existing workspaces keep their stored `settings` until a later
cleanup; nothing reads it anymore. The new derivation drives all DDL
going forward.
- Schema changes are gated behind the 2.18 instance commands above.

## Known follow-up (separate PR)
https://github.com/twentyhq/core-team-issues/issues/2620
- The column rebuild (`DROP`/`ADD` of the `searchVector` STORED column)
cascade-drops its GIN index and does not recreate it - a pre-existing
regression on `main` inherited here. A follow-up PR will fix the rebuild
handler to recreate the GIN index and add a 2.18 workspace command to
recompute every search vector + strip the deprecated settings.
(Planned.)

## Test plan
- [ ] `npx nx typecheck twenty-server` / `twenty-front`
- [ ] `npx nx lint:diff-with-main twenty-server` / `twenty-front`
- [ ] Server integration: create/update/delete field, rename indexed
field, update object - search returns expected records
- [ ] Run the 2.18 instance commands on a seeded DB; verify
`tsVectorFieldMetadataId` backfilled and FKs deferrable
- [ ] Frontend: object Search settings tab lists the correct searchable
fields (no `asExpression` parsing)

close https://github.com/twentyhq/core-team-issues/issues/2587

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22287?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-30 09:16:01 +00:00
Raphaël Bosi dd9ad876a4 Reduce published twenty-ui npm package size (#22087)
The published `twenty-ui@1.0.0-alpha.0` tarball was ~181 MB unpacked (27
MB compressed, 2,701 files). This was a build-config issue, so a clean
CI build would reproduce the same size.

Main fix: externalize `@tabler/icons-react` instead of bundling it. It
was forced into the bundle and aliased to the full icon barrel, inlining
the entire icon set into every entry point in both ESM and CJS (~81% of
the package). It stays a `dependency`, so consumers still get it; the
dynamic `<Icon name>` registry still resolves icons at runtime.

Also:
- Stop emitting/shipping declaration maps (`declarationMap: false`).
- Exclude the internal `dist/individual` build and `*.map` from the
tarball via `files` (it still builds locally for
`twenty-front-component-renderer`).
- Clean up the stale `files` / `project.json` build outputs at their
source, `scripts/generateBarrels.ts`.
- Add a `pack-size` CI guard (30 MB unpacked budget) and wire `size` +
`pack-size` into `ci-ui.yaml`.

Result: ~181 MB to ~2.3 MB unpacked (0.40 MB tarball, 400 files). All
export subpaths, types, and icon rendering verified intact in both
module formats.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22087?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-24 14:53:14 +00:00
Thomas des Francs 9e57ec3153 Update data model object settings labels (#22070)
## Summary

- Update Data Model object list copy: rename the section to **Objects**
and the count column to **Records**.
- Improve relation rows by showing the related object name with the
field name as a secondary label, including morph relation-specific
labels/icons.
- Hide relations to system objects unless Advanced mode is enabled, and
default the System objects filter to on while Advanced mode is on.
- Reuse a shared secondary-label component for the light
subtitle/deactivated text treatment.

## Screenshots

### Before

Mix of field name & object name. Not all relations are navigable

<img width="1642" height="950" alt="image"
src="https://github.com/user-attachments/assets/e04fb710-e333-4dd7-a29f-82c226202e77"
/>


### After

<img width="1690" height="1112" alt="image"
src="https://github.com/user-attachments/assets/f7d75974-a5cc-401b-bbd2-ab82a2006cf9"
/>
2026-06-24 15:15:41 +02:00
Thomas Trompette a59ed80422 fix(front): allow null subfields in Phones default value so Save enables (#21847)
## Problem

Closes #21780.

When editing a **Phones** field in Settings → Data Model, changing the
**Default Country Code** does not enable the Save button — the form
becomes dirty but never valid, so the change can't be saved.

## Root cause

The settings form validates `defaultValue` with the record-value
`phonesFieldValueSchema`, which requires non-null strings:

```ts
primaryPhoneNumber: z.string(),
primaryPhoneCountryCode: z.string(),
```

But a Phones default value can legitimately have **null** subfields — a
default country code with no default number. The backend normalizes
empty subfields to `null`
(`nullify-empty-phones-default-value.util.ts`), and the shared contract
`FieldMetadataDefaultValuePhones` is `string | null`. So an existing
field whose stored default has `primaryPhoneNumber: null` makes the form
**permanently invalid**: changing the country code preserves the null
number → `isValid` stays `false` → `canSave = isDirty && isValid` keeps
Save disabled.

The sibling **address** field doesn't have this bug because
`addressFieldValueSchema` already makes every subfield `.nullable()`.
Phones was simply inconsistent.

## Fix

Add a dedicated `phonesFieldDefaultValueSchema` with nullable subfields
(mirroring the address pattern and matching
`FieldMetadataDefaultValuePhones`) and use it in the Phones settings
form. The stricter record-value `phonesFieldValueSchema` is left
untouched, so record input/persistence/empty-checks are unaffected.

## Test plan

- [x] Unit test covering the partial-null default value (and asserting
the record-value schema still rejects it)
- [x] `nx typecheck twenty-front` clean
- [x] `nx lint:diff-with-main twenty-front` clean
- Manual: open a Phones field, set a Default Country Code and save,
re-open, change the country code → Save now enables.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21847?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: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 13:07:37 +00:00
Weiko 9f30915f6f fix(metadata): remove deprecated isCustom from Objects and Fields (#21799)
## Context

Follow-up to #21228, which deprecated `isCustom` on object/field
metadata but kept it exposed because the frontend still relied on it.
This removes it from the GraphQL API and the frontend entirely.

## Implementation

### Server
- Remove `isCustom` `@Field` from the `Object`, `Field`, and
`MinimalObjectMetadata` GraphQL types
- Remove the `isCustom` `@ResolveField` resolvers and the
`isCustomLoader` dataloader (+ payload/interface)
- Remove `isCustom` as an internal `@HideField()` on the Object/Field
DTOs used by the i18n standard-override gate > Use an explicit
isStandard instead (which is the correct gating)

### Frontend
- Add `getIsMetadataItemCustom` helper + `useGetIsMetadataItemCustom`
hook: an item is custom when `applicationId ===
currentWorkspace.workspaceCustomApplication.id`
- Migrate all consumers off `objectMetadataItem.isCustom` /
`fieldMetadataItem.isCustom`; `isRecordFieldReadOnly` now takes a
precomputed `isFieldCustom`
- Drop `isCustom` from the metadata fragment/mutations/minimal query, FE
types, zod schemas, and mock generators; regenerate GraphQL types

## Notes
- Breaking change on the (already-deprecated) `Object.isCustom` /
`Field.isCustom` GraphQL fields and the `isCustom` filter
- FE semantic is "belongs to the workspace custom app" (third-party-app
objects/fields are treated as non-custom)
- `isCustom` on IndexMetadata / View / Skill / Agent is a separate
column and is untouched
- Breaking changes on REST metadata API
2026-06-19 01:19:49 +02:00
Raphaël Bosi 8034c7725f Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.

**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).

**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.

**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).

Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?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-18 10:31:29 +02:00
Raphaël Bosi 9c9c34fccf Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and
`twenty-front-component-renderer` from `twenty-ui-deprecated` to
`twenty-ui` (mechanical import swap — the packages have API parity) and
deletes the deprecated package along with its workspace/CI/config
wiring.

Also adds `@linaria/react`/`@linaria/core` as direct deps of
`twenty-front` (it used them transitively via the deprecated package).

Note: move the required status check from `ci-ui-status-check` to
`ci-new-ui-status-check`.

Argos: the Storybook box-model/button-reset baseline shift (the bulk of
the visual diffs) is isolated in #21665 — Storybook now loads
twenty-ui's global `reset.scss`, which the production app already ships.
Once #21665 merges and this branch is rebased, the remaining Argos diffs
are component-level visual-parity items only.
2026-06-17 09:41:11 +00:00
Raphaël Bosi d4c1fc86e3 Stabilize flaky Argos stories in twenty-front storybook (#21691)
Three twenty-front stories rendered non-deterministically and
intermittently tripped Argos as false positives (flaky on `main`, not
caused by any recent change). Each is now deterministic:

- **SettingsDataModelFieldSettingsFormCard › WithRelationForm**: the
relation preview was screenshotted mid-settle — it briefly shows the
fallback object/record before the form default and the sample record
load. Added a `play` that waits for the settled state so Argos captures
it consistently.
- **MultiSelectInput › SingleSelection**: the final deselect click left
a transient hover/tooltip. The play now moves the pointer off the option
and waits for the tooltip to disappear.
- **Breadcrumb › Default**: ambiguous intrinsic width made the last
crumb flip between "New" and "N…". Gave the story a fixed container
width.

Verified the three stories pass in the Storybook vitest runner across
repeated runs.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21691?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-16 16:37:53 +00:00
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
Félix Malfait 9d7f0c405f fix(twenty-front): new layout fast-follows — command menu, field options & logs (#21429)
Fast-follows for the new layout — the remaining open sub-issues of
twentyhq/core-team-issues#2478.

## Changes

- **Command menu items should not have extra right padding**
(twentyhq/core-team-issues#2500)
`SidePanelList` set `width: calc(100% - spacing[4])` on top of its own
8px left/right padding. Under the global `box-sizing: border-box`, that
extra `-16px` shrinks the list and, because it's left-aligned, dumps the
whole gap on the right. Switched to `width: 100%` so item highlights
inset 8px symmetrically. This is shared by every side-panel list — they
all had the same right-only gutter, so they're all corrected the same
way.

- **Field options should not be cropped and should keep row gaps**
(twentyhq/core-team-issues#2503)
The option row used a fixed `height: spacing[6]`, so under border-box
the `6px` vertical padding was absorbed and consecutive rows sat flush.
Changed `height` → `min-height` so the padding separates the rows again.

- **Logs table with filters should use Background secondary**
(twentyhq/core-team-issues#2505)
The Logs filter card and the upgrade card defaulted to a transparent
background, showing the white page through. Passed
`backgroundColor={themeCssVariables.background.secondary}`, matching
`SettingsTableCard`. The results table stays on the primary surface, per
the Figma reference.

- **Command menu back chevron** (twentyhq/core-team-issues#2504)
`SidePanelTopBar` showed a back chevron whenever the nav stack had more
than one entry. A command-menu page is the root of a fresh command-menu
session, so it now only shows the chevron when it was opened from
another command-menu page. Every other side-panel page keeps standard
history-based back navigation, so workflow / page-layout / record stacks
are unaffected.

## Verification
- oxlint (`--type-aware`, full `src/`): 0 errors
- oxfmt: clean
- tsgo typecheck: no errors in the changed files (the one reported error
is pre-existing in `RestPlayground.tsx`, which this PR does not touch)
- Verified live at apple.localhost:
- #2500 — command menu item highlight insets measured 8px left / 8px
right (was 8 / 24)
- #2503 — option rows render at 38px tall with ~14px gaps, text no
longer cropped
- #2505 — filter + upgrade cards compute to background-secondary;
results table stays on primary
- #2504 — direct command menu shows the close-X with no chevron; pages
opened from the command menu still show the chevron

## Open question for review (#2504)
The issue also describes the page-header three-dots toggle: *"the three
dots icon button should remain visible if the side panel is on a page
that is not a child of the command menu (AI chat, or a page opened
directly)."* Today that toggle morphs to an X for any non-command-menu
side-panel page (e.g. Ask AI). Honoring that touches the shared
`SidePanelToggleButton`, and there's a related decision: search / Ask AI
opened from the command menu currently reset the nav stack rather than
push, so they don't get a "back to command menu" chevron. I left those
out here since they're a behavior change to a shared control with a
product call attached — happy to follow up once you confirm the intended
toggle behavior.

Closes twentyhq/core-team-issues#2500
Closes twentyhq/core-team-issues#2503
Closes twentyhq/core-team-issues#2505
Refs twentyhq/core-team-issues#2504
Refs twentyhq/core-team-issues#2478
2026-06-11 08:58:23 +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
Weiko 9c66975520 isCustom deprecation for Objects and Fields (#21228)
## Context

`isCustom` was a legacy denormalized boolean on `ObjectMetadataEntity`
and `FieldMetadataEntity`.
Now that every metadata row carries `applicationId` (via
`SyncableEntity`), "is this custom" is fully derivable, and the stored
boolean was a redundant second source of truth that could drift.

The real meaning of `isCustom` is **"the owning application is not the
twenty-standard application"** — i.e. `!belongsToTwentyStandardApp`.
Note this is *not* "belongs to the workspace custom app" as I initially
thought: third-party-application
objects/fields are custom too. 
The standard application has a globally stable `universalIdentifier`, so
the value derives with no per-workspace lookup.

## Changed
## `isCustom` checks — before → after

`isCustom` is no longer a stored column. The table below lists every
site that branched on it and how it resolves now. The unifying rule:
`isCustom ≡
!isTwentyStandardApplicationUniversalIdentifier(applicationUniversalIdentifier)`.

### Server — behavioural checks

| Location | Purpose | Before | Now |
|---|---|---|---|
| `utils/compute-object-target-table.util.ts` | Physical table name `_`
prefix | `computeTableName(nameSingular, objectMetadata.isCustom)` |
derives from `applicationUniversalIdentifier` (single source for all
table-name callers) |
| `twenty-orm/factories/entity-schema.factory.ts` +
`…/entity-schema-metadata.type.ts` | ORM table name (hot path) |
`object.isCustom` | `object.applicationId !== standardApplicationId`
(computed in `buildEntitySchemaMetadataMaps`) |
|
`twenty-orm/repository/workspace-{delete,soft-delete,update}-query-builder.ts`
| Table name for mutations | `computeTableName(nameSingular,
objectMetadata.isCustom)` | `computeObjectTargetTable(objectMetadata)` |
| `index-metadata/utils/generate-deterministic-index-name-v2.ts` | Index
name hash (must stay bit-identical) | `flatObjectMetadata.isCustom` |
derives from `applicationUniversalIdentifier` |
| `object-metadata/object-record-count.service.ts` | Table name for
record count | `computeTableName(nameSingular, isCustom)` |
`computeObjectTargetTable(flatObjectMetadata)` |
|
`workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts`
| Match seed config by table name | `computeTableName(item.nameSingular,
item.isCustom)` | `computeObjectTargetTable(item)` |
| `commands/workspace-export/workspace-export.service.ts` +
`…/utils/generate-workspace-schema-ddl.util.ts` | Export table name (raw
entity) | `objectMetadata.isCustom` |
`!isTwentyStandard…(objectMetadata.application?.universalIdentifier)` |
|
`flat-field-metadata/services/flat-field-metadata-type-validator.service.ts`
| Block users creating reserved field types |
`args.flatEntityToValidate.isCustom` |
`!args.flatEntityToValidate.isSystem` |
| `api/common/.../common-create-many-query-runner.service.ts` | Don't
let client overwrite system `createdBy` |
`createdByFieldMetadata.isCustom === false` |
`createdByFieldMetadata.isSystem === true` |
|
`field-metadata/utils/resolve-field-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom fields | `if (fieldMetadata.isCustom)
return raw` | **removed** — falls through on
`isDefined(standardOverrides)` |
|
`object-metadata/utils/resolve-object-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom objects | `if (objectMetadata.isCustom)
return raw` | **removed** — same fall-through |
|
`command-menu-item/utils/build-navigation-interpolation-context.util.ts`
| Override context for nav labels | passed `isCustom` into resolver |
dropped (resolver no longer needs it) |
| `api/common/.../data-arg-processor.service.ts` | `isCustom` for
record-position table name | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
| `metadata-modules/minimal-metadata/minimal-metadata.service.ts` |
Minimal DTO + override context | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
|
`commands/upgrade-version-command/1-23/…backfill-record-page-layouts.command.ts`
| Filter to custom objects | `objectMetadata.isCustom` |
`!isTwentyStandard…(applicationUniversalIdentifier)` |

### Server — DTO / API population

| Location | Before | Now |
|---|---|---|
|
`flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`object-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
|
`field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
| `dataloaders/dataloader.service.ts` | passed
`flatFieldMetadata.isCustom` into override resolver | dropped (resolver
no longer needs it) |

> REST controllers (`object-metadata.controller.ts`,
`field-metadata.controller.ts`) resolve `standardApplicationId` once per
request from the cached `flatApplicationMaps`.

### Frontend

| Location | Purpose | Before | Now |
|---|---|---|---|
| `settings/.../SettingsObjectFieldDisabledActionDropdown.tsx` | Whether
an inactive field is deletable | `isDeletable = isCustomField` |
`isDeletable = isCustomField && !isSystemField` |

### Unchanged (out of scope)

`isCustom` on `IndexMetadata` / `View` / `Skill` / `Agent` and their
guards still read the persisted column.

Breaking change is on the isCustom filter on field and object APIs, this
is never used in the FE and unlikely used by external consumers
2026-06-09 13:57:19 +00: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 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 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
Félix Malfait 3c97d9648b fix(address): show saved address in record detail when street1 is null (#21033)
## Fixes #20084

### Problem
A saved address is visible in the **table view** but shows **"Empty"**
in the **record detail page** when `addressStreet1` is `null`.

This reproduces with the default seed data out of the box — e.g.
**Google** (city "Mountain View", no street), **Microsoft** (Redmond),
**Meta** (Menlo Park) — which is why several users reported hitting it
immediately.

### Root cause
The frontend zod schema required `addressStreet1` to be a **non-null**
string:

```ts
// isFieldAddressValue.ts
export const addressSchema = z.object({
  addressStreet1: z.string(),          // ← required non-null
  addressStreet2: z.string().nullable(),
  ...
});
```

…but the backend composite type marks it `isRequired: false`
(`address.composite-type.ts`), and the DB column is nullable. So the API
legitimately returns `addressStreet1: null` when only other subfields
are filled.

The two views diverge on how they render:

- **Record detail** gates the value behind `useIsFieldEmpty()` →
`isFieldValueEmpty()`, which for addresses calls
`isFieldAddressValue()`. With `addressStreet1: null` the `safeParse`
**fails**, so `isFieldValueEmpty` returns `true` and the `"Empty"`
placeholder is shown (`RecordInlineCellDisplayMode`).
- **Table view** (`RecordTableCellDisplayMode`) renders
`AddressFieldDisplay` directly with **no** empty check, so the address
stays visible.

This was a latent mismatch since the address guard was introduced.

### Fix
Make `addressStreet1` nullable to match the backend and the other
subfields:

- `addressSchema` → `addressStreet1: z.string().nullable()`
- `FieldAddressValue.addressStreet1` → `string | null`
- `FieldAddressDraftValue.addressStreet1` → `string | null` (keeps the
input/draft type consistent; the text input already renders `?? ''`)

The change is strictly more permissive — persisting and the settings
default-value form still accept string values; they now also accept
`null`.

### Tests
- `isFieldAddressValue.test.ts` — guard returns `true` for
`addressStreet1: null` with other subfields filled.
- `isFieldValueEmpty.test.ts` — new address coverage: empty address is
empty; **`street1: null` + city filled is NOT empty**; normal address is
not empty. (Added an `addressFieldDefinition` mock.)

Both new assertions were confirmed to **fail before the fix** and pass
after.

### Verification
- `npx jest isFieldValueEmpty isFieldAddressValue
normalize-address-field-value-for-persist` → 17 passed
- `npx nx typecheck twenty-front` → pass
- `npx nx lint:diff-with-main twenty-front` → 0 warnings, 0 errors
2026-05-29 08:38:47 +02: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
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 e16977f97b [breaking: deploy server before front] feat(view-sort): pick sort sub-field inline on the chip (#20445)
## Summary

Lets users choose which sub-field of a composite column to sort by —
directly from the sort chip — by clicking the sub-field label and
picking from a dropdown. Persists per view via a new nullable
\`subFieldName\` column on \`ViewSort\`.

Replaces #20438, which proposed a field-settings (admin) configuration
for the same problem. The chip-level approach is more discoverable (the
option lives where the user is looking) and per-view, so different views
on the same object can sort by different sub-fields.

### What changes for users

- **FullName columns**: previously sorted by \`firstName\` and
\`lastName\` together as a stable dual-key sort. Now the user can pick
which sub-field is primary (the other is the tie-breaker). Default
remains \`firstName\` primary, \`lastName\` tie-breaker.
- **Address columns**: previously not sortable at all (not in
\`SORTABLE_FIELD_METADATA_TYPES\`). Now sortable, with a chip dropdown
listing each enabled sub-field. Default is \`addressCity\` if enabled,
else the first enabled sub-field. Disabling a sub-field at the
field-metadata level (existing setting) removes it from the dropdown.
- **Other composite types** (Currency, Phones, Emails, Links, Actor) and
scalar fields keep their existing single-key sort behavior.

### UX

```
┌─────────────────────────┐    ┌─────────────────────────┐
│ ↑ Name · Last name  ✕ │    │ ↑ Address · City  ✕ │
└────────┬────────────────┘    └────────┬────────────────┘
         ▼ (click sub-field)            ▼
   ┌────────────┐                ┌────────────┐
   │ First name │                │ Address 1  │
   │ Last name ✓│                │ Address 2  │
   └────────────┘                │ City      ✓│
                                 │ State      │
                                 │ Postcode   │
                                 │ Country    │
                                 └────────────┘
```

The chip body still toggles direction on click — the \`Dropdown\`'s
internal wrapper calls \`stopPropagation\` so the sub-field click
doesn't bubble to the chip's onClick.

## What changed

**Backend:**
- \`ViewSortEntity\` — new nullable \`subFieldName: varchar\` column
- \`ViewSortDTO\`, \`CreateViewSortInput\`,
\`UpdateViewSortInputUpdates\` — new \`@Field(() => String, { nullable:
true })\`
- \`FLAT_VIEW_SORT_EDITABLE_PROPERTIES\` — \`'subFieldName'\` added so
the property flows through the update merge path
- \`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME.viewSort\` —
new \`subFieldName\` entry with \`toCompare: true\` so cache diffs
notice it
- \`fromCreateViewSortInputToFlatViewSortToCreate\` — threads
\`subFieldName\` through
- Instance command migration (\`add-sub-field-name-to-view-sort\`) —
single \`ALTER TABLE core.viewSort ADD subFieldName varchar\` / \`DROP\`

**Frontend:**
- \`RecordSort\` and \`ViewSort\` types — \`subFieldName?: string |
null\`
- \`VIEW_SORT_FRAGMENT\` — adds \`subFieldName\` so the field
round-trips
- \`mapRecordSortToViewSort\` + \`areViewSortsEqual\` — carry the new
field through, include it in the diff so the usual
\`useSaveRecordSortsToViewSorts\` create/update flow fires when it
changes
- \`useSaveRecordSortsToViewSorts\` — passes \`subFieldName\` in both
\`CreateViewSortInput\` and \`UpdateViewSortInputUpdates\`
- \`getOrderByForFieldMetadataType(field, direction, subFieldName?)\` —
new optional third arg. \`turnSortsIntoOrderBy\` threads
\`sort.subFieldName\` into it.
- \`Address\` added to \`SORTABLE_FIELD_METADATA_TYPES\`
- New helpers: \`getEnabledAddressSubFields\` (filters by the field's
\`subFields\` setting, falls back to the 6 default visible address
sub-fields), \`getDefaultSortSubFieldForAddress\`,
\`getDefaultSortSubFieldForFullName\`
- New shared types/constants: \`AllowedFullNameSubField\`,
\`ALLOWED_FULL_NAME_SUBFIELDS\`, \`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS\`
- \`SortOrFilterChip\` — new \`labelSubField?: ReactNode\` slot; renders
as \` · {sub-field}\` with subdued weight after the main label
- \`EditableSortChip\` — builds options from field metadata
(\`ALLOWED_FULL_NAME_SUBFIELDS\` for FullName,
\`getEnabledAddressSubFields\` for Address), uses i18n-wrapped labels,
persists picks via \`upsertRecordSort\`

## Test plan

- [x] \`npx nx typecheck\` passes for twenty-shared, twenty-front,
twenty-server
- [x] \`oxlint --type-aware\` on all 19 frontend + 9 server changed
files: 0 errors
- [x] \`prettier --check\`: clean
- [x] 16 unit tests pass — \`getOrderByForFieldMetadataType\` covers the
new \`subFieldName\` override branch for FULL_NAME and ADDRESS;
\`getDefaultSortSubFieldForAddress\` covers the city/first-enabled
fallback path; \`getDefaultSortSubFieldForFullName\` exercises its
constant
- [ ] Manual: sort a People view by Full Name → click the chip's
sub-field label → switch between First name and Last name → reload page
→ choice is preserved
- [ ] Manual: sort a Company view by Address → confirm dropdown lists
only enabled sub-fields → disable Address \`addressCity\` in field
settings → confirm dropdown options update and runtime falls back to the
first enabled sub-field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-13 21:25:15 +02:00
MkDev11 bbc55193f5 Fix phone unique constraints (#20261)
## Summary

Closes #20195

Fix phone field unique constraints so phone numbers are considered
unique by both `primaryPhoneNumber` and `primaryPhoneCallingCode`.

- Include `primaryPhoneCallingCode` in the shared phone composite unique
constraint metadata
- Align the frontend settings composite field config with the backend
metadata
- Return all included unique composite subfields when building
create-many conflict fields
- Match composite unique conflict fields as a group during create-many
upserts

## Root Cause

Phone composite metadata only marked `primaryPhoneNumber` as part of the
unique constraint. That made different international phone numbers with
the same national number conflict, for example `+1 123456789` and `+32
123456789`.

## Test Plan

- `yarn workspace twenty-shared build`
- `jest --runTestsByPath <index action handler and create-many utility
specs>`
- `prettier --check <touched files>`
- `oxlint --type-aware <touched files>`
- `nx run twenty-shared:typecheck`
- `nx run twenty-server:typecheck`
- `nx run twenty-front:typecheck`

---------

Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-05-13 11:57:19 +00:00
Thomas des Francs 68d509e98d Update settings application illustrations and app metadata previews (#19964)
## Summary
- Refresh the settings application visuals with new light/dark PNG
covers for the data model card
- Replace the custom and standard application carousel assets with the
new provided illustrations
- Align app chips, type tags, and application detail previews with the
updated icon and description treatment
- Keep the data model cover container and overlay button behavior intact
while swapping the underlying imagery

## Testing
- Not run (not requested)
- Existing frontend typecheck and formatting checks were exercised
during implementation

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-22 13:07:41 +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
Weiko 56d6e13b5d Fix fields widget flash during reset (#19726)
Summary

- Remove the evictViewMetadataForViewIds step from the page-layout reset
flow. It synchronously cleared viewFields/viewFieldGroups rows from the
metadata store, leaving a window where
useFieldsWidgetGroups saw a view with no fields and fell back to
buildDefaultFieldsWidgetGroups, briefly rendering a synthetic "General"
+ "Other" layout before the real reset defaults
arrived.
- invalidateMetadataStore() alone is sufficient: it marks the
collections stale and triggers MinimalMetadataLoadEffect to refetch,
which replaces current atomically. The UI now
transitions old-layout → new-default with no synthetic flash.
- Simplified refreshPageLayoutAfterReset to no longer take a
collectAffectedViewIds callback, and updated both tab/widget reset call
sites plus ObjectLayout.tsx accordingly.
- Deleted the now-unused evictViewMetadataForViewIds and
collectViewIdsFromWidgets utils.
2026-04-15 16:08:24 +00:00
Weiko edba7fe085 Reset to default page layout (#19682)
- Add resetPageLayoutToDefault GraphQL mutation that resets an entire
page layout (all tabs, widgets, view field groups, and view fields) to
their default state in a single operation
- Add a "Reset to default" button in the settings Layout tab
(/settings/objects/:object#layout) with a confirmation modal


<img width="673" height="426" alt="Screenshot 2026-04-14 at 13 13 53"
src="https://github.com/user-attachments/assets/002d33c9-9ea1-49f2-bef6-179ce034c126"
/>
2026-04-14 12:19:48 +00:00
martmull 64a7725ac7 Add banner for not vetted apps (#19655)
https://github.com/user-attachments/assets/4038e21a-d5d9-4b93-8589-7a4baf35ef5b

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-13 17:28:50 +00:00
Abdul Rahman 34a903b4fa Object icon visual parity (#19374)
## Summary

Aligns **object metadata** icons with the **tinted tile** look
everywhere we show a workspace object, and **retires** the
navigation-only `NavigationMenuItemStyleIcon` wrapper in favor of
**shared** UI primitives under `@/ui/display` and `@/object-metadata`.

## What changed

### Global tinted icon building blocks (`@/ui/display`)

- **`TintedIconTile`** / **`StyledTintedIconTileContainer`** support
optional **`size`** and **`stroke`**, and grow the tile when **`size`**
is set so layouts match previous `theme.icon` usage.
- Shared helpers and constants for theme color parsing and tinted
backgrounds/borders/icon color (e.g.
**`getTintedIconTileStyleFromColor`**, **`parseThemeColor`**,
**`getColorFromTheme`**, related constants).

### Object metadata icon (`@/object-metadata`)

- **`ObjectMetadataIcon`** composes **`TintedIconTile`** with
**`getObjectColorWithFallback`**, forwards optional **`size`** /
**`stroke`** for **visual parity** with old `getIcon` + explicit sizing.
- **`getSelectOptionIconFromObjectMetadataItem`** returns an
**`IconComponent`** for selects/menus that expect a component, not a
React node.

### Navigation module cleanup

- **Removed** **`NavigationMenuItemStyleIcon`**; call sites use
**`ObjectMetadataIcon`**, **`TintedIconTile`**, and/or the shared
**`getTintedIconTileStyleFromColor`** pipeline so the same treatment is
**not** tied to the navigation package.
- **`NavigationMenuItemIcon`**, view/link overlays, DnD handle, and
sidebar editor flows updated to use the shared pattern where they render
object (or tinted) icons.

### Product surfaces updated (non-exhaustive)

- **Settings:** role object picker/rows, data model
tables/graph/overview, object preview summary, webhooks entity list,
morph relation multiselect.
- **Workflows:** create/update/delete/upsert/find records, triggers,
filters, variables dropdowns, AI agent object rows, object dropdowns.
- **Shell:** side panel object filter / data sources / folder chrome
where object icons appear.
- **Records:** index header icon, show breadcrumb styling.
- **Activity:** timeline event icon when linked object metadata applies.
- **`NavigationDrawerItem`:** tinted branch aligned with shared
**`TintedIconTile`** behavior.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-10 10:14:20 +00:00
Félix Malfait 67e7f05a68 feat: email attachments and open-in-app click action (#19485)
## Changes

### Email Attachments
- Added `EmailAttachmentsField` component for uploading and managing
email attachments
- New `useUploadEmailAttachment` hook for handling file uploads with
size validation
- New `UPLOAD_EMAIL_ATTACHMENT_FILE` mutation for backend file
persistence
- Integrated attachments into email composer with file validation
- Added `EmailRecipientLimits` constant to enforce max recipients (100)
on frontend

### Open in App Click Action
- New `useOpenEmailInAppOrFallback` hook to open emails in the in-app
composer
- Email fields now default to "Open in app" action instead of "Open as
link"
- New `SettingsDataModelFieldOnClickActionForm` support for
`OPEN_IN_APP` action
- Email secondary table cell button now offers in-app composer as
alternative action
- `AttachmentChip` component moved from advanced-text-editor to file
module for reuse

### Refactoring & New Utilities
- Extracted `useComposeEmailForTargetRecord` hook for consistent email
composer opening
- New `useResolveDefaultEmailRecipient` hook to resolve recipient based
on record type
- New `getPrimaryEmailFromRecord` utility for safe email field access
- New `EmptyInboxPlaceholder` component with CTA button
- Simplified `ComposeEmailButton` using new hooks
- Enhanced `ComposeEmailCommand` to support bulk Person selections
- Updated `useSendEmail` to accept and forward attachments
- Recipient count validation with warning in composer footer

### Backend
- New `FileEmailAttachmentModule` with resolver and service
- New `file-email-attachment.command` for record selection menu items
- Updated `SendEmailInput` GraphQL type to include `files` field
- Email tool constants and exceptions updated

### Type Updates
- Added `SendEmailAttachmentInput` GraphQL type
- Added `FileFolder.EMAIL_ATTACHMENT` to file folder interface

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-10 10:01:50 +02:00
Arturo Mantinetti b3d46b0fa3 feat: Add support for CLF currency code (#19420)
## Summary
This PR adds support for the **CLF (Unidad de Fomento)** currency code
across the application.

## Changes
- Added `CLF` to the supported currency list
- Updated validation logic to recognize CLF as a valid currency
- Adjusted formatting and handling where applicable

## Motivation
CLF is a widely used unit of account in Chile, commonly used for
financial operations such as real estate, contracts, and indexed
payments.
Supporting CLF improves localization and enables better adoption of
Twenty CRM in the Chilean market.

## Files Modified
- Updated 3 files to integrate CLF support (currency configuration,
validation, and related logic)

## Testing
- Verified that CLF can be selected and processed correctly
- Confirmed no regression in existing currency behavior
2026-04-08 05:10:36 +00:00
Abdul Rahman a0484f686a Add color to object icon picker in data model (#19368)
Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291)
2026-04-08 01:27:08 +00:00
Marie 2d6c8be7df [Apps] Fix - app-synced object should be searchable (#19206)
## Summary

- **Make app-synced objects searchable**: `isSearchable` was hardcoded
to `false` and the `searchVector` field was missing the `GENERATED
ALWAYS AS (...)` expression, causing all records to have a `NULL` search
vector and be excluded from search results. Fixed by defaulting
`isSearchable` to `true` (configurable via the object manifest),
computing the `asExpression` from the label identifier field, and
allowing the update-field-action-handler to handle the `null` → defined
`asExpression` transition.
- **Make `isSearchable` updatable on an object**: The property had
`toCompare: false` in the entity properties configuration, so updates
via the API were silently ignored and never persisted. Fixed by setting
`toCompare: true`.
2026-04-02 17:14:37 +00:00
Thomas Trompette 779e613df3 Fixes: relation settings design + workflow input border (#19269)
Before 

<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 57
06"
src="https://github.com/user-attachments/assets/1b2c3f56-f696-407d-a00f-ae567842dbfc"
/>
<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 57
22"
src="https://github.com/user-attachments/assets/51ee6db8-0974-46e1-a65f-25fb8aef7126"
/>

After

<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 56
38"
src="https://github.com/user-attachments/assets/4843c36c-3c04-4c5a-b2dc-f48b02c9a907"
/>
<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 56
58"
src="https://github.com/user-attachments/assets/ec404b4d-6c37-4694-a68c-63f98cdf7388"
/>
2026-04-02 15:43:35 +00:00
Charles Bochet 81f10c586f Add workspace DDL lock env var and maintenance mode UI (#19130)
## Summary

- Add `WORKSPACE_SCHEMA_DDL_LOCKED` env-only boolean config variable
that blocks all workspace schema DDL changes when set to `true`. This is
intended for hot upgrades where logical replication cannot handle DDL
changes. Enforced at two chokepoints:
- `WorkspaceMigrationRunnerService.run` — blocks all metadata-driven DDL
(object/field/index CRUD, app sync/uninstall, standard app sync, upgrade
commands)
- `WorkspaceDataSourceService.createWorkspaceDBSchema` /
`deleteWorkspaceDBSchema` — blocks workspace creation (sign-up) and hard
deletion. Uses a dedicated `WorkspaceDataSourceException` (not
ForbiddenException)

- Add maintenance mode feature with Admin Panel UI and user-facing
banner:
- **Backend**: `MaintenanceModeService` stores maintenance window
(startAt, endAt, optional link) in `core.keyValuePair` as
`CONFIG_VARIABLE`. Validates endAt > startAt. Uses `GraphQLISODateTime`
scalar for date fields. Exposed via `clientConfig` REST endpoint and
admin GraphQL mutations (`setMaintenanceMode`, `clearMaintenanceMode`)
- **Admin Panel**: New "Maintenance Mode" section in Health tab with UTC
datetime pickers and activate/deactivate controls
- **Banner**: `InformationBannerMaintenance` displayed at the top of
`DefaultLayout` for all users, using Temporal API for timezone-aware
formatting with an optional "Learn more" link

These two features are **independent** — the DDL lock is controlled via
env var for operational use, while maintenance mode is a UI notification
mechanism controlled from the admin panel.
2026-04-02 10:17:04 +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
Harshit Singh 34832815e6 fix: update settings page for mobile viewport (#18991)
## Description

- This PR fixes
https://github.com/twentyhq/core-team-issues/issues/2313#issuecomment-4116380772
- Fixes Admin Panel Apps table, Data model table and Roles table
- made data model table scrollable for mobile viewport keeping name
column fixed.


## Visual Appearance

## Before

- Roles Table

<img width="636" height="1039" alt="Screenshot 2026-03-26 at 1 28 30 PM"
src="https://github.com/user-attachments/assets/fa7532bd-aeb0-4c8f-a40e-08cc976cd2c2"
/>



- Admin Apps table

<img width="610" height="998" alt="Screenshot 2026-03-26 at 1 28 12 PM"
src="https://github.com/user-attachments/assets/8712473c-350f-46f8-9e68-9cb0a5fa9d80"
/>





- Data Model table



https://github.com/user-attachments/assets/c48075c5-56dd-4b76-acd4-76330d6dab94






## After

- Roles Table

<img width="761" height="1045" alt="Screenshot 2026-03-26 at 1 23 19 PM"
src="https://github.com/user-attachments/assets/17099956-816a-4d41-b987-563f6931a995"
/>

- Admin Apps table

<img width="794" height="1044" alt="Screenshot 2026-03-26 at 1 23 34 PM"
src="https://github.com/user-attachments/assets/0463a087-2363-4192-9adb-7d27076f303a"
/>


- Data model Table


https://github.com/user-attachments/assets/fd44e353-cc7c-44cf-bda0-d2f3cd531f2e

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-26 16:47:40 +01:00
Félix Malfait 5f0d6553f6 Add object type filter dropdown to side panel record searches (#18912)
## Summary

- Adds an object type filter dropdown to the side panel, allowing users
to scope record search results to a specific object type (e.g. People,
Companies, Opportunities)
- The filter appears as a funnel icon next to the search input in both
the global search (SearchRecords page) and the "Pick a record" sidebar
flow
- Replaces the AI sparkles button on the search records page with the
filter icon
- Uses colored icons matching the navigation menu style
(NavigationMenuItemStyleIcon + getStandardObjectIconColor)

## Test plan

- [ ] Open the side panel, type something to enter SearchRecords mode,
verify the filter icon appears
- [ ] Click the filter icon, verify the dropdown opens with "Object"
header, search input, "All Objects" and individual object types with
colored icons
- [ ] Select an object type, verify only records of that type appear in
search results
- [ ] Select "All Objects", verify all record types appear again
- [ ] Verify the filter icon turns blue when a filter is active
- [ ] In layout customization mode, add a new sidebar item > Record,
verify the filter dropdown also works there
- [ ] Close and reopen the side panel, verify the filter resets to "All
Objects"
- [ ] Verify the AI sparkles button no longer appears on the command
menu root page
- [ ] Verify the AI edit icon still appears on Ask AI pages


Made with [Cursor](https://cursor.com)
2026-03-25 16:02:24 +01:00
BugIsGod d16b94bde6 fix: reset form defaultValues after save to fix dirty detection (#18835)
fixes #18833 
I have put some log video and explained the details in pr #18630.
<img width="929" height="454" alt="image"
src="https://github.com/user-attachments/assets/894d0b89-fb0c-478e-ba42-a4c004a98550"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 21:11:02 +00:00
Charles Bochet d5a7dec117 refactor: rename ObjectMetadataItem to EnrichedObjectMetadataItem and clean up metadata flows (#18830)
## Summary

- Renames `ObjectMetadataItem` to `EnrichedObjectMetadataItem` across
the entire frontend (~440 files) to clarify that this type includes
derived fields (`readableFields`, `updatableFields`, nested `fields[]`,
`indexMetadatas[]`) computed at read time from the metadata store
- Creates `splitObjectMetadataGqlResponse` that goes directly from a
GraphQL `ObjectMetadataItemsQuery` response to flat store items
(combining the old
`mapPaginatedObjectMetadataItemsToObjectMetadataItems` +
`splitObjectMetadataItemWithRelated` two-step flow into one call)
- Removes `ObjectMetadataItemWithRelated` type and all "WithRelated"
naming
- Renames `generatedMockObjectMetadataItems` to
`generateTestEnrichedObjectMetadataItemsMock` to make it clear this is
test-only enriched data
- Deletes `useLoadMockedObjectMetadataItems` hook (consolidated into
`useLoadMockedMinimalMetadata`)
- Ensures nothing destined for the metadata store computes
`readableFields`/`updatableFields` (preventing the localStorage bloat
from #18809)

## Type hierarchy (before → after)

**Before:**
```
ObjectMetadataItemsQuery → mapPaginated → ObjectMetadataItemWithRelated → enrich → ObjectMetadataItem
                                        → split → FlatObjectMetadataItem (store)
```

**After:**
```
ObjectMetadataItemsQuery → splitObjectMetadataGqlResponse → FlatObjectMetadataItem (store)
                         → mapPaginated + enrich (tests only) → EnrichedObjectMetadataItem
```

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx test twenty-front` passes (767 suites, 4505 tests)
- [x] `npx nx lint twenty-front` passes
- [ ] CI checks pass


Made with [Cursor](https://cursor.com)
2026-03-22 15:53:47 +01:00
BugIsGod 8ccaf635bc fix: replace > button with button selector in styled wrappers (#18568)
fix CSS selector > button not reaching Button component's inner element
### Reproduce
- when you click Data model and create a new field, you will see this
bug.
- When you import record and check the height of remove button in the
final validation step.

### Root Reason
the Button component internally renders a wrapper div around the
<button> element, so > button only reaches the intermediate div, not the
actual button.

### Fix
- padding-right not applied → chevron overlapped with text
- ValidationStep: height: 24px not applied → Remove button was 32px
instead of 24px

<img width="1288" height="376" alt="image"
src="https://github.com/user-attachments/assets/885cd8b0-1fe2-484a-8425-70f52b784ecb"
/>

<img width="1001" height="291" alt="image"
src="https://github.com/user-attachments/assets/0b489478-8fbc-4f7e-886a-38012eeb07ef"
/>
2026-03-18 15:34:33 +00:00
Charles Bochet 1be87eb97b chore: frontend dead code removal and naming cleanup (#18690)
## Summary

- **Delete 10 unused files**: 7 hooks (`useWorkflowRunUnsafe`,
`useGetViewById`, `useCreateViewFieldGroup`, `useDeleteViewFieldGroup`,
`useUpdateViewFieldGroup`, `useCreateManyViewFieldGroups`,
`useMoveViewColumns` + test), 1 component (`SettingsSummaryCard`), 1
utility (`createEventContext`)
- **Rename `objectMetadataItemsState` → `objectMetadataItemsSelector`**
across ~85 files to accurately reflect it is a derived selector (via
`createAtomSelector`), not a base Jotai atom

## Details

### Dead code removed

| Type | Name | Reason |
|------|------|--------|
| Hook | `useWorkflowRunUnsafe` | Never imported — duplicate of
`useWorkflowRun` without schema validation |
| Hook | `useGetViewById` | Never imported — `useViewById` is used
instead |
| Hook | `useCreateViewFieldGroup` | Never imported — CRUD done via
`usePerformViewFieldGroupAPIPersist` |
| Hook | `useDeleteViewFieldGroup` | Same as above |
| Hook | `useUpdateViewFieldGroup` | Same as above |
| Hook | `useCreateManyViewFieldGroups` | Same as above |
| Hook | `useMoveViewColumns` | Only imported by its own test — no
production usage |
| Component | `SettingsSummaryCard` | Never imported anywhere |
| Utility | `createEventContext` | Never imported anywhere |

### Rename

`objectMetadataItemsState` is created via `createAtomSelector` (it
derives from `objectMetadataItemsWithFieldsSelector`), so naming it
`*State` is misleading. Renamed to `objectMetadataItemsSelector` for
consistency with sibling selectors like
`objectMetadataItemsByNamePluralMapSelector`.
2026-03-17 00:49:37 +01:00
Félix Malfait c4e55d08ff fix: allow identical singular and plural labels for objects (#18678)
## Summary

Closes #18673

Some languages (e.g., German "Unternehmen") and even English words
(sheep, deer, aircraft, series) have identical singular and plural
forms. Twenty previously blocked saving when labels matched, making it
impossible to correctly name objects in these cases.

- **Labels** are purely display strings — removed the equality
validation from both the frontend Zod schema and backend validator
- **API names** (nameSingular/namePlural) must stay different since they
generate distinct GraphQL resolvers (`findOne` vs `findMany`,
`createOne` vs `createMany`, etc.) and REST endpoints — this validation
is preserved
- Added a shared `computeMetadataNamesFromLabels` util in
`twenty-shared` that auto-appends `'s'` to the plural API name when both
labels produce the same camelCase name (e.g., "Unternehmen" →
`unternehmen` / `unternehmens`)
- Both the frontend form and backend sync-check use the same shared util
— single source of truth, no duplicated logic

**No retroactive impact**: since the old code prevented identical labels
from ever being saved, no existing workspace has `labelSingular ===
labelPlural`.

## Test plan

- [x] New unit tests for `computeMetadataNamesFromLabels` (7 tests:
standard labels, Sheep, Unternehmen, Aircraft, empty labels, different
labels, applyCustomSuffix)
- [x] Updated frontend schema validation tests (identical labels with
different names now passes; identical names still fails)
- [x] Updated backend integration test cases (removed identical-label
failing cases)
- [ ] Manual: create a new object with identical singular/plural labels
(e.g. "Sheep" / "Sheep") — should save successfully with API names
`sheep` / `sheeps`
- [ ] Manual: verify existing objects with different labels still work
unchanged


Made with [Cursor](https://cursor.com)
2026-03-16 18:07:34 +01:00