Generate Field widgets for relations (#17047)

To release version 1, we must ensure feature parity with the current
production version.

In the future, all widgets will be stored in the backend. For now, let's
compute Field widgets for relations at runtime.


https://github.com/user-attachments/assets/acffe2f9-0f9b-4eff-bb89-c41f7caf5441
This commit is contained in:
Baptiste Devessier
2026-01-09 17:24:35 +01:00
committed by GitHub
parent b1c821b0e3
commit dec2b44808
3 changed files with 111 additions and 268 deletions
@@ -17,6 +17,7 @@ import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT } from '@/page-layout/constants/Defaul
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowVersionPageLayout';
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
import { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
@@ -84,12 +85,14 @@ export const PageLayoutInitializationQueryEffect = ({
skip: isDefaultLayout,
});
const pageLayout: PageLayout | undefined = isDefaultLayout
const basePageLayout: PageLayout | undefined = isDefaultLayout
? getDefaultLayoutById(pageLayoutId)
: data?.getPageLayout
? transformPageLayout(data.getPageLayout)
: undefined;
const pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
const pageLayoutPersistedComponentCallbackState =
useRecoilComponentCallbackState(pageLayoutPersistedComponentState);
@@ -35,280 +35,17 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
title: 'Fields',
type: WidgetType.FIELDS,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 3,
column: 0,
rowSpan: 12,
columnSpan: 12,
},
configuration: {
__typename: 'FieldsConfiguration',
configurationType: WidgetConfigurationType.FIELDS,
sections: [],
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-fields-2',
pageLayoutTabId: 'company-tab-fields',
title: 'Fields',
type: WidgetType.FIELDS,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 3,
column: 0,
rowSpan: 12,
columnSpan: 12,
},
configuration: {
__typename: 'FieldsConfiguration',
configurationType: WidgetConfigurationType.FIELDS,
sections: [],
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-account-owner',
pageLayoutTabId: 'company-tab-fields',
title: 'Account Owner',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 1,
rowSpan: 12,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'accountOwner',
layout: 'CARD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-opportunities',
pageLayoutTabId: 'company-tab-fields',
title: 'Opportunities',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 1,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'opportunities',
layout: 'CARD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-people',
pageLayoutTabId: 'company-tab-fields',
title: 'People',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'people',
layout: 'CARD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-people-2',
pageLayoutTabId: 'company-tab-fields',
title: 'People',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'people',
layout: 'CARD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-people-3',
pageLayoutTabId: 'company-tab-fields',
title: 'People (FIELD)',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'people',
layout: 'FIELD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-name',
pageLayoutTabId: 'company-tab-fields',
title: 'Name',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'name',
layout: 'FIELD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-name-2',
pageLayoutTabId: 'company-tab-fields',
title: 'Name',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'name',
layout: 'FIELD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-employees',
pageLayoutTabId: 'company-tab-fields',
title: 'Employees',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'employees',
layout: 'FIELD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-linkedin',
pageLayoutTabId: 'company-tab-fields',
title: 'LinkedIn',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'linkedinLink',
layout: 'FIELD',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
__typename: 'PageLayoutWidget',
id: 'company-widget-visa',
pageLayoutTabId: 'company-tab-fields',
title: 'Visa Sponsorship',
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition',
row: 2,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration',
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: 'visaSponsorship',
layout: 'FIELD',
__typename: 'FieldsConfiguration',
configurationType: WidgetConfigurationType.FIELDS,
sections: [],
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -0,0 +1,103 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { isDefined } from 'twenty-shared/utils';
import {
PageLayoutType,
WidgetConfigurationType,
WidgetType,
} from '~/generated/graphql';
const createRelationFieldWidget = (
field: FieldMetadataItem,
tabId: string,
): PageLayoutWidget => ({
__typename: 'PageLayoutWidget' as const,
id: `dynamic-relation-widget-${field.id}`,
pageLayoutTabId: tabId,
title: field.label,
type: WidgetType.FIELD,
objectMetadataId: null,
gridPosition: {
__typename: 'GridPosition' as const,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
configuration: {
__typename: 'FieldConfiguration' as const,
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
layout: 'CARD' as const,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
});
const createRelationFieldWidgets = (
relationFields: FieldMetadataItem[],
tabId: string,
): PageLayoutWidget[] => {
return relationFields.map((field) => createRelationFieldWidget(field, tabId));
};
const injectRelationWidgetsIntoLayout = (
layout: PageLayout,
boxedRelationFieldMetadataItems: FieldMetadataItem[],
): PageLayout => {
if (boxedRelationFieldMetadataItems.length === 0) {
return layout;
}
const firstTab = layout.tabs[0];
if (!isDefined(firstTab)) {
return layout;
}
const relationWidgets = createRelationFieldWidgets(
boxedRelationFieldMetadataItems,
firstTab.id,
);
return {
...layout,
tabs: layout.tabs.map((tab) => {
if (tab.id === firstTab.id) {
return {
...tab,
widgets: [...tab.widgets, ...relationWidgets],
};
}
return tab;
}),
};
};
export const usePageLayoutWithRelationWidgets = (
basePageLayout: PageLayout | undefined,
): PageLayout | undefined => {
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({
objectNameSingular: targetRecordIdentifier?.targetObjectNameSingular ?? '',
});
if (!isDefined(basePageLayout)) {
return undefined;
}
const isRecordPage = layoutType === PageLayoutType.RECORD_PAGE;
if (!isRecordPage) {
return basePageLayout;
}
return injectRelationWidgetsIntoLayout(
basePageLayout,
boxedRelationFieldMetadataItems,
);
};