Commit Graph

1736 Commits

Author SHA1 Message Date
bitloi db31b83a86 fix: allow board column tag to shrink so aggregate value remains visible (#17372)
## Summary
When a select option label is long, it was pushing the aggregate value
(e.g., '12.6m') out of view because the tag had `flex-shrink: 0`.

Changed to `min-width: 0` and `overflow: hidden` to allow the tag to
shrink and truncate with ellipsis, keeping the aggregate value visible.

## Changes
- Modified `StyledTag` in
[RecordBoardColumnHeader.tsx](cci:7://file:///root/74/bronze/twenty/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx:0:0-0:0)
to allow shrinking

The
[Tag](cci:1://file:///root/74/bronze/twenty/packages/twenty-ui/src/components/tag/Tag.tsx:100:0-141:2)
component already has built-in text truncation with
`OverflowingTextWithTooltip`, so this change enables that functionality
to work properly.

Fixes #17350
```**

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-01-30 18:35:52 +00:00
BOHEUS 2bcae523ac Added option to prevent sending requests to twenty-icons (#16723)
Solution for #15891


https://github.com/user-attachments/assets/82ce5c4b-0a78-45a8-8196-413473887820

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-30 16:56:22 +01:00
neo773 cc1ca630fd Workflow Send Email Node Multiple Recipients Support (#17458)
This PR adds support sending emails to multiple recipients

Figma Reference:

https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=88868-88963&t=Ya0csmNlN4xxczvV-11

Demo:


https://github.com/user-attachments/assets/ecaeaaec-fe42-4fb5-96d3-a91d08b30148
2026-01-30 16:54:21 +01:00
Marie 06e803d18b [Fix] Various typeError fixes (#17508)
Fixes
[sentry](https://twenty-v7.sentry.io/issues/6539621673/?environment=prod&environment=prod-eu&project=4507072563183616&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D%20timesSeen%3A%3E3%20TypeError&referrer=issue-stream&sort=date):
fieldValue.map on potentially null fieldValue

Fixes
[sentry](https://twenty-v7.sentry.io/issues/6996079360/?environment=prod&environment=prod-eu&project=4507072563183616&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D%20timesSeen%3A%3E3%20TypeError&referrer=issue-stream&sort=date):
note.id on potentially null note due to recent useActivities change

Fixes sentry
([1](https://twenty-v7.sentry.io/issues/6707703562/?environment=prod&environment=prod-eu&project=4507072563183616&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D%20timesSeen%3A%3E3%20TypeError&referrer=issue-stream&sort=date),
[2](https://twenty-v7.sentry.io/issues/6707703563/?environment=prod&environment=prod-eu&project=4507072563183616&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D%20timesSeen%3A%3E3%20TypeError&referrer=issue-stream&sort=date)
- same issue): selectableListHotKey issue

Fixes
[sentry](https://twenty-v7.sentry.io/issues/7087281049/?environment=prod&environment=prod-eu&project=4507072563183616&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D%20timesSeen%3A%3E3%20TypeError&referrer=issue-stream&sort=date):
issue when reordering fields (putting field in last position)
2026-01-29 10:50:28 +00:00
Lucas Bordeau 2ffe2d8aa6 Add delete and restore event handling for table and board (#17489)
This PR adds what is required to handle soft-delete and restore SSE
events in virtualized table and board.

Restore is not handled in board for now as it requires respecting sorts
when inserting record ids.

Since virtualized table is refetching small chunks, we just refetch for
now.

The long term goal is to handle event handling without refetching in all
main components, and also handle SSE events that have the same origin
that the current tab. But for now we implement what is easily doable.

# QA

Delete between table and board (delete only) :


https://github.com/user-attachments/assets/715dd44a-007a-44ab-bf49-5ef039cd57c3

Delete and restore between table and table : 


https://github.com/user-attachments/assets/f2122519-e969-491f-b71c-018d0f85bd86
2026-01-27 21:09:20 +00:00
Marie dd98146c99 [Fix] display current object label in morph relation picker after rename 2/2 (#17484)
Following https://github.com/twentyhq/twenty/pull/17209

The goal is to display the updated label. For
SingleRecordPickerMenuItemsWithSearch, we did it in two parts to avoid
the breaking change by calling `objectLabelSingular` after its addition
has been deployed to prod
<img width="410" height="295" alt="image"
src="https://github.com/user-attachments/assets/d6014391-b943-4fdb-a15f-e62717463d74"
/>
2026-01-27 16:26:14 +00:00
Abdul Rahman 2a8f834377 Integrate NavigationMenuItem with feature flag support (#17268)
## Implement Navigation Menu Items Frontend

Implements the frontend for navigation menu items, the new system
replacing favorites.

### Changes
- Added GraphQL fragments and queries for navigation menu items
- Added hooks for managing navigation menu items (create, update,
delete, sorting, filtering)
- Updated components to use navigation menu items instead of favorites
- Added test coverage for utility functions

### Migration Note
The favorites and navigation menu item modules currently exist in
parallel. The favorites code will be removed once all data has been
migrated to navigation menu items.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Replaces Favorites with feature-flagged `NavigationMenuItem` across
frontend and backend, while keeping Favorites as fallback until
migration completes.
> 
> - UI: new `navigation-menu-item` components (folders, orphan items,
drag provider/droppable, icons, skeleton), dispatcher components to
switch from Favorites, and updated “Add to favorites” action to create
`NavigationMenuItem` when `IS_NAVIGATION_MENU_ITEM_ENABLED`
> - DnD: shared `validateAndExtractFolderId` and droppable id utils
moved to `ui/layout/draggable-list`; favorites DnD updated to use shared
utils
> - GraphQL (client): add fragments, queries, mutations, hooks
(create/update/delete/find), and generated types; added
`RecordIdentifier` and `targetRecordIdentifier` on `NavigationMenuItem`
> - Prefetch: new prefetch state/effect for navigation menu items; skip
favorites prefetch when flag enabled
> - Backend: add DTOs (`NavigationMenuItem`, `RecordIdentifier`),
resolver `targetRecordIdentifier` field, service logic to fetch record
identifiers with permission-aware access and image signing,
`getRecordImageIdentifier` util, entity relation to `view`, and
migration adding FK on `viewId`
> - Feature flags & seeding: add `IS_NAVIGATION_MENU_ITEM_ENABLED` to
enums, dev seeder enables it; standard app seeds workspace navigation
menu items instead of favorites when flag on
> - Tests: add unit tests for sorting/labels/folder id and related utils
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c99746f08b9f84fc8cec4fcc3a7d7afb8ea92db7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-27 10:42:27 +00:00
Mazen-Saeed 8931f4681a fix isCalendarFieldReadOnly function to check if calender field is re… (#17319)
This PR fixes the inconsistency between Calendar and Table views when
trying to edit createdAt date field.

Previously, calendar cards could be dragged even though the createdAt
field are UI read-only, resulting in the confusing and inconsistent
behavior compared to the Table view where the field is not editable.

The issue was caused by the drag logic checking only user permissions
and not the field’s UI read-only metadata. This change updates the
calendar drag logic to also check for isUIReadOnly, ensuring that
records are not draggable when the selected calendar date field is
read-only.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-01-26 16:28:53 +00:00
Richman018 9c9c0a7595 fix: prevent record title reset on focus for notes and tasks (#17439)
Fixes issue where focusing the record title input would reset the title
to empty. This ensures the draft value is properly initialized from the
field value if it's undefined or empty when the input is focused.

Fixes #17437

---------

Co-authored-by: Daniel <daniel@example.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-01-26 15:35:06 +00:00
Arun 4a5ffcc9d2 [Fix] Bug with one to many update in table #16340 (#17416)
fixes #16340 

we are updating the cache partially to prevent the flow of the corrupted
fields into the cache

the fields get corrupted during the mutation process potentially
overriding the recoil cache as we are passing the `newRecordCache`
directly in `upsertRecordsInStore`, the newRecordCache is thin and hence
the recoil wipes out the fields that are undefined or empty



we prevent this by extracting the partial data ( only the data which is
being updated in the field ) and passing it to `upsertRecordsInStore`,
as it only touched the specific fields and updates the data, leaving the
other fields untouched.


https://github.com/user-attachments/assets/5256bef7-70c3-47b3-b2ce-dd02ee1a2de8

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-01-26 13:27:56 +00:00
Aman Raj 62845cac87 Fix the default value of search record if the filter is boolean type (#17297)
This fixes the #15896. For problem statement. Please refer to the shared
video mentioned in the issue.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Aligns filter defaults across UI by centralizing initial value
computation.
> 
> - Add boolean handling in `useGetInitialFilterValue` to return
`value/displayValue` of `'false'` when operand is `IS`
> - Update `WorkflowDropdownStepOutputItems` to use
`getInitialFilterValue` for initial `value` instead of an empty string,
based on field type and default operand
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3ce05fa31aec1c9e771cb1b3ff20ddea3fab304f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-01-24 22:27:05 +00:00
Abdullah. fa0615d41e Migrate attachments to morph relations + fix morph join column filtering (#17381)
Closes [1744](https://github.com/twentyhq/core-team-issues/issues/1744).

This PR migrates attachments to morph relations behind a feature flag,
following the TimelineActivity pattern. it introduces the
`IS_ATTACHMENT_MIGRATED` flag, updates standard field metadata and
indexes to use morph relations, adds a workspace migration that renames
`attachment.*Id` columns to `target*Id` and converts the corresponding
field metadata to `MORPH_RELATION` with a shared `morphId`. On the
frontend, attachment read/write paths now switch to `target*Id` when the
flag is enabled.

It also fixes optimistic filtering for morph join columns. The metadata
API deduplicates morph fields, so attachments now expose a single target
field of type `MORPH_RELATION` plus a `morphRelations` array listing
each target object. Because only one `settings.joinColumnName` is
returned (e.g. `targetRocketId`), filters like `targetCompanyId` don’t
map to any field and the optimistic cache code throws.
`doesMorphRelationJoinColumnMatch` resolves this by computing all valid
join column names from `morphRelations` using
`computeMorphRelationFieldName` and comparing them to the filter key.
That makes filters like `targetCompanyId` resolvable even with a single
target field, so attachment uploads and list matching no longer crash.

<img width="477" height="474" alt="image"
src="https://github.com/user-attachments/assets/50e19418-3438-4d1e-9f1f-1bc1a03174a9"
/>

<br />
<br />

Today the metadata API returns one morph field called `target` and a
list of possible targets (`morphRelations`), but it does not tell us the
join column for each target. That’s why the Frontend had to compute join
column names.

If we want to fix this at the API level, there are two options:

- Add join column names to each target in `morphRelations` (e.g. company
→ `targetCompanyId`). This is additive and low‑risk.
- Return each target as its own field (`targetCompany`, `targetPerson`,
etc.) instead of a single target. This is a larger change because it
changes the shape of metadata and would require more UI updates.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Introduces morph relations for attachments behind
`IS_ATTACHMENT_MIGRATED`, aligning server schema/metadata and frontend
behavior.
> 
> - Adds `IS_ATTACHMENT_MIGRATED` flag (frontend/server) and
seeds/defaults; updates generated GraphQL enums
> - New workspace upgrade `1.17` command migrates data: renames
`attachment.*Id` → `target*Id` and converts related fields to
`MORPH_RELATION` with shared `morphId`
> - Updates standard field metadata and indexes to `target*` (attachment
+ related objects), dev seeds, snapshots, and workspace entity types
> - Frontend: switches attachment read/write filters via
`getActivityTargetObjectFieldIdName` using the feature flag; updates
hooks/components (`useAttachments`, `useUploadAttachmentFile`, editors);
expands `Attachment` type
> - Fixes optimistic cache filtering to recognize morph join columns in
`isRecordMatchingFilter` by computing valid join-column keys from
`morphRelations`
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f208fa23b1135af114f484f6d65041a816d6e718. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-01-24 21:17:24 +00:00
Arun 84d140025a fix: refresh virtualized table when field metadata is updated ( #16388 ) (#17214)
Fixes #16388 

Now we have used the metadata fetching from network using
resetVirtualizationBecauseDataChanged(), as we navigate back to the
table after updating the field content. As the virtualised table uses
the "cache-first" strategy, it fails to give fresh data from the data
base

Please let me know i we need to add the loading state (and it's UI
requirements) while it fetches the cell content, as currently the cell
is empty until it's populated by fresh data




https://github.com/user-attachments/assets/901085ba-4337-4a5d-86c8-15ac84250e8f

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-01-24 18:56:00 +00:00
martmull 0091ef5f6c Sync built files (#17379)
as title, upload built files to local storage

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-23 13:43:53 +01:00
Raphaël Bosi 78dd43dbb0 Release dashboards V1 (#17386)
Remove `IS_PAGE_LAYOUT_ENABLED` feature flag entirely
2026-01-23 10:30:05 +00:00
Félix Malfait 41dd9856e6 fix(twenty-front): fix tsconfig to properly typecheck all files with tsgo (#17380)
## Summary

This PR fixes the `tsconfig` setup in `twenty-front` so that `tsgo -p
tsconfig.json` properly type-checks all files.

### Root Cause

The previous setup used TypeScript project references with `files: []`
in the main `tsconfig.json`. When running `tsgo -p tsconfig.json`, this
checks nothing because `tsgo` requires the `-b` (build) flag for project
references, but the configs weren't set up for composite mode.

### Changes

**Simplified tsconfig architecture (4 files → 2):**
- `tsconfig.json` - All files (dev, tests, stories) for
typecheck/IDE/lint
- `tsconfig.build.json` - Production files only (excludes tests/stories)

**Removed redundant configs:**
- `tsconfig.dev.json`
- `tsconfig.spec.json` 
- `tsconfig.storybook.json`

**Updated references:**
- `jest.config.mjs` → uses `tsconfig.json`
- `eslint.config.mjs` → uses `tsconfig.json`
- `vite.config.ts` → uses `tsconfig.json` for dev

**Type fixes (pre-existing errors revealed by proper typechecking):**
- Made `applicationId` optional in `FieldMetadataItem` and
`ObjectMetadataItem`
- Added missing `navigationMenuItem` translation
- Added `objectLabelSingular` to Search GraphQL query
- Fixed `sortMorphItems.test.ts` mock data

## Test plan

- [ ] Run `npx nx typecheck twenty-front` - should pass
- [ ] Run `npx nx lint twenty-front` - should work
- [ ] Run `npx nx test twenty-front` - should work
- [ ] Run `npx nx build twenty-front` - should work
- [ ] Verify IDE type checking works correctly
2026-01-23 11:22:23 +01:00
Marie 8868ef4ec3 Remove useUpdateOneRecordV2 (#17355)
Previously we introduced useUpdateOneRecordV2 to handle morph relations.
In this PR we are removing it to only use useUpdateOneRecord which has
been adapted not to requires objectMetadataNameSingular in its props.
2026-01-22 16:27:57 +00:00
Baptiste Devessier 267691a403 Render missing context provider for layouts (#17349)
## Before

<img width="1656" height="892" alt="image"
src="https://github.com/user-attachments/assets/05c6ced5-ff13-4987-a978-bae5111da9d4"
/>


## After



https://github.com/user-attachments/assets/a3b34590-8006-4363-8fa4-b52fb099bbff
2026-01-22 16:17:04 +00:00
Ayesha Waseem 669da1a87d fix: display current object name in morph relation picker after rename (#17209)
**Summary**

Issue #16963: When an object is renamed, the morph relation picker still
shows the old name.

**Root cause**

The picker used searchRecord.objectNameSingular from the GraphQL search
response, which can be stale after a rename.
The search record stores the object name at query time, not the current
metadata.

**Solution**

- Updated SingleRecordPickerMenuItem:
- Added useObjectMetadataItems to access current object metadata.
- Look up the current object metadata by morphItem.objectMetadataId.
- Use labelSingular (or nameSingular as fallback) instead of
searchRecord.objectNameSingular for display.
- Updated MultipleRecordPickerMenuItemContent:
- Use objectMetadataItem.labelSingular (already available as a prop)
instead of searchRecord.objectNameSingular.

---------

Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
2026-01-22 15:40:57 +00:00
Weiko 9ee2cd0fe3 Enable RLS in lab (#17328) 2026-01-22 12:27:11 +01:00
Thomas Trompette 981956a636 Use new SSE setup for workflow runs (#17309)
- Add a specific subscriber for workflow run
- Workflow runs use apollo cache. Adding updateRecordFromCache on record
updates
2026-01-21 17:26:43 +00:00
bitloi a4f5197e45 fix: implement polymorphic relation detach when selecting 'No {Field}' (#17264)
Fixes #17253

When clicking 'No {Field}' in a polymorphic relation picker, the
relation was not being cleared. This commit implements the missing
detach logic:

- RecordDetailMorphRelationSectionDropdownManyToOne: Call onSubmit with
newValue: null when no item is selected
- MorphRelationManyToOneFieldInput: Call persistMorphManyToOne with
valueToPersist: null for detach case
- useMorphPersistManyToOne: Implement actual detach logic that sets all
morph relation ID fields to null via updateOneRecord

Also adds unit tests for the useMorphPersistManyToOne hook.
2026-01-21 15:45:14 +00:00
Lucas Bordeau 2835935f11 Handled SSE create event (#17293)
This PR handles SSE create event.

It also fixes a regression on board, which was making it flash with
skeletons on any update / create.

This PR was a good test case to experience how each main components of
the app : table, board, calendar, reacts to a create event.

It is not straightforward for now, because each component handles
records with its own state management to turn those records into a
coherent display of rows or cards.

The requests for each component are also different : fetch more, group
by, multiple requests in parallel, so the cleanest way to handle
optimistic effect requires to create one small optimistic engine per
component tuned for its internal data logic.

For now we've decided to implement what's doable in a reasonable amount
of time and that includes not handling table with groups for now.

Creating a clean optimistic logic for each component will be done later.

# QA

## Importing 100 records via the API (could be a script, a workflow, an
AI calling tools, a CSV import, etc.)


https://github.com/user-attachments/assets/d38a3770-8b0a-4f83-8275-5e7d1be0a5c6

## Creating from different components and seeing the SSE event being
processed by other components


https://github.com/user-attachments/assets/106b2f49-19cb-4190-92b7-0653c8373366
2026-01-21 10:49:20 +00:00
Félix Malfait 3ed67b825e feat: implement generic many-to-many junction relation support (#16820)
## Overview

This PR implements **generic many-to-many relation support** through
junction tables (also known as associative entities or join tables).
This replaces the need for hardcoded taskTarget/noteTarget logic and
provides a flexible foundation for modeling complex entity
relationships.

## Architecture

### Data Model

Many-to-many relationships are implemented using a **junction object
pattern**:

```
┌─────────┐       ┌──────────────────┐       ┌─────────┐
│  Pet    │──────>│   PetRocket      │<──────│ Rocket  │
│         │ 1:N   │  (junction)      │  N:1  │         │
│ rockets ├───────┤ pet    : Pet     ├───────┤         │
└─────────┘       │ rocket : Rocket  │       └─────────┘
                  └──────────────────┘
```

The junction object (PetRocket) has:
- A `MANY_TO_ONE` relation to **Pet** (the source)
- A `MANY_TO_ONE` relation to **Rocket** (the target)

The source object (Pet) has a `ONE_TO_MANY` relation pointing to the
junction, with **field settings** that specify which target field to
follow.

### Field Settings Schema

Junction configuration is stored in `FieldMetadataRelationSettings`:

```typescript
{
  relationType: "ONE_TO_MANY",
  // Points to the target field on the junction object
  junctionTargetFieldId?: string;      // For regular relations
  junctionTargetMorphId?: string;      // For polymorphic relations
}
```

**Two configuration modes:**
1. **`junctionTargetFieldId`** - References a specific `RELATION` field
on the junction
2. **`junctionTargetMorphId`** - References a `morphId` group for
polymorphic targets (e.g., link to Person OR Company)

### GraphQL Query Generation

When a junction relation is detected, the GraphQL fields are generated
to fetch the nested target:

```graphql
query GetPetWithRockets {
  pet(id: "...") {
    rockets {           # ONE_TO_MANY to junction
      id
      rocket {          # Target field on junction
        id
        name
        __typename
      }
    }
  }
}
```

For polymorphic junction targets:
```graphql
caretakerPerson { id, name }
caretakerCompany { id, name }
```

## Frontend Architecture

### Display Flow

1. **Detection**: `hasJunctionConfig()` checks if field has junction
settings
2. **Config Resolution**: `getJunctionConfig()` resolves junction object
metadata and target fields
3. **Record Extraction**: `extractTargetRecordsFromJunction()` extracts
target records from junction records
4. **Rendering**: Target records displayed as chips (not junction
records)

### Edit Flow

1. **Picker Opening**: Initializes the multi-record picker with:
   - Searchable object types (derived from junction target fields)
   - Pre-selected items (extracted from existing junction records)
   
2. **Selection Handling**: Manages create/delete of junction records:
   - **Select**: Creates new junction record with source + target IDs
   - **Deselect**: Finds and deletes the junction record
- **Optimistic Updates**: Manually updates Recoil store before API call

### Key Trade-offs

| Decision | Trade-off |
|----------|-----------|
| Junction records managed manually | More control over optimistic
updates, but requires manual cache management |
| Settings stored per-field | Flexible (same junction can power
different views), but requires UI to configure |
| Polymorphic via morphId groups | Supports N target types, but adds
query complexity |
| Feature flag gated | Safe rollout, but requires flag management |

## Backend Changes

- **Validation**: Junction target field must exist and be a valid
`MANY_TO_ONE` relation
- **Settings**: Extended `FieldMetadataRelationSettings` type with
junction fields
- **Dev Seeder**: Added sample junction objects (PetRocket,
EmploymentHistory, PetCareAgreement) for testing

## How to Test

1. Enable the `IS_JUNCTION_RELATIONS_ENABLED` feature flag
2. Create objects with junction pattern (Pet → PetRocket → Rocket)
3. Configure the junction target in field settings (advanced mode)
4. Verify:
- Display shows target objects (Rockets), not junction records
(PetRockets)
   - Picker allows selecting/deselecting targets
   - Changes persist correctly



https://github.com/user-attachments/assets/d04f057a-228c-4de8-af48-76bb2d72cac1

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-20 21:58:13 +01:00
Etienne 30c13fc947 Files v2 - Add new FILES field type (#17236)
In this PR : 
- New field type FieldMetadataType.FILES added (as jsonb in DB)
- Backend: GraphQL types, validation, REST API schema, data processor
handling
- Frontend: field configuration in settings
- Feature flag IS_FILES_FIELD_ENABLED to gate the feature
- Integration tests for create/filter validation
- Dev seed: Feature flag enabled + FILES field on survey result object


To do in next PRs : 
- Backend : 
-- new workspaceFile controller (for download) & new workspaceFile
upload resolver (for upload)
-- pre-hook/post-hook to ensure fileId existence + listener to ensure
cleaning
- Frontend : FILES field display/edit in table view, record, ...
2026-01-20 17:03:49 +00:00
Weiko b6635ba272 Add RLS Entitlement check (#17179)
## Context
- Add RLS entitlement to billing
- Check value in the backend (for RLS predicate entity
queries/mutations)
- Expose billingEntitlements to the API inside currentWorkspace to check
available features to the workspace and display the role components
accordingly
- Cleanup RLS when plan changes back to one without RLS.

This should cover almost everything, imho we don't need to check in the
ORM because => We can't create RLS without the correct PLAN and
switching back to a PLAN without RLS deletes existing RLS through
stripes webhooks
2026-01-20 16:06:37 +00:00
Weiko b64951634e fix rls create new record with composite (#17243)
## Context
buildValueFromFilter was not handling composite filters which was needed
for RLS. This PR implements that and fix some issues with RLS

Tested with a few composite + relation fields
<img width="559" height="206" alt="Screenshot 2026-01-19 at 15 38 42"
src="https://github.com/user-attachments/assets/d64afa6e-3e12-4843-a215-a693665112c5"
/>
2026-01-20 05:36:38 +00:00
Raphaël Bosi d0bc8b9ffe [DASHBOARDS] Move all the graph computing logic to the backend (#17189)
- Create resolvers for each type of charts which needs data
transformation after the group by operation: Bar Chart, Line Chart and
Pie Chart
- Move all the utils to the backend and refactored some into services

This allows all the computation to be done in the backend, improving
performances in the frontend.
2026-01-19 17:25:45 +00:00
Lucas Bordeau d51c988a9e Implemented SSE update across main front components (#17205)
This PR implements SSE update events across the main components of the
application : tables, boards, calendars, show pages.

There is still work to do on other event type and on making sure
everything works fine, but this first implementation should be robust
enough to start with.

Some problems encountered along the way : 
- Events are returning raw Postgres output, because they are not
normalized by the GraphQL layer, so we ended up with amountMicros as
string values, which the frontend does not like, so I implemented a
small util in our `formatResult` generic pipeline to turn amountMicros
to a number if it's a string value. We could implement other formatters
for composite fields if we see problems with events.
-`action` property was missing in SSE events, which is required by the
frontend to know what kind of event it is.

# QA


https://github.com/user-attachments/assets/393b45ac-59d2-48b0-855b-2ce9c4b8ae57


https://github.com/user-attachments/assets/cb214e7a-1595-4b85-bdb6-87630993d2e2
2026-01-18 16:14:36 +00:00
Abdullah. 92eff62523 Replace test-runner with vitest for storybook (#17187) 2026-01-18 03:25:45 +00:00
Félix Malfait c737028dd6 Move tools/eslint-rules to packages/twenty-eslint-rules (#17203)
## Summary

Moves the custom ESLint rules from `tools/eslint-rules` to
`packages/twenty-eslint-rules` for better organization within the
monorepo packages structure.

## Changes

- Move `eslint-rules` from `tools/` to `packages/twenty-eslint-rules`
- Use `loadWorkspaceRules` from `@nx/eslint-plugin` to load custom rules
- Update all ESLint configs to use the `twenty/` rule prefix instead of
`@nx/workspace-`
- Update `project.json`, `jest.config.mjs` with new paths
- Update `package.json` workspaces and `nx.json` cache inputs
- Update Dockerfile reference

## Technical Details

The custom ESLint rules are now loaded using Nx's `loadWorkspaceRules`
utility which:
- Handles TypeScript transpilation automatically
- Allows loading workspace rules from any directory
- Provides a cleaner approach than the previous `@nx/workspace-`
convention

## Testing

- Verified all 17 custom ESLint rules load correctly from the new
location
- Verified linting works on dependent packages (twenty-front,
twenty-server, etc.)
2026-01-17 07:37:17 +01:00
Baptiste Devessier 5bcbe43596 Various layout fixes so that RPL v1 look the same as production version (#17195)
> [!WARNING]
> Most of the quick fixes will be dropped once we release the feature
and deprecate the old show pages code.

This PR fixes padding and alignment issues so that the new Record Page
Layout feature looks like the old show pages.

## This PR

<img width="3456" height="2160" alt="CleanShot 2026-01-16 at 17 24
40@2x"
src="https://github.com/user-attachments/assets/f3640553-2316-498c-8dd0-14b09ed913f1"
/>

<img width="3456" height="2160" alt="CleanShot 2026-01-16 at 17 27
14@2x"
src="https://github.com/user-attachments/assets/c4ccb28f-5629-4e68-83ad-863f21066962"
/>


## The production

<img width="3456" height="2160" alt="CleanShot 2026-01-16 at 17 25
11@2x"
src="https://github.com/user-attachments/assets/a91f7216-0b57-404b-9f9d-f6f45d4858d6"
/>

<img width="3456" height="2234" alt="CleanShot 2026-01-16 at 17 27
19@2x"
src="https://github.com/user-attachments/assets/aaf1ee6b-b323-4844-9f1a-a59807cdbe40"
/>
2026-01-16 17:06:34 +00:00
Weiko 8413c6f3dd Fix insert new record with RLS (#17164)
## Context
Now that RLS predicates are applied, creating a record through the FE
(which is empty by default) is failing if your role has predicates and
your input does not respect them (which will always be true since, as
said above, input will be pretty much empty)

## Implementation
- Moved isMatching* filters to twenty-shared
- Implemented isMatchingRlsPredicates utils in the backend (ORM) to
check before insertion/update if the record is matching the current user
role Rls predicates, reusing the isMatching* filters utils moved to
twenty-shared
- Frontend now applies RLS predicates before creating a new record
(similarly to what we do with view filters)

Note:
It seems composite were not properly handled with view-filter insertion
logic, since I'm reusing the util for now, the issue remains for RLS and
will need to be addressed
2026-01-15 16:40:47 +00:00
Lucas Bordeau 2cff75d772 Refactored object operation dispatch for SSE (#17174)
This PR refactors object record operation dispatch through a browser
event instead of a state that was registering all operations.

This is a cleaner pattern as it is a synchronous event code path instead
of relying on a useEffect to watch state change, which is not ideal.

We introduce this change first to then rely on this new pattern to
dispatch SSE events in a following PR.
2026-01-15 16:38:21 +00:00
Lucas Bordeau 6b14cfaa89 Fixes two critical bugs for DATE type (#17143)
This PR fixes two critical bugs with the DATE type, following up the
recent refactor around dates and time zones :
https://github.com/twentyhq/twenty/pull/16544

There are a lot of related bugs in Sentry, this PR should fix all bugs
that are of the form :

`Cannot parse: 2026-02-04T00:00:00.000Z`

# `node-postgres` date type

The package `node-postgres`, used by TypeORM, returns by default a Date
object for the date OID type.

But we can change this behavior without patching anything.

See the documentation for `pg-types` :
https://github.com/brianc/node-pg-types

Our fix is to call `setTypeParser` from `pg` to have the postgres "date"
type returned as a string always.

```ts
export const setPgDateTypeParser = () => {
  types.setTypeParser(PG_DATE_TYPE_OID, (val: string) => val);
};
```

Then in server's main.ts : 

```ts
const bootstrap = async () => {
  setPgDateTypeParser();
```

## Are we safe with this very low-level fix ?

Since TypeORM bypasses a string value returned by `pg` we are safe with
this modification at a very low-level.

```ts
else if (columnMetadata.type === "date") {
  value = DateUtils.mixedDateToDateString(value)
}
```


https://github.com/typeorm/typeorm/blob/0ec4079cd3760dc49247a02c54415f16a2a51766/src/driver/postgres/PostgresDriver.ts#L838

```ts
/**
* Converts given value into date string in a "YYYY-MM-DD" format.
*/
static mixedDateToDateString(value: string | Date): string {
  if (value instanceof Date) {
      return (
          this.formatZerolessValue(value.getFullYear(), 4) +
          "-" +
          this.formatZerolessValue(value.getMonth() + 1) +
          "-" +
          this.formatZerolessValue(value.getDate())
      )
  }

  return value
}
```


https://github.com/typeorm/typeorm/blob/6f3788b83730463e3b787b2a98bb41695e13caf8/src/util/DateUtils.ts#L28

For reference the original type parser seems to be configured in
node-pg-types, on the 1182 OID, which is an array of date / 1082 OID,
this type parser calls another small library `postgres-date` which
creates a JS Date. I couldn't find a type parser explicitly on 1082 in
the stack TypeORM -> node-postgres -> node-pg-types -> postgres-date

```ts
register(1182, parseStringArray) // date[]
``` 


https://github.com/brianc/node-pg-types/blob/26bfe645a8ddc0c73830b1b8c63f2c4f8265b24f/lib/textParsers.js#L168C19-L168C45

```ts
 static parse (dateString) {
  return new PGDateParser(dateString).getJSDate()
}
```


https://github.com/bendrucker/postgres-date/blob/b3060560ed62c250f7800d29e4b68a9d5a0622bd/index.js#L285


# Calendar view mixing DATE and DATE_TIME

At the time of the refactor, DATE type wasn't properly tested with
calendar view, thus the drag and drop of a card with a calendar view on
a DATE field and not a DATE_TIME field, was not working, this is now
fixed.
2026-01-14 13:38:24 +00:00
Thomas Trompette ef8d7c18c1 Set back amount micros (#17124)
The amount to amount micros in UI breaks variable. Setting back amount
micros in UI with a hint

<img width="399" height="178" alt="Capture d’écran 2026-01-13 à 15 54
04"
src="https://github.com/user-attachments/assets/52f53a78-05a8-421d-80fe-f5cd0e8aad19"
/>
2026-01-14 14:18:28 +01:00
Abdullah. 2c8d3f02e1 feat: upgrade to Storybook version 10 (#17110)
Upgraded to Storybook 10. We still use `@storybook/test-runner` for
testing since it appears it'd require more work to move from Jest to
Vitest than I initially anticipated, but I completed this PR to fix
`storybook:serve:dev` - it takes time to load, but it works the way it
used to with Storybook 8.


https://github.com/user-attachments/assets/7afc32c6-4bcf-4b37-b83b-8d00d28dda15
2026-01-13 08:18:07 +00:00
Lucas Bordeau 04a370e043 Implemented SSE subscription mechanism on the frontend (#17017)
This PR is a follow-up of https://github.com/twentyhq/twenty/pull/16966
and implements a new mechanism to handle SSE events.

It creates only one event stream per browser tab, then use mutations to
tell the backend which query to listen to, without re-mounting the event
stream connexion.

Then each event that comes from this unique subscription is then
dispatched in a new JavaScript CustomEvent, per queryId, on which
specific hooks add an event listener.

This PR introduces the generic tooling as well as the handling of update
events on table.
2026-01-12 18:58:46 +01:00
Weiko c795b8c52a RLS FE implementation (#17062)
## Summary
This PR introduces row-level security (RLS) permissions for roles in the
frontend, allowing fine-grained access control at the record level.
Users can now define permission rules that determine which specific
records a role can access based on dynamic conditions and filters.
## What's Changed
Implemented UI for configuring record-level permissions on object
permissions screens
Added support for defining permission predicates using filter conditions
(similar to advanced filters)
Introduced variable picker for dynamic permission rules (e.g., "me"
context for user-specific access)
Built predicate conversion layer to sync UI state with backend
permission structure
Extended GraphQL schema with mutations for upserting row-level
permission predicates
Fixed handling of orphaned RLS groups to prevent data inconsistencies
Added enterprise key validation for RLS features
The implementation enables scenarios like "users can only see their own
records" or "users can access records associated with their team."

<img width="687" height="702" alt="Screenshot 2026-01-09 at 23 07 02"
src="https://github.com/user-attachments/assets/33fe736e-6cbf-40bd-b2eb-c8a90c8d21bc"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-12 17:39:17 +00:00
Lucas Bordeau 9b95d28cb3 Refactored table virtualization real index state (#17074)
This PR refactors the table virtualization real index main state that
was creating performance problems.

The problem was that we kept track of all the real rows even if we only
print 200 at a time in the application.

This caused a crash when some users with 10k+ rows on a table tried to
iterate over this state 10.000 times or more.

The trade-off is that now we keep all real indices in a Map, and each
virtual row uses a selector to go to this Map.

This way if we want to reset the full Map, we just have to overwrite the
base state that contains the map, and the 200 selectors will recompute.
This happens for example when we delete a row. This now has a limited
and negligible performance impact.
2026-01-12 11:41:59 +00:00
Ayesha Waseem 89af749b94 fix: hide "Add new" button for workspace member relations (#17080)
**Description**

Fixes the "Add new" button appearing in relation picker dropdowns for
workspace member relations (e.g., Account Owner on companies). Workspace
members cannot be created from relation pickers, so this button should
not appear.

**Changes Made**

- Modified RelationManyToOneFieldInput.tsx to conditionally pass the
onCreate prop to SingleRecordPicker only when
createNewRecordAndOpenRightDrawer is defined.
- Moved the conditional check to the JSX level for clarity.

**Technical Details**

The useAddNewRecordAndOpenRightDrawer hook already returns undefined for
workspace member relations, but the component was still passing a
handler function to SingleRecordPicker, causing the "Add new" button to
appear. The fix ensures that when createNewRecordAndOpenRightDrawer is
undefined, onCreate is also undefined, which prevents
SingleRecordPickerMenuItemsWithSearch from rendering the button.

Testing

- Verified that the "Add new" button no longer appears in the Account
Owner dropdown for companies.
- Confirmed that the "Add new" button still appears for other relation
fields where it's appropriate (e.g., People, Opportunities).
- Tested that existing functionality for selecting workspace members
from the dropdown still works correctly.

Fixes #17060
2026-01-12 09:08:45 +00:00
Abdullah. 5240a1818f feat: upgrade Storybook to version 9 (#17077)
Upgraded from 8.6.15 to 9.1.17 in two steps: 
- 8.6.15 -> 9.0.0 
- 9.0.0  -> 9.1.17

I had to disable `storybook-addon-cookie` since it is not supported for
Storybook 9. However, I do intend to upgrade to Storybook 10 when this
is merged, so we can replace the aforementioned add-on with this fork
specifically created to support Storybook 10 and above:
https://www.npmjs.com/package/@storybook-community/storybook-addon-cookie.

Additionally, once we upgrade to Version 10 successfully, I will start
looking into integrating the official Vitest add-on.
2026-01-11 13:54:41 +00:00
Thomas Trompette 3acc87a620 [SSE] Add backend for SSE subscriptions (#17022)
- moved a few gql types to twenty shared to re-use in server
- added a new endpoint, onEventSubscription that expect a streamId to
create a connection
- two new endpoints to store queries in Redis
- updated the existing batch channel to directly use object record type
2026-01-10 14:44:02 +00:00
Match 9e22c74b2d Fix sort direction toggle when clicking on existing sort (#17046)
Closes #17032

  Summary

When a sort is already applied to a column, clicking "Sort" from the
column header dropdown now toggles the sort direction (ASC → DESC or
DESC → ASC) instead of doing nothing.

  Problem

Previously, clicking "Sort" on a column that already had a sort applied
would always try to create a new sort with ASC direction. Since the
upsertRecordSort function updates an existing sort with the same field,
this effectively did nothing when the sort was already ASC—which felt
unintuitive.

  Solution

  Modified useHandleToggleColumnSort to:
  - Check if a sort already exists for the clicked field
  - If it exists, toggle the direction
- If it doesn't exist, create a new sort with ASC direction (existing
behavior)

---

Contribution by Gittensor, see my contribution statistics at
https://gittensor.io/miners/details?githubId=132382032
2026-01-10 14:04:26 +00:00
Félix Malfait 1a5675d63e feat: add sorting on relation fields (Many-to-One) (#17021)
## Summary

This PR enables sorting records by fields of related objects. For
example, sorting **People by their Company's name**.

### Before
Only scalar and composite fields could be sorted. Relation fields showed
in the sort dropdown but produced errors.

### After
Many-to-One relation fields can now be sorted using the related object's
**label identifier field** (e.g., Company's `name`).

---

## Changes

### Frontend
- Added `RELATION` to sortable field types (restricted to `MANY_TO_ONE`
relations)
- New `getOrderByForRelationField()` generates nested orderBy structures
using the related object's label identifier
- Updated `turnSortsIntoOrderBy()` to handle relation fields by looking
up related object metadata

### Backend
- Extended `GraphqlQueryOrderFieldParser.parse()` to detect nested
relation ordering like `{ company: { name: 'AscNullsLast' } }`
- Returns `ParseOrderByResult` containing both `orderBy` conditions and
`relationJoins` info
- Added LEFT JOINs for relation ordering in `applyOrderToBuilder()`
- Added `addRelationOrderColumnsToBuilder()` for TypeORM DISTINCT
compatibility

### Tests
- Added unit tests for `filterSortableFieldMetadataItems`,
`getOrderByForRelationField`, and `turnSortsIntoOrderBy`
- Added integration tests covering ascending/descending order and
composite label identifiers

---

## TypeORM Bug Workaround

We encountered a significant TypeORM limitation when implementing this
feature. When using `getMany()` with `ORDER BY` on joined relation
columns, TypeORM generates a DISTINCT subquery that has specific
requirements:

### Issue 1: Alias Parsing
TypeORM's `orderBy()` method fails with **"alias not found"** when using
quoted SQL identifiers like `"company"."name"`. TypeORM internally
expects unquoted property paths (e.g., `company.name`) for its alias
resolution mechanism.

### Issue 2: setFindOptions Clears addSelect
`setFindOptions({ select })` **clears any previously added `addSelect()`
columns**. This caused `"column distinctAlias.company_name does not
exist"` errors because the relation columns needed for ORDER BY were
being removed.

### Solution
We split the logic into two methods:
1. `applyOrderToBuilder()` - adds JOINs and ORDER BY (before
`setFindOptions`)
2. `addRelationOrderColumnsToBuilder()` - adds relation columns for
SELECT (AFTER `setFindOptions`)

This ensures the relation columns are present in the final SQL query's
SELECT clause with the proper underscore aliases (`company_name`) that
TypeORM's DISTINCT subquery expects.

**Related TypeORM issue**:
https://github.com/typeorm/typeorm/issues/9921

---

## Screenshots/Demo

_Add screenshots if applicable_
2026-01-10 11:02:16 +01:00
Abdul Rahman b1c821b0e3 Implement hide empty groups for grouped table view (#16494)
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-01-09 17:31:22 +01:00
Baptiste Devessier f1aee7fd18 Add a CARD layout to the Field widget (#16995)
Closes https://github.com/twentyhq/core-team-issues/issues/1943

## Demo



https://github.com/user-attachments/assets/ea86e1e6-6495-42b8-8dd3-e2bf7d295bab

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-01-08 15:43:34 +00:00
Marie f414f4799b Enable switching field to group records by on existing kanban view (#17015)
Fixes https://github.com/twentyhq/twenty/issues/16982 and
https://github.com/twentyhq/private-issues/issues/403

Re-introducing the feature to allow to switch field to group records by
on a kanban view


https://github.com/user-attachments/assets/a552d3f2-7900-4771-b512-98cb99cdd8de
2026-01-08 13:08:06 +00:00
Aman Raj 7ce76277d2 Fix the crash of the app page when object view is configured to open only on record page. (#16977)
### Problem:
If the command menu page is kept opened while navigating to record show
page then the app crashes.

### Root cause

When navigatiing to a record show page while the command menu page is
kept opened, it tries to open the record show page with command menu
open, as its state were set to true , before navigating to a new page
along with the instance id of previous context which are used in the
current context while rendering the page. Hence, leading to an error
page.

### Changes

while navigation to a record show page the command menu open state is
set to false. This is to handle the navigation gracefully

Fixes: #16577
2026-01-08 11:51:07 +00:00
Lucas Bordeau e9b7ad21d2 Fix view picker small bugs (#16987)
This PR solves small bugs around the view picker.

- Couldn’t obtain optimistic update after a re-order of a view by drag
and drop, we needed to refresh the page
- Picking a new icon wouldn’t trigger optimistic update (same problem)
- Picking a new icon would change the view (difficult to understand
behavior)
- Picking a new icon would trigger left drawer collapse (z-index
problem)

Since core views are not being handled by object metadata items anymore,
and that all view logic is plugged on coreViewsState, this PR
implemented optimistic effect by upserting into this state.

Fixes https://github.com/twentyhq/twenty/issues/15422
Fixes https://github.com/twentyhq/twenty/issues/16986

# Before


https://github.com/user-attachments/assets/64099c21-df9f-4772-ab0d-9ea449aed761

# After


https://github.com/user-attachments/assets/f4e844b3-6530-4178-abdb-b7a10d2327b8

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-08 12:05:45 +01:00