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
@@ -74,8 +74,11 @@ export class PageLayoutWidgetDTO {
@Field(() => Date, { nullable: true })
deletedAt?: Date;
@Field(() => Boolean, { nullable: false })
isOverridden: boolean;
@Field(() => Boolean, {
nullable: true,
deprecationReason: 'isOverridden is deprecated',
})
isOverridden?: boolean;
@HideField()
overrides?: PageLayoutWidgetOverrides | null;
@@ -6,9 +6,7 @@ import {
} from '@nestjs/common';
import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -23,7 +21,6 @@ import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-wid
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
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(() => PageLayoutWidgetDTO)
@@ -98,30 +95,8 @@ export class PageLayoutWidgetResolver {
});
}
@ResolveField(() => String)
title(@Parent() widget: PageLayoutWidgetDTO): string {
return resolveOverridableEntityProperty(widget, 'title');
}
@ResolveField(() => GraphQLJSON, { nullable: true })
position(@Parent() widget: PageLayoutWidgetDTO) {
return resolveOverridableEntityProperty(widget, 'position');
}
@ResolveField(() => GraphQLJSON, { nullable: true })
conditionalDisplay(@Parent() widget: PageLayoutWidgetDTO) {
return resolveOverridableEntityProperty(widget, 'conditionalDisplay');
}
@ResolveField(() => WidgetConfiguration, { nullable: true })
configuration(@Parent() widget: PageLayoutWidgetDTO) {
return widget.configuration;
}
@ResolveField(() => Boolean)
isOverridden(@Parent() widget: PageLayoutWidgetDTO): boolean {
return (
isDefined(widget.overrides) && Object.keys(widget.overrides).length > 0
);
}
}
@@ -1,18 +1,22 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
flatPageLayoutWidget: FlatPageLayoutWidget,
): PageLayoutWidgetDTO => {
const { createdAt, updatedAt, deletedAt, objectMetadataId, ...rest } =
flatPageLayoutWidget;
const {
createdAt,
updatedAt,
deletedAt,
objectMetadataId,
overrides,
...rest
} = flatPageLayoutWidget;
return {
...rest,
isOverridden:
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
...(overrides ?? {}),
isOverridden: false,
objectMetadataId: objectMetadataId ?? undefined,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),