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:
@@ -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,
|
||||
|
||||
-2
@@ -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,
|
||||
|
||||
-5
@@ -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,
|
||||
|
||||
+1
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-1
@@ -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',
|
||||
});
|
||||
|
||||
-1
@@ -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',
|
||||
});
|
||||
|
||||
-2
@@ -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,
|
||||
|
||||
-2
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-5
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user