[DASHBOARDS] Allow dashboards to be restored (#17042)

This PR introduces a few changes:
- Add three actions: see deleted dashboards, destroy dashboard and
restore dashboard
- Remove the soft delete and restore on all the page layout entities
- Cascade the destruction of a dashboard to a page layout

Video QA:


https://github.com/user-attachments/assets/ab993b11-dd9c-4e88-880c-92691a521cc2
This commit is contained in:
Raphaël Bosi
2026-01-12 13:59:17 +01:00
committed by GitHub
parent 3ada8e5168
commit 655f1eef5f
60 changed files with 1178 additions and 2067 deletions
@@ -54,10 +54,10 @@ export class PageLayoutWidgetController {
);
}
return this.pageLayoutWidgetService.findByPageLayoutTabId(
workspace.id,
return this.pageLayoutWidgetService.findByPageLayoutTabId({
workspaceId: workspace.id,
pageLayoutTabId,
);
});
}
@Get(':id')
@@ -66,7 +66,10 @@ export class PageLayoutWidgetController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO | null> {
return this.pageLayoutWidgetService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutWidgetService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Post()
@@ -75,7 +78,10 @@ export class PageLayoutWidgetController {
@Body() input: CreatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.create(input, workspace.id);
return this.pageLayoutWidgetService.create({
input,
workspaceId: workspace.id,
});
}
@Patch(':id')
@@ -85,7 +91,11 @@ export class PageLayoutWidgetController {
@Body() input: UpdatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.update(id, workspace.id, input);
return this.pageLayoutWidgetService.update({
id,
workspaceId: workspace.id,
updateData: input,
});
}
@Delete(':id')
@@ -93,7 +103,10 @@ export class PageLayoutWidgetController {
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.delete(id, workspace.id);
): Promise<boolean> {
return this.pageLayoutWidgetService.destroy({
id,
workspaceId: workspace.id,
});
}
}
@@ -45,10 +45,10 @@ export class PageLayoutWidgetResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('pageLayoutTabId', { type: () => String }) pageLayoutTabId: string,
): Promise<PageLayoutWidgetDTO[]> {
return this.pageLayoutWidgetService.findByPageLayoutTabId(
workspace.id,
return this.pageLayoutWidgetService.findByPageLayoutTabId({
workspaceId: workspace.id,
pageLayoutTabId,
);
});
}
@Query(() => PageLayoutWidgetDTO)
@@ -57,7 +57,10 @@ export class PageLayoutWidgetResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutWidgetService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutWidgetDTO)
@@ -66,7 +69,10 @@ export class PageLayoutWidgetResolver {
@Args('input') input: CreatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.create(input, workspace.id);
return this.pageLayoutWidgetService.create({
input,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutWidgetDTO)
@@ -76,16 +82,11 @@ export class PageLayoutWidgetResolver {
@Args('input') input: UpdatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.update(id, workspace.id, input);
}
@Mutation(() => PageLayoutWidgetDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async deletePageLayoutWidget(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.delete(id, workspace.id);
return this.pageLayoutWidgetService.update({
id,
workspaceId: workspace.id,
updateData: input,
});
}
@Mutation(() => Boolean)
@@ -94,16 +95,10 @@ export class PageLayoutWidgetResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
return this.pageLayoutWidgetService.destroy(id, workspace.id);
}
@Mutation(() => PageLayoutWidgetDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async restorePageLayoutWidget(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.restore(id, workspace.id);
return this.pageLayoutWidgetService.destroy({
id,
workspaceId: workspace.id,
});
}
@ResolveField(() => WidgetConfiguration, { nullable: true })
@@ -8,9 +8,7 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util';
import { fromDeletePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-delete-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
import { fromDestroyPageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-destroy-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
import { fromRestorePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-restore-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
import {
fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThrow,
type UpdatePageLayoutWidgetInputWithId,
@@ -87,10 +85,13 @@ export class PageLayoutWidgetService {
}
}
async findByPageLayoutTabId(
workspaceId: string,
pageLayoutTabId: string,
): Promise<PageLayoutWidgetDTO[]> {
async findByPageLayoutTabId({
workspaceId,
pageLayoutTabId,
}: {
workspaceId: string;
pageLayoutTabId: string;
}): Promise<PageLayoutWidgetDTO[]> {
const flatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -109,10 +110,13 @@ export class PageLayoutWidgetService {
.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<PageLayoutWidgetDTO> {
async findByIdOrThrow({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<PageLayoutWidgetDTO> {
const flatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -131,10 +135,13 @@ export class PageLayoutWidgetService {
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(flatWidget);
}
async create(
createPageLayoutWidgetInput: CreatePageLayoutWidgetInput,
workspaceId: string,
): Promise<PageLayoutWidgetDTO> {
async create({
input,
workspaceId,
}: {
input: CreatePageLayoutWidgetInput;
workspaceId: string;
}): Promise<PageLayoutWidgetDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
@@ -142,7 +149,7 @@ export class PageLayoutWidgetService {
const flatPageLayoutWidgetToCreate =
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
createPageLayoutWidgetInput,
createPageLayoutWidgetInput: input,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
});
@@ -174,11 +181,15 @@ export class PageLayoutWidgetService {
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(createdWidget);
}
async update(
id: string,
workspaceId: string,
updateData: UpdatePageLayoutWidgetInput,
): Promise<PageLayoutWidgetDTO> {
async update({
id,
workspaceId,
updateData,
}: {
id: string;
workspaceId: string;
updateData: UpdatePageLayoutWidgetInput;
}): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -252,44 +263,13 @@ export class PageLayoutWidgetService {
return existingWidget;
}
async delete(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
const flatPageLayoutWidgetToDelete =
fromDeletePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
deletePageLayoutWidgetInput: { id },
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
});
await this.validateAndRunWidgetMigration({
workspaceId,
operations: {
flatEntityToCreate: [],
flatEntityToUpdate: [flatPageLayoutWidgetToDelete],
flatEntityToDelete: [],
},
errorMessage:
'Multiple validation errors occurred while deleting page layout widget',
});
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
const deletedWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByWidgetId({
widgetId: id,
workspaceId,
updatedAt: new Date(deletedWidget.updatedAt),
});
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(deletedWidget);
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
async destroy({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<boolean> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -318,41 +298,4 @@ export class PageLayoutWidgetService {
return true;
}
async restore(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
const flatPageLayoutWidgetToRestore =
fromRestorePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
restorePageLayoutWidgetInput: { id },
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
});
await this.validateAndRunWidgetMigration({
workspaceId,
operations: {
flatEntityToCreate: [],
flatEntityToUpdate: [flatPageLayoutWidgetToRestore],
flatEntityToDelete: [],
},
errorMessage:
'Multiple validation errors occurred while restoring page layout widget',
});
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
const restoredWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByWidgetId({
widgetId: id,
workspaceId,
updatedAt: new Date(restoredWidget.updatedAt),
});
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(restoredWidget);
}
}