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 7c07f2302c..928e8e0092 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
@@ -1,22 +1,4 @@
-import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout';
-import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultCompanyRecordPageLayout';
-import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
-import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultNoteRecordPageLayout';
-import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
-import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultOpportunityRecordPageLayout';
-import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
-import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultPersonRecordPageLayout';
-import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
-import { DEFAULT_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultRecordPageLayout';
-import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
-import { DEFAULT_TASK_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultTaskRecordPageLayout';
-import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
-import { DEFAULT_WORKFLOW_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowPageLayout';
-import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
-import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowRunPageLayout';
-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 { useBasePageLayout } from '@/page-layout/hooks/useBasePageLayout';
import { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -24,40 +6,14 @@ import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/page
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
-import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
-import { useQuery } from '@apollo/client';
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
-const getDefaultLayoutById = (layoutId: string): PageLayout => {
- switch (layoutId) {
- case DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID:
- return DEFAULT_COMPANY_RECORD_PAGE_LAYOUT;
- case DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID:
- return DEFAULT_PERSON_RECORD_PAGE_LAYOUT;
- case DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID:
- return DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT;
- case DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID:
- return DEFAULT_NOTE_RECORD_PAGE_LAYOUT;
- case DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID:
- return DEFAULT_TASK_RECORD_PAGE_LAYOUT;
- case DEFAULT_WORKFLOW_PAGE_LAYOUT_ID:
- return DEFAULT_WORKFLOW_PAGE_LAYOUT;
- case DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID:
- return DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT;
- case DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID:
- return DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT;
- case DEFAULT_RECORD_PAGE_LAYOUT_ID:
- default:
- return DEFAULT_RECORD_PAGE_LAYOUT;
- }
-};
-
type PageLayoutInitializationQueryEffectProps = {
pageLayoutId: string;
onInitialized?: (pageLayout: PageLayout) => void;
@@ -71,29 +27,7 @@ export const PageLayoutInitializationQueryEffect = ({
pageLayoutIsInitializedComponentState,
);
- const isDefaultLayout =
- pageLayoutId === DEFAULT_RECORD_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_WORKFLOW_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID ||
- pageLayoutId === DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID;
-
- const { data } = useQuery(FIND_ONE_PAGE_LAYOUT, {
- variables: {
- id: pageLayoutId,
- },
- skip: isDefaultLayout,
- });
-
- const basePageLayout: PageLayout | undefined = isDefaultLayout
- ? getDefaultLayoutById(pageLayoutId)
- : data?.getPageLayout
- ? transformPageLayout(data.getPageLayout)
- : undefined;
+ const basePageLayout = useBasePageLayout(pageLayoutId);
const pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx
new file mode 100644
index 0000000000..d31a1679d2
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx
@@ -0,0 +1,107 @@
+import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
+import { useBasePageLayout } from '@/page-layout/hooks/useBasePageLayout';
+import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { type PageLayout } from '@/page-layout/types/PageLayout';
+import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
+import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
+import { useEffect } from 'react';
+import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+import { PageLayoutType } from '~/generated/graphql';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+
+type PageLayoutRelationWidgetsSyncEffectProps = {
+ pageLayoutId: string;
+};
+
+export const PageLayoutRelationWidgetsSyncEffect = ({
+ pageLayoutId,
+}: PageLayoutRelationWidgetsSyncEffectProps) => {
+ const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
+
+ const isInitialized = useRecoilComponentValue(
+ pageLayoutIsInitializedComponentState,
+ );
+
+ const basePageLayout = useBasePageLayout(pageLayoutId);
+
+ const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({
+ objectNameSingular: targetRecordIdentifier?.targetObjectNameSingular ?? '',
+ });
+
+ const pageLayoutPersistedComponentCallbackState =
+ useRecoilComponentCallbackState(pageLayoutPersistedComponentState);
+
+ const pageLayoutDraftComponentCallbackState = useRecoilComponentCallbackState(
+ pageLayoutDraftComponentState,
+ );
+
+ const pageLayoutCurrentLayoutsComponentCallbackState =
+ useRecoilComponentCallbackState(pageLayoutCurrentLayoutsComponentState);
+
+ const syncPageLayoutWithRelationWidgets = useRecoilCallback(
+ ({ set, snapshot }) =>
+ (layout: PageLayout) => {
+ const currentPersisted = getSnapshotValue(
+ snapshot,
+ pageLayoutPersistedComponentCallbackState,
+ );
+
+ if (!isDeeplyEqual(layout, currentPersisted)) {
+ set(pageLayoutPersistedComponentCallbackState, layout);
+ set(pageLayoutDraftComponentCallbackState, {
+ id: layout.id,
+ name: layout.name,
+ type: layout.type,
+ objectMetadataId: layout.objectMetadataId,
+ tabs: layout.tabs,
+ });
+
+ const tabLayouts = convertPageLayoutToTabLayouts(layout);
+ set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
+ }
+ },
+ [
+ pageLayoutCurrentLayoutsComponentCallbackState,
+ pageLayoutDraftComponentCallbackState,
+ pageLayoutPersistedComponentCallbackState,
+ ],
+ );
+
+ useEffect(() => {
+ if (!isInitialized) {
+ return;
+ }
+
+ if (!isDefined(basePageLayout)) {
+ return;
+ }
+
+ const isRecordPage = layoutType === PageLayoutType.RECORD_PAGE;
+ if (!isRecordPage) {
+ return;
+ }
+
+ const layoutWithRelationWidgets = injectRelationWidgetsIntoLayout(
+ basePageLayout,
+ boxedRelationFieldMetadataItems,
+ );
+
+ syncPageLayoutWithRelationWidgets(layoutWithRelationWidgets);
+ }, [
+ basePageLayout,
+ boxedRelationFieldMetadataItems,
+ isInitialized,
+ layoutType,
+ syncPageLayoutWithRelationWidgets,
+ ]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
index dd4daccffc..ed01f85795 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
@@ -1,4 +1,5 @@
import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect';
+import { PageLayoutRelationWidgetsSyncEffect } from '@/page-layout/components/PageLayoutRelationWidgetsSyncEffect';
import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
@@ -42,6 +43,7 @@ export const PageLayoutRenderer = ({
pageLayoutId={pageLayoutId}
onInitialized={onInitialized}
/>
+
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useBasePageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useBasePageLayout.ts
new file mode 100644
index 0000000000..04d95e79c5
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useBasePageLayout.ts
@@ -0,0 +1,78 @@
+import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout';
+import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultCompanyRecordPageLayout';
+import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
+import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultNoteRecordPageLayout';
+import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
+import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultOpportunityRecordPageLayout';
+import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
+import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultPersonRecordPageLayout';
+import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
+import { DEFAULT_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultRecordPageLayout';
+import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
+import { DEFAULT_TASK_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultTaskRecordPageLayout';
+import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
+import { DEFAULT_WORKFLOW_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowPageLayout';
+import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
+import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowRunPageLayout';
+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 { type PageLayout } from '@/page-layout/types/PageLayout';
+import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
+import { useQuery } from '@apollo/client';
+
+const getDefaultLayoutById = (layoutId: string): PageLayout => {
+ switch (layoutId) {
+ case DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID:
+ return DEFAULT_COMPANY_RECORD_PAGE_LAYOUT;
+ case DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID:
+ return DEFAULT_PERSON_RECORD_PAGE_LAYOUT;
+ case DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID:
+ return DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT;
+ case DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID:
+ return DEFAULT_NOTE_RECORD_PAGE_LAYOUT;
+ case DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID:
+ return DEFAULT_TASK_RECORD_PAGE_LAYOUT;
+ case DEFAULT_WORKFLOW_PAGE_LAYOUT_ID:
+ return DEFAULT_WORKFLOW_PAGE_LAYOUT;
+ case DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID:
+ return DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT;
+ case DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID:
+ return DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT;
+ case DEFAULT_RECORD_PAGE_LAYOUT_ID:
+ default:
+ return DEFAULT_RECORD_PAGE_LAYOUT;
+ }
+};
+
+const isDefaultLayoutId = (layoutId: string): boolean =>
+ layoutId === DEFAULT_RECORD_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_WORKFLOW_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID ||
+ layoutId === DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID;
+
+export const useBasePageLayout = (
+ pageLayoutId: string,
+): PageLayout | undefined => {
+ const isDefaultLayout = isDefaultLayoutId(pageLayoutId);
+
+ const { data } = useQuery(FIND_ONE_PAGE_LAYOUT, {
+ variables: {
+ id: pageLayoutId,
+ },
+ skip: isDefaultLayout,
+ });
+
+ const basePageLayout: PageLayout | undefined = isDefaultLayout
+ ? getDefaultLayoutById(pageLayoutId)
+ : data?.getPageLayout
+ ? transformPageLayout(data.getPageLayout)
+ : undefined;
+
+ return basePageLayout;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts
index 2d1507da5e..ed513a538d 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts
@@ -1,122 +1,9 @@
-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 { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
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}-${field.label}`,
- 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) {
- const firstFieldsWidgetIndex = tab.widgets.findIndex(
- (widget) => widget.type === WidgetType.FIELDS,
- );
-
- if (firstFieldsWidgetIndex === -1) {
- return {
- ...tab,
- widgets: [...tab.widgets, ...relationWidgets],
- };
- }
-
- // TODO: This note widget repositioning logic is temporary and will be deleted soon.
- // We need this to ensure the note editor is displayed before record relations,
- // matching the behavior of the old show page.
- const noteWidgetIndex = tab.widgets.findIndex(
- (widget) => widget.type === WidgetType.NOTES,
- );
-
- const widgetsBeforeRelation = tab.widgets.slice(
- 0,
- firstFieldsWidgetIndex + 1,
- );
- const widgetsAfterRelation =
- noteWidgetIndex === -1
- ? tab.widgets.slice(firstFieldsWidgetIndex + 1)
- : [
- ...tab.widgets.slice(
- firstFieldsWidgetIndex + 1,
- noteWidgetIndex,
- ),
- ...tab.widgets.slice(noteWidgetIndex + 1),
- ];
-
- const noteWidget =
- noteWidgetIndex !== -1 ? [tab.widgets[noteWidgetIndex]] : [];
-
- return {
- ...tab,
- widgets: [
- ...widgetsBeforeRelation,
- ...relationWidgets,
- ...noteWidget,
- ...widgetsAfterRelation,
- ],
- };
- }
- return tab;
- }),
- };
-};
+import { PageLayoutType } from '~/generated/graphql';
export const usePageLayoutWithRelationWidgets = (
basePageLayout: PageLayout | undefined,
diff --git a/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts b/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts
new file mode 100644
index 0000000000..c68537a874
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/injectRelationWidgetsIntoLayout.ts
@@ -0,0 +1,115 @@
+import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { type PageLayout } from '@/page-layout/types/PageLayout';
+import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
+import { isDefined } from 'twenty-shared/utils';
+import { WidgetConfigurationType, WidgetType } from '~/generated/graphql';
+
+const getRelationFieldWidgetToInsert = (
+ field: FieldMetadataItem,
+ tabId: string,
+): PageLayoutWidget => ({
+ __typename: 'PageLayoutWidget' as const,
+ id: `dynamic-relation-widget-${field.id}-${field.label}`,
+ 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: '2024-01-01T00:00:00.000Z',
+ updatedAt: '2024-01-01T00:00:00.000Z',
+ deletedAt: null,
+});
+
+const getRelationFieldWidgetsToInsert = (
+ relationFields: FieldMetadataItem[],
+ tabId: string,
+): PageLayoutWidget[] => {
+ return relationFields.map((field) =>
+ getRelationFieldWidgetToInsert(field, tabId),
+ );
+};
+
+export 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 = getRelationFieldWidgetsToInsert(
+ boxedRelationFieldMetadataItems,
+ firstTab.id,
+ );
+
+ return {
+ ...layout,
+ tabs: layout.tabs.map((tab) => {
+ if (tab.id === firstTab.id) {
+ const firstFieldsWidgetIndex = tab.widgets.findIndex(
+ (widget) => widget.type === WidgetType.FIELDS,
+ );
+
+ if (firstFieldsWidgetIndex === -1) {
+ return {
+ ...tab,
+ widgets: [...tab.widgets, ...relationWidgets],
+ };
+ }
+
+ // TODO: This note widget repositioning logic is temporary and will be deleted soon.
+ // We need this to ensure the note editor is displayed before record relations,
+ // matching the behavior of the old show page.
+ const noteWidgetIndex = tab.widgets.findIndex(
+ (widget) => widget.type === WidgetType.NOTES,
+ );
+
+ const widgetsBeforeRelation = tab.widgets.slice(
+ 0,
+ firstFieldsWidgetIndex + 1,
+ );
+ const widgetsAfterRelation =
+ noteWidgetIndex === -1
+ ? tab.widgets.slice(firstFieldsWidgetIndex + 1)
+ : [
+ ...tab.widgets.slice(
+ firstFieldsWidgetIndex + 1,
+ noteWidgetIndex,
+ ),
+ ...tab.widgets.slice(noteWidgetIndex + 1),
+ ];
+
+ const noteWidget =
+ noteWidgetIndex !== -1 ? [tab.widgets[noteWidgetIndex]] : [];
+
+ return {
+ ...tab,
+ widgets: [
+ ...widgetsBeforeRelation,
+ ...relationWidgets,
+ ...noteWidget,
+ ...widgetsAfterRelation,
+ ],
+ };
+ }
+ return tab;
+ }),
+ };
+};