Update pageLayout* data models for backend recordPageLayout refactor (#17446)

<img width="814" height="356" alt="Screenshot 2026-01-26 at 16 09 27"
src="https://github.com/user-attachments/assets/91d95a1d-cc33-4bf0-a63e-4d1815030983"
/>
This commit is contained in:
Weiko
2026-01-29 14:39:46 +01:00
committed by GitHub
parent 09de762285
commit 7d69000ab7
31 changed files with 194 additions and 9 deletions
@@ -306,6 +306,11 @@ export const ALL_METADATA_RELATIONS = {
foreignKey: 'objectMetadataId',
},
application: null,
defaultTabToFocusOnMobileAndSidePanel: {
metadataName: 'pageLayoutTab',
flatEntityForeignKeyAggregator: null,
foreignKey: 'defaultTabToFocusOnMobileAndSidePanelId',
},
},
oneToMany: {
tabs: { metadataName: 'pageLayoutTab' },
@@ -62,6 +62,7 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
},
pageLayout: {
objectMetadata: true,
pageLayoutTab: true,
},
pageLayoutTab: {
pageLayout: true,
@@ -9,13 +9,13 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewField",
"commandMenuItem",
"index",
"pageLayout",
"roleTarget",
"rowLevelPermissionPredicateGroup",
"viewGroup",
"agent",
"frontComponent",
"logicFunction",
"pageLayout",
"pageLayoutTab",
"skill",
"view",
@@ -1,3 +1,4 @@
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@@ -36,5 +37,7 @@ export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({
universalIdentifier: pageLayoutTabId,
applicationId: workspaceCustomApplicationId,
widgetIds: [],
icon: null,
layoutMode: PageLayoutTabLayoutMode.GRID,
};
};
@@ -46,6 +46,8 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({
universalIdentifier: pageLayoutTabEntity.universalIdentifier,
applicationId: pageLayoutTabEntity.applicationId,
widgetIds: pageLayoutTabEntity.widgets.map((widget) => widget.id),
icon: pageLayoutTabEntity.icon,
layoutMode: pageLayoutTabEntity.layoutMode,
__universal: {
universalIdentifier: pageLayoutTabEntity.universalIdentifier,
applicationUniversalIdentifier,
@@ -44,5 +44,6 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
gridPosition: createPageLayoutWidgetInput.gridPosition,
configuration: createPageLayoutWidgetInput.configuration,
applicationId: workspaceCustomApplicationId,
conditionalDisplay: null,
};
};
@@ -87,6 +87,8 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({
objectMetadataUniversalIdentifier,
gridPosition: pageLayoutWidgetEntityWithoutRelations.gridPosition,
configuration: configurationWithUniversalIdentifiers,
conditionalDisplay:
pageLayoutWidgetEntityWithoutRelations.conditionalDisplay,
},
};
};
@@ -70,6 +70,8 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const pageLayoutTabIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(pageLayoutTabs);
const flatPageLayoutMaps = createEmptyFlatEntityMaps();
@@ -81,6 +83,7 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid
},
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
pageLayoutTabIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
@@ -37,5 +37,6 @@ export const fromCreatePageLayoutInputToFlatPageLayoutToCreate = ({
universalIdentifier: pageLayoutId,
applicationId: workspaceCustomApplicationId,
tabIds: [],
defaultTabToFocusOnMobileAndSidePanelId: null,
};
};
@@ -11,6 +11,7 @@ export const transformPageLayoutEntityToFlatPageLayout = ({
entity: pageLayoutEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
pageLayoutTabIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'pageLayout'>): FlatPageLayout => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(pageLayoutEntity.applicationId);
@@ -38,6 +39,23 @@ export const transformPageLayoutEntityToFlatPageLayout = ({
}
}
let defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: string | null =
null;
if (isDefined(pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId)) {
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier =
pageLayoutTabIdToUniversalIdentifierMap.get(
pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId,
) ?? null;
if (!isDefined(defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PageLayoutTab with id ${pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId} not found for pageLayout ${pageLayoutEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
return {
createdAt: pageLayoutEntity.createdAt.toISOString(),
deletedAt: pageLayoutEntity.deletedAt?.toISOString() ?? null,
@@ -50,6 +68,8 @@ export const transformPageLayoutEntityToFlatPageLayout = ({
universalIdentifier: pageLayoutEntity.universalIdentifier,
applicationId: pageLayoutEntity.applicationId,
tabIds: pageLayoutEntity.tabs.map((tab) => tab.id),
defaultTabToFocusOnMobileAndSidePanelId:
pageLayoutEntity.defaultTabToFocusOnMobileAndSidePanelId,
__universal: {
universalIdentifier: pageLayoutEntity.universalIdentifier,
applicationUniversalIdentifier,
@@ -57,6 +77,7 @@ export const transformPageLayoutEntityToFlatPageLayout = ({
tabUniversalIdentifiers: pageLayoutEntity.tabs.map(
(tab) => tab.universalIdentifier,
),
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier,
},
};
};
@@ -1,10 +1,15 @@
import { Field, Float, ObjectType } from '@nestjs/graphql';
import { Field, Float, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
registerEnumType(PageLayoutTabLayoutMode, {
name: 'PageLayoutTabLayoutMode',
});
@ObjectType('PageLayoutTab')
export class PageLayoutTabDTO {
@IDField(() => UUIDScalarType)
@@ -22,6 +27,15 @@ export class PageLayoutTabDTO {
@Field(() => [PageLayoutWidgetDTO], { nullable: true })
widgets?: PageLayoutWidgetDTO[] | null;
@Field(() => String, { nullable: true })
icon?: string | null;
@Field(() => PageLayoutTabLayoutMode, {
nullable: true, // Not nullable in the database, but we need to make it nullable here until the frontend consumes the new type
defaultValue: PageLayoutTabLayoutMode.GRID,
})
layoutMode: PageLayoutTabLayoutMode;
@Field()
createdAt: Date;
@@ -1,5 +1,6 @@
import { ObjectType } from '@nestjs/graphql';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import {
Column,
CreateDateColumn,
@@ -52,6 +53,17 @@ export class PageLayoutTabEntity
})
widgets: Relation<PageLayoutWidgetEntity[]>;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({
type: 'enum',
enum: Object.values(PageLayoutTabLayoutMode),
nullable: false,
default: PageLayoutTabLayoutMode.GRID,
})
layoutMode: PageLayoutTabLayoutMode;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -1,6 +1,8 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import GraphQLJSON from 'graphql-type-json';
import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
@@ -47,6 +49,9 @@ export class PageLayoutWidgetDTO {
@Field(() => WidgetConfiguration, { nullable: false })
configuration: AllPageLayoutWidgetConfiguration;
@Field(() => GraphQLJSON, { nullable: true })
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
@Field()
createdAt: Date;
@@ -1,5 +1,6 @@
import { ObjectType } from '@nestjs/graphql';
import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types';
import {
Column,
CreateDateColumn,
@@ -70,6 +71,9 @@ export class PageLayoutWidgetEntity<
@JoinColumn({ name: 'objectMetadataId' })
objectMetadata: Relation<ObjectMetadataEntity> | null;
@Column({ type: 'jsonb', nullable: true })
conditionalDisplay: JsonbProperty<PageLayoutWidgetConditionalDisplay | null>;
@Column({ type: 'jsonb', nullable: false })
gridPosition: JsonbProperty<GridPosition>;
@@ -1,6 +1,7 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { SerializedRelation } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
@@ -28,6 +29,9 @@ export class PageLayoutDTO {
@Field(() => [PageLayoutTabDTO], { nullable: true })
tabs?: PageLayoutTabDTO[] | null;
@Field(() => UUIDScalarType, { nullable: true })
defaultTabToFocusOnMobileAndSidePanelId?: SerializedRelation;
@Field()
createdAt: Date;
@@ -59,6 +59,16 @@ export class PageLayoutEntity
})
tabs: Relation<PageLayoutTabEntity[]>;
@Column({ nullable: true, type: 'uuid' })
defaultTabToFocusOnMobileAndSidePanelId: string | null;
@ManyToOne(() => PageLayoutTabEntity, {
onDelete: 'SET NULL',
nullable: true,
})
@JoinColumn({ name: 'defaultTabToFocusOnMobileAndSidePanelId' })
defaultTabToFocusOnMobileAndSidePanel: Relation<PageLayoutTabEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@@ -236,6 +237,8 @@ export class PageLayoutUpdateService {
universalIdentifier: tabId,
applicationId: workspaceCustomApplicationId,
widgetIds: [],
icon: null,
layoutMode: PageLayoutTabLayoutMode.GRID,
};
},
);
@@ -392,6 +395,7 @@ export class PageLayoutUpdateService {
deletedAt: null,
universalIdentifier: widgetId,
applicationId: workspaceCustomApplicationId,
conditionalDisplay: null,
};
},
);
@@ -9,6 +9,7 @@ export const fromFlatPageLayoutToPageLayoutDto = (
updatedAt,
deletedAt,
tabIds: _tabIds,
defaultTabToFocusOnMobileAndSidePanelId,
...rest
} = flatPageLayout;
@@ -17,5 +18,7 @@ export const fromFlatPageLayoutToPageLayoutDto = (
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
deletedAt: deletedAt ? new Date(deletedAt) : null,
defaultTabToFocusOnMobileAndSidePanelId:
defaultTabToFocusOnMobileAndSidePanelId ?? undefined,
};
};
@@ -1,4 +1,5 @@
import { isDefined } from 'class-validator';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { STANDARD_PAGE_LAYOUTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout.constant';
@@ -64,5 +65,7 @@ export const createStandardPageLayoutTabFlatMetadata = <
createdAt: now,
updatedAt: now,
deletedAt: null,
icon: null,
layoutMode: PageLayoutTabLayoutMode.GRID,
};
};
@@ -87,5 +87,6 @@ export const createStandardPageLayoutWidgetFlatMetadata = <
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
};
};
@@ -41,5 +41,6 @@ export const createStandardPageLayoutFlatMetadata = ({
createdAt: now,
updatedAt: now,
deletedAt: null,
defaultTabToFocusOnMobileAndSidePanelId: null,
};
};
@@ -798,6 +798,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
flatPageLayoutTabMaps:
optimisticAllFlatEntityMaps.flatPageLayoutTabMaps,
},
workspaceId,
},
@@ -42,6 +42,7 @@ export const ALL_JSONB_PROPERTIES_BY_METADATA_NAME = {
pageLayoutWidget: {
gridPosition: 'gridPosition',
configuration: 'configuration',
conditionalDisplay: 'conditionalDisplay',
},
commandMenuItem: {},
navigationMenuItem: {},