Reset Tab Page Layout (#19453)

## Context
- Add "Reset to default" for page layout tabs and widgets — backend
mutation resets overrides, reactivates deactivated children, and deletes
custom children
- Fix override write bug where mutating an overridable property (e.g.
widget title) on a standard-app entity incorrectly overwrote the base
column instead of writing to the overrides JSONB —
PageLayoutUpdateService now uses resolveFlatEntityOverridableProperties
for accurate diffing and routes properties through
sanitizeOverridableEntityInput
- Deprecate isOverridden - We need more time to think about this
feature. Currently this adds too much complexity for a very small
benefit



https://github.com/user-attachments/assets/a84546c8-1e15-4d9e-a489-0825cf8b8ed2
This commit is contained in:
Weiko
2026-04-08 18:43:57 +02:00
committed by GitHub
parent 2267c56f15
commit 238018dc7b
94 changed files with 1000 additions and 619 deletions
@@ -55,8 +55,11 @@ export class PageLayoutTabDTO {
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
@Field(() => Boolean, { nullable: false })
isOverridden: boolean;
@Field(() => Boolean, {
nullable: true,
deprecationReason: 'isOverridden is deprecated',
})
isOverridden?: boolean;
@HideField()
overrides?: PageLayoutTabOverrides | null;
@@ -7,7 +7,6 @@ import {
import {
Args,
Context,
Float,
Mutation,
Parent,
Query,
@@ -15,7 +14,6 @@ import {
} from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -32,7 +30,6 @@ import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dt
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@MetadataResolver(() => PageLayoutTabDTO)
@@ -51,31 +48,15 @@ export class PageLayoutTabResolver {
@Parent() tab: PageLayoutTabDTO,
@Context() context: I18nContext,
): Promise<string> {
const resolvedTitle = resolveOverridableEntityProperty(tab, 'title');
const i18n = this.i18nService.getI18nInstance(context.req.locale);
return resolvePageLayoutTabTitle({
title: resolvedTitle,
title: tab.title,
applicationId: tab.applicationId,
i18nInstance: i18n,
});
}
@ResolveField(() => Float)
position(@Parent() tab: PageLayoutTabDTO): number {
return resolveOverridableEntityProperty(tab, 'position');
}
@ResolveField(() => String, { nullable: true })
icon(@Parent() tab: PageLayoutTabDTO): string | null | undefined {
return resolveOverridableEntityProperty(tab, 'icon');
}
@ResolveField(() => Boolean)
isOverridden(@Parent() tab: PageLayoutTabDTO): boolean {
return isDefined(tab.overrides) && Object.keys(tab.overrides).length > 0;
}
@Query(() => [PageLayoutTabDTO])
@UseGuards(NoPermissionGuard)
async getPageLayoutTabs(
@@ -1,5 +1,3 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
@@ -11,13 +9,14 @@ export const fromFlatPageLayoutTabToPageLayoutTabDto = (
updatedAt,
deletedAt,
widgetIds: _widgetIds,
overrides,
...rest
} = flatPageLayoutTab;
return {
...rest,
isOverridden:
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
...(overrides ?? {}),
isOverridden: false,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
deletedAt: deletedAt ? new Date(deletedAt) : null,