Command to deduplicate command menu items (#19202)

- Deduplicate standard record command menu items by merging
single-record and multi-record delete, restore, destroy, and export
actions into shared record commands.
- Update frontend command registrations and engine component mappings to
use the new unified keys, while keeping deprecated engine keys
temporarily mapped for backward compatibility.
- Add a 1-21 upgrade command that removes legacy command menu items from
workspaces and creates the new unified standard items.
This commit is contained in:
Raphaël Bosi
2026-04-02 11:51:15 +02:00
committed by GitHub
parent 16033e9f99
commit f688db30a9
18 changed files with 388 additions and 384 deletions
@@ -1,25 +1,21 @@
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
import { HeadlessOpenSidePanelPageEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand';
import { DeleteMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand';
import { DestroyMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand';
import { ExportMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand';
import { MergeMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand';
import { RestoreMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/RestoreMultipleRecordsCommand';
import { UpdateMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand';
import { CreateNewIndexRecordNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand';
import { CreateNewViewNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewViewNoSelectionRecordCommand';
import { HideDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand';
import { ImportRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand';
import { SeeDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand';
import { DeleteRecordsCommand } from '@/command-menu-item/engine-command/record/components/DeleteRecordsCommand';
import { DestroyRecordsCommand } from '@/command-menu-item/engine-command/record/components/DestroyRecordsCommand';
import { ExportRecordsCommand } from '@/command-menu-item/engine-command/record/components/ExportRecordsCommand';
import { RestoreRecordsCommand } from '@/command-menu-item/engine-command/record/components/RestoreRecordsCommand';
import { AddToFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand';
import { DeleteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand';
import { DestroySingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand';
import { ExportNoteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand';
import { ExportSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand';
import { NavigateToNextRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand';
import { NavigateToPreviousRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand';
import { RemoveFromFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand';
import { RestoreSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand';
import { CancelDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand';
import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
import { EditDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand';
@@ -57,18 +53,9 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
[EngineComponentKey.CREATE_NEW_RECORD]: (
<CreateNewIndexRecordNoSelectionRecordCommand />
),
[EngineComponentKey.DELETE_SINGLE_RECORD]: <DeleteSingleRecordCommand />,
[EngineComponentKey.DELETE_MULTIPLE_RECORDS]: (
<DeleteMultipleRecordsCommand />
),
[EngineComponentKey.RESTORE_SINGLE_RECORD]: <RestoreSingleRecordCommand />,
[EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: (
<RestoreMultipleRecordsCommand />
),
[EngineComponentKey.DESTROY_SINGLE_RECORD]: <DestroySingleRecordCommand />,
[EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: (
<DestroyMultipleRecordsCommand />
),
[EngineComponentKey.DELETE_RECORDS]: <DeleteRecordsCommand />,
[EngineComponentKey.RESTORE_RECORDS]: <RestoreRecordsCommand />,
[EngineComponentKey.DESTROY_RECORDS]: <DestroyRecordsCommand />,
[EngineComponentKey.ADD_TO_FAVORITES]: <AddToFavoritesSingleRecordCommand />,
[EngineComponentKey.REMOVE_FROM_FAVORITES]: (
<RemoveFromFavoritesSingleRecordCommand />
@@ -107,20 +94,14 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
<NavigateToPreviousRecordSingleRecordCommand />
),
[EngineComponentKey.EXPORT_NOTE_TO_PDF]: <ExportNoteSingleRecordCommand />,
[EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: (
<ExportMultipleRecordsCommand />
),
[EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: <ExportSingleRecordCommand />,
[EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: (
<ExportMultipleRecordsCommand />
),
[EngineComponentKey.EXPORT_RECORDS]: <ExportRecordsCommand />,
[EngineComponentKey.UPDATE_MULTIPLE_RECORDS]: (
<UpdateMultipleRecordsCommand />
),
[EngineComponentKey.IMPORT_RECORDS]: (
<ImportRecordsNoSelectionRecordCommand />
),
[EngineComponentKey.EXPORT_VIEW]: <ExportMultipleRecordsCommand />,
[EngineComponentKey.EXPORT_VIEW]: <ExportRecordsCommand />,
[EngineComponentKey.SEE_DELETED_RECORDS]: (
<SeeDeletedRecordsNoSelectionRecordCommand />
),
@@ -259,4 +240,15 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
[EngineComponentKey.FRONT_COMPONENT_RENDERER]: (
<HeadlessFrontComponentRendererEngineCommand />
),
// Deprecated keys kept for backward compatibility until migration runs
[EngineComponentKey.DELETE_SINGLE_RECORD]: <DeleteRecordsCommand />,
[EngineComponentKey.DELETE_MULTIPLE_RECORDS]: <DeleteRecordsCommand />,
[EngineComponentKey.RESTORE_SINGLE_RECORD]: <RestoreRecordsCommand />,
[EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: <RestoreRecordsCommand />,
[EngineComponentKey.DESTROY_SINGLE_RECORD]: <DestroyRecordsCommand />,
[EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: <DestroyRecordsCommand />,
[EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: <ExportRecordsCommand />,
[EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: <ExportRecordsCommand />,
[EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: <ExportRecordsCommand />,
};
@@ -2,34 +2,37 @@ import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-c
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
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 { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const DeleteSingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords } =
export const DeleteRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords, graphqlFilter } =
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
if (
!isDefined(recordId) ||
!isDefined(recordIndexId) ||
!isDefined(objectMetadataItem)
) {
if (!isDefined(recordIndexId) || !isDefined(objectMetadataItem)) {
throw new Error(
'Record ID, record index ID, and object metadata are required to delete single record',
'Record index ID and object metadata are required to delete records',
);
}
const recordId = selectedRecords[0]?.id;
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
const { removeSelectedRecordsFromRecordBoard } =
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
const { deleteOneRecord } = useDeleteOneRecord({
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
const { incrementalDeleteManyRecords } = useIncrementalDeleteManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
filter: graphqlFilter ?? noMatchFilter,
pageSize: DEFAULT_QUERY_PAGE_SIZE,
delayInMsBetweenMutations: 50,
});
const { navigationMenuItems, workspaceNavigationMenuItems } =
@@ -40,19 +43,24 @@ export const DeleteSingleRecordCommand = () => {
const handleExecute = async () => {
removeSelectedRecordsFromRecordBoard();
resetTableRowSelection();
const foundNavigationMenuItem = [
...navigationMenuItems,
...workspaceNavigationMenuItems,
].find((item) => item.targetRecordId === recordId);
if (isDefined(recordId)) {
const foundNavigationMenuItem = [
...navigationMenuItems,
...workspaceNavigationMenuItems,
].find((item) => item.targetRecordId === recordId);
if (isDefined(foundNavigationMenuItem)) {
removeNavigationMenuItemsByTargetRecordIds([recordId]);
if (isDefined(foundNavigationMenuItem)) {
removeNavigationMenuItemsByTargetRecordIds([recordId]);
}
}
await deleteOneRecord(recordId);
if (!isDefined(graphqlFilter)) {
throw new Error('Cannot delete records without a valid filter');
}
await incrementalDeleteManyRecords();
};
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
@@ -5,33 +5,36 @@ import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncre
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { t } from '@lingui/core/macro';
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { AppPath, type RecordGqlOperationFilter } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const DestroyMultipleRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, graphqlFilter } =
export const DestroyRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords, graphqlFilter } =
useHeadlessCommandContextApi();
if (
!isDefined(recordIndexId) ||
!isDefined(objectMetadataItem) ||
!isDefined(graphqlFilter)
) {
if (!isDefined(recordIndexId) || !isDefined(objectMetadataItem)) {
throw new Error(
'Record index ID, object metadata item, and graphql filter are required to destroy multiple records',
'Record index ID and object metadata are required to destroy records',
);
}
const isSingleRecord = selectedRecords.length === 1;
const navigateApp = useNavigateApp();
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
const { removeSelectedRecordsFromRecordBoard } =
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
const deletedAtFilter: RecordGqlOperationFilter = {
deletedAt: { is: 'NOT_NULL' },
};
const combinedFilter = {
...graphqlFilter,
const combinedFilter: RecordGqlOperationFilter = {
...(graphqlFilter ?? noMatchFilter),
...deletedAtFilter,
};
@@ -44,17 +47,36 @@ export const DestroyMultipleRecordsCommand = () => {
const handleExecute = async () => {
removeSelectedRecordsFromRecordBoard();
resetTableRowSelection();
if (!isDefined(graphqlFilter)) {
throw new Error('Cannot destroy records without a valid filter');
}
await incrementalDestroyManyRecords();
if (isSingleRecord) {
navigateApp(AppPath.RecordIndexPage, {
objectNamePlural: objectMetadataItem.namePlural,
});
}
};
const objectLabel = isSingleRecord
? objectMetadataItem.labelSingular
: objectMetadataItem.labelPlural;
const title = t`Permanently Destroy ${objectLabel}`;
const subtitle = isSingleRecord
? t`Are you sure you want to destroy this ${objectMetadataItem.labelSingular}? It cannot be recovered anymore.`
: t`Are you sure you want to destroy these ${objectMetadataItem.labelPlural}? They won't be recoverable anymore.`;
const confirmButtonText = `${t`Permanently Destroy`} ${objectLabel}`;
return (
<HeadlessConfirmationModalEngineCommandEffect
title={t`Permanently Destroy Records`}
subtitle={t`Are you sure you want to destroy these records? They won't be recoverable anymore.`}
confirmButtonText={t`Destroy Records`}
title={title}
subtitle={subtitle}
confirmButtonText={confirmButtonText}
execute={handleExecute}
/>
);
@@ -4,13 +4,14 @@ import { CommandComponentInstanceContext } from '@/command-menu-item/engine-comm
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useRecordIndexExportRecords } from '@/object-record/record-index/export/hooks/useRecordIndexExportRecords';
import { useExportSingleRecord } from '@/object-record/record-show/hooks/useExportSingleRecord';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
const ExportMultipleRecordsCommandContent = ({
const ExportIndexRecordsContent = ({
objectMetadataItem,
recordIndexId,
setCommandMenuItemProgress,
@@ -42,8 +43,26 @@ const ExportMultipleRecordsCommandContent = ({
return <HeadlessEngineCommandWrapperEffect execute={download} />;
};
export const ExportMultipleRecordsCommand = () => {
const { objectMetadataItem, recordIndexId } = useHeadlessCommandContextApi();
const ExportShowRecordContent = ({
objectMetadataItem,
recordId,
}: {
objectMetadataItem: EnrichedObjectMetadataItem;
recordId: string;
}) => {
const filename = `${objectMetadataItem.nameSingular}.csv`;
const { download } = useExportSingleRecord({
filename,
objectMetadataItem,
recordId,
});
return <HeadlessEngineCommandWrapperEffect execute={download} />;
};
export const ExportRecordsCommand = () => {
const { objectMetadataItem, recordIndexId, selectedRecords } =
useHeadlessCommandContextApi();
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
CommandComponentInstanceContext,
@@ -54,9 +73,25 @@ export const ExportMultipleRecordsCommand = () => {
engineCommandId,
);
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
if (!isDefined(objectMetadataItem)) {
throw new Error('Object metadata item is required to export records');
}
const recordId = selectedRecords[0]?.id;
const isShowPageExport = !isDefined(recordIndexId) && isDefined(recordId);
if (isShowPageExport) {
return (
<ExportShowRecordContent
objectMetadataItem={objectMetadataItem}
recordId={recordId}
/>
);
}
if (!isDefined(recordIndexId)) {
throw new Error(
'Object metadata item and record index ID are required to export multiple records',
'Record index ID is required to export records from index page',
);
}
@@ -64,7 +99,7 @@ export const ExportMultipleRecordsCommand = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ExportMultipleRecordsCommandContent
<ExportIndexRecordsContent
objectMetadataItem={objectMetadataItem}
recordIndexId={recordIndexId}
setCommandMenuItemProgress={setCommandMenuItemProgress}
@@ -9,20 +9,18 @@ import { t } from '@lingui/core/macro';
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const RestoreMultipleRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, graphqlFilter } =
export const RestoreRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords, graphqlFilter } =
useHeadlessCommandContextApi();
if (
!isDefined(recordIndexId) ||
!isDefined(objectMetadataItem) ||
!isDefined(graphqlFilter)
) {
if (!isDefined(recordIndexId) || !isDefined(objectMetadataItem)) {
throw new Error(
'Record index ID, object metadata item, and graphql filter are required to restore multiple records',
'Record index ID and object metadata are required to restore records',
);
}
const isSingleRecord = selectedRecords.length === 1;
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
const { removeSelectedRecordsFromRecordBoard } =
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
@@ -31,12 +29,14 @@ export const RestoreMultipleRecordsCommand = () => {
objectNameSingular: objectMetadataItem.nameSingular,
});
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
const deletedAtFilter: RecordGqlOperationFilter = {
deletedAt: { is: 'NOT_NULL' },
};
const combinedFilter = {
...graphqlFilter,
const combinedFilter: RecordGqlOperationFilter = {
...(graphqlFilter ?? noMatchFilter),
...deletedAtFilter,
};
@@ -49,6 +49,11 @@ export const RestoreMultipleRecordsCommand = () => {
const handleExecute = async () => {
removeSelectedRecordsFromRecordBoard();
if (!isDefined(graphqlFilter)) {
throw new Error('Cannot restore records without a valid filter');
}
const recordsToRestore = await fetchAllRecordIds();
const recordIdsToRestore = recordsToRestore.map((record) => record.id);
@@ -59,11 +64,20 @@ export const RestoreMultipleRecordsCommand = () => {
});
};
const objectLabel = isSingleRecord
? objectMetadataItem.labelSingular
: objectMetadataItem.labelPlural;
const title = t`Restore ${objectLabel}`;
const subtitle = isSingleRecord
? t`Are you sure you want to restore this ${objectMetadataItem.labelSingular}?`
: t`Are you sure you want to restore these ${objectMetadataItem.labelPlural}?`;
return (
<HeadlessConfirmationModalEngineCommandEffect
title={t`Restore Records`}
subtitle={t`Are you sure you want to restore these records?`}
confirmButtonText={t`Restore Records`}
title={title}
subtitle={subtitle}
confirmButtonText={title}
confirmButtonAccent="default"
execute={handleExecute}
/>
@@ -1,42 +0,0 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
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 { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { isDefined } from 'twenty-shared/utils';
export const DeleteMultipleRecordsCommand = () => {
const { recordIndexId, objectMetadataItem, graphqlFilter } =
useHeadlessCommandContextApi();
if (
!isDefined(recordIndexId) ||
!isDefined(objectMetadataItem) ||
!isDefined(graphqlFilter)
) {
throw new Error(
'Record index ID, object metadata item, and graphql filter are required to delete multiple records',
);
}
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
const { removeSelectedRecordsFromRecordBoard } =
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
const { incrementalDeleteManyRecords } = useIncrementalDeleteManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
filter: graphqlFilter,
pageSize: DEFAULT_QUERY_PAGE_SIZE,
delayInMsBetweenMutations: 50,
});
const handleExecute = async () => {
removeSelectedRecordsFromRecordBoard();
resetTableRowSelection();
await incrementalDeleteManyRecords();
};
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
};
@@ -1,56 +0,0 @@
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { t } from '@lingui/core/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const DestroySingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords } =
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
if (
!isDefined(recordId) ||
!isDefined(recordIndexId) ||
!isDefined(objectMetadataItem)
) {
throw new Error(
'Record ID, record index ID, and object metadata are required to destroy single record',
);
}
const navigateApp = useNavigateApp();
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
const { removeSelectedRecordsFromRecordBoard } =
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const handleExecute = async () => {
removeSelectedRecordsFromRecordBoard();
resetTableRowSelection();
await destroyOneRecord(recordId);
navigateApp(AppPath.RecordIndexPage, {
objectNamePlural: objectMetadataItem.namePlural,
});
};
return (
<HeadlessConfirmationModalEngineCommandEffect
title={t`Permanently Destroy Record`}
subtitle={t`Are you sure you want to destroy this record? It cannot be recovered anymore.`}
confirmButtonText={t`Permanently Destroy Record`}
execute={handleExecute}
/>
);
};
@@ -1,30 +0,0 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useExportSingleRecord } from '@/object-record/record-show/hooks/useExportSingleRecord';
import { isDefined } from 'twenty-shared/utils';
export const ExportSingleRecordCommand = () => {
const { objectMetadataItem, currentViewId, selectedRecords } =
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
if (
!isDefined(currentViewId) ||
!isDefined(recordId) ||
!isDefined(objectMetadataItem)
) {
throw new Error(
'Current view ID, record ID, and object metadata are required to export single record',
);
}
const filename = `${objectMetadataItem.nameSingular}.csv`;
const { download } = useExportSingleRecord({
filename,
objectMetadataItem,
recordId,
});
return <HeadlessEngineCommandWrapperEffect execute={download} />;
};
@@ -1,51 +0,0 @@
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const RestoreSingleRecordCommand = () => {
const { recordIndexId, objectMetadataItem, selectedRecords } =
useHeadlessCommandContextApi();
const recordId = selectedRecords[0]?.id;
if (
!isDefined(recordId) ||
!isDefined(recordIndexId) ||
!isDefined(objectMetadataItem)
) {
throw new Error(
'Record ID, record index ID, and object metadata are required to restore single record',
);
}
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
const { removeSelectedRecordsFromRecordBoard } =
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
const { restoreManyRecords } = useRestoreManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
});
const handleExecute = async () => {
removeSelectedRecordsFromRecordBoard();
resetTableRowSelection();
await restoreManyRecords({
idsToRestore: [recordId],
});
};
return (
<HeadlessConfirmationModalEngineCommandEffect
title={t`Restore Record`}
subtitle={t`Are you sure you want to restore this record?`}
confirmButtonText={t`Restore Record`}
confirmButtonAccent="default"
execute={handleExecute}
/>
);
};