Page layout tab v2 (#16319)

# Introduction
Migrating `pageLayoutTab` to the v2 engine
- Types and constants
- Builder and validate
- Runner

Introduced a new `StrictSyncableEntity` that enforces that
`universalIdentifier` and `applicationId` are defined
As these entities are brand new we could enforce this rule already
This still requires a migration command to associate the existing
entities to custom workspace application instance and define
universalIdentifier

Handled retro-comp of the migration through a migration as upgrade
command fallback

---------

Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
Paul Rastoin
2025-12-04 17:37:16 +01:00
committed by GitHub
parent 1981cf0ed6
commit b2d785de4b
56 changed files with 1284 additions and 13 deletions
@@ -14,6 +14,8 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { StrictSyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/strict-syncable-entity.interface';
import { PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/core-modules/page-layout/entities/page-layout.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -25,7 +27,10 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
['workspaceId', 'pageLayoutId'],
{ where: '"deletedAt" IS NULL' },
)
export class PageLayoutTabEntity implements Required<PageLayoutTabEntity> {
export class PageLayoutTabEntity
extends StrictSyncableEntity
implements Required<PageLayoutTabEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -15,6 +15,7 @@ import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/servic
import { PageLayoutUpdateService } from 'src/engine/core-modules/page-layout/services/page-layout-update.service';
import { PageLayoutWidgetService } from 'src/engine/core-modules/page-layout/services/page-layout-widget.service';
import { PageLayoutService } from 'src/engine/core-modules/page-layout/services/page-layout.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
@@ -24,6 +25,7 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
PageLayoutEntity,
PageLayoutTabEntity,
PageLayoutWidgetEntity,
WorkspaceEntity,
]),
TwentyORMModule,
PermissionsModule,
@@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity';
import { type PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity';
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
@@ -18,6 +19,7 @@ import {
} from 'src/engine/core-modules/page-layout/exceptions/page-layout.exception';
import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/services/page-layout-tab.service';
import { PageLayoutService } from 'src/engine/core-modules/page-layout/services/page-layout.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
describe('PageLayoutTabService', () => {
let pageLayoutTabService: PageLayoutTabService;
@@ -36,6 +38,9 @@ describe('PageLayoutTabService', () => {
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
application: {} as ApplicationEntity,
applicationId: 'application-id',
universalIdentifier: 'universal-identifier',
} as PageLayoutTabEntity;
const mockWidget = {
@@ -71,6 +76,14 @@ describe('PageLayoutTabService', () => {
insert: jest.fn(),
},
},
{
provide: getRepositoryToken(WorkspaceEntity),
useValue: {
findOneOrFail: jest.fn().mockResolvedValue({
workspaceCustomApplicationId: 'application-id',
}),
},
},
{
provide: PageLayoutService,
useValue: {
@@ -261,6 +274,8 @@ describe('PageLayoutTabService', () => {
expect(pageLayoutTabRepository.insert).toHaveBeenCalledWith({
...pageLayoutTabData,
workspaceId,
applicationId: 'application-id',
universalIdentifier: expect.any(String),
});
expect(result).toEqual(mockPageLayoutTab);
});
@@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { DataSource, type EntityManager } from 'typeorm';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-tab-with-widgets.input';
import { type UpdatePageLayoutWidgetWithIdInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input';
import { type PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity';
@@ -49,6 +50,9 @@ describe('PageLayoutUpdateService', () => {
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
application: {} as ApplicationEntity,
applicationId: 'application-id',
universalIdentifier: 'universal-identifier',
} as PageLayoutTabEntity;
const mockWidget = {
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { EntityManager, IsNull, Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { v4 } from 'uuid';
import { CreatePageLayoutTabInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-tab.input';
import { PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity';
@@ -18,12 +19,15 @@ import {
PageLayoutExceptionCode,
} from 'src/engine/core-modules/page-layout/exceptions/page-layout.exception';
import { PageLayoutService } from 'src/engine/core-modules/page-layout/services/page-layout.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable()
export class PageLayoutTabService {
constructor(
@InjectRepository(PageLayoutTabEntity)
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly pageLayoutService: PageLayoutService,
) {}
@@ -113,11 +117,18 @@ export class PageLayoutTabService {
transactionManager,
);
const workspace = await this.workspaceRepository.findOneOrFail({
where: { id: workspaceId },
select: ['workspaceCustomApplicationId'],
});
const repository = this.getPageLayoutTabRepository(transactionManager);
const insertResult = await repository.insert({
...pageLayoutTabData,
workspaceId,
universalIdentifier: v4(),
applicationId: workspace.workspaceCustomApplicationId,
});
return this.findByIdOrThrow(