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:
+5
-24
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-1
@@ -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) => {
|
||||
|
||||
+2
-13
@@ -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,
|
||||
|
||||
+3
-4
@@ -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'] =
|
||||
|
||||
+6
-1
@@ -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>
|
||||
);
|
||||
|
||||
+6
-19
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user