Never display broken relations in record page layouts (#17738)

We generate Field widgets for relations on-the-fly, when a record page
layout is first requested by the user. When the user changed their data
model and then returned to a record page layout, the Field widgets
weren't updated. **This PR ensures Field widgets are recomputed when
relations change.**

Future subjects:

- Now that we will fetch the configuration from the backend and start
storing updates, we will have to think about how we deal with these
generated relation Field widgets.

## Before


https://github.com/user-attachments/assets/99d53b19-b231-435f-b14f-4473ba269ad2

## After


https://github.com/user-attachments/assets/357d956d-8b3b-448c-a983-82569a7dd0a0

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Baptiste Devessier
2026-02-05 15:07:04 +01:00
committed by GitHub
parent df516a904b
commit 5b701f5ba4
6 changed files with 306 additions and 183 deletions
@@ -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);
@@ -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;
};
@@ -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}
/>
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
<PageLayoutRendererContent />
</TabListComponentInstanceContext.Provider>
</PageLayoutComponentInstanceContext.Provider>
@@ -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;
};
@@ -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,
@@ -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;
}),
};
};