Sync page Layout (#18034)

## Sync page layouts, tabs, and widgets

Adds the ability for SDK applications to synchronize `pageLayout`,
`pageLayoutTab`, and `pageLayoutWidget` entities, following the same
pattern established in #18003 for views and navigation menu items.

### Changes

**`twenty-shared`**
- New `PageLayoutManifest`, `PageLayoutTabManifest`, and
`PageLayoutWidgetManifest` types with a hierarchical structure (page
layout → tabs → widgets)
- Added `pageLayouts: PageLayoutManifest[]` to the `Manifest` type

**`twenty-sdk`**
- New `definePageLayout()` SDK function with validation for
universalIdentifier, name, and nested tabs/widgets
- Wired into the manifest extraction and build pipeline
(`DefinePageLayout` target function, `PageLayouts` entity key)
- Exported from the SDK entry point

**`twenty-server`**
- Added `pageLayout`, `pageLayoutTab`, `pageLayoutWidget` to
`APPLICATION_MANIFEST_METADATA_NAMES`
- New conversion utilities: manifest → universal flat entity for all
three entity types
- Updated `computeApplicationManifestAllUniversalFlatEntity
This commit is contained in:
Charles Bochet
2026-02-18 17:55:04 +01:00
committed by GitHub
parent e9b5cb830c
commit c3781e87cc
28 changed files with 692 additions and 1 deletions
@@ -412,6 +412,7 @@ export const EXPECTED_MANIFEST: Manifest = {
],
views: [],
navigationMenuItems: [],
pageLayouts: [],
logicFunctions: [
{
builtHandlerChecksum: '[checksum]',
@@ -142,6 +142,7 @@ export const EXPECTED_MANIFEST: Manifest = {
],
views: [],
navigationMenuItems: [],
pageLayouts: [],
roles: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',
@@ -3,6 +3,7 @@ import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-c
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
@@ -113,6 +114,16 @@ export class EntityAddCommand {
return { name, file };
}
case SyncableEntity.PageLayout: {
const name = await this.getEntityName(entity);
const file = getPageLayoutBaseFile({
name,
});
return { name, file };
}
default:
assertUnreachable(entity);
}
@@ -35,6 +35,7 @@ const validManifest: Manifest = {
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
describe('manifestValidate', () => {
@@ -11,6 +11,7 @@ import {
type LogicFunctionConfig,
} from '@/sdk';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import { glob } from 'fast-glob';
import { readFile } from 'fs-extra';
@@ -25,6 +26,7 @@ import {
type Manifest,
type NavigationMenuItemManifest,
type ObjectManifest,
type PageLayoutManifest,
type RoleManifest,
type ViewManifest,
} from 'twenty-shared/application';
@@ -67,6 +69,7 @@ export const buildManifest = async (
const publicAssets: AssetManifest[] = [];
const views: ViewManifest[] = [];
const navigationMenuItems: NavigationMenuItemManifest[] = [];
const pageLayouts: PageLayoutManifest[] = [];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
@@ -77,6 +80,7 @@ export const buildManifest = async (
const publicAssetsFilePaths: string[] = [];
const viewsFilePaths: string[] = [];
const navigationMenuItemsFilePaths: string[] = [];
const pageLayoutsFilePaths: string[] = [];
for (const filePath of filePaths) {
const fileContent = await readFile(filePath, 'utf-8');
@@ -240,6 +244,21 @@ export const buildManifest = async (
navigationMenuItemsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.PageLayouts: {
const extract = await extractManifestFromFile<PageLayoutConfig>({
appPath,
filePath,
});
const pageLayoutManifest: PageLayoutManifest = {
...extract.config,
};
pageLayouts.push(pageLayoutManifest);
errors.push(...extract.errors);
pageLayoutsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.PublicAssets: {
// Public assets are handled below
break;
@@ -280,6 +299,7 @@ export const buildManifest = async (
publicAssets,
views,
navigationMenuItems,
pageLayouts,
};
const entityFilePaths: EntityFilePaths = {
@@ -292,6 +312,7 @@ export const buildManifest = async (
publicAssets: publicAssetsFilePaths,
views: viewsFilePaths,
navigationMenuItems: navigationMenuItemsFilePaths,
pageLayouts: pageLayoutsFilePaths,
};
return { manifest, filePaths: entityFilePaths, errors };
@@ -9,6 +9,7 @@ export enum TargetFunction {
DefineFrontComponent = 'defineFrontComponent',
DefineView = 'defineView',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
DefinePageLayout = 'definePageLayout',
}
export enum ManifestEntityKey {
@@ -21,6 +22,7 @@ export enum ManifestEntityKey {
PublicAssets = 'publicAssets',
Views = 'views',
NavigationMenuItems = 'navigationMenuItems',
PageLayouts = 'pageLayouts',
}
export type EntityFilePaths = Record<ManifestEntityKey, string[]>;
@@ -38,6 +40,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineView]: ManifestEntityKey.Views,
[TargetFunction.DefineNavigationMenuItem]:
ManifestEntityKey.NavigationMenuItems,
[TargetFunction.DefinePageLayout]: ManifestEntityKey.PageLayouts,
};
const computeIsTargetFunctionCall = (node: ts.Node): string | undefined => {
@@ -69,6 +69,7 @@ const ENTITY_TYPE_TO_SYNCABLE: Record<string, SyncableEntity | undefined> = {
logicFunctions: SyncableEntity.LogicFunction,
frontComponents: SyncableEntity.FrontComponent,
roles: SyncableEntity.Role,
pageLayouts: SyncableEntity.PageLayout,
};
const MAX_EVENT_COUNT = 200;
@@ -97,6 +97,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.LogicFunction]: 'Logic functions',
[SyncableEntity.FrontComponent]: 'Front components',
[SyncableEntity.Role]: 'Roles',
[SyncableEntity.PageLayout]: 'Page layouts',
};
export const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[];
@@ -0,0 +1,19 @@
import { v4 as uuidv4 } from 'uuid';
export const getPageLayoutBaseFile = ({ name }: { name: string }) => {
return `import { definePageLayout } from 'twenty-sdk';
export default definePageLayout({
universalIdentifier: '${uuidv4()}',
name: '${name}',
tabs: [
{
universalIdentifier: '${uuidv4()}',
title: 'Overview',
position: 0,
widgets: [],
},
],
});
`;
};
@@ -2,6 +2,7 @@ import { type ApplicationConfig } from '@/sdk/application/application-config';
import { type FrontComponentConfig } from '@/sdk/front-component-config';
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import {
type FieldManifest,
@@ -23,7 +24,8 @@ export type DefinableEntity =
| LogicFunctionConfig
| RoleManifest
| ViewConfig
| NavigationMenuItemManifest;
| NavigationMenuItemManifest
| PageLayoutConfig;
export type DefineEntity<T extends DefinableEntity = DefinableEntity> = (
config: T,
+2
View File
@@ -47,6 +47,8 @@ export type {
export type { RoutePayload } from './logic-functions/triggers/route-payload-type';
export { defineNavigationMenuItem } from './navigation-menu-items/define-navigation-menu-item';
export { defineObject } from './objects/define-object';
export { definePageLayout } from './page-layouts/define-page-layout';
export type { PageLayoutConfig } from './page-layouts/page-layout-config';
export { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from './objects/standard-object-ids';
export { defineRole } from './roles/define-role';
export { PermissionFlag } from './roles/permission-flag-type';
@@ -0,0 +1,42 @@
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
export const definePageLayout: DefineEntity<PageLayoutConfig> = (config) => {
const errors: string[] = [];
if (!config.universalIdentifier) {
errors.push('PageLayout must have a universalIdentifier');
}
if (!config.name) {
errors.push('PageLayout must have a name');
}
if (config.tabs) {
for (const tab of config.tabs) {
if (!tab.universalIdentifier) {
errors.push('PageLayoutTab must have a universalIdentifier');
}
if (!tab.title) {
errors.push('PageLayoutTab must have a title');
}
if (tab.widgets) {
for (const widget of tab.widgets) {
if (!widget.universalIdentifier) {
errors.push('PageLayoutWidget must have a universalIdentifier');
}
if (!widget.title) {
errors.push('PageLayoutWidget must have a title');
}
if (!widget.type) {
errors.push('PageLayoutWidget must have a type');
}
}
}
}
}
return createValidationResult({ config, errors });
};
@@ -0,0 +1,3 @@
import { type PageLayoutManifest } from 'twenty-shared/application';
export type PageLayoutConfig = PageLayoutManifest;
@@ -13,6 +13,9 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
'viewFilterGroup',
'viewGroup',
'navigationMenuItem',
'pageLayout',
'pageLayoutTab',
'pageLayoutWidget',
] as const satisfies AllMetadataName[];
export type ApplicationManifestMetadataName =
@@ -0,0 +1,51 @@
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/utils/from-page-layout-manifest-to-universal-flat-page-layout.util';
describe('fromPageLayoutManifestToUniversalFlatPageLayout', () => {
const now = '2026-01-01T00:00:00.000Z';
const applicationUniversalIdentifier = 'app-uuid-1';
it('should convert a minimal page layout manifest', () => {
const result = fromPageLayoutManifestToUniversalFlatPageLayout({
pageLayoutManifest: {
universalIdentifier: 'pl-uuid-1',
name: 'My Page Layout',
},
applicationUniversalIdentifier,
now,
});
expect(result.universalIdentifier).toBe('pl-uuid-1');
expect(result.applicationUniversalIdentifier).toBe(
applicationUniversalIdentifier,
);
expect(result.name).toBe('My Page Layout');
expect(result.type).toBe(PageLayoutType.RECORD_PAGE);
expect(result.objectMetadataUniversalIdentifier).toBeNull();
expect(
result.defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier,
).toBeNull();
expect(result.tabUniversalIdentifiers).toEqual([]);
});
it('should convert a fully specified page layout manifest', () => {
const result = fromPageLayoutManifestToUniversalFlatPageLayout({
pageLayoutManifest: {
universalIdentifier: 'pl-uuid-2',
name: 'Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectUniversalIdentifier: 'obj-uuid-1',
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: 'tab-uuid-1',
},
applicationUniversalIdentifier,
now,
});
expect(result.name).toBe('Dashboard Layout');
expect(result.type).toBe(PageLayoutType.DASHBOARD);
expect(result.objectMetadataUniversalIdentifier).toBe('obj-uuid-1');
expect(
result.defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier,
).toBe('tab-uuid-1');
});
});
@@ -0,0 +1,55 @@
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/utils/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
describe('fromPageLayoutTabManifestToUniversalFlatPageLayoutTab', () => {
const now = '2026-01-01T00:00:00.000Z';
const applicationUniversalIdentifier = 'app-uuid-1';
const pageLayoutUniversalIdentifier = 'pl-uuid-1';
it('should convert a minimal page layout tab manifest', () => {
const result = fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest: {
universalIdentifier: 'tab-uuid-1',
title: 'Overview',
position: 0,
},
pageLayoutUniversalIdentifier,
applicationUniversalIdentifier,
now,
});
expect(result.universalIdentifier).toBe('tab-uuid-1');
expect(result.applicationUniversalIdentifier).toBe(
applicationUniversalIdentifier,
);
expect(result.title).toBe('Overview');
expect(result.position).toBe(0);
expect(result.pageLayoutUniversalIdentifier).toBe(
pageLayoutUniversalIdentifier,
);
expect(result.icon).toBeNull();
expect(result.layoutMode).toBe(PageLayoutTabLayoutMode.GRID);
expect(result.widgetUniversalIdentifiers).toEqual([]);
});
it('should convert a fully specified page layout tab manifest', () => {
const result = fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest: {
universalIdentifier: 'tab-uuid-2',
title: 'Details',
position: 1,
icon: 'IconLayout',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
},
pageLayoutUniversalIdentifier,
applicationUniversalIdentifier,
now,
});
expect(result.title).toBe('Details');
expect(result.position).toBe(1);
expect(result.icon).toBe('IconLayout');
expect(result.layoutMode).toBe(PageLayoutTabLayoutMode.VERTICAL_LIST);
});
});
@@ -0,0 +1,76 @@
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/utils/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
describe('fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget', () => {
const now = '2026-01-01T00:00:00.000Z';
const applicationUniversalIdentifier = 'app-uuid-1';
const pageLayoutTabUniversalIdentifier = 'tab-uuid-1';
it('should convert a minimal page layout widget manifest', () => {
const result = fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest: {
universalIdentifier: 'widget-uuid-1',
title: 'My Widget',
type: WidgetType.VIEW,
configuration: { configurationType: 'VIEW' },
},
pageLayoutTabUniversalIdentifier,
applicationUniversalIdentifier,
now,
});
expect(result.universalIdentifier).toBe('widget-uuid-1');
expect(result.applicationUniversalIdentifier).toBe(
applicationUniversalIdentifier,
);
expect(result.pageLayoutTabUniversalIdentifier).toBe(
pageLayoutTabUniversalIdentifier,
);
expect(result.title).toBe('My Widget');
expect(result.type).toBe(WidgetType.VIEW);
expect(result.objectMetadataUniversalIdentifier).toBeNull();
expect(result.conditionalDisplay).toBeNull();
expect(result.gridPosition).toEqual({
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
});
expect(result.position).toBeNull();
expect(result.universalConfiguration).toEqual({
configurationType: 'VIEW',
});
});
it('should convert a fully specified page layout widget manifest', () => {
const result = fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest: {
universalIdentifier: 'widget-uuid-2',
title: 'Iframe Widget',
type: 'IFRAME',
objectUniversalIdentifier: 'obj-uuid-1',
configuration: {
configurationType: 'IFRAME',
url: 'https://example.com',
},
},
pageLayoutTabUniversalIdentifier,
applicationUniversalIdentifier,
now,
});
expect(result.title).toBe('Iframe Widget');
expect(result.type).toBe('IFRAME');
expect(result.objectMetadataUniversalIdentifier).toBe('obj-uuid-1');
expect(result.gridPosition).toEqual({
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
});
expect(result.universalConfiguration).toEqual({
configurationType: 'IFRAME',
url: 'https://example.com',
});
});
});
@@ -6,6 +6,9 @@ import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/eng
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/utils/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/utils/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/utils/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/utils/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util';
import { fromViewFieldGroupManifestToUniversalFlatViewFieldGroup } from 'src/engine/core-modules/application/utils/from-view-field-group-manifest-to-universal-flat-view-field-group.util';
import { fromViewFieldManifestToUniversalFlatViewField } from 'src/engine/core-modules/application/utils/from-view-field-manifest-to-universal-flat-view-field.util';
@@ -236,5 +239,55 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
);
}
for (const pageLayoutManifest of manifest.pageLayouts ?? []) {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
metadataName: 'pageLayout',
universalFlatEntity: fromPageLayoutManifestToUniversalFlatPageLayout({
pageLayoutManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps,
},
);
for (const pageLayoutTabManifest of pageLayoutManifest.tabs ?? []) {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
metadataName: 'pageLayoutTab',
universalFlatEntity:
fromPageLayoutTabManifestToUniversalFlatPageLayoutTab({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier:
pageLayoutManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps,
},
);
for (const pageLayoutWidgetManifest of pageLayoutTabManifest.widgets ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
metadataName: 'pageLayoutWidget',
universalFlatEntity:
fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier:
pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityAndRelatedMapsToMutate:
allUniversalFlatEntityMaps,
},
);
}
}
}
return allUniversalFlatEntityMaps;
};
@@ -0,0 +1,31 @@
import { type PageLayoutManifest } from 'twenty-shared/application';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
export const fromPageLayoutManifestToUniversalFlatPageLayout = ({
pageLayoutManifest,
applicationUniversalIdentifier,
now,
}: {
pageLayoutManifest: PageLayoutManifest;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPageLayout => {
return {
universalIdentifier: pageLayoutManifest.universalIdentifier,
applicationUniversalIdentifier,
name: pageLayoutManifest.name,
type:
(pageLayoutManifest.type as PageLayoutType) ?? PageLayoutType.RECORD_PAGE,
objectMetadataUniversalIdentifier:
pageLayoutManifest.objectUniversalIdentifier ?? null,
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier:
pageLayoutManifest.defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier ??
null,
tabUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -0,0 +1,31 @@
import { type PageLayoutTabManifest } from 'twenty-shared/application';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
export const fromPageLayoutTabManifestToUniversalFlatPageLayoutTab = ({
pageLayoutTabManifest,
pageLayoutUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
pageLayoutTabManifest: PageLayoutTabManifest;
pageLayoutUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPageLayoutTab => {
return {
universalIdentifier: pageLayoutTabManifest.universalIdentifier,
applicationUniversalIdentifier,
title: pageLayoutTabManifest.title,
position: pageLayoutTabManifest.position,
pageLayoutUniversalIdentifier,
icon: pageLayoutTabManifest.icon ?? null,
layoutMode:
pageLayoutTabManifest.layoutMode ?? PageLayoutTabLayoutMode.GRID,
widgetUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -0,0 +1,34 @@
import { type PageLayoutWidgetManifest } from 'twenty-shared/application';
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
export const fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget = ({
pageLayoutWidgetManifest,
pageLayoutTabUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
pageLayoutWidgetManifest: PageLayoutWidgetManifest;
pageLayoutTabUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPageLayoutWidget => {
return {
universalIdentifier: pageLayoutWidgetManifest.universalIdentifier,
applicationUniversalIdentifier,
pageLayoutTabUniversalIdentifier,
title: pageLayoutWidgetManifest.title,
type: pageLayoutWidgetManifest.type as WidgetType,
objectMetadataUniversalIdentifier:
pageLayoutWidgetManifest.objectUniversalIdentifier ?? null,
conditionalDisplay: pageLayoutWidgetManifest.conditionalDisplay ?? null,
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
position: pageLayoutWidgetManifest.position ?? null,
universalConfiguration:
pageLayoutWidgetManifest.configuration as UniversalFlatPageLayoutWidget['universalConfiguration'],
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -108,6 +108,7 @@ describe('syncApplication', () => {
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
const { data: firstSyncData } = await syncApplication({
@@ -4,4 +4,5 @@ export enum SyncableEntity {
LogicFunction = 'logicFunction',
FrontComponent = 'frontComponent',
Role = 'role',
PageLayout = 'pageLayout',
}
@@ -36,6 +36,11 @@ export type { Manifest } from './manifestType';
export type { NavigationMenuItemManifest } from './navigationMenuItemManifestType';
export type { ObjectFieldManifest } from './objectFieldManifest.type';
export type { ObjectManifest } from './objectManifestType';
export type {
PageLayoutWidgetManifest,
PageLayoutTabManifest,
PageLayoutManifest,
} from './pageLayoutManifestType';
export type {
ObjectPermissionManifest,
FieldPermissionManifest,
@@ -5,6 +5,7 @@ import { type FrontComponentManifest } from './frontComponentManifestType';
import { type LogicFunctionManifest } from './logicFunctionManifestType';
import { type NavigationMenuItemManifest } from './navigationMenuItemManifestType';
import { type ObjectManifest } from './objectManifestType';
import { type PageLayoutManifest } from './pageLayoutManifestType';
import { type RoleManifest } from './roleManifestType';
import { type ViewManifest } from './viewManifestType';
@@ -18,4 +19,5 @@ export type Manifest = {
publicAssets: AssetManifest[];
views: ViewManifest[];
navigationMenuItems: NavigationMenuItemManifest[];
pageLayouts: PageLayoutManifest[];
};
@@ -0,0 +1,32 @@
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
import {
type PageLayoutTabLayoutMode,
type PageLayoutWidgetConditionalDisplay,
type PageLayoutWidgetPosition,
type PageLayoutWidgetUniversalConfiguration,
} from '@/types';
export type PageLayoutWidgetManifest = SyncableEntityOptions & {
title: string;
type: string;
objectUniversalIdentifier?: string;
conditionalDisplay?: PageLayoutWidgetConditionalDisplay;
position?: PageLayoutWidgetPosition;
configuration: PageLayoutWidgetUniversalConfiguration;
};
export type PageLayoutTabManifest = SyncableEntityOptions & {
title: string;
position: number;
icon?: string;
layoutMode?: PageLayoutTabLayoutMode;
widgets?: PageLayoutWidgetManifest[];
};
export type PageLayoutManifest = SyncableEntityOptions & {
name: string;
type?: string;
objectUniversalIdentifier?: string;
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier?: string;
tabs?: PageLayoutTabManifest[];
};
@@ -143,6 +143,7 @@ export type {
PageLayoutWidgetCanvasPosition,
PageLayoutWidgetPosition,
} from './page-layout/page-layout-widget-position.type';
export type { PageLayoutWidgetUniversalConfiguration } from './page-layout/page-layout-widget-universal-configuration.type';
export { PageLayoutTabLayoutMode } from './page-layout/PageLayoutTabLayoutMode';
export type { PageLayoutWidgetConditionalDisplay } from './page-layout/PageLayoutWidgetConditionalDisplay';
export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem';
@@ -0,0 +1,207 @@
import { type AggregateOperations } from '../AggregateOperations';
type ChartFilterRecordFilter = {
id: string;
fieldMetadataUniversalIdentifier: string;
operand: string;
value?: string | null;
type?: string;
recordFilterGroupId?: string | null;
subFieldName?: string | null;
};
type ChartFilterRecordFilterGroup = {
id: string;
logicalOperator: string;
parentRecordFilterGroupId?: string | null;
};
type UniversalChartFilter = {
recordFilters?: ChartFilterRecordFilter[];
recordFilterGroups?: ChartFilterRecordFilterGroup[];
};
type RatioAggregateConfig = {
fieldMetadataUniversalIdentifier: string | null;
optionValue: string;
};
type BaseChartFields = {
aggregateFieldMetadataUniversalIdentifier: string | null;
aggregateOperation: AggregateOperations;
displayDataLabel?: boolean;
description?: string;
color?: string;
filter?: UniversalChartFilter;
timezone?: string;
firstDayOfTheWeek?: number;
};
type AggregateChartUniversalConfiguration = BaseChartFields & {
configurationType: 'AGGREGATE_CHART';
label?: string;
format?: string;
prefix?: string;
suffix?: string;
ratioAggregateConfig?: RatioAggregateConfig;
};
type GaugeChartUniversalConfiguration = BaseChartFields & {
configurationType: 'GAUGE_CHART';
};
type PieChartUniversalConfiguration = BaseChartFields & {
configurationType: 'PIE_CHART';
groupByFieldMetadataUniversalIdentifier: string | null;
groupBySubFieldName?: string;
dateGranularity?: string;
orderBy?: string;
manualSortOrder?: string[];
showCenterMetric?: boolean;
displayLegend?: boolean;
hideEmptyCategory?: boolean;
splitMultiValueFields?: boolean;
};
type BarChartUniversalConfiguration = BaseChartFields & {
configurationType: 'BAR_CHART';
primaryAxisGroupByFieldMetadataUniversalIdentifier: string | null;
primaryAxisGroupBySubFieldName?: string;
primaryAxisDateGranularity?: string;
primaryAxisOrderBy?: string;
primaryAxisManualSortOrder?: string[];
secondaryAxisGroupByFieldMetadataUniversalIdentifier?: string | null;
secondaryAxisGroupBySubFieldName?: string;
secondaryAxisGroupByDateGranularity?: string;
secondaryAxisOrderBy?: string;
secondaryAxisManualSortOrder?: string[];
omitNullValues?: boolean;
splitMultiValueFields?: boolean;
axisNameDisplay?: string;
displayLegend?: boolean;
rangeMin?: number;
rangeMax?: number;
groupMode?: string;
layout?: string;
isCumulative?: boolean;
};
type LineChartUniversalConfiguration = BaseChartFields & {
configurationType: 'LINE_CHART';
primaryAxisGroupByFieldMetadataUniversalIdentifier: string | null;
primaryAxisGroupBySubFieldName?: string;
primaryAxisDateGranularity?: string;
primaryAxisOrderBy?: string;
primaryAxisManualSortOrder?: string[];
secondaryAxisGroupByFieldMetadataUniversalIdentifier?: string | null;
secondaryAxisGroupBySubFieldName?: string;
secondaryAxisGroupByDateGranularity?: string;
secondaryAxisOrderBy?: string;
secondaryAxisManualSortOrder?: string[];
omitNullValues?: boolean;
splitMultiValueFields?: boolean;
axisNameDisplay?: string;
displayLegend?: boolean;
rangeMin?: number;
rangeMax?: number;
isStacked?: boolean;
isCumulative?: boolean;
};
type ViewUniversalConfiguration = {
configurationType: 'VIEW';
};
type FieldUniversalConfiguration = {
configurationType: 'FIELD';
};
type FieldsUniversalConfiguration = {
configurationType: 'FIELDS';
viewId?: string | null;
newFieldDefaultConfiguration?: {
isVisible: boolean;
viewFieldGroupId: string | null;
} | null;
};
type FieldRichTextUniversalConfiguration = {
configurationType: 'FIELD_RICH_TEXT';
};
type StandaloneRichTextUniversalConfiguration = {
configurationType: 'STANDALONE_RICH_TEXT';
body: {
blocknote?: string | null;
markdown: string | null;
};
};
type IframeUniversalConfiguration = {
configurationType: 'IFRAME';
url?: string;
};
type FrontComponentUniversalConfiguration = {
configurationType: 'FRONT_COMPONENT';
frontComponentId: string;
};
type TimelineUniversalConfiguration = {
configurationType: 'TIMELINE';
};
type TasksUniversalConfiguration = {
configurationType: 'TASKS';
};
type NotesUniversalConfiguration = {
configurationType: 'NOTES';
};
type FilesUniversalConfiguration = {
configurationType: 'FILES';
};
type EmailsUniversalConfiguration = {
configurationType: 'EMAILS';
};
type CalendarUniversalConfiguration = {
configurationType: 'CALENDAR';
};
type WorkflowUniversalConfiguration = {
configurationType: 'WORKFLOW';
};
type WorkflowVersionUniversalConfiguration = {
configurationType: 'WORKFLOW_VERSION';
};
type WorkflowRunUniversalConfiguration = {
configurationType: 'WORKFLOW_RUN';
};
export type PageLayoutWidgetUniversalConfiguration =
| AggregateChartUniversalConfiguration
| GaugeChartUniversalConfiguration
| PieChartUniversalConfiguration
| BarChartUniversalConfiguration
| LineChartUniversalConfiguration
| ViewUniversalConfiguration
| FieldUniversalConfiguration
| FieldsUniversalConfiguration
| FieldRichTextUniversalConfiguration
| StandaloneRichTextUniversalConfiguration
| IframeUniversalConfiguration
| FrontComponentUniversalConfiguration
| TimelineUniversalConfiguration
| TasksUniversalConfiguration
| NotesUniversalConfiguration
| FilesUniversalConfiguration
| EmailsUniversalConfiguration
| CalendarUniversalConfiguration
| WorkflowUniversalConfiguration
| WorkflowVersionUniversalConfiguration
| WorkflowRunUniversalConfiguration;