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; + }), + }; +};