Pre hook for dashboard creation + updates on the entity (#14354)

Closes https://github.com/twentyhq/core-team-issues/issues/1417

- The pre-hook creates a page-layout and links it to the dashboard
- Added `position`, `createdBy`, `attachments`, `searchVector`,
`favorites` to the dashboard entity
- Updated the view seed
- Updated the page layout services to work within a transaction
This commit is contained in:
Raphaël Bosi
2025-09-09 14:55:22 +02:00
committed by GitHub
parent 861cb1c222
commit 3d2d14eb71
19 changed files with 657 additions and 115 deletions
@@ -29,5 +29,5 @@ export class CreatePageLayoutInput {
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
objectMetadataId?: string;
objectMetadataId?: string | null;
}
@@ -20,5 +20,5 @@ export class UpdatePageLayoutInput {
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
objectMetadataId?: string;
objectMetadataId?: string | null;
}
@@ -13,6 +13,7 @@ import { PageLayoutResolver } from 'src/engine/core-modules/page-layout/resolver
import { PageLayoutTabService } from 'src/engine/core-modules/page-layout/services/page-layout-tab.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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
@Module({
imports: [
@@ -21,6 +22,7 @@ import { PageLayoutService } from 'src/engine/core-modules/page-layout/services/
PageLayoutTabEntity,
PageLayoutWidgetEntity,
]),
TwentyORMModule,
],
controllers: [
PageLayoutController,
@@ -2,9 +2,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { EntityManager, IsNull, Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
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';
import {
PageLayoutTabException,
@@ -26,11 +27,22 @@ export class PageLayoutTabService {
private readonly pageLayoutService: PageLayoutService,
) {}
private getPageLayoutTabRepository(
transactionManager?: EntityManager,
): Repository<PageLayoutTabEntity> {
return transactionManager
? transactionManager.getRepository(PageLayoutTabEntity)
: this.pageLayoutTabRepository;
}
async findByPageLayoutId(
workspaceId: string,
pageLayoutId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutTabEntity[]> {
return this.pageLayoutTabRepository.find({
const repository = this.getPageLayoutTabRepository(transactionManager);
return repository.find({
where: {
pageLayoutId,
pageLayout: { workspaceId },
@@ -44,8 +56,11 @@ export class PageLayoutTabService {
async findByIdOrThrow(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutTabEntity> {
const pageLayoutTab = await this.pageLayoutTabRepository.findOne({
const repository = this.getPageLayoutTabRepository(transactionManager);
const pageLayoutTab = await repository.findOne({
where: {
id,
workspaceId,
@@ -68,8 +83,9 @@ export class PageLayoutTabService {
}
async create(
pageLayoutTabData: Partial<PageLayoutTabEntity>,
pageLayoutTabData: CreatePageLayoutTabInput,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutTabEntity> {
if (!isDefined(pageLayoutTabData.title)) {
throw new PageLayoutTabException(
@@ -93,14 +109,21 @@ export class PageLayoutTabService {
await this.pageLayoutService.findByIdOrThrow(
pageLayoutTabData.pageLayoutId,
workspaceId,
transactionManager,
);
const pageLayoutTab = this.pageLayoutTabRepository.create({
const repository = this.getPageLayoutTabRepository(transactionManager);
const insertResult = await repository.insert({
...pageLayoutTabData,
workspaceId,
});
return this.pageLayoutTabRepository.save(pageLayoutTab);
return this.findByIdOrThrow(
insertResult.identifiers[0].id,
workspaceId,
transactionManager,
);
} catch (error) {
if (
error instanceof PageLayoutException &&
@@ -121,8 +144,11 @@ export class PageLayoutTabService {
id: string,
workspaceId: string,
updateData: QueryDeepPartialEntity<PageLayoutTabEntity>,
transactionManager?: EntityManager,
): Promise<PageLayoutTabEntity> {
const existingTab = await this.pageLayoutTabRepository.findOne({
const repository = this.getPageLayoutTabRepository(transactionManager);
const existingTab = await repository.findOne({
where: {
id,
workspaceId,
@@ -140,21 +166,37 @@ export class PageLayoutTabService {
);
}
await this.pageLayoutTabRepository.update({ id }, updateData);
await repository.update({ id }, updateData);
return this.findByIdOrThrow(id, workspaceId);
return this.findByIdOrThrow(id, workspaceId, transactionManager);
}
async delete(id: string, workspaceId: string): Promise<PageLayoutTabEntity> {
const pageLayoutTab = await this.findByIdOrThrow(id, workspaceId);
async delete(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutTabEntity> {
const pageLayoutTab = await this.findByIdOrThrow(
id,
workspaceId,
transactionManager,
);
await this.pageLayoutTabRepository.softDelete(id);
const repository = this.getPageLayoutTabRepository(transactionManager);
await repository.softDelete(id);
return pageLayoutTab;
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
const pageLayoutTab = await this.pageLayoutTabRepository.findOne({
async destroy(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<boolean> {
const repository = this.getPageLayoutTabRepository(transactionManager);
const pageLayoutTab = await repository.findOne({
where: {
id,
workspaceId,
@@ -172,13 +214,19 @@ export class PageLayoutTabService {
);
}
await this.pageLayoutTabRepository.delete(id);
await repository.delete(id);
return true;
}
async restore(id: string, workspaceId: string): Promise<PageLayoutTabEntity> {
const pageLayoutTab = await this.pageLayoutTabRepository.findOne({
async restore(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutTabEntity> {
const repository = this.getPageLayoutTabRepository(transactionManager);
const pageLayoutTab = await repository.findOne({
select: {
id: true,
deletedAt: true,
@@ -214,6 +262,7 @@ export class PageLayoutTabService {
await this.pageLayoutService.findByIdOrThrow(
pageLayoutTab.pageLayoutId,
workspaceId,
transactionManager,
);
} catch (error) {
if (
@@ -230,9 +279,13 @@ export class PageLayoutTabService {
throw error;
}
await this.pageLayoutTabRepository.restore(id);
await repository.restore(id);
const restoredPageLayoutTab = await this.findByIdOrThrow(id, workspaceId);
const restoredPageLayoutTab = await this.findByIdOrThrow(
id,
workspaceId,
transactionManager,
);
return restoredPageLayoutTab;
}
@@ -2,9 +2,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { EntityManager, IsNull, Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { CreatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-widget.input';
import { UpdatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget.input';
import { PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity';
import {
@@ -27,11 +28,22 @@ export class PageLayoutWidgetService {
private readonly pageLayoutTabService: PageLayoutTabService,
) {}
private getPageLayoutWidgetRepository(
transactionManager?: EntityManager,
): Repository<PageLayoutWidgetEntity> {
return transactionManager
? transactionManager.getRepository(PageLayoutWidgetEntity)
: this.pageLayoutWidgetRepository;
}
async findByPageLayoutTabId(
workspaceId: string,
pageLayoutTabId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutWidgetEntity[]> {
return this.pageLayoutWidgetRepository.find({
const repository = this.getPageLayoutWidgetRepository(transactionManager);
return repository.find({
where: {
pageLayoutTabId,
workspaceId,
@@ -44,8 +56,11 @@ export class PageLayoutWidgetService {
async findByIdOrThrow(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutWidgetEntity> {
const pageLayoutWidget = await this.pageLayoutWidgetRepository.findOne({
const repository = this.getPageLayoutWidgetRepository(transactionManager);
const pageLayoutWidget = await repository.findOne({
where: {
id,
workspaceId,
@@ -67,8 +82,9 @@ export class PageLayoutWidgetService {
}
async create(
pageLayoutWidgetData: Partial<PageLayoutWidgetEntity>,
pageLayoutWidgetData: CreatePageLayoutWidgetInput,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutWidgetEntity> {
if (!isDefined(pageLayoutWidgetData.title)) {
throw new PageLayoutWidgetException(
@@ -101,14 +117,21 @@ export class PageLayoutWidgetService {
await this.pageLayoutTabService.findByIdOrThrow(
pageLayoutWidgetData.pageLayoutTabId,
workspaceId,
transactionManager,
);
const pageLayoutWidget = this.pageLayoutWidgetRepository.create({
const repository = this.getPageLayoutWidgetRepository(transactionManager);
const insertResult = await repository.insert({
...pageLayoutWidgetData,
workspaceId,
});
} as QueryDeepPartialEntity<PageLayoutWidgetEntity>);
return this.pageLayoutWidgetRepository.save(pageLayoutWidget);
return this.findByIdOrThrow(
insertResult.identifiers[0].id,
workspaceId,
transactionManager,
);
} catch (error) {
if (
error instanceof PageLayoutTabException &&
@@ -129,8 +152,11 @@ export class PageLayoutWidgetService {
id: string,
workspaceId: string,
updateData: UpdatePageLayoutWidgetInput,
transactionManager?: EntityManager,
): Promise<PageLayoutWidgetEntity> {
const existingWidget = await this.pageLayoutWidgetRepository.findOne({
const repository = this.getPageLayoutWidgetRepository(transactionManager);
const existingWidget = await repository.findOne({
where: {
id,
workspaceId,
@@ -148,27 +174,40 @@ export class PageLayoutWidgetService {
);
}
await this.pageLayoutWidgetRepository.update(
await repository.update(
{ id },
updateData as QueryDeepPartialEntity<PageLayoutWidgetEntity>,
);
return this.findByIdOrThrow(id, workspaceId);
return this.findByIdOrThrow(id, workspaceId, transactionManager);
}
async delete(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutWidgetEntity> {
const pageLayoutWidget = await this.findByIdOrThrow(id, workspaceId);
const pageLayoutWidget = await this.findByIdOrThrow(
id,
workspaceId,
transactionManager,
);
await this.pageLayoutWidgetRepository.softDelete(id);
const repository = this.getPageLayoutWidgetRepository(transactionManager);
await repository.softDelete(id);
return pageLayoutWidget;
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
const pageLayoutWidget = await this.pageLayoutWidgetRepository.findOne({
async destroy(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<boolean> {
const repository = this.getPageLayoutWidgetRepository(transactionManager);
const pageLayoutWidget = await repository.findOne({
where: {
id,
workspaceId,
@@ -186,7 +225,7 @@ export class PageLayoutWidgetService {
);
}
await this.pageLayoutWidgetRepository.delete(id);
await repository.delete(id);
return true;
}
@@ -194,8 +233,11 @@ export class PageLayoutWidgetService {
async restore(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutWidgetEntity> {
const pageLayoutWidget = await this.pageLayoutWidgetRepository.findOne({
const repository = this.getPageLayoutWidgetRepository(transactionManager);
const pageLayoutWidget = await repository.findOne({
select: {
id: true,
deletedAt: true,
@@ -231,6 +273,7 @@ export class PageLayoutWidgetService {
await this.pageLayoutTabService.findByIdOrThrow(
pageLayoutWidget.pageLayoutTabId,
workspaceId,
transactionManager,
);
} catch (error) {
if (
@@ -247,11 +290,12 @@ export class PageLayoutWidgetService {
throw error;
}
await this.pageLayoutWidgetRepository.restore(id);
await repository.restore(id);
const restoredPageLayoutWidget = await this.findByIdOrThrow(
id,
workspaceId,
transactionManager,
);
return restoredPageLayoutWidget;
@@ -1,27 +1,46 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { EntityManager, IsNull, Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { CreatePageLayoutInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout.input';
import { PageLayoutEntity } from 'src/engine/core-modules/page-layout/entities/page-layout.entity';
import { PageLayoutType } from 'src/engine/core-modules/page-layout/enums/page-layout-type.enum';
import {
PageLayoutException,
PageLayoutExceptionCode,
PageLayoutExceptionMessageKey,
generatePageLayoutExceptionMessage,
} from 'src/engine/core-modules/page-layout/exceptions/page-layout.exception';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Injectable()
export class PageLayoutService {
private readonly logger = new Logger(PageLayoutService.name);
constructor(
@InjectRepository(PageLayoutEntity)
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async findByWorkspaceId(workspaceId: string): Promise<PageLayoutEntity[]> {
return this.pageLayoutRepository.find({
private getPageLayoutRepository(
transactionManager?: EntityManager,
): Repository<PageLayoutEntity> {
return transactionManager
? transactionManager.getRepository(PageLayoutEntity)
: this.pageLayoutRepository;
}
async findByWorkspaceId(
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity[]> {
const repository = this.getPageLayoutRepository(transactionManager);
return repository.find({
where: {
workspaceId,
deletedAt: IsNull(),
@@ -33,8 +52,11 @@ export class PageLayoutService {
async findByObjectMetadataId(
workspaceId: string,
objectMetadataId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity[]> {
return this.pageLayoutRepository.find({
const repository = this.getPageLayoutRepository(transactionManager);
return repository.find({
where: {
workspaceId,
objectMetadataId,
@@ -47,8 +69,11 @@ export class PageLayoutService {
async findByIdOrThrow(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity> {
const pageLayout = await this.pageLayoutRepository.findOne({
const repository = this.getPageLayoutRepository(transactionManager);
const pageLayout = await repository.findOne({
where: {
id,
workspaceId,
@@ -71,8 +96,9 @@ export class PageLayoutService {
}
async create(
pageLayoutData: Partial<PageLayoutEntity>,
pageLayoutData: CreatePageLayoutInput,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity> {
if (!isDefined(pageLayoutData.name)) {
throw new PageLayoutException(
@@ -83,36 +109,65 @@ export class PageLayoutService {
);
}
const pageLayout = this.pageLayoutRepository.create({
const repository = this.getPageLayoutRepository(transactionManager);
const insertResult = await repository.insert({
...pageLayoutData,
workspaceId,
});
return this.pageLayoutRepository.save(pageLayout);
return this.findByIdOrThrow(
insertResult.identifiers[0].id,
workspaceId,
transactionManager,
);
}
async update(
id: string,
workspaceId: string,
updateData: QueryDeepPartialEntity<PageLayoutEntity>,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity> {
await this.pageLayoutRepository.update({ id, workspaceId }, updateData);
const repository = this.getPageLayoutRepository(transactionManager);
const updatedPageLayout = await this.findByIdOrThrow(id, workspaceId);
await repository.update({ id, workspaceId }, updateData);
const updatedPageLayout = await this.findByIdOrThrow(
id,
workspaceId,
transactionManager,
);
return updatedPageLayout;
}
async delete(id: string, workspaceId: string): Promise<PageLayoutEntity> {
const pageLayout = await this.findByIdOrThrow(id, workspaceId);
async delete(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity> {
const pageLayout = await this.findByIdOrThrow(
id,
workspaceId,
transactionManager,
);
await this.pageLayoutRepository.softDelete(id);
const repository = this.getPageLayoutRepository(transactionManager);
await repository.softDelete(id);
return pageLayout;
}
async destroy(id: string, workspaceId: string): Promise<PageLayoutEntity> {
const pageLayout = await this.pageLayoutRepository.findOne({
async destroy(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<PageLayoutEntity> {
const repository = this.getPageLayoutRepository(transactionManager);
const pageLayout = await repository.findOne({
where: {
id,
workspaceId,
@@ -130,11 +185,43 @@ export class PageLayoutService {
);
}
await this.pageLayoutRepository.delete(id);
if (pageLayout.type === PageLayoutType.DASHBOARD) {
await this.destroyAssociatedDashboards(id, workspaceId);
}
await repository.delete(id);
return pageLayout;
}
private async destroyAssociatedDashboards(
pageLayoutId: string,
workspaceId: string,
): Promise<void> {
try {
const dashboardRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
},
});
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
} catch (error) {
this.logger.error(
`Failed to destroy associated dashboards for page layout ${pageLayoutId}: ${error}`,
);
}
}
async restore(id: string, workspaceId: string): Promise<PageLayoutEntity> {
const pageLayout = await this.pageLayoutRepository.findOne({
select: {
@@ -51,15 +51,6 @@ describe('PageLayoutTabService', () => {
deletedAt: null,
} as PageLayoutWidgetEntity;
const mockPageLayout = {
id: 'page-layout-id',
workspaceId: 'workspace-id',
title: 'Test Layout',
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
};
beforeEach(async () => {
jest.clearAllMocks();
@@ -77,6 +68,7 @@ describe('PageLayoutTabService', () => {
softDelete: jest.fn(),
delete: jest.fn(),
restore: jest.fn(),
insert: jest.fn(),
},
},
{
@@ -240,20 +232,21 @@ describe('PageLayoutTabService', () => {
it('should create a new page layout tab successfully', async () => {
const workspaceId = 'workspace-id';
const pageLayoutTabData = {
id: 'page-layout-tab-id',
title: 'New Tab',
pageLayoutId: 'page-layout-id',
position: 1,
};
jest
.spyOn(pageLayoutService, 'findByIdOrThrow')
.mockResolvedValue(mockPageLayout as any);
jest
.spyOn(pageLayoutTabRepository, 'create')
.mockReturnValue(mockPageLayoutTab);
jest
.spyOn(pageLayoutTabRepository, 'save')
.mockResolvedValue(mockPageLayoutTab);
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
.mockResolvedValue(mockPageLayoutTab as any);
jest.spyOn(pageLayoutTabRepository, 'insert').mockResolvedValue({
identifiers: [{ id: 'page-layout-tab-id' }],
generatedMaps: [],
raw: [],
});
const result = await pageLayoutTabService.create(
pageLayoutTabData,
@@ -263,14 +256,12 @@ describe('PageLayoutTabService', () => {
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
pageLayoutTabData.pageLayoutId,
workspaceId,
undefined,
);
expect(pageLayoutTabRepository.create).toHaveBeenCalledWith({
expect(pageLayoutTabRepository.insert).toHaveBeenCalledWith({
...pageLayoutTabData,
workspaceId,
});
expect(pageLayoutTabRepository.save).toHaveBeenCalledWith(
mockPageLayoutTab,
);
expect(result).toEqual(mockPageLayoutTab);
});
@@ -281,9 +272,11 @@ describe('PageLayoutTabService', () => {
};
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
).rejects.toThrow(PageLayoutTabException);
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
).rejects.toHaveProperty(
'code',
@@ -298,6 +291,11 @@ describe('PageLayoutTabService', () => {
pageLayoutId: 'non-existent-page-layout-id',
};
jest.spyOn(pageLayoutTabRepository, 'insert').mockResolvedValue({
identifiers: [{ id: 'page-layout-tab-id' }],
generatedMaps: [],
raw: [],
});
jest
.spyOn(pageLayoutService, 'findByIdOrThrow')
.mockRejectedValue(
@@ -586,6 +584,7 @@ describe('PageLayoutTabService', () => {
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
'deleted-page-layout-id',
workspaceId,
undefined,
);
});
});
@@ -38,18 +38,6 @@ describe('PageLayoutWidgetService', () => {
deletedAt: null,
} as PageLayoutWidgetEntity;
const mockPageLayoutTab = {
id: 'page-layout-tab-id',
title: 'Test Tab',
position: 0,
pageLayoutId: 'page-layout-id',
pageLayout: { workspaceId: 'workspace-id' },
widgets: [],
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
};
beforeEach(async () => {
jest.clearAllMocks();
@@ -63,6 +51,7 @@ describe('PageLayoutWidgetService', () => {
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
softDelete: jest.fn(),
delete: jest.fn(),
@@ -177,6 +166,7 @@ describe('PageLayoutWidgetService', () => {
describe('create', () => {
const validPageLayoutWidgetData = {
id: 'page-layout-widget-id',
title: 'New Widget',
pageLayoutTabId: 'page-layout-tab-id',
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
@@ -186,14 +176,13 @@ describe('PageLayoutWidgetService', () => {
it('should create a new page layout widget successfully', async () => {
const workspaceId = 'workspace-id';
jest.spyOn(pageLayoutWidgetRepository, 'insert').mockResolvedValue({
identifiers: [{ id: 'page-layout-widget-id' }],
generatedMaps: [],
raw: [],
});
jest
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
.mockResolvedValue(mockPageLayoutTab as any);
jest
.spyOn(pageLayoutWidgetRepository, 'create')
.mockReturnValue(mockPageLayoutWidget);
jest
.spyOn(pageLayoutWidgetRepository, 'save')
.spyOn(pageLayoutWidgetService, 'findByIdOrThrow')
.mockResolvedValue(mockPageLayoutWidget);
const result = await pageLayoutWidgetService.create(
@@ -201,17 +190,10 @@ describe('PageLayoutWidgetService', () => {
workspaceId,
);
expect(pageLayoutTabService.findByIdOrThrow).toHaveBeenCalledWith(
validPageLayoutWidgetData.pageLayoutTabId,
workspaceId,
);
expect(pageLayoutWidgetRepository.create).toHaveBeenCalledWith({
expect(pageLayoutWidgetRepository.insert).toHaveBeenCalledWith({
...validPageLayoutWidgetData,
workspaceId,
});
expect(pageLayoutWidgetRepository.save).toHaveBeenCalledWith(
mockPageLayoutWidget,
);
expect(result).toEqual(mockPageLayoutWidget);
});
@@ -223,9 +205,11 @@ describe('PageLayoutWidgetService', () => {
};
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
).rejects.toThrow(PageLayoutWidgetException);
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
).rejects.toHaveProperty(
'code',
@@ -241,9 +225,11 @@ describe('PageLayoutWidgetService', () => {
};
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
).rejects.toThrow(PageLayoutWidgetException);
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
).rejects.toHaveProperty(
'code',
@@ -259,9 +245,11 @@ describe('PageLayoutWidgetService', () => {
};
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
).rejects.toThrow(PageLayoutWidgetException);
await expect(
// @ts-expect-error - we are testing the exception
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
).rejects.toHaveProperty(
'code',
@@ -578,6 +566,7 @@ describe('PageLayoutWidgetService', () => {
expect(pageLayoutTabService.findByIdOrThrow).toHaveBeenCalledWith(
'deleted-tab-id',
workspaceId,
undefined,
);
});
});
@@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { IsNull, type Repository } from 'typeorm';
import { type CreatePageLayoutInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout.input';
import { PageLayoutEntity } from 'src/engine/core-modules/page-layout/entities/page-layout.entity';
import { PageLayoutType } from 'src/engine/core-modules/page-layout/enums/page-layout-type.enum';
import {
@@ -12,10 +13,12 @@ import {
generatePageLayoutExceptionMessage,
} 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
describe('PageLayoutService', () => {
let pageLayoutService: PageLayoutService;
let pageLayoutRepository: Repository<PageLayoutEntity>;
let twentyORMGlobalManager: TwentyORMGlobalManager;
const mockPageLayout = {
id: 'page-layout-id',
@@ -46,6 +49,13 @@ describe('PageLayoutService', () => {
softDelete: jest.fn(),
delete: jest.fn(),
restore: jest.fn(),
insert: jest.fn(),
},
},
{
provide: TwentyORMGlobalManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
},
},
],
@@ -55,6 +65,9 @@ describe('PageLayoutService', () => {
pageLayoutRepository = module.get<Repository<PageLayoutEntity>>(
getRepositoryToken(PageLayoutEntity),
);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
);
});
describe('findByWorkspaceId', () => {
@@ -142,17 +155,20 @@ describe('PageLayoutService', () => {
describe('create', () => {
const validPageLayoutData = {
id: 'page-layout-id',
name: 'Test Page Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: 'object-metadata-id',
};
it('should create a page layout successfully', async () => {
jest.spyOn(pageLayoutRepository, 'insert').mockResolvedValue({
identifiers: [{ id: 'page-layout-id' }],
generatedMaps: [],
raw: [],
});
jest
.spyOn(pageLayoutRepository, 'create')
.mockReturnValue(mockPageLayout);
jest
.spyOn(pageLayoutRepository, 'save')
.spyOn(pageLayoutService, 'findByIdOrThrow')
.mockResolvedValue(mockPageLayout);
const result = await pageLayoutService.create(
@@ -160,11 +176,10 @@ describe('PageLayoutService', () => {
'workspace-id',
);
expect(pageLayoutRepository.create).toHaveBeenCalledWith({
expect(pageLayoutRepository.insert).toHaveBeenCalledWith({
...validPageLayoutData,
workspaceId: 'workspace-id',
});
expect(pageLayoutRepository.save).toHaveBeenCalledWith(mockPageLayout);
expect(result).toEqual(mockPageLayout);
});
@@ -173,7 +188,10 @@ describe('PageLayoutService', () => {
const workspaceId = 'workspace-id';
await expect(
pageLayoutService.create(invalidData, workspaceId),
pageLayoutService.create(
invalidData as unknown as CreatePageLayoutInput,
workspaceId,
),
).rejects.toThrow(
new PageLayoutException(
generatePageLayoutExceptionMessage(
@@ -210,6 +228,7 @@ describe('PageLayoutService', () => {
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
id,
workspaceId,
undefined,
);
expect(result).toEqual(updatedPageLayout);
});
@@ -262,6 +281,7 @@ describe('PageLayoutService', () => {
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
id,
workspaceId,
undefined,
);
expect(pageLayoutRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockPageLayout);
@@ -344,6 +364,51 @@ describe('PageLayoutService', () => {
),
);
});
it('should destroy associated dashboards when page layout is a dashboard', async () => {
const id = 'page-layout-id';
const workspaceId = 'workspace-id';
const mockDashboardRepository = {
find: jest.fn(),
delete: jest.fn(),
};
const mockDashboards = [{ id: 'dashboard', pageLayoutId: id }];
jest.spyOn(pageLayoutRepository, 'findOne').mockResolvedValue({
...mockPageLayout,
type: PageLayoutType.DASHBOARD,
});
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.mockResolvedValue(mockDashboardRepository as any);
jest
.spyOn(mockDashboardRepository, 'find')
.mockResolvedValue(mockDashboards);
jest
.spyOn(mockDashboardRepository, 'delete')
.mockResolvedValue({} as any);
jest.spyOn(pageLayoutRepository, 'delete').mockResolvedValue({} as any);
const result = await pageLayoutService.destroy(id, workspaceId);
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith(workspaceId, 'dashboard', {
shouldBypassPermissionChecks: true,
});
expect(mockDashboardRepository.find).toHaveBeenCalledWith({
where: {
pageLayoutId: id,
},
});
expect(mockDashboardRepository.delete).toHaveBeenCalledWith('dashboard');
expect(pageLayoutRepository.delete).toHaveBeenCalledWith(id);
expect(result).toEqual({
...mockPageLayout,
type: PageLayoutType.DASHBOARD,
});
});
});
describe('restore', () => {