From 89516f06bbe5ccdca9e98e0959e56e8440109ffd Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Mon, 16 Mar 2026 09:36:46 +0000 Subject: [PATCH] fix: guard NavigateTo*RecordSingleRecordCommand against missing record selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/7596?type=bug The `NavigateToPreviousRecordSingleRecordCommand` and `NavigateToNextRecordSingleRecordCommand` components throw "Selected record ID is required" when rendered on an index page (or during a show-to-index page transition) because the server-driven command menu path lacks the `availableOn` view-type guard present in the legacy path. Fix: Created a new `useSelectedRecordIdOrNull` hook that returns `null` instead of throwing when no record is selected. Updated `NavigateToPreviousRecordSingleRecordCommand` and `NavigateToNextRecordSingleRecordCommand` to use this non-throwing variant. Both components now return `null` (render nothing) when there's no selected record, preventing the "Selected record ID is required" error during page transitions. The root cause is that the server-driven command menu path (`IS_COMMAND_MENU_ITEM_ENABLED` feature flag) can render these components during show-to-index page navigation transitions before the context store atoms are fully updated via `useEffect`. The legacy path had synchronous `availableOn: [SHOW_PAGE]` filtering that prevented this. Rather than restructuring the server-driven path's filtering architecture, this fix makes the two affected components resilient to the transient state. **Files changed:** 1. `useSelectedRecordIdOrNull.tsx` (new) — Non-throwing variant of `useSelectedRecordIdOrThrow`, returns `null` when no record is selected 2. `NavigateToPreviousRecordSingleRecordCommand.tsx` — Uses `useSelectedRecordIdOrNull`, returns `null` when no record 3. `NavigateToNextRecordSingleRecordCommand.tsx` — Same change as above --- ...NavigateToNextRecordSingleRecordCommand.tsx | 10 +++++++--- ...gateToPreviousRecordSingleRecordCommand.tsx | 10 +++++++--- .../hooks/useSelectedRecordIdOrNull.tsx | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull.tsx diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx index 682e00a4371..ec9f32aa631 100644 --- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx @@ -1,17 +1,21 @@ import { Command } from '@/command-menu-item/display/components/Command'; -import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow'; +import { useSelectedRecordIdOrNull } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination'; export const NavigateToNextRecordSingleRecordCommand = () => { const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); - const recordId = useSelectedRecordIdOrThrow(); + const recordId = useSelectedRecordIdOrNull(); const { navigateToNextRecord } = useRecordShowPagePagination( objectMetadataItem.nameSingular, - recordId, + recordId ?? '', ); + if (!recordId) { + return null; + } + return ; }; diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx index 15b7e7d0f96..5f4cc85db0a 100644 --- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx @@ -1,17 +1,21 @@ import { Command } from '@/command-menu-item/display/components/Command'; -import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow'; +import { useSelectedRecordIdOrNull } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination'; export const NavigateToPreviousRecordSingleRecordCommand = () => { const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); - const recordId = useSelectedRecordIdOrThrow(); + const recordId = useSelectedRecordIdOrNull(); const { navigateToPreviousRecord } = useRecordShowPagePagination( objectMetadataItem.nameSingular, - recordId, + recordId ?? '', ); + if (!recordId) { + return null; + } + return ; }; diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull.tsx new file mode 100644 index 00000000000..566bb63b981 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull.tsx @@ -0,0 +1,18 @@ +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; + +export const useSelectedRecordIdOrNull = (): string | null => { + const contextStoreTargetedRecordsRule = useAtomComponentStateValue( + contextStoreTargetedRecordsRuleComponentState, + ); + + if ( + contextStoreTargetedRecordsRule.mode === 'exclusion' || + (contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length === 0) + ) { + return null; + } + + return contextStoreTargetedRecordsRule.selectedRecordIds[0]; +};