0f06fccdee
Reported on Discord: an app declared a `STANDALONE_PAGE` layout with one `FRONT_COMPONENT` widget and got a small bordered card instead of a full-bleed page, and setting `layoutMode` afterwards changed nothing. Two bugs: - `pageLayoutTab.layoutMode` was `toCompare: false`, so it was written once at create and never diffed again. Changing it in a manifest and redeploying was a silent no-op, and `updatePageLayoutTab(layoutMode:)` was accepted by the API then dropped by the runner's update sanitizer. - A manifest tab that omits `layoutMode` defaulted to `GRID` regardless of page layout type, and a `GRID` tab always renders its widgets as cards on a 12-column grid. Standalone pages now default to `VERTICAL_LIST`, where a lone widget owns the tab. Also fixes the SDK scaffolder (`twenty add page-layout` emitted a tab with no `position`, which does not typecheck) and the docs claim that a single widget is always full-bleed. Worth knowing for review: this does not migrate workspaces holding legacy `CANVAS` tabs. The standard-app sync only runs against a fresh schema, so those rows stay `CANVAS` and keep rendering correctly through the derived presentation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23596?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. -->
131 lines
6.2 KiB
Plaintext
131 lines
6.2 KiB
Plaintext
---
|
|
title: Page Layouts
|
|
description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab.
|
|
icon: "table-columns"
|
|
---
|
|
|
|
A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one).
|
|
|
|
| Use case | Entity |
|
|
|----------|--------|
|
|
| Define the entire layout for a record page on an object you own | `definePageLayout` |
|
|
| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` |
|
|
|
|
## definePageLayout
|
|
|
|
Use this when you own the entire detail page — typically for a custom object you defined yourself.
|
|
|
|
```ts src/page-layouts/example-record-page-layout.ts
|
|
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
|
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
|
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
|
|
|
export default definePageLayout({
|
|
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
|
name: 'Example Record Page',
|
|
type: 'RECORD_PAGE',
|
|
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
|
tabs: [
|
|
{
|
|
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
|
title: 'Hello World',
|
|
position: 50,
|
|
icon: 'IconWorld',
|
|
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
|
widgets: [
|
|
{
|
|
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
|
title: 'Hello World',
|
|
type: 'FRONT_COMPONENT',
|
|
configuration: {
|
|
configurationType: 'FRONT_COMPONENT',
|
|
frontComponentUniversalIdentifier:
|
|
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
### Key points
|
|
|
|
- `type` is one of `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` or `'STANDALONE_PAGE'`. Use `'RECORD_PAGE'` to customize the detail view of a specific object.
|
|
- `objectUniversalIdentifier` specifies which object this layout applies to.
|
|
- Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode`: `VERTICAL_LIST` for record pages and standalone pages, `GRID` for dashboards. In a `VERTICAL_LIST` tab, a single widget renders full-bleed and owns the whole tab; with several widgets they stack as cards. A `GRID` tab always lays its widgets out as cards on a 12-column grid, whatever their number, so pick `VERTICAL_LIST` when you want one widget to fill the page.
|
|
- Set `layoutMode` explicitly. Omitting it gives you `VERTICAL_LIST` on a `STANDALONE_PAGE` and `GRID` everywhere else, which is rarely what you want on a record page.
|
|
- Each `widget` inside a tab can render a [front component](/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types.
|
|
- `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones.
|
|
|
|
## definePageLayoutTab
|
|
|
|
Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout.
|
|
|
|
```ts src/page-layouts/example-extra-tab.ts
|
|
import {
|
|
definePageLayoutTab,
|
|
PageLayoutTabLayoutMode,
|
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
|
} from 'twenty-sdk/define';
|
|
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
|
|
|
export default definePageLayoutTab({
|
|
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
|
pageLayoutUniversalIdentifier:
|
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
|
.universalIdentifier,
|
|
title: 'Hello World',
|
|
position: 1000,
|
|
icon: 'IconWorld',
|
|
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
|
widgets: [
|
|
{
|
|
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
|
|
title: 'Hello World',
|
|
type: 'FRONT_COMPONENT',
|
|
configuration: {
|
|
configurationType: 'FRONT_COMPONENT',
|
|
frontComponentUniversalIdentifier:
|
|
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
### Key points
|
|
|
|
- `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error.
|
|
- For standard Twenty layouts, import identifiers from `twenty-sdk/define`:
|
|
|
|
```ts
|
|
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
|
|
|
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
|
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
|
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
|
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
|
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
|
// …
|
|
```
|
|
|
|
Each layout entry also exposes its `tabs` and their `widgets`, so you can reference any level:
|
|
|
|
```ts
|
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
|
```
|
|
|
|
A short alias `STANDARD_PAGE_LAYOUT` is also available:
|
|
|
|
```ts
|
|
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
|
|
|
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
|
```
|
|
|
|
- `widgets` are scoped to this tab only — they reference [front components](/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`.
|
|
- `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs.
|
|
- Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout.
|