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
@@ -22,7 +22,6 @@ describe('addWidgetToTab', () => {
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -36,7 +35,6 @@ describe('addWidgetToTab', () => {
position: 0,
pageLayoutId: 'layout-1',
widgets: [],
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -48,7 +46,6 @@ describe('addWidgetToTab', () => {
position: 1,
pageLayoutId: 'layout-1',
widgets: [],
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -28,7 +28,6 @@ describe('convertLayoutsToWidgets', () => {
aggregateFieldMetadataId: 'id',
displayDataLabel: false,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -53,7 +52,6 @@ describe('convertLayoutsToWidgets', () => {
orderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: false,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -38,7 +38,6 @@ describe('convertPageLayoutToTabLayouts', () => {
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: 'object-metadata-1',
isOverridden: false,
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z',
deletedAt: null,
@@ -59,13 +58,11 @@ describe('convertPageLayoutToTabLayouts', () => {
},
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: 'object-metadata-1',
isOverridden: false,
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z',
deletedAt: null,
},
],
isOverridden: false,
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z',
deletedAt: null,
@@ -118,13 +115,11 @@ describe('convertPageLayoutToTabLayouts', () => {
},
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
objectMetadataId: null,
isOverridden: false,
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z',
deletedAt: null,
},
],
isOverridden: false,
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-01T00:00:00.000Z',
deletedAt: null,
@@ -50,8 +50,7 @@ describe('createDefaultRecordTableWidget', () => {
});
});
it('should set objectMetadataId to null and isOverridden to false', () => {
it('should set objectMetadataId to null', () => {
expect(widget.objectMetadataId).toBeNull();
expect(widget.isOverridden).toBe(false);
});
});
@@ -28,7 +28,6 @@ const createMockWidget = (
objectMetadataId: null,
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
pageLayoutTabId: 'tab-1',
isOverridden: false,
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
...overrides,
@@ -28,7 +28,6 @@ describe('filterVisibleWidgets', () => {
configurationType: WidgetConfigurationType.FIELDS,
viewId: null,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
deletedAt: null,
@@ -9,7 +9,6 @@ describe('getPageLayoutTabListInitialActiveTabId', () => {
title: `Tab ${id}`,
position: 0,
widgets: [],
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
});
@@ -14,7 +14,6 @@ describe('getTabsByDisplayMode', () => {
title: `Tab ${id}`,
position: 0,
widgets: [],
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
});
@@ -28,7 +28,6 @@ describe('getTabsWithVisibleWidgets', () => {
configurationType: WidgetConfigurationType.FIELDS,
viewId: null,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
deletedAt: null,
@@ -46,7 +45,6 @@ describe('getTabsWithVisibleWidgets', () => {
title: `Tab ${id}`,
position: 0,
widgets,
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
deletedAt: null,
@@ -40,7 +40,6 @@ const createMockWidget = (
configurationType: WidgetConfigurationType.FIELDS,
viewId: null,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
deletedAt: null,
@@ -58,7 +57,6 @@ const createMockTab = (
title: `Tab ${id}`,
position: 0,
widgets,
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
deletedAt: null,
@@ -27,7 +27,6 @@ describe('prepareGridLayoutItemsWithPlaceholders', () => {
aggregateFieldMetadataId: 'field-id',
displayDataLabel: false,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -30,7 +30,6 @@ describe('removeWidgetFromTab', () => {
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -51,13 +50,11 @@ describe('removeWidgetFromTab', () => {
},
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
],
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -81,13 +78,11 @@ describe('removeWidgetFromTab', () => {
},
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
objectMetadataId: null,
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
],
isOverridden: false,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
@@ -13,7 +13,6 @@ const makeWidget = (id: string, index: number): PageLayoutWidget =>
pageLayoutTabId: 'tab-1',
title: id,
type: WidgetType.FIELDS,
isOverridden: false,
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
configuration: {
__typename: 'FieldsConfiguration',
@@ -0,0 +1,20 @@
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
export const collectViewIdsFromWidgets = (
widgets: PageLayoutWidget[],
): Set<string> => {
const viewIds = new Set<string>();
for (const widget of widgets) {
const viewId = getWidgetConfigurationViewId(widget.configuration);
if (isDefined(viewId)) {
viewIds.add(viewId);
}
}
return viewIds;
};
@@ -46,7 +46,6 @@ export const createDefaultFieldWidget = ({
index: positionIndex,
},
objectMetadataId: objectMetadataId ?? null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -42,7 +42,6 @@ export const createDefaultFieldsWidget = ({
index: positionIndex,
},
objectMetadataId: objectMetadataId ?? null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -34,7 +34,6 @@ export const createDefaultFrontComponentWidget = (
columnSpan: gridPosition.columnSpan,
},
objectMetadataId: null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -84,7 +84,6 @@ export const createDefaultGraphWidget = ({
columnSpan: gridPosition.columnSpan,
},
objectMetadataId: resolvedObjectMetadataId,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -34,7 +34,6 @@ export const createDefaultIframeWidget = (
columnSpan: gridPosition.columnSpan,
},
objectMetadataId: objectMetadataId ?? null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -31,7 +31,6 @@ export const createDefaultRecordTableWidget = (
columnSpan: gridPosition.columnSpan,
},
objectMetadataId: null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -34,7 +34,6 @@ export const createDefaultStandaloneRichTextWidget = (
columnSpan: gridPosition.columnSpan,
},
objectMetadataId: objectMetadataId ?? null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
@@ -0,0 +1,31 @@
import type { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import {
type MetadataEntityKey,
metadataStoreState,
} from '@/metadata-store/states/metadataStoreState';
const VIEW_RELATED_METADATA_KEYS: MetadataEntityKey[] = [
'viewFields',
'viewFieldGroups',
];
export const evictViewMetadataForViewIds = (
store: ReturnType<typeof useStore>,
viewIds: Set<string>,
) => {
if (viewIds.size === 0) {
return;
}
for (const key of VIEW_RELATED_METADATA_KEYS) {
store.set(metadataStoreState.atomFamily(key), (prev) => ({
...prev,
current: (prev.current as { viewId?: string }[]).filter(
(item) => !isDefined(item.viewId) || !viewIds.has(item.viewId),
),
currentCollectionHash: undefined,
}));
}
};
@@ -41,7 +41,6 @@ const getRelationFieldWidgetToInsert = (
fieldMetadataId: field.id,
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
deletedAt: null,
@@ -0,0 +1,12 @@
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { type PageLayout } from '@/page-layout/types/PageLayout';
export const toDraftPageLayout = (pageLayout: PageLayout): DraftPageLayout => ({
id: pageLayout.id,
name: pageLayout.name,
type: pageLayout.type,
objectMetadataId: pageLayout.objectMetadataId,
tabs: pageLayout.tabs,
defaultTabToFocusOnMobileAndSidePanelId:
pageLayout.defaultTabToFocusOnMobileAndSidePanelId,
});