Migrate field widgets to backend (#18808)

- Renamed FieldConfiguration's layout field to fieldDisplayMode as it
caused issues with the layout field of BarChartConfiguration
- Create relation Field widgets for standard objects

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Baptiste Devessier
2026-03-23 01:26:00 +01:00
committed by GitHub
parent d90d2e3151
commit be89ef30cd
38 changed files with 1388 additions and 790 deletions
@@ -60,19 +60,19 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
expect(result.tabs[0].widgets[0].id).toBe('w1');
});
it('should filter out dynamic relation widgets', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
it('should handle multiple widget types', () => {
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
const timelineWidget = makeWidget({
id: 'w2',
type: WidgetType.TIMELINE,
});
const draft = makeDraft([makeTab('tab-1', [regularWidget, dynamicWidget])]);
const draft = makeDraft([makeTab('tab-1', [fieldsWidget, timelineWidget])]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets).toHaveLength(1);
expect(result.tabs[0].widgets[0].id).toBe('w1');
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
});
it('should map gridPosition correctly', () => {
@@ -121,24 +121,25 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
expect(result.tabs[1].id).toBe('tab-2');
});
it('should keep all non-dynamic widgets when multiple widget types exist', () => {
it('should keep all widgets when multiple widget types exist', () => {
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
const timelineWidget = makeWidget({
id: 'w2',
type: WidgetType.TIMELINE,
});
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
const fieldWidget = makeWidget({
id: 'w3',
type: WidgetType.FIELD,
});
const draft = makeDraft([
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
makeTab('tab-1', [fieldsWidget, fieldWidget, timelineWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
expect(result.tabs[0].widgets).toHaveLength(3);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w3', 'w2']);
});
it('should produce VERTICAL_LIST position with index from widget.position when tab is VERTICAL_LIST', () => {
@@ -326,4 +327,81 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
columnSpan: 6,
});
});
describe('shouldFilterDynamicRelationWidgets', () => {
it('should filter out dynamic relation widgets when shouldFilterDynamicRelationWidgets is true', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
});
const draft = makeDraft([
makeTab('tab-1', [regularWidget, dynamicWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets: true,
});
expect(result.tabs[0].widgets).toHaveLength(1);
expect(result.tabs[0].widgets[0].id).toBe('w1');
});
it('should keep all non-dynamic widgets when shouldFilterDynamicRelationWidgets is true and multiple widget types exist', () => {
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
const timelineWidget = makeWidget({
id: 'w2',
type: WidgetType.TIMELINE,
});
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
});
const draft = makeDraft([
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets: true,
});
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
});
it('should not filter dynamic relation widgets when shouldFilterDynamicRelationWidgets is false', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
});
const draft = makeDraft([
makeTab('tab-1', [regularWidget, dynamicWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets: false,
});
expect(result.tabs[0].widgets).toHaveLength(2);
});
it('should not filter dynamic relation widgets by default', () => {
const regularWidget = makeWidget({ id: 'w1' });
const dynamicWidget = makeWidget({
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
type: WidgetType.VIEW,
});
const draft = makeDraft([
makeTab('tab-1', [regularWidget, dynamicWidget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets).toHaveLength(2);
});
});
});
@@ -4,6 +4,7 @@ import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
import {
FieldDisplayMode,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
@@ -195,7 +196,7 @@ describe('injectRelationWidgetsIntoLayout', () => {
expect(injectedWidget.configuration).toEqual(
expect.objectContaining({
fieldMetadataId: 'f1',
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
}),
);
});
@@ -40,19 +40,25 @@ const buildWidgetPosition = (
export const convertPageLayoutDraftToUpdateInput = (
pageLayoutDraft: DraftPageLayout,
options?: { shouldFilterDynamicRelationWidgets?: boolean },
): UpdatePageLayoutWithTabsInput => {
const shouldFilter = options?.shouldFilterDynamicRelationWidgets ?? false;
return {
name: pageLayoutDraft.name,
type: pageLayoutDraft.type,
objectMetadataId: pageLayoutDraft.objectMetadataId ?? null,
tabs: pageLayoutDraft.tabs.map((tab) => ({
id: tab.id,
title: tab.title,
position: tab.position,
layoutMode: tab.layoutMode,
widgets: tab.widgets
.filter((widget) => !isDynamicRelationWidget(widget))
.map((widget, widgetIndex) => ({
tabs: pageLayoutDraft.tabs.map((tab) => {
const widgets = shouldFilter
? tab.widgets.filter((widget) => !isDynamicRelationWidget(widget))
: tab.widgets;
return {
id: tab.id,
title: tab.title,
position: tab.position,
layoutMode: tab.layoutMode,
widgets: widgets.map((widget, widgetIndex) => ({
id: widget.id,
pageLayoutTabId: widget.pageLayoutTabId,
title: widget.title,
@@ -71,6 +77,7 @@ export const convertPageLayoutDraftToUpdateInput = (
),
configuration: widget.configuration ?? null,
})),
})),
};
}),
};
};
@@ -1,5 +1,6 @@
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import {
FieldDisplayMode,
PageLayoutTabLayoutMode,
WidgetConfigurationType,
WidgetType,
@@ -28,7 +29,7 @@ export const createDefaultFieldWidget = ({
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId,
layout: 'CARD',
fieldDisplayMode: FieldDisplayMode.CARD,
},
gridPosition: {
__typename: 'GridPosition',
@@ -4,6 +4,7 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
import { isDefined } from 'twenty-shared/utils';
import {
FieldDisplayMode,
PageLayoutTabLayoutMode,
WidgetConfigurationType,
WidgetType,
@@ -38,7 +39,7 @@ const getRelationFieldWidgetToInsert = (
__typename: 'FieldConfiguration' as const,
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
layout: 'CARD' as const,
fieldDisplayMode: FieldDisplayMode.CARD,
},
isOverridden: false,
createdAt: '2024-01-01T00:00:00.000Z',