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:
+19
-14
@@ -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>
|
||||
|
||||
+8
-4
@@ -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 };
|
||||
|
||||
+2
@@ -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';
|
||||
|
||||
+2
@@ -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 = {
|
||||
|
||||
+7
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user