Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
251 lines
15 KiB
Plaintext
251 lines
15 KiB
Plaintext
---
|
|
title: Page Layouts
|
|
description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout, definePageLayoutTab and definePageLayoutWidget.
|
|
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, `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one), or `definePageLayoutWidget()` to add a single widget to a tab that already exists.
|
|
|
|
| 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` |
|
|
| Add one widget to an existing tab (the Home tab of a record page, or a tab you defined) | `definePageLayoutWidget` |
|
|
|
|
## 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',
|
|
heightBehavior: 'TAB_VIEWPORT',
|
|
configuration: {
|
|
configurationType: 'FRONT_COMPONENT',
|
|
frontComponentUniversalIdentifier:
|
|
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
### Key points
|
|
|
|
- `type` is one of `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'`, `'STANDALONE_PAGE'` or `'RECORD_FORM'`. Use `'RECORD_PAGE'` to customize the detail view of a specific object. `'RECORD_FORM'` describes the creation form of an object and holds `FORM_FIELD` widgets, each rendering a single field. Twenty provisions one `'RECORD_FORM'` layout per object with one widget per creatable field, so you rarely need to author one; it does not render record forms yet.
|
|
- `objectUniversalIdentifier` specifies which object this layout applies to.
|
|
- Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode`: use `VERTICAL_LIST` for record pages and standalone pages, and `GRID` for dashboards. A `VERTICAL_LIST` tab stacks widgets vertically. Built-in widgets that manage their own scrolling, such as timelines, files, notes, tasks, and workflows, fill one viewport by default; fields, front components, graphs, and other fit-content widgets render at their content or configured height. A `GRID` tab always lays its widgets out as cards on a 12-column grid.
|
|
- Widgets in a `VERTICAL_LIST` tab inherit the tab's layout mode, and their order comes from the `widgets` array. Do not set `position`; set the optional top-level `heightBehavior` to `'TAB_VIEWPORT'` when a widget should occupy one full tab viewport and own its scrolling, or to `'FIT_CONTENT'` to override a built-in full-height default. Preceding widgets do not reduce that height. Only one active `TAB_VIEWPORT` widget is allowed per tab, and it must come after fit-content widgets.
|
|
- `GRID` widgets continue to use `position: { layoutMode: PageLayoutTabLayoutMode.GRID, row, column, rowSpan, columnSpan }`. Top-level `heightBehavior` is only valid for `VERTICAL_LIST` tabs; using it with `GRID` or `CANVAS` is a validation error.
|
|
- Existing Canvas tabs and explicit widget positions remain supported. The SDK shows deprecation warnings with the recommended replacements.
|
|
- 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.
|
|
- A `FRONT_COMPONENT` widget can set `headerCommandMenuItemUniversalIdentifiers` to an ordered array of command menu item universal identifiers from the same app. These actions appear as icon buttons in the widget card header and keep their command-level availability and permission checks. Identifiers must be unique and must resolve when the app is installed.
|
|
- `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones.
|
|
- `yarn twenty pull` writes page layouts back with their tabs and widgets inline. A widget the export cannot carry yet, such as one showing your app's own front component or one of the deprecated `VIEW` type, is reported by pull instead of written; see [what pull does not write](/developers/extend/apps/operations/sync-and-recovery#pulling-an-installed-app-back-to-source).
|
|
|
|
### Field widgets
|
|
|
|
A `FIELD` widget renders one field of the current record. For relation fields, it can also embed a view of records related to the current record.
|
|
|
|
To embed a view that is not scoped to the current record, use a [record table widget](#record-table-widgets).
|
|
|
|
```ts
|
|
{
|
|
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
|
|
title: 'People → Opportunities',
|
|
type: 'FIELD',
|
|
configuration: {
|
|
configurationType: 'FIELD',
|
|
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
|
|
fieldDisplayMode: 'TABLE',
|
|
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
}
|
|
```
|
|
|
|
- `fieldMetadataId` takes the universal identifier of a field on the layout's object.
|
|
- `fieldDisplayMode` is one of `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` or `'TABLE'`. `TABLE` embeds a view listing the records of a one-to-many relation field.
|
|
- `isUIEditable` applies to relation fields in `TABLE` display mode. It defaults to `true`; set it to `false` to prevent editing records from the embedded table or kanban.
|
|
- `nestedRelationFieldMetadataId` is optional and takes the universal identifier of a one-to-many relation field on the relation target object, to list records two relation hops away (e.g. a Company page listing the opportunities of the company's people, or a Person page listing the opportunities of the person's company). The first hop can be a one-to-many or a many-to-one relation field, the second must be one-to-many (junction relations are not supported), and it requires `fieldDisplayMode: 'TABLE'` — combining it with any other display mode is a validation error, since a nested widget always renders as an embedded view.
|
|
|
|
### Record table widgets
|
|
|
|
A `RECORD_TABLE` widget embeds a saved view on a dashboard, record page, or standalone page. The saved view determines the records, fields, filters, sorts, and layout shown in the widget.
|
|
|
|
```ts
|
|
{
|
|
universalIdentifier: 'd1d2d3d4-d5d6-4000-8000-000000000004',
|
|
title: 'Open opportunities',
|
|
type: 'RECORD_TABLE',
|
|
configuration: {
|
|
configurationType: 'RECORD_TABLE',
|
|
viewUniversalIdentifier: OPPORTUNITIES_VIEW_UNIVERSAL_IDENTIFIER,
|
|
recordLimit: 8,
|
|
isUIEditable: false,
|
|
},
|
|
}
|
|
```
|
|
|
|
- `viewUniversalIdentifier` identifies the view to display.
|
|
- `recordLimit` is optional and caps how many records the widget displays.
|
|
- `isUIEditable` applies to table and kanban views. It enables table cell editing or kanban card editing, moving, and creation. It defaults to `true` on `RECORD_PAGE` layouts and `false` on other layouts.
|
|
- `isUIEditable` controls UI actions only. Record permissions still apply, and view settings remain editable only in the page layout editor.
|
|
|
|
Workspace admins can configure the same behavior with **Allow editing** in the [dashboard widget](/user-guide/dashboards/capabilities/widgets#editing-records) and [record page](/user-guide/layout/capabilities/record-pages#editing-related-records) editors.
|
|
|
|
## 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.
|
|
- `yarn twenty pull` writes a tab you added to a page layout you do not own back as a `definePageLayoutTab()` file, with its widgets inline, under the same limits as widgets of a `definePageLayout()`.
|
|
|
|
## definePageLayoutWidget
|
|
|
|
Use this when you only want to **add** a widget to a tab that already exists — for example, a front component on the Home tab of the standard Company page, or a chart on the Home tab of your own object's record page.
|
|
|
|
```ts src/page-layout-widgets/example-home-widget.ts
|
|
import {
|
|
definePageLayoutWidget,
|
|
PageLayoutTabLayoutMode,
|
|
PageLayoutWidgetVerticalListHeightBehavior,
|
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
|
} from 'twenty-sdk/define';
|
|
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
|
|
|
export default definePageLayoutWidget({
|
|
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000003',
|
|
pageLayoutTabUniversalIdentifier:
|
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.tabs.home
|
|
.universalIdentifier,
|
|
title: 'Hello World',
|
|
type: 'FRONT_COMPONENT',
|
|
position: {
|
|
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
|
index: 1000,
|
|
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
|
|
},
|
|
configuration: {
|
|
configurationType: 'FRONT_COMPONENT',
|
|
frontComponentUniversalIdentifier:
|
|
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
});
|
|
```
|
|
|
|
### Key points
|
|
|
|
- `pageLayoutTabUniversalIdentifier` is **required** and must point to a tab that already exists at install time: a standard Twenty tab (`STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.<object>RecordPage.tabs.<tab>.universalIdentifier`), a tab of one of your own layouts, or a tab of the record page Twenty generates for one of your objects. The generated tabs have deterministic identifiers, so you can compute them:
|
|
|
|
```ts
|
|
import {
|
|
getSystemPageLayoutTabUniversalIdentifier,
|
|
getSystemRecordPageLayoutUniversalIdentifier,
|
|
} from 'twenty-sdk/define';
|
|
|
|
const EXAMPLE_HOME_TAB_UNIVERSAL_IDENTIFIER =
|
|
getSystemPageLayoutTabUniversalIdentifier({
|
|
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
pageLayoutUniversalIdentifier: getSystemRecordPageLayoutUniversalIdentifier({
|
|
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
|
}),
|
|
title: 'Home',
|
|
});
|
|
```
|
|
|
|
- `position` is **required**, since the tab is not part of your manifest and its layout mode cannot be inferred. Match the tab's layout mode: record page tabs are vertical lists, so use `{ layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index }` with an index above the built-in widgets (the Fields widget sits at 0).
|
|
- For a vertical list, set `position.heightBehavior` to `PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT` to fill one tab viewport, or `PageLayoutWidgetVerticalListHeightBehavior.FIT_CONTENT` to use the widget's content or configured height. Import this enum from `twenty-sdk/define`, as in the example above. If omitted, the widget keeps its built-in default. Only one active widget per tab can fill the viewport, and its index must place it after all fit-content widgets. `heightBehavior` is not supported in `GRID` or `CANVAS` positions.
|
|
- `configuration` works exactly like a widget defined inline in `definePageLayout` or `definePageLayoutTab`: it references [front components](/developers/extend/apps/layout/front-components), views, fields and so on.
|
|
- Use this instead of `definePageLayoutTab` when the tab already exists and you only want to place one widget on it. Use `definePageLayoutTab` when you want a tab of your own.
|
|
- `yarn twenty pull` writes a widget you added to a tab you do not own back as a `definePageLayoutWidget()` file, under the same limits as widgets of a `definePageLayout()`.
|