Skip to content

Commit 9a9daf7

Browse files
authored
Keep fallback record page layouts read-only in edition mode (#20023)
When a record has no associated `pageLayoutId`, the FE falls back to a hardcoded default page layout (`DEFAULT_*_RECORD_PAGE_LAYOUT`). These mocks use non-UUID ids for the layout, tabs, and widgets (e.g. `default-person-page-layout`). Today nothing distinguishes these fallbacks from real layouts at edit time. Clicking "Edit layout" flips the global customization mode, `PageLayoutRecordPageCustomizationSessionRegistrationEffect` registers the mock id, and on Save `useSaveLayoutCustomization` fires `UpdatePageLayoutWithTabsAndWidgets` with those mock ids — the BE rejects them and we end up in a partial-save state (nav / command menu commit while the page layout fails). This PR keeps fallback record page layouts in read mode even when global layout-edition mode is on, and defends the save loop so mock ids can never be sent to the BE. ## Non editable "base" record page layout (=mock) <img width="1508" height="616" alt="Screenshot 2026-04-24 at 10 35 02" src="https://github.com/user-attachments/assets/34b093d1-f4bb-4076-ab8c-ce97ecb945a4" /> ## Editable record page layout <img width="1512" height="597" alt="Screenshot 2026-04-24 at 10 35 20" src="https://github.com/user-attachments/assets/5ff6f3ff-79a5-4e6f-aa1c-01c7e09273b3" />
1 parent e8f58fd commit 9a9daf7

6 files changed

Lines changed: 46 additions & 15 deletions

File tree

packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
1616
import { type PageLayout } from '@/page-layout/types/PageLayout';
1717
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
1818
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
19+
import { isDefaultPageLayoutId } from '@/page-layout/utils/isDefaultPageLayoutId';
1920
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
2021
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
2122
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -76,6 +77,10 @@ export const useSaveLayoutCustomization = () => {
7677
let hasAnyFailure = false;
7778

7879
for (const pageLayoutId of activePageLayoutIds) {
80+
if (isDefaultPageLayoutId(pageLayoutId)) {
81+
continue;
82+
}
83+
7984
const draft = store.get(
8085
pageLayoutDraftComponentState.atomFamily({
8186
instanceId: pageLayoutId,

packages/twenty-front/src/modules/page-layout/components/PageLayoutEditModeProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const PageLayoutEditModeProvider = ({
2525

2626
if (layoutType === PageLayoutType.RECORD_PAGE) {
2727
return (
28-
<RecordPageLayoutEditModeProvider>
28+
<RecordPageLayoutEditModeProvider pageLayoutId={pageLayoutId}>
2929
{children}
3030
</RecordPageLayoutEditModeProvider>
3131
);

packages/twenty-front/src/modules/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
22
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
33
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
4+
import { isDefaultPageLayoutId } from '@/page-layout/utils/isDefaultPageLayoutId';
45
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
56
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
67
import { useStore } from 'jotai';
@@ -31,6 +32,10 @@ export const PageLayoutRecordPageCustomizationSessionRegistrationEffect =
3132
return;
3233
}
3334

35+
if (isDefaultPageLayoutId(pageLayoutPersisted.id)) {
36+
return;
37+
}
38+
3439
store.set(activeCustomizationPageLayoutIdsState.atom, (activeIds) =>
3540
activeIds.includes(pageLayoutPersisted.id)
3641
? activeIds

packages/twenty-front/src/modules/page-layout/components/RecordPageLayoutEditModeProvider.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
22
import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
3+
import { isDefaultPageLayoutId } from '@/page-layout/utils/isDefaultPageLayoutId';
34
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
45
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
56
import { type ReactNode } from 'react';
67

78
type RecordPageLayoutEditModeProviderProps = {
9+
pageLayoutId: string;
810
children: ReactNode;
911
};
1012

1113
export const RecordPageLayoutEditModeProvider = ({
14+
pageLayoutId,
1215
children,
1316
}: RecordPageLayoutEditModeProviderProps) => {
1417
const isLayoutCustomizationModeEnabled = useAtomStateValue(
@@ -17,10 +20,13 @@ export const RecordPageLayoutEditModeProvider = ({
1720

1821
const { isInSidePanel } = useLayoutRenderingContext();
1922

23+
const isEditable = !isDefaultPageLayoutId(pageLayoutId);
24+
2025
return (
2126
<PageLayoutEditModeProviderContext
2227
value={{
23-
isInEditMode: isLayoutCustomizationModeEnabled && !isInSidePanel,
28+
isInEditMode:
29+
isLayoutCustomizationModeEnabled && !isInSidePanel && isEditable,
2430
}}
2531
>
2632
{children}

packages/twenty-front/src/modules/page-layout/hooks/useBasePageLayout.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT } from '@/page-layout/constants/De
2020
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
2121
import { recordPageLayoutFromIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutFromIdFamilySelector';
2222
import { type PageLayout } from '@/page-layout/types/PageLayout';
23+
import { isDefaultPageLayoutId } from '@/page-layout/utils/isDefaultPageLayoutId';
2324
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
2425
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
2526
import { useQuery } from '@apollo/client/react';
@@ -52,22 +53,10 @@ const getDefaultLayoutById = (layoutId: string): PageLayout => {
5253
}
5354
};
5455

55-
const isDefaultLayoutId = (layoutId: string): boolean =>
56-
layoutId === DEFAULT_RECORD_PAGE_LAYOUT_ID ||
57-
layoutId === DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID ||
58-
layoutId === DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID ||
59-
layoutId === DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID ||
60-
layoutId === DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID ||
61-
layoutId === DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID ||
62-
layoutId === DEFAULT_WORKFLOW_PAGE_LAYOUT_ID ||
63-
layoutId === DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID ||
64-
layoutId === DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID ||
65-
layoutId === DEFAULT_MESSAGE_THREAD_RECORD_PAGE_LAYOUT_ID;
66-
6756
export const useBasePageLayout = (
6857
pageLayoutId: string,
6958
): PageLayout | undefined => {
70-
const isDefaultLayout = isDefaultLayoutId(pageLayoutId);
59+
const isDefaultLayout = isDefaultPageLayoutId(pageLayoutId);
7160

7261
const cachedRecordPageLayout = useAtomFamilySelectorValue(
7362
recordPageLayoutFromIdFamilySelector,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
2+
import { DEFAULT_MESSAGE_THREAD_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultMessageThreadRecordPageLayoutId';
3+
import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
4+
import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
5+
import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
6+
import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
7+
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
8+
import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
9+
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
10+
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
11+
12+
const DEFAULT_PAGE_LAYOUT_IDS = new Set<string>([
13+
DEFAULT_RECORD_PAGE_LAYOUT_ID,
14+
DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID,
15+
DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID,
16+
DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID,
17+
DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
18+
DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
19+
DEFAULT_WORKFLOW_PAGE_LAYOUT_ID,
20+
DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID,
21+
DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID,
22+
DEFAULT_MESSAGE_THREAD_RECORD_PAGE_LAYOUT_ID,
23+
]);
24+
25+
export const isDefaultPageLayoutId = (pageLayoutId: string): boolean =>
26+
DEFAULT_PAGE_LAYOUT_IDS.has(pageLayoutId);

0 commit comments

Comments
 (0)