Fix dashboard creation + role permission page design (#18565)

1. **Creating a new dashboard crashes with "Tab not found"** and widgets
can't be added after the crash is prevented.

**Root cause:** `initializePageLayout` wrapped both the persisted and
draft state updates behind an `isDeeplyEqual` guard. After navigation,
`resetPageLayoutEditMode` resets the draft atom to its default but
leaves the persisted atom untouched. On re-initialization,
`isDeeplyEqual` returns true (persisted unchanged), so the draft is
never repopulated. But edit mode is still activated.

**Fix**: Move the draft store.set outside the isDeeplyEqual guard so
it's always set on initialization. Also add a defensive check in
`PageLayoutRendererContent` to prevent the crash when activeTabId
doesn't match available tabs.


https://github.com/user-attachments/assets/bcd69866-63eb-4e5e-a1bb-655e71ba6dc5

2. **Permission role page design broken**
Before
<img width="573" height="1130" alt="role-page-broken"
src="https://github.com/user-attachments/assets/09f60fd2-ef08-4133-bb28-034b15579481"
/>

After
<img width="573" height="266" alt="Capture d’écran 2026-03-11 à 14 25
55"
src="https://github.com/user-attachments/assets/c34f9993-51e1-4108-a7e6-f434f558edfd"
/>
This commit is contained in:
Thomas Trompette
2026-03-11 18:57:54 +01:00
committed by GitHub
parent a024a04e01
commit 2af3121c51
8 changed files with 45 additions and 39 deletions
@@ -1,11 +1,13 @@
import { useBasePageLayout } from '@/page-layout/hooks/useBasePageLayout';
import { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
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 { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useStore } from 'jotai';
@@ -15,12 +17,10 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type PageLayoutInitializationQueryEffectProps = {
pageLayoutId: string;
onInitialized?: (pageLayout: PageLayout) => void;
};
export const PageLayoutInitializationQueryEffect = ({
pageLayoutId,
onInitialized,
}: PageLayoutInitializationQueryEffectProps) => {
const [pageLayoutIsInitialized, setPageLayoutIsInitialized] =
useAtomComponentState(pageLayoutIsInitializedComponentState);
@@ -29,6 +29,9 @@ export const PageLayoutInitializationQueryEffect = ({
const pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const pageLayoutPersistedComponentCallbackState =
useAtomComponentStateCallbackState(pageLayoutPersistedComponentState);
@@ -48,22 +51,26 @@ export const PageLayoutInitializationQueryEffect = ({
if (!isDeeplyEqual(layout, currentPersisted)) {
store.set(pageLayoutPersistedComponentCallbackState, layout);
store.set(pageLayoutDraftComponentCallbackState, {
id: layout.id,
name: layout.name,
type: layout.type,
objectMetadataId: layout.objectMetadataId,
tabs: layout.tabs,
});
const tabLayouts = convertPageLayoutToTabLayouts(layout);
store.set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
}
store.set(pageLayoutDraftComponentCallbackState, {
id: layout.id,
name: layout.name,
type: layout.type,
objectMetadataId: layout.objectMetadataId,
tabs: layout.tabs,
});
const tabLayouts = convertPageLayoutToTabLayouts(layout);
store.set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
setIsPageLayoutInEditMode(isPageLayoutEmpty(layout));
},
[
pageLayoutCurrentLayoutsComponentCallbackState,
pageLayoutDraftComponentCallbackState,
pageLayoutPersistedComponentCallbackState,
setIsPageLayoutInEditMode,
store,
],
);
@@ -71,14 +78,12 @@ export const PageLayoutInitializationQueryEffect = ({
useEffect(() => {
if (!pageLayoutIsInitialized && isDefined(pageLayout)) {
initializePageLayout(pageLayout);
onInitialized?.(pageLayout);
setPageLayoutIsInitialized(true);
}
}, [
initializePageLayout,
pageLayoutIsInitialized,
pageLayout,
onInitialized,
setPageLayoutIsInitialized,
]);
@@ -1,11 +1,8 @@
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';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import 'react-grid-layout/css/styles.css';
@@ -18,19 +15,8 @@ type PageLayoutRendererProps = {
export const PageLayoutRenderer = ({
pageLayoutId,
}: PageLayoutRendererProps) => {
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
const onInitialized = (pageLayout: PageLayout) => {
if (isPageLayoutEmpty(pageLayout)) {
setIsPageLayoutInEditMode(true);
} else {
setIsPageLayoutInEditMode(false);
}
};
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType,
@@ -48,10 +34,7 @@ export const PageLayoutRenderer = ({
instanceId: tabListInstanceId,
}}
>
<PageLayoutInitializationQueryEffect
pageLayoutId={pageLayoutId}
onInitialized={onInitialized}
/>
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
<PageLayoutRendererContent />
</TabListComponentInstanceContext.Provider>
@@ -1,4 +1,3 @@
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
@@ -15,17 +14,18 @@ import { getTabsByDisplayMode } from '@/page-layout/utils/getTabsByDisplayMode';
import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibleWidgets';
import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledContainer = styled.div<{ hasPinnedTab: boolean }>`
display: grid;
@@ -115,6 +115,10 @@ export const PageLayoutRendererContent = () => {
const sortedTabs = sortTabsByPosition(tabsToRenderInTabList);
const activeTabExistsInCurrentPageLayout = currentPageLayout.tabs.some(
(tab) => tab.id === activeTabId,
);
return (
<StyledContainer hasPinnedTab={isDefined(pinnedLeftTab)}>
{isDefined(pinnedLeftTab) && (
@@ -151,7 +155,7 @@ export const PageLayoutRendererContent = () => {
)}
defaultEnableXScroll={false}
>
{isDefined(activeTabId) && (
{isDefined(activeTabId) && activeTabExistsInCurrentPageLayout && (
<PageLayoutMainContent tabId={activeTabId} />
)}
</ScrollWrapper>
@@ -2,6 +2,7 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPag
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isNonEmptyString } from '@sniptt/guards';
export const useCurrentPageLayout = () => {
const pageLayoutPersisted = useAtomComponentStateValue(
@@ -16,8 +17,12 @@ export const useCurrentPageLayout = () => {
isPageLayoutInEditModeComponentState,
);
const isDraftInitialized = isNonEmptyString(pageLayoutDraft.id);
const currentPageLayout = isPageLayoutInEditMode
? pageLayoutDraft
? isDraftInitialized
? pageLayoutDraft
: pageLayoutPersisted
: pageLayoutPersisted;
return { currentPageLayout };
@@ -19,6 +19,7 @@ const StyledPermissionContent = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
white-space: nowrap;
`;
const StyledPermissionLabel = styled.span`
@@ -30,6 +31,7 @@ const StyledOverrideInfo = styled.div`
color: ${themeCssVariables.font.color.tertiary};
display: flex;
gap: ${themeCssVariables.spacing[1]};
white-space: nowrap;
`;
type OverridableCheckboxType = 'no_cta' | 'default' | 'override';
@@ -12,6 +12,7 @@ const StyledPermissionContent = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
white-space: nowrap;
`;
const StyledPermissionLabel = styled.span`
@@ -23,6 +24,7 @@ const StyledOverrideInfo = styled.div`
color: ${themeCssVariables.font.color.tertiary};
display: flex;
gap: ${themeCssVariables.spacing[1]};
white-space: nowrap;
`;
type SettingsRolePermissionsObjectsTableRowProps = {
@@ -1,7 +1,9 @@
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const usePageLayoutIdFromContextStoreTargetedRecord = () => {
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
@@ -21,5 +23,9 @@ export const usePageLayoutIdFromContextStoreTargetedRecord = () => {
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
return { pageLayoutId: recordStore?.pageLayoutId };
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
return {
pageLayoutId: recordStore?.pageLayoutId ?? currentPageLayoutId,
};
};
@@ -29,7 +29,6 @@ export const SettingsTextInput = ({
autoFocusOnMount,
autoSelectOnMount,
dataTestId,
className,
...props
}: SettingsTextInputProps) => {
const inputRef = useRef<HTMLInputElement>(null);