fix: add missing LayoutRenderingProvider in SettingsApplicationCustomTab (#19679)

## Summary

- `SettingsApplicationCustomTab` renders `FrontComponentRenderer` which
calls `useFrontComponentExecutionContext` →
`useLayoutRenderingContext()`, but the settings page never provided a
`LayoutRenderingProvider`
- Every other render site (side panel, command menu, record pages) wraps
`FrontComponentRenderer` with this provider — it was just missed here
- Opening the "Custom" tab in Settings → Applications crashes with:
`LayoutRenderingContext Context not found`
- Fix: wrap with `LayoutRenderingProvider` using `DASHBOARD` layout type
and no target record, matching the pattern used in
`SidePanelFrontComponentPage`

## Test plan

- [ ] Open Settings → Applications → any app with a custom settings tab
- [ ] Click the "Custom" tab — should render the front component without
crashing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lazare Rossillon
2026-04-16 12:01:28 +02:00
committed by GitHub
parent 60701c2cf9
commit d54092b0e2
6 changed files with 29 additions and 62 deletions
@@ -3,10 +3,7 @@ import { Suspense, lazy } from 'react';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
import { isHeadlessFrontComponentCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessFrontComponentCommandContextApi';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
@@ -27,34 +24,18 @@ export const HeadlessFrontComponentRendererEngineCommand = () => {
);
}
const objectNameSingular = context.objectMetadataItem?.nameSingular;
const recordId =
context.selectedRecords.length === 1
? context.selectedRecords[0].id
: undefined;
// TODO: Remove layout rendering provider once we have refactored FrontComponentRenderer to have one headless renderer and a standard renderer
return (
<Suspense fallback={null}>
<LayoutRenderingProvider
value={{
targetRecordIdentifier:
isDefined(objectNameSingular) && isDefined(recordId)
? {
id: recordId,
targetObjectNameSingular: objectNameSingular,
}
: undefined,
layoutType: PageLayoutType.DASHBOARD,
isInSidePanel: false,
}}
>
<FrontComponentRenderer
frontComponentId={context.frontComponentId}
commandMenuItemId={commandMenuItemId}
/>
</LayoutRenderingProvider>
<FrontComponentRenderer
frontComponentId={context.frontComponentId}
commandMenuItemId={commandMenuItemId}
recordId={recordId}
/>
</Suspense>
);
};
@@ -18,11 +18,13 @@ import { FindOneFrontComponentDocument } from '~/generated-metadata/graphql';
type FrontComponentRendererProps = {
frontComponentId: string;
commandMenuItemId?: string;
recordId?: string;
};
export const FrontComponentRenderer = ({
frontComponentId,
commandMenuItemId,
recordId,
}: FrontComponentRendererProps) => {
const { colorScheme } = useContext(ThemeContext);
const { enqueueErrorSnackBar } = useSnackBar();
@@ -33,7 +35,11 @@ export const FrontComponentRenderer = ({
);
const { executionContext, frontComponentHostCommunicationApi } =
useFrontComponentExecutionContext({ frontComponentId, commandMenuItemId });
useFrontComponentExecutionContext({
frontComponentId,
commandMenuItemId,
recordId,
});
const handleError = useCallback(
(error?: Error) => {
@@ -17,9 +17,6 @@ const mockCloseSidePanelMenu = jest.fn();
const mockSetCommandMenuItemProgress = jest.fn();
let mockCurrentUser: { id: string } | null = { id: 'user-123' };
let mockTargetRecordIdentifier: { id: string } | undefined = {
id: 'record-456',
};
jest.mock('~/hooks/useNavigateApp', () => ({
useNavigateApp: () => mockNavigateApp,
@@ -86,12 +83,6 @@ jest.mock('@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState', () => ({
useSetAtomFamilyState: () => mockSetCommandMenuItemProgress,
}));
jest.mock('@/ui/layout/contexts/LayoutRenderingContext', () => ({
useLayoutRenderingContext: () => ({
targetRecordIdentifier: mockTargetRecordIdentifier,
}),
}));
const FRONT_COMPONENT_ID = 'fc-test-id';
const COMMAND_MENU_ITEM_ID = 'cmd-item-1';
@@ -99,7 +90,6 @@ describe('useFrontComponentExecutionContext', () => {
beforeEach(() => {
jest.clearAllMocks();
mockCurrentUser = { id: 'user-123' };
mockTargetRecordIdentifier = { id: 'record-456' };
});
describe('executionContext', () => {
@@ -107,6 +97,7 @@ describe('useFrontComponentExecutionContext', () => {
const { result } = renderHook(() =>
useFrontComponentExecutionContext({
frontComponentId: FRONT_COMPONENT_ID,
recordId: 'record-456',
}),
);
@@ -129,9 +120,7 @@ describe('useFrontComponentExecutionContext', () => {
expect(result.current.executionContext.userId).toBeNull();
});
it('should return null recordId when no target record', () => {
mockTargetRecordIdentifier = undefined;
it('should return null recordId when no recordId provided', () => {
const { result } = renderHook(() =>
useFrontComponentExecutionContext({
frontComponentId: FRONT_COMPONENT_ID,
@@ -14,7 +14,6 @@ import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
@@ -24,9 +23,11 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useFrontComponentExecutionContext = ({
frontComponentId,
commandMenuItemId,
recordId,
}: {
frontComponentId: string;
commandMenuItemId?: string;
recordId?: string;
}): {
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
@@ -123,12 +124,10 @@ export const useFrontComponentExecutionContext = ({
}
};
const { targetRecordIdentifier } = useLayoutRenderingContext();
const executionContext: FrontComponentExecutionContext = {
frontComponentId,
userId: currentUser?.id ?? null,
recordId: targetRecordIdentifier?.id ?? null,
recordId: recordId ?? null,
};
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
@@ -7,6 +7,7 @@ import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutIn
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
const StyledContainer = styled.div<{ isInEditMode: boolean }>`
height: 100%;
@@ -29,6 +30,7 @@ export const FrontComponentWidgetRenderer = ({
widget,
}: FrontComponentWidgetRendererProps) => {
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const { targetRecordIdentifier } = useLayoutRenderingContext();
const configuration = widget.configuration;
@@ -44,7 +46,10 @@ export const FrontComponentWidgetRenderer = ({
return (
<StyledContainer isInEditMode={isPageLayoutInEditMode}>
<Suspense fallback={null}>
<FrontComponentRenderer frontComponentId={frontComponentId} />
<FrontComponentRenderer
frontComponentId={frontComponentId}
recordId={targetRecordIdentifier?.id}
/>
</Suspense>
</StyledContainer>
);
@@ -2,10 +2,8 @@ import { Suspense, lazy } from 'react';
import { viewableFrontComponentIdComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentIdComponentState';
import { viewableFrontComponentRecordContextComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentRecordContextComponentState';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
@@ -27,22 +25,11 @@ export const SidePanelFrontComponentPage = () => {
}
return (
<LayoutRenderingProvider
value={{
targetRecordIdentifier: isDefined(viewableFrontComponentRecordContext)
? {
id: viewableFrontComponentRecordContext.recordId,
targetObjectNameSingular:
viewableFrontComponentRecordContext.objectNameSingular,
}
: undefined,
layoutType: PageLayoutType.DASHBOARD,
isInSidePanel: true,
}}
>
<Suspense fallback={null}>
<FrontComponentRenderer frontComponentId={viewableFrontComponentId} />
</Suspense>
</LayoutRenderingProvider>
<Suspense fallback={null}>
<FrontComponentRenderer
frontComponentId={viewableFrontComponentId}
recordId={viewableFrontComponentRecordContext?.recordId}
/>
</Suspense>
);
};