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
2026-06-11 11:02:28 +02:00

Twenty logo

The #1 Open-Source CRM

Website · Documentation · Roadmap · Discord · Figma

Twenty banner


Why Twenty

Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.

Learn more about why we built Twenty


Installation

Cloud

The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.

Build an app

Scaffold a new app with the Twenty CLI:

npx create-twenty-app my-app

Define objects, fields, and views as code:

import { defineObject, FieldType } from 'twenty-sdk/define';

export default defineObject({
  nameSingular: 'deal',
  namePlural: 'deals',
  labelSingular: 'Deal',
  labelPlural: 'Deals',
  fields: [
    { name: 'name', label: 'Name', type: FieldType.TEXT },
    { name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
    { name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
  ],
});

Then ship it to your workspace:

npx twenty app:publish --private

See the app development guide for objects, views, agents, and logic functions.

Self-hosting

Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.



Everything you need

Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.

Want to go deeper? Read the User Guide for product walkthroughs, or the Documentation for developer reference.

Create your apps

Learn more about apps in doc

Stay on top with version control

Learn more about version control in doc

All the tools you need to build anything

Learn more about primitives in doc

Customize your layouts

Learn more about layouts in doc

AI agents and chats

Learn more about AI in doc

Plus all the tools of a good CRM

Learn more about CRM features in doc


Stack

Thanks

Greptile      Sentry      Crowdin

Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).

Join the Community

Star the repo · Discord · Feature requests · Releases · X · LinkedIn · Crowdin · Contribute

S
Description
The open alternative to Salesforce, designed for AI.
Readme AGPL-3.0 1.4 GiB
Languages
TypeScript 79.6%
MDX 17.3%
JavaScript 2.7%
Python 0.2%
SCSS 0.1%