From dec2b44808e29abc9e0c5955d1f2863bf05efc46 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Fri, 9 Jan 2026 17:24:35 +0100 Subject: [PATCH] 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 --- .../PageLayoutInitializationQueryEffect.tsx | 5 +- .../DefaultCompanyRecordPageLayout.ts | 271 +----------------- .../hooks/usePageLayoutWithRelationWidgets.ts | 103 +++++++ 3 files changed, 111 insertions(+), 268 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx index d9bf2738e3..7fcabedac1 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts index e81eb340e2..163508face 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultCompanyRecordPageLayout.ts @@ -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(), diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts new file mode 100644 index 0000000000..158a3b5854 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts @@ -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, + ); +};