3ef789dab4
Until now, it wasn’t possible to navigate between page layouts. Dashboards don’t contain links to other dashboards. With record page layouts, however, a page layout can now contain a link to another page layout. If the user opens a record in the side panel and then clicks a link to another record, the current page layout is replaced with the page layout for the clicked record. In this scenario, the `PageLayoutRenderer` component is not remounted. As a result, the `isInitialized` state was not reset to `false`, and the corresponding initialization `useEffect` was not triggered again. All page layout states are component states bound to `PageLayoutComponentInstanceContext`. For example, after navigating to another record, `pageLayoutPersistedComponentState` was actually `undefined` because the context's `instanceId` had changed. Replacing the `isInitialized` state with a component state fixes the bug and is consistent with the existing pattern of using component states to store everything related to page layouts. ## Before https://github.com/user-attachments/assets/24217145-51e5-49ef-8180-de62a6acfe10 ## After https://github.com/user-attachments/assets/e0ffe107-8fae-4f3a-9016-72c85bc735f4
155 lines
7.1 KiB
TypeScript
155 lines
7.1 KiB
TypeScript
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 { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
|
|
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 { 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;
|
|
};
|
|
|
|
export const PageLayoutInitializationQueryEffect = ({
|
|
pageLayoutId,
|
|
onInitialized,
|
|
}: PageLayoutInitializationQueryEffectProps) => {
|
|
const [isInitialized, setIsInitialized] = useRecoilComponentState(
|
|
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 pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
|
|
|
|
const pageLayoutPersistedComponentCallbackState =
|
|
useRecoilComponentCallbackState(pageLayoutPersistedComponentState);
|
|
|
|
const pageLayoutDraftComponentCallbackState = useRecoilComponentCallbackState(
|
|
pageLayoutDraftComponentState,
|
|
);
|
|
|
|
const pageLayoutCurrentLayoutsComponentCallbackState =
|
|
useRecoilComponentCallbackState(pageLayoutCurrentLayoutsComponentState);
|
|
|
|
const initializePageLayout = 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 && isDefined(pageLayout)) {
|
|
initializePageLayout(pageLayout);
|
|
onInitialized?.(pageLayout);
|
|
setIsInitialized(true);
|
|
}
|
|
}, [
|
|
initializePageLayout,
|
|
isInitialized,
|
|
pageLayout,
|
|
onInitialized,
|
|
setIsInitialized,
|
|
]);
|
|
|
|
return null;
|
|
};
|