Files
twenty/packages/twenty-server
Félix Malfait 647a6aec58 Add nested relation Field widgets on record page layouts (#23815)
## Context

Record pages can show a list of directly related records (Field widget
in Table display mode), but not records two relation hops away. The
canonical ask: on a Client page, list the Transactions of the
Client's Wallets.

Stacked on #23814 (merged) and #23832 (merged); their commits are
included in this branch. #23836 stacks on this PR to add many-to-one
first hops.

## How it works

The 2-hop case does not need any new query capability. It reuses the
relation traversal filter shipped for advanced filters: the widget
embeds a view on the terminal object (Transaction) with one seeded
filter `inverse relation IS current record`, traversed one hop
(`fieldMetadataId` = Transaction.wallet, `relationTargetFieldMetadataId`
= Wallet.client, value `isCurrentRecordSelected`). At query time this
compiles to `{ wallet: { clientId: { in: [currentRecordId] } } }`, which
is within the backend's `MAX_RELATION_FILTER_DEPTH = 1` since the
second hop lands on the join column. Records from all intermediate
records (all wallets of the client) are listed, so one-to-many fan-out
on the first hop works out of the box.

## Changes

Configuration
- `FieldConfiguration` gains an optional `nestedRelationFieldMetadataId`
(shared type, DTO, GraphQL fragment, regenerated metadata types).
Backward compatible: existing widgets are untouched.

UI
- The Field picker drills into one-to-many relation fields, mirroring
the advanced filter submenu pattern: back header, an entry to select the
relation itself (previous behavior), then the target object's
one-to-many relations. Selecting a nested field creates a widget titled
`First hop → Second hop` in Table display mode. First-level rows that
open a submenu never show the checkmark; the selected chain is only
visible inside the submenu, matching the chart group by field selection.
- The layout dropdown, settings panel and renderer resolve the terminal
object of the chain; a widget whose second hop was deleted or
deactivated renders nothing instead of silently showing first-hop
records.
- Nested widgets only offer embedded view layouts (Table / Kanban /
Calendar), since inline display modes would render the first hop's
relation field.
- The relation table view resolver regenerates the embedded view
whenever the selection results in a table widget and the chain changed
or the view id is missing, so a table widget can never carry a view
belonging to a different chain.

Server
- `FieldConfigurationDTO` accepts the new optional field.
- Both universal configuration mappers (to and from universal
identifiers) carry it for app manifest sync.
- New `validateFieldConfigurationNestedRelationOrThrow` enforces that
both hops are active one-to-many relation fields on the right objects,
wired next to the existing chart field reference validation.

Record creation
- `buildRecordInputFromFilter` skips relation-traversal filters: they
constrain a related record's column, so prefilling the created
record's own foreign key from them would link the wrong record (e.g.
`walletId = clientId`).
- Add New in a nested widget table instead prompts for the record to
create through: the row opens a picker listing the current record's
first-hop records (the client's wallets), scoped with a find filter
on the relation join column, and creates the record with the picked id
prefilled. Covers the plain table and per-group add rows. Board and
calendar layouts hide their create buttons in nested widgets since they
cannot know the record to create through.
- Matching the created record against the widget's traversal filter
client side is handled by #23832.

Out of scope, deliberately: depth stays at exactly two levels (matches
the backend filter depth cap), junction and morph relations are not
drillable, and chart widgets on record pages are untouched.

## Tests

- Unit: nested chain resolution util, draft view seeding with the
traversal filter, view id change resolver, picker parameter derivation,
server-side validation. Full `page-layout` and `record-filter` front
suites pass (187 suites / 1233 tests), server `page-layout-widget`
suites pass.
- Manual, on seeded data: created a `People → Opportunities` widget on a
Company page; it lists exactly the opportunities whose point of contact
belongs to that company, persists across save and reload, and scopes per
record. Add New opens a picker showing only that company's people;
picking one creates an opportunity with `pointOfContactId` set (verified
in DB) and the row appears in the widget immediately.

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

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23815?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-08-06 09:26:53 +02:00
..