[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
@@ -7,9 +7,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util';
import { fromDeletePageLayoutTabInputToFlatPageLayoutTabOrThrow } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-delete-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util';
import { fromDestroyPageLayoutTabInputToFlatPageLayoutTabOrThrow } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-destroy-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util';
import { fromRestorePageLayoutTabInputToFlatPageLayoutTabOrThrow } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-restore-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util';
import {
fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow,
type UpdatePageLayoutTabInputWithId,
@@ -40,10 +38,13 @@ export class PageLayoutTabService {
private readonly dashboardSyncService: DashboardSyncService,
) {}
async findByPageLayoutId(
workspaceId: string,
pageLayoutId: string,
): Promise<PageLayoutTabDTO[]> {
async findByPageLayoutId({
workspaceId,
pageLayoutId,
}: {
workspaceId: string;
pageLayoutId: string;
}): Promise<PageLayoutTabDTO[]> {
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
@@ -63,10 +64,13 @@ export class PageLayoutTabService {
);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<PageLayoutTabDTO> {
async findByIdOrThrow({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<PageLayoutTabDTO> {
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
@@ -102,10 +106,13 @@ export class PageLayoutTabService {
);
}
async create(
createPageLayoutTabInput: CreatePageLayoutTabInput,
workspaceId: string,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
async create({
createPageLayoutTabInput,
workspaceId,
}: {
createPageLayoutTabInput: CreatePageLayoutTabInput;
workspaceId: string;
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
if (!isDefined(createPageLayoutTabInput.title)) {
throw new PageLayoutTabException(
generatePageLayoutTabExceptionMessage(
@@ -180,11 +187,15 @@ export class PageLayoutTabService {
return fromFlatPageLayoutTabToPageLayoutTabDto(createdTab);
}
async update(
id: string,
workspaceId: string,
updateData: UpdatePageLayoutTabInput,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
async update({
id,
workspaceId,
updateData,
}: {
id: string;
workspaceId: string;
updateData: UpdatePageLayoutTabInput;
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -248,69 +259,13 @@ export class PageLayoutTabService {
return fromFlatPageLayoutTabToPageLayoutTabDto(updatedTab);
}
async delete(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const flatPageLayoutTabToDelete =
fromDeletePageLayoutTabInputToFlatPageLayoutTabOrThrow({
deletePageLayoutTabInput: { id },
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutTab: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatPageLayoutTabToDelete],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting page layout tab',
);
}
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const deletedTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
tabId: id,
workspaceId,
updatedAt: new Date(deletedTab.updatedAt),
});
return fromFlatPageLayoutTabToPageLayoutTabDto(deletedTab);
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
async destroy({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<boolean> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -355,66 +310,4 @@ export class PageLayoutTabService {
return true;
}
async restore(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const flatPageLayoutTabToRestore =
fromRestorePageLayoutTabInputToFlatPageLayoutTabOrThrow({
restorePageLayoutTabInput: { id },
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutTab: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatPageLayoutTabToRestore],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while restoring page layout tab',
);
}
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const restoredTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
tabId: id,
workspaceId,
updatedAt: new Date(restoredTab.updatedAt),
});
return fromFlatPageLayoutTabToPageLayoutTabDto(restoredTab);
}
}