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:
+44
@@ -0,0 +1,44 @@
|
||||
import { getPageLayoutTabBaseFile } from '@/cli/utilities/entity/entity-page-layout-tab-template';
|
||||
|
||||
describe('getPageLayoutTabBaseFile', () => {
|
||||
it('should render proper file using definePageLayoutTab', () => {
|
||||
const result = getPageLayoutTabBaseFile({
|
||||
name: 'My Custom Tab',
|
||||
});
|
||||
|
||||
expect(result).toContain(
|
||||
"import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';",
|
||||
);
|
||||
expect(result).toContain('export default definePageLayoutTab({');
|
||||
expect(result).toContain("title: 'My Custom Tab'");
|
||||
expect(result).toContain('pageLayoutUniversalIdentifier:');
|
||||
expect(result).toContain('widgets: []');
|
||||
expect(result).toContain('layoutMode: PageLayoutTabLayoutMode.CANVAS');
|
||||
});
|
||||
|
||||
it('should generate a valid UUID for the tab', () => {
|
||||
const result = getPageLayoutTabBaseFile({
|
||||
name: 'tab',
|
||||
});
|
||||
|
||||
const uuidRegex =
|
||||
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/g;
|
||||
const matches = result.match(uuidRegex);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should generate unique UUIDs across calls', () => {
|
||||
const result1 = getPageLayoutTabBaseFile({ name: 'tab-1' });
|
||||
const result2 = getPageLayoutTabBaseFile({ name: 'tab-2' });
|
||||
|
||||
const uuidRegex =
|
||||
/universalIdentifier: '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
||||
const uuid1 = result1.match(uuidRegex)?.[1];
|
||||
const uuid2 = result2.match(uuidRegex)?.[1];
|
||||
|
||||
expect(uuid1).toBeDefined();
|
||||
expect(uuid2).toBeDefined();
|
||||
expect(uuid1).not.toBe(uuid2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getPageLayoutTabBaseFile = ({ name }: { name: string }) => {
|
||||
return `import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
pageLayoutUniversalIdentifier: 'replace-with-existing-page-layout-uuid',
|
||||
title: '${name}',
|
||||
position: 1000,
|
||||
icon: 'IconLayout',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [],
|
||||
});
|
||||
`;
|
||||
};
|
||||
Reference in New Issue
Block a user