Fix delete/destroy/restore record commands on pages without a record index (#22952)
## Bug On a standalone page (`/page/:pageLayoutId` — a custom app page or standalone page layout), opening a record in the side panel and running **Delete** from the Options menu fails with an error toast: > Record index ID and object metadata are required to delete records The record is not deleted. The same guard breaks **Destroy** and **Restore**. ## Root cause `buildHeadlessCommandContextApi` only derives `recordIndexId` when the context store holds a `currentViewId`. On standalone pages there is no view, and `useOpenRecordInSidePanel` copies that null view id into the side panel context, so the delete/destroy/restore commands throw at mount — before executing anything. The throw is caught by `CommandMenuItemErrorBoundary` and surfaces as the toast (also reported to Sentry). The commands only use `recordIndexId` to reset table row selection and remove records from the record board — cleanup that is meaningless when no record index is on screen. The mutation itself only needs `objectMetadataItem` and the graphql filter, which are both available. ## Fix - Keep throwing when `objectMetadataItem` is missing (genuinely required). - Make `recordIndexId` optional: pass the existing `PLACEHOLDER_RECORD_INDEX_ID` to the selection hooks (they must be called unconditionally) and skip the selection cleanup at execute time when there is no record index — same pattern `useResetRecordIndexSelection` already uses. The constant is extracted to a shared file. ## Verified - **Bug path**: on a standalone page, opened a record in the side panel via search, ran Delete Task from the Options menu → record soft-deleted (checked `deletedAt` in DB), side panel closed, no error toast, no console error. - **Regression**: on the tasks index table, selected a row and ran Delete Task from the command menu → record deleted, row removed, table selection reset, no errors. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22952?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+13
-8
@@ -5,6 +5,7 @@ import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useIncrementalDeleteManyRecords } from '@/object-record/hooks/useIncrementalDeleteManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { PLACEHOLDER_RECORD_INDEX_ID } from '@/object-record/record-index/constants/PlaceholderRecordIndexId';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
|
||||
@@ -14,18 +15,20 @@ export const DeleteRecordsCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, selectedRecords, graphqlFilter } =
|
||||
useHeadlessCommandContextApi();
|
||||
|
||||
if (!isDefined(recordIndexId) || !isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Record index ID and object metadata are required to delete records',
|
||||
);
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error('Object metadata is required to delete records');
|
||||
}
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(
|
||||
recordIndexId ?? PLACEHOLDER_RECORD_INDEX_ID,
|
||||
);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
useRemoveSelectedRecordsFromRecordBoard(
|
||||
recordIndexId ?? PLACEHOLDER_RECORD_INDEX_ID,
|
||||
);
|
||||
|
||||
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
|
||||
|
||||
@@ -45,8 +48,10 @@ export const DeleteRecordsCommand = () => {
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
if (isDefined(recordIndexId)) {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
}
|
||||
closeSidePanelMenu();
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
|
||||
+13
-8
@@ -3,6 +3,7 @@ import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncrementalDestroyManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { PLACEHOLDER_RECORD_INDEX_ID } from '@/object-record/record-index/constants/PlaceholderRecordIndexId';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -19,10 +20,8 @@ export const DestroyRecordsCommand = () => {
|
||||
isInSidePanel,
|
||||
} = useHeadlessCommandContextApi();
|
||||
|
||||
if (!isDefined(recordIndexId) || !isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Record index ID and object metadata are required to destroy records',
|
||||
);
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error('Object metadata is required to destroy records');
|
||||
}
|
||||
|
||||
const isSingleRecord = selectedRecords.length === 1;
|
||||
@@ -30,9 +29,13 @@ export const DestroyRecordsCommand = () => {
|
||||
const navigateApp = useNavigateApp();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(
|
||||
recordIndexId ?? PLACEHOLDER_RECORD_INDEX_ID,
|
||||
);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
useRemoveSelectedRecordsFromRecordBoard(
|
||||
recordIndexId ?? PLACEHOLDER_RECORD_INDEX_ID,
|
||||
);
|
||||
|
||||
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
|
||||
|
||||
@@ -53,8 +56,10 @@ export const DestroyRecordsCommand = () => {
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
if (isDefined(recordIndexId)) {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
}
|
||||
|
||||
if (!isDefined(graphqlFilter)) {
|
||||
throw new Error('Cannot destroy records without a valid filter');
|
||||
|
||||
+15
-8
@@ -4,6 +4,7 @@ import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryP
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { PLACEHOLDER_RECORD_INDEX_ID } from '@/object-record/record-index/constants/PlaceholderRecordIndexId';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -14,17 +15,19 @@ export const RestoreRecordsCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, selectedRecords, graphqlFilter } =
|
||||
useHeadlessCommandContextApi();
|
||||
|
||||
if (!isDefined(recordIndexId) || !isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Record index ID and object metadata are required to restore records',
|
||||
);
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error('Object metadata is required to restore records');
|
||||
}
|
||||
|
||||
const isSingleRecord = selectedRecords.length === 1;
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(
|
||||
recordIndexId ?? PLACEHOLDER_RECORD_INDEX_ID,
|
||||
);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
useRemoveSelectedRecordsFromRecordBoard(
|
||||
recordIndexId ?? PLACEHOLDER_RECORD_INDEX_ID,
|
||||
);
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { restoreManyRecords } = useRestoreManyRecords({
|
||||
@@ -50,7 +53,9 @@ export const RestoreRecordsCommand = () => {
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
if (isDefined(recordIndexId)) {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
}
|
||||
closeSidePanelMenu();
|
||||
|
||||
if (!isDefined(graphqlFilter)) {
|
||||
@@ -60,7 +65,9 @@ export const RestoreRecordsCommand = () => {
|
||||
const recordsToRestore = await fetchAllRecordIds();
|
||||
const recordIdsToRestore = recordsToRestore.map((record) => record.id);
|
||||
|
||||
resetTableRowSelection();
|
||||
if (isDefined(recordIndexId)) {
|
||||
resetTableRowSelection();
|
||||
}
|
||||
|
||||
await restoreManyRecords({
|
||||
idsToRestore: recordIdsToRestore,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const PLACEHOLDER_RECORD_INDEX_ID = 'placeholder-record-index-id';
|
||||
+1
-2
@@ -3,6 +3,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection';
|
||||
import { PLACEHOLDER_RECORD_INDEX_ID } from '@/object-record/record-index/constants/PlaceholderRecordIndexId';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
@@ -11,8 +12,6 @@ import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const PLACEHOLDER_RECORD_INDEX_ID = 'placeholder-record-index-id';
|
||||
|
||||
export const useResetRecordIndexSelection = (
|
||||
contextStoreInstanceId?: string,
|
||||
) => {
|
||||
|
||||
Reference in New Issue
Block a user