Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code b41ce33e21 fix: guard DestroySingleRecordCommand against missing record selection
https://sonarly.com/issue/15498?type=bug

`DestroySingleRecordCommand` throws an unhandled "Selected record ID is required" error when rendered via the server-driven command menu path (`IS_COMMAND_MENU_ITEM_ENABLED` flag) because the server path lacks the client-side `shouldBeRegistered` guard that prevents rendering single-record commands in exclusion mode or empty selection.

Fix: Created `useSelectedRecordIdOrNull` hook (non-throwing variant of `useSelectedRecordIdOrThrow`) and updated `DestroySingleRecordCommand` to use it, returning `null` when no single record is selected.

This follows the exact pattern established in the prior fix for `NavigateToNextRecordSingleRecordCommand` and `NavigateToPreviousRecordSingleRecordCommand` (commit 89516f06bb on another branch).

**Changes:**

1. **`useSelectedRecordIdOrNull.tsx` (new)** — Same logic as `useSelectedRecordIdOrThrow` but returns `null` instead of throwing when in exclusion mode or when no records are selected.

2. **`DestroySingleRecordCommand.tsx`** — Switched from `useSelectedRecordIdOrThrow` to `useSelectedRecordIdOrNull`. Added early return of `null` when `recordId` is null, preventing the component from rendering the `CommandModal`. Also added a guard in `handleDeleteClick` for safety.

The root issue is that the server-driven command menu path (`IS_COMMAND_MENU_ITEM_ENABLED` feature flag) doesn't replicate the legacy path's `shouldBeRegistered` guard that checked `isDefined(selectedRecord?.deletedAt)`. Rather than restructuring the server-driven path's filtering architecture, this makes the component resilient to being rendered without a selected record.
2026-03-17 08:16:21 +00:00
2 changed files with 28 additions and 2 deletions
@@ -1,5 +1,5 @@
import { CommandModal } from '@/command-menu-item/display/components/CommandModal';
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
import { useSelectedRecordIdOrNull } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrNull';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
@@ -12,7 +12,7 @@ export const DestroySingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem } =
useRecordIndexIdFromCurrentContextStore();
const recordId = useSelectedRecordIdOrThrow();
const recordId = useSelectedRecordIdOrNull();
const navigateApp = useNavigateApp();
@@ -26,6 +26,10 @@ export const DestroySingleRecordCommand = () => {
});
const handleDeleteClick = async () => {
if (!recordId) {
return;
}
removeSelectedRecordsFromRecordBoard();
resetTableRowSelection();
@@ -35,6 +39,10 @@ export const DestroySingleRecordCommand = () => {
});
};
if (!recordId) {
return null;
}
return (
<CommandModal
title={t`Permanently Destroy Record`}
@@ -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];
};