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:
+1
@@ -412,6 +412,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
logicFunctions: [
|
||||
{
|
||||
builtHandlerChecksum: '[checksum]',
|
||||
|
||||
+1
@@ -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);
|
||||
}
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
+3
@@ -13,6 +13,9 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
|
||||
'viewFilterGroup',
|
||||
'viewGroup',
|
||||
'navigationMenuItem',
|
||||
'pageLayout',
|
||||
'pageLayoutTab',
|
||||
'pageLayoutWidget',
|
||||
] as const satisfies AllMetadataName[];
|
||||
|
||||
export type ApplicationManifestMetadataName =
|
||||
|
||||
+51
@@ -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');
|
||||
});
|
||||
});
|
||||
+55
@@ -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);
|
||||
});
|
||||
});
|
||||
+76
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
+53
@@ -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;
|
||||
};
|
||||
|
||||
+31
@@ -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,
|
||||
};
|
||||
};
|
||||
+31
@@ -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,
|
||||
};
|
||||
};
|
||||
+34
@@ -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,
|
||||
};
|
||||
};
|
||||
+1
@@ -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';
|
||||
|
||||
+207
@@ -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;
|
||||
Reference in New Issue
Block a user