feat(sdk): add definePageLayoutTab for extending existing page layouts (#20004)

## Summary

Introduces `definePageLayoutTab` so apps can attach a single tab (with
optional widgets) to an **existing** `pageLayout` referenced by
`pageLayoutUniversalIdentifier`. The parent layout can be standard, from
the same app, or from another app — mirroring how `defineField`
references an object via `objectUniversalIdentifier`.

This complements `definePageLayout`: use `definePageLayout` when you own
the entire layout, use `definePageLayoutTab` when you only want to add
to one.

```ts
import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';

export default definePageLayoutTab({
  universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
  pageLayoutUniversalIdentifier: 'STANDARD-OR-OTHER-APP-PAGE-LAYOUT-UUID',
  title: 'Hello World',
  position: 1000,
  icon: 'IconWorld',
  layoutMode: PageLayoutTabLayoutMode.CANVAS,
  widgets: [/* ... */],
});
```

## Changes

- **twenty-shared**: new top-level `pageLayoutTabs:
PageLayoutTabManifest[]` on `Manifest`, optional
`pageLayoutUniversalIdentifier` on `PageLayoutTabManifest`, new
`SyncableEntity.PageLayoutTab`.
- **twenty-sdk**:
  - new `definePageLayoutTab` + `PageLayoutTabConfig` exports;
- manifest extraction wiring (`TargetFunction.DefinePageLayoutTab`,
`ManifestEntityKey.PageLayoutTabs`);
  - dev-mode label/state for the new entity;
- CLI scaffold (`getPageLayoutTabBaseFile`) + unit tests for `npx
twenty-cli add`.
- **twenty-server**: convert top-level `pageLayoutTabs` (and their
widgets) into universal flat entities in
`computeApplicationManifestAllUniversalFlatEntityMaps`. Cross-app FK
validation on `pageLayoutUniversalIdentifier` is already handled by the
existing `FlatPageLayoutTab` validator.
- **docs**: new `definePageLayoutTab` accordion in `apps/layout.mdx`
with usage example and guidance vs `definePageLayout`.
- **CI / rich-app fixture**: `extra-tab.page-layout-tab.ts` exercises
the new flow with a front-component widget; `expected-manifest.ts` and
`manifest.tests.ts` updated.
This commit is contained in:
Charles Bochet
2026-04-23 14:00:10 +02:00
committed by GitHub
parent 20f7ba82d7
commit a445f4a6fa
28 changed files with 509 additions and 2 deletions
@@ -78,6 +78,7 @@ export class ApplicationManifestMigrationService {
views: [],
navigationMenuItems: [],
pageLayouts: [],
pageLayoutTabs: [],
};
const now = new Date().toISOString();
@@ -397,5 +397,42 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
}
}
for (const pageLayoutTabManifest of manifest.pageLayoutTabs ?? []) {
if (!isDefined(pageLayoutTabManifest.pageLayoutUniversalIdentifier)) {
throw new Error(
`Top-level pageLayoutTab "${pageLayoutTabManifest.universalIdentifier}" is missing required pageLayoutUniversalIdentifier`,
);
}
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier:
pageLayoutTabManifest.pageLayoutUniversalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutTabMaps,
});
for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier:
pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPageLayoutWidgetMaps,
});
}
}
return allUniversalFlatEntityMaps;
};
@@ -25,6 +25,7 @@ const buildMinimalManifest = (
views: [],
navigationMenuItems: [],
pageLayouts: [],
pageLayoutTabs: [],
});
describe('resolveManifestAssetUrls', () => {