From f688db30a96d8debbb05507b9e19fbaba943454b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:51:15 +0200 Subject: [PATCH] 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. --- .../src/metadata/generated/schema.graphql | 22 ++- .../src/metadata/generated/schema.ts | 26 +-- .../src/generated-metadata/graphql.ts | 4 + ...EngineComponentKeyHeadlessComponentMap.tsx | 48 +++--- .../DeleteRecordsCommand.tsx} | 48 +++--- .../DestroyRecordsCommand.tsx} | 52 ++++-- .../ExportRecordsCommand.tsx} | 47 +++++- .../RestoreRecordsCommand.tsx} | 40 +++-- .../DeleteMultipleRecordsCommand.tsx | 42 ----- .../components/DestroySingleRecordCommand.tsx | 56 ------- .../components/ExportSingleRecordCommand.tsx | 30 ---- .../components/RestoreSingleRecordCommand.tsx | 51 ------ .../record/types/RecordCommandKeys.ts | 6 + ...-21-deduplicate-engine-commands.command.ts | 156 ++++++++++++++++++ .../1-21-upgrade-version-command.module.ts | 3 + .../upgrade.command.ts | 3 + .../enums/engine-component-key.enum.ts | 24 ++- .../standard-command-menu-item.constant.ts | 114 +++---------- 18 files changed, 388 insertions(+), 384 deletions(-) rename packages/twenty-front/src/modules/command-menu-item/engine-command/record/{single-record/components/DeleteSingleRecordCommand.tsx => components/DeleteRecordsCommand.tsx} (55%) rename packages/twenty-front/src/modules/command-menu-item/engine-command/record/{multiple-records/components/DestroyMultipleRecordsCommand.tsx => components/DestroyRecordsCommand.tsx} (52%) rename packages/twenty-front/src/modules/command-menu-item/engine-command/record/{multiple-records/components/ExportMultipleRecordsCommand.tsx => components/ExportRecordsCommand.tsx} (68%) rename packages/twenty-front/src/modules/command-menu-item/engine-command/record/{multiple-records/components/RestoreMultipleRecordsCommand.tsx => components/RestoreRecordsCommand.tsx} (67%) delete mode 100644 packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx delete mode 100644 packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx delete mode 100644 packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx delete mode 100644 packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx create mode 100644 packages/twenty-front/src/modules/command-menu-item/record/types/RecordCommandKeys.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index b3a49b4beb..17c12ee169 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2567,20 +2567,15 @@ enum EngineComponentKey { NAVIGATE_TO_NEXT_RECORD NAVIGATE_TO_PREVIOUS_RECORD CREATE_NEW_RECORD - DELETE_SINGLE_RECORD - DELETE_MULTIPLE_RECORDS - RESTORE_SINGLE_RECORD - RESTORE_MULTIPLE_RECORDS - DESTROY_SINGLE_RECORD - DESTROY_MULTIPLE_RECORDS + DELETE_RECORDS + RESTORE_RECORDS + DESTROY_RECORDS ADD_TO_FAVORITES REMOVE_FROM_FAVORITES EXPORT_NOTE_TO_PDF - EXPORT_FROM_RECORD_INDEX - EXPORT_FROM_RECORD_SHOW + EXPORT_RECORDS UPDATE_MULTIPLE_RECORDS MERGE_MULTIPLE_RECORDS - EXPORT_MULTIPLE_RECORDS IMPORT_RECORDS EXPORT_VIEW SEE_DELETED_RECORDS @@ -2623,6 +2618,15 @@ enum EngineComponentKey { VIEW_PREVIOUS_AI_CHATS TRIGGER_WORKFLOW_VERSION FRONT_COMPONENT_RENDERER + DELETE_SINGLE_RECORD + DELETE_MULTIPLE_RECORDS + RESTORE_SINGLE_RECORD + RESTORE_MULTIPLE_RECORDS + DESTROY_SINGLE_RECORD + DESTROY_MULTIPLE_RECORDS + EXPORT_FROM_RECORD_INDEX + EXPORT_FROM_RECORD_SHOW + EXPORT_MULTIPLE_RECORDS } enum CommandMenuItemAvailabilityType { diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index c1f4731c2d..28984f6949 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2272,7 +2272,7 @@ export interface CommandMenuItem { __typename: 'CommandMenuItem' } -export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'EXPORT_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'GO_TO_WORKFLOWS' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'GO_TO_RUNS' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' +export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'GO_TO_WORKFLOWS' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'GO_TO_RUNS' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS' export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'RECORD_SELECTION' | 'FALLBACK' @@ -9140,20 +9140,15 @@ export const enumEngineComponentKey = { NAVIGATE_TO_NEXT_RECORD: 'NAVIGATE_TO_NEXT_RECORD' as const, NAVIGATE_TO_PREVIOUS_RECORD: 'NAVIGATE_TO_PREVIOUS_RECORD' as const, CREATE_NEW_RECORD: 'CREATE_NEW_RECORD' as const, - DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const, - DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const, - RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const, - RESTORE_MULTIPLE_RECORDS: 'RESTORE_MULTIPLE_RECORDS' as const, - DESTROY_SINGLE_RECORD: 'DESTROY_SINGLE_RECORD' as const, - DESTROY_MULTIPLE_RECORDS: 'DESTROY_MULTIPLE_RECORDS' as const, + DELETE_RECORDS: 'DELETE_RECORDS' as const, + RESTORE_RECORDS: 'RESTORE_RECORDS' as const, + DESTROY_RECORDS: 'DESTROY_RECORDS' as const, ADD_TO_FAVORITES: 'ADD_TO_FAVORITES' as const, REMOVE_FROM_FAVORITES: 'REMOVE_FROM_FAVORITES' as const, EXPORT_NOTE_TO_PDF: 'EXPORT_NOTE_TO_PDF' as const, - EXPORT_FROM_RECORD_INDEX: 'EXPORT_FROM_RECORD_INDEX' as const, - EXPORT_FROM_RECORD_SHOW: 'EXPORT_FROM_RECORD_SHOW' as const, + EXPORT_RECORDS: 'EXPORT_RECORDS' as const, UPDATE_MULTIPLE_RECORDS: 'UPDATE_MULTIPLE_RECORDS' as const, MERGE_MULTIPLE_RECORDS: 'MERGE_MULTIPLE_RECORDS' as const, - EXPORT_MULTIPLE_RECORDS: 'EXPORT_MULTIPLE_RECORDS' as const, IMPORT_RECORDS: 'IMPORT_RECORDS' as const, EXPORT_VIEW: 'EXPORT_VIEW' as const, SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const, @@ -9195,7 +9190,16 @@ export const enumEngineComponentKey = { ASK_AI: 'ASK_AI' as const, VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const, TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const, - FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const + FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const, + DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const, + DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const, + RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const, + RESTORE_MULTIPLE_RECORDS: 'RESTORE_MULTIPLE_RECORDS' as const, + DESTROY_SINGLE_RECORD: 'DESTROY_SINGLE_RECORD' as const, + DESTROY_MULTIPLE_RECORDS: 'DESTROY_MULTIPLE_RECORDS' as const, + EXPORT_FROM_RECORD_INDEX: 'EXPORT_FROM_RECORD_INDEX' as const, + EXPORT_FROM_RECORD_SHOW: 'EXPORT_FROM_RECORD_SHOW' as const, + EXPORT_MULTIPLE_RECORDS: 'EXPORT_MULTIPLE_RECORDS' as const } export const enumCommandMenuItemAvailabilityType = { diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 64f8ee53e5..2d2f59ee40 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1563,8 +1563,10 @@ export enum EngineComponentKey { CREATE_NEW_VIEW = 'CREATE_NEW_VIEW', DEACTIVATE_WORKFLOW = 'DEACTIVATE_WORKFLOW', DELETE_MULTIPLE_RECORDS = 'DELETE_MULTIPLE_RECORDS', + DELETE_RECORDS = 'DELETE_RECORDS', DELETE_SINGLE_RECORD = 'DELETE_SINGLE_RECORD', DESTROY_MULTIPLE_RECORDS = 'DESTROY_MULTIPLE_RECORDS', + DESTROY_RECORDS = 'DESTROY_RECORDS', DESTROY_SINGLE_RECORD = 'DESTROY_SINGLE_RECORD', DISCARD_DRAFT_WORKFLOW = 'DISCARD_DRAFT_WORKFLOW', DUPLICATE_DASHBOARD = 'DUPLICATE_DASHBOARD', @@ -1575,6 +1577,7 @@ export enum EngineComponentKey { EXPORT_FROM_RECORD_SHOW = 'EXPORT_FROM_RECORD_SHOW', EXPORT_MULTIPLE_RECORDS = 'EXPORT_MULTIPLE_RECORDS', EXPORT_NOTE_TO_PDF = 'EXPORT_NOTE_TO_PDF', + EXPORT_RECORDS = 'EXPORT_RECORDS', EXPORT_VIEW = 'EXPORT_VIEW', FRONT_COMPONENT_RENDERER = 'FRONT_COMPONENT_RENDERER', GO_TO_COMPANIES = 'GO_TO_COMPANIES', @@ -1593,6 +1596,7 @@ export enum EngineComponentKey { NAVIGATE_TO_PREVIOUS_RECORD = 'NAVIGATE_TO_PREVIOUS_RECORD', REMOVE_FROM_FAVORITES = 'REMOVE_FROM_FAVORITES', RESTORE_MULTIPLE_RECORDS = 'RESTORE_MULTIPLE_RECORDS', + RESTORE_RECORDS = 'RESTORE_RECORDS', RESTORE_SINGLE_RECORD = 'RESTORE_SINGLE_RECORD', SAVE_DASHBOARD_LAYOUT = 'SAVE_DASHBOARD_LAYOUT', SEARCH_RECORDS = 'SEARCH_RECORDS', diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx index c85a0c8ba9..57101f4477 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx @@ -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]: ( ), - [EngineComponentKey.DELETE_SINGLE_RECORD]: , - [EngineComponentKey.DELETE_MULTIPLE_RECORDS]: ( - - ), - [EngineComponentKey.RESTORE_SINGLE_RECORD]: , - [EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: ( - - ), - [EngineComponentKey.DESTROY_SINGLE_RECORD]: , - [EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: ( - - ), + [EngineComponentKey.DELETE_RECORDS]: , + [EngineComponentKey.RESTORE_RECORDS]: , + [EngineComponentKey.DESTROY_RECORDS]: , [EngineComponentKey.ADD_TO_FAVORITES]: , [EngineComponentKey.REMOVE_FROM_FAVORITES]: ( @@ -107,20 +94,14 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record< ), [EngineComponentKey.EXPORT_NOTE_TO_PDF]: , - [EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: ( - - ), - [EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: , - [EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: ( - - ), + [EngineComponentKey.EXPORT_RECORDS]: , [EngineComponentKey.UPDATE_MULTIPLE_RECORDS]: ( ), [EngineComponentKey.IMPORT_RECORDS]: ( ), - [EngineComponentKey.EXPORT_VIEW]: , + [EngineComponentKey.EXPORT_VIEW]: , [EngineComponentKey.SEE_DELETED_RECORDS]: ( ), @@ -259,4 +240,15 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record< [EngineComponentKey.FRONT_COMPONENT_RENDERER]: ( ), + + // Deprecated keys kept for backward compatibility until migration runs + [EngineComponentKey.DELETE_SINGLE_RECORD]: , + [EngineComponentKey.DELETE_MULTIPLE_RECORDS]: , + [EngineComponentKey.RESTORE_SINGLE_RECORD]: , + [EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: , + [EngineComponentKey.DESTROY_SINGLE_RECORD]: , + [EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: , + [EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: , + [EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: , + [EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: , }; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DeleteRecordsCommand.tsx similarity index 55% rename from packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand.tsx rename to packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DeleteRecordsCommand.tsx index 75f84b29ac..d58632be4d 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DeleteRecordsCommand.tsx @@ -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 ; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DestroyRecordsCommand.tsx similarity index 52% rename from packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand.tsx rename to packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DestroyRecordsCommand.tsx index 13ed419b0e..1fde0f7f98 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DestroyRecordsCommand.tsx @@ -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 ( ); diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/ExportRecordsCommand.tsx similarity index 68% rename from packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx rename to packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/ExportRecordsCommand.tsx index fc1260dfa6..5fd590fd0e 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/ExportRecordsCommand.tsx @@ -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 ; }; -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 ; +}; + +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 ( + + ); + } + + 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 = () => { - { - 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 ( diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx deleted file mode 100644 index 9c3b92ff20..0000000000 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx +++ /dev/null @@ -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 ; -}; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx deleted file mode 100644 index 822ead630a..0000000000 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx +++ /dev/null @@ -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 ( - - ); -}; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx deleted file mode 100644 index 6b3d34cb03..0000000000 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx +++ /dev/null @@ -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 ; -}; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx deleted file mode 100644 index a4fcd70878..0000000000 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx +++ /dev/null @@ -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 ( - - ); -}; diff --git a/packages/twenty-front/src/modules/command-menu-item/record/types/RecordCommandKeys.ts b/packages/twenty-front/src/modules/command-menu-item/record/types/RecordCommandKeys.ts new file mode 100644 index 0000000000..efcfcff71b --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu-item/record/types/RecordCommandKeys.ts @@ -0,0 +1,6 @@ +export enum RecordCommandKeys { + DELETE = 'delete-records', + RESTORE = 'restore-records', + DESTROY = 'destroy-records', + EXPORT = 'export-records', +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command.ts new file mode 100644 index 0000000000..28fde94772 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command.ts @@ -0,0 +1,156 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; + +const OLD_UNIVERSAL_IDENTIFIERS_TO_DELETE = new Set([ + '6652773f-b9a9-4fa3-a52c-e2f2e259e430', // deleteSingleRecord + 'cde86f1f-2c13-42b1-812b-f2b2b468cb83', // deleteMultipleRecords + '8b3a1cae-3e4d-43c1-a71f-48592b2e47ff', // restoreSingleRecord + '8b740c9d-d99a-45a8-812f-809caaf420ac', // restoreMultipleRecords + '44a78417-c394-4bc8-961f-98b503030ddb', // destroySingleRecord + 'c630b3fb-7920-40d1-9906-77d0aa797608', // destroyMultipleRecords + 'a934ba8a-ac8f-487d-9cd9-06dfdaec1f49', // exportFromRecordIndex + 'ba339455-f3c2-4ed1-bf77-3e316d7d6a66', // exportFromRecordShow + 'f71f68e5-7b6e-4c03-8161-c48434d7777c', // exportMultipleRecords +]); + +const NEW_UNIVERSAL_IDENTIFIERS = new Set([ + 'd5a55d57-ed1d-4791-89b8-53b7e121d69d', // deleteRecords + '2d733846-8cc5-4314-ab79-916ae0801baa', // restoreRecords + '0ea2ebc4-02ca-4d15-b424-5352b9e487df', // destroyRecords + 'c6f5c54d-d52b-4e75-8188-2190d77126f2', // exportRecords +]); + +@Command({ + name: 'upgrade:1-21:deduplicate-engine-commands', + description: + 'Merge single/multiple record engine command menu items into unified commands (delete, restore, destroy, export)', +}) +export class DeduplicateEngineCommandsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + private readonly applicationService: ApplicationService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceCacheService: WorkspaceCacheService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Starting deduplication of engine commands for workspace ${workspaceId}`, + ); + + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatCommandMenuItemMaps', + ]); + + const itemsToDelete = Object.values( + existingFlatCommandMenuItemMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((item) => + OLD_UNIVERSAL_IDENTIFIERS_TO_DELETE.has(item.universalIdentifier), + ); + + const { allFlatEntityMaps: standardAllFlatEntityMaps } = + computeTwentyStandardApplicationAllFlatEntityMaps({ + shouldIncludeRecordPageLayouts: true, + now: new Date().toISOString(), + workspaceId, + twentyStandardApplicationId: twentyStandardFlatApplication.id, + }); + + const itemsToCreate = Object.values( + standardAllFlatEntityMaps.flatCommandMenuItemMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((item) => NEW_UNIVERSAL_IDENTIFIERS.has(item.universalIdentifier)) + .filter( + (item) => + !isDefined( + existingFlatCommandMenuItemMaps.byUniversalIdentifier[ + item.universalIdentifier + ], + ), + ); + + const totalChanges = itemsToDelete.length + itemsToCreate.length; + + if (totalChanges === 0) { + this.logger.log( + `No engine command deduplication needed for workspace ${workspaceId}`, + ); + + return; + } + + this.logger.log( + `Found ${itemsToDelete.length} old command menu item(s) to delete and ${itemsToCreate.length} unified item(s) to create for workspace ${workspaceId}`, + ); + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would delete ${itemsToDelete.length} and create ${itemsToCreate.length} command menu item(s) for workspace ${workspaceId}`, + ); + + return; + } + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + commandMenuItem: { + flatEntityToCreate: itemsToCreate, + flatEntityToDelete: itemsToDelete, + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to deduplicate engine commands:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + ); + + throw new Error( + `Failed to deduplicate engine commands for workspace ${workspaceId}`, + ); + } + + this.logger.log( + `Successfully deduplicated engine commands for workspace ${workspaceId} (deleted ${itemsToDelete.length}, created ${itemsToCreate.length})`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts index e20f3f6e8d..33c2ad1ba2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-datasource-to-workspace.command'; import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command'; +import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command'; import { MigrateAiAgentTextToJsonResponseFormatCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-ai-agent-text-to-json-response-format.command'; import { UpdateEditLayoutCommandMenuItemLabelCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-update-edit-layout-command-menu-item-label.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; @@ -25,12 +26,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace providers: [ BackfillDatasourceToWorkspaceCommand, BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand, + DeduplicateEngineCommandsCommand, MigrateAiAgentTextToJsonResponseFormatCommand, UpdateEditLayoutCommandMenuItemLabelCommand, ], exports: [ BackfillDatasourceToWorkspaceCommand, BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand, + DeduplicateEngineCommandsCommand, MigrateAiAgentTextToJsonResponseFormatCommand, UpdateEditLayoutCommandMenuItemLabelCommand, ], diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 12f1a45a48..154b64b80a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -27,6 +27,7 @@ import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upg import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command'; import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-datasource-to-workspace.command'; import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command'; +import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command'; import { MigrateAiAgentTextToJsonResponseFormatCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-ai-agent-text-to-json-response-format.command'; import { UpdateEditLayoutCommandMenuItemLabelCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-update-edit-layout-command-menu-item-label.command'; import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service'; @@ -73,6 +74,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 1.21 Commands protected readonly backfillDatasourceToWorkspaceCommand: BackfillDatasourceToWorkspaceCommand, protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand, + protected readonly deduplicateEngineCommandsCommand: DeduplicateEngineCommandsCommand, protected readonly migrateAiAgentTextToJsonResponseFormatCommand: MigrateAiAgentTextToJsonResponseFormatCommand, protected readonly updateEditLayoutCommandMenuItemLabelCommand: UpdateEditLayoutCommandMenuItemLabelCommand, ) { @@ -110,6 +112,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { const commands_1210: VersionCommands = [ this.backfillDatasourceToWorkspaceCommand, this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand, + this.deduplicateEngineCommandsCommand, this.migrateAiAgentTextToJsonResponseFormatCommand, this.updateEditLayoutCommandMenuItemLabelCommand, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts index 6509fd1fd6..6b751bf568 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts @@ -4,20 +4,15 @@ export enum EngineComponentKey { NAVIGATE_TO_NEXT_RECORD = 'NAVIGATE_TO_NEXT_RECORD', NAVIGATE_TO_PREVIOUS_RECORD = 'NAVIGATE_TO_PREVIOUS_RECORD', CREATE_NEW_RECORD = 'CREATE_NEW_RECORD', - DELETE_SINGLE_RECORD = 'DELETE_SINGLE_RECORD', - DELETE_MULTIPLE_RECORDS = 'DELETE_MULTIPLE_RECORDS', - RESTORE_SINGLE_RECORD = 'RESTORE_SINGLE_RECORD', - RESTORE_MULTIPLE_RECORDS = 'RESTORE_MULTIPLE_RECORDS', - DESTROY_SINGLE_RECORD = 'DESTROY_SINGLE_RECORD', - DESTROY_MULTIPLE_RECORDS = 'DESTROY_MULTIPLE_RECORDS', + DELETE_RECORDS = 'DELETE_RECORDS', + RESTORE_RECORDS = 'RESTORE_RECORDS', + DESTROY_RECORDS = 'DESTROY_RECORDS', ADD_TO_FAVORITES = 'ADD_TO_FAVORITES', REMOVE_FROM_FAVORITES = 'REMOVE_FROM_FAVORITES', EXPORT_NOTE_TO_PDF = 'EXPORT_NOTE_TO_PDF', - EXPORT_FROM_RECORD_INDEX = 'EXPORT_FROM_RECORD_INDEX', - EXPORT_FROM_RECORD_SHOW = 'EXPORT_FROM_RECORD_SHOW', + EXPORT_RECORDS = 'EXPORT_RECORDS', UPDATE_MULTIPLE_RECORDS = 'UPDATE_MULTIPLE_RECORDS', MERGE_MULTIPLE_RECORDS = 'MERGE_MULTIPLE_RECORDS', - EXPORT_MULTIPLE_RECORDS = 'EXPORT_MULTIPLE_RECORDS', IMPORT_RECORDS = 'IMPORT_RECORDS', EXPORT_VIEW = 'EXPORT_VIEW', SEE_DELETED_RECORDS = 'SEE_DELETED_RECORDS', @@ -60,6 +55,17 @@ export enum EngineComponentKey { VIEW_PREVIOUS_AI_CHATS = 'VIEW_PREVIOUS_AI_CHATS', TRIGGER_WORKFLOW_VERSION = 'TRIGGER_WORKFLOW_VERSION', FRONT_COMPONENT_RENDERER = 'FRONT_COMPONENT_RENDERER', + + // Deprecated keys kept for backward compatibility until migration runs + DELETE_SINGLE_RECORD = 'DELETE_SINGLE_RECORD', + DELETE_MULTIPLE_RECORDS = 'DELETE_MULTIPLE_RECORDS', + RESTORE_SINGLE_RECORD = 'RESTORE_SINGLE_RECORD', + RESTORE_MULTIPLE_RECORDS = 'RESTORE_MULTIPLE_RECORDS', + DESTROY_SINGLE_RECORD = 'DESTROY_SINGLE_RECORD', + DESTROY_MULTIPLE_RECORDS = 'DESTROY_MULTIPLE_RECORDS', + EXPORT_FROM_RECORD_INDEX = 'EXPORT_FROM_RECORD_INDEX', + EXPORT_FROM_RECORD_SHOW = 'EXPORT_FROM_RECORD_SHOW', + EXPORT_MULTIPLE_RECORDS = 'EXPORT_MULTIPLE_RECORDS', } registerEnumType(EngineComponentKey, { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts index 91b5725b68..ab8f06bd35 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts @@ -50,95 +50,49 @@ export const STANDARD_COMMAND_MENU_ITEMS = { engineComponentKey: EngineComponentKey.CREATE_NEW_RECORD, hotKeys: null, }, - deleteSingleRecord: { - universalIdentifier: '6652773f-b9a9-4fa3-a52c-e2f2e259e430', - label: 'Delete', + deleteRecords: { + universalIdentifier: 'd5a55d57-ed1d-4791-89b8-53b7e121d69d', + label: 'Delete ${capitalize(objectMetadataLabel)}', icon: 'IconTrash', isPinned: false, position: 3, shortLabel: 'Delete', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: - 'numberOfSelectedRecords == 1 and not hasAnySoftDeleteFilterOnView and objectPermissions.canSoftDeleteObjectRecords and noneDefined(selectedRecords, "deletedAt")', + 'numberOfSelectedRecords >= 1 and not hasAnySoftDeleteFilterOnView and objectPermissions.canSoftDeleteObjectRecords and noneDefined(selectedRecords, "deletedAt") and numberOfSelectedRecords < 10000', availabilityObjectMetadataUniversalIdentifier: null, frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.DELETE_SINGLE_RECORD, + engineComponentKey: EngineComponentKey.DELETE_RECORDS, hotKeys: null, }, - deleteMultipleRecords: { - universalIdentifier: 'cde86f1f-2c13-42b1-812b-f2b2b468cb83', - label: 'Delete ${capitalize(objectMetadataItem.labelPlural)}', - icon: 'IconTrash', - isPinned: false, - position: 4, - shortLabel: 'Delete', - availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - conditionalAvailabilityExpression: - 'numberOfSelectedRecords >= 2 and objectPermissions.canSoftDeleteObjectRecords and not hasAnySoftDeleteFilterOnView and numberOfSelectedRecords < 10000', - availabilityObjectMetadataUniversalIdentifier: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.DELETE_MULTIPLE_RECORDS, - hotKeys: null, - }, - restoreSingleRecord: { - universalIdentifier: '8b3a1cae-3e4d-43c1-a71f-48592b2e47ff', - label: 'Restore ${capitalize(objectMetadataItem.labelSingular)}', + restoreRecords: { + universalIdentifier: '2d733846-8cc5-4314-ab79-916ae0801baa', + label: 'Restore ${capitalize(objectMetadataLabel)}', icon: 'IconRefresh', isPinned: true, position: 5, shortLabel: 'Restore', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: - 'numberOfSelectedRecords == 1 and everyDefined(selectedRecords, "deletedAt") and objectPermissions.canSoftDeleteObjectRecords and (pageType == "RECORD_PAGE" or hasAnySoftDeleteFilterOnView)', + 'numberOfSelectedRecords >= 1 and everyDefined(selectedRecords, "deletedAt") and objectPermissions.canSoftDeleteObjectRecords and (pageType == "RECORD_PAGE" or hasAnySoftDeleteFilterOnView) and numberOfSelectedRecords < 10000', availabilityObjectMetadataUniversalIdentifier: null, frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.RESTORE_SINGLE_RECORD, + engineComponentKey: EngineComponentKey.RESTORE_RECORDS, hotKeys: null, }, - restoreMultipleRecords: { - universalIdentifier: '8b740c9d-d99a-45a8-812f-809caaf420ac', - label: 'Restore ${capitalize(objectMetadataItem.labelPlural)}', - icon: 'IconRefresh', - isPinned: true, - position: 6, - shortLabel: 'Restore', - availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - conditionalAvailabilityExpression: - 'numberOfSelectedRecords >= 2 and objectPermissions.canSoftDeleteObjectRecords and hasAnySoftDeleteFilterOnView and numberOfSelectedRecords < 10000', - availabilityObjectMetadataUniversalIdentifier: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.RESTORE_MULTIPLE_RECORDS, - hotKeys: null, - }, - destroySingleRecord: { - universalIdentifier: '44a78417-c394-4bc8-961f-98b503030ddb', - label: - 'Permanently destroy ${capitalize(objectMetadataItem.labelSingular)}', + destroyRecords: { + universalIdentifier: '0ea2ebc4-02ca-4d15-b424-5352b9e487df', + label: 'Permanently destroy ${capitalize(objectMetadataLabel)}', icon: 'IconTrashX', isPinned: false, position: 7, shortLabel: 'Destroy', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, conditionalAvailabilityExpression: - 'numberOfSelectedRecords == 1 and objectPermissions.canDestroyObjectRecords and everyDefined(selectedRecords, "deletedAt")', + 'numberOfSelectedRecords >= 1 and objectPermissions.canDestroyObjectRecords and everyDefined(selectedRecords, "deletedAt") and numberOfSelectedRecords < 10000', availabilityObjectMetadataUniversalIdentifier: null, frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.DESTROY_SINGLE_RECORD, - hotKeys: null, - }, - destroyMultipleRecords: { - universalIdentifier: 'c630b3fb-7920-40d1-9906-77d0aa797608', - label: 'Permanently destroy ${capitalize(objectMetadataItem.labelPlural)}', - icon: 'IconTrashX', - isPinned: false, - position: 8, - shortLabel: 'Destroy', - availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - conditionalAvailabilityExpression: - 'numberOfSelectedRecords >= 2 and objectPermissions.canDestroyObjectRecords and hasAnySoftDeleteFilterOnView and numberOfSelectedRecords < 10000', - availabilityObjectMetadataUniversalIdentifier: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.DESTROY_MULTIPLE_RECORDS, + engineComponentKey: EngineComponentKey.DESTROY_RECORDS, hotKeys: null, }, addToFavorites: { @@ -186,32 +140,18 @@ export const STANDARD_COMMAND_MENU_ITEMS = { engineComponentKey: EngineComponentKey.EXPORT_NOTE_TO_PDF, hotKeys: null, }, - exportFromRecordIndex: { - universalIdentifier: 'a934ba8a-ac8f-487d-9cd9-06dfdaec1f49', - label: 'Export', + exportRecords: { + universalIdentifier: 'c6f5c54d-d52b-4e75-8188-2190d77126f2', + label: 'Export ${capitalize(objectMetadataLabel)}', icon: 'IconFileExport', isPinned: false, position: 12, shortLabel: 'Export', availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - conditionalAvailabilityExpression: 'pageType == "INDEX_PAGE"', + conditionalAvailabilityExpression: null, availabilityObjectMetadataUniversalIdentifier: null, frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.EXPORT_FROM_RECORD_INDEX, - hotKeys: null, - }, - exportFromRecordShow: { - universalIdentifier: 'ba339455-f3c2-4ed1-bf77-3e316d7d6a66', - label: 'Export', - icon: 'IconFileExport', - isPinned: false, - position: 13, - shortLabel: 'Export', - availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - conditionalAvailabilityExpression: 'pageType == "RECORD_PAGE"', - availabilityObjectMetadataUniversalIdentifier: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.EXPORT_FROM_RECORD_SHOW, + engineComponentKey: EngineComponentKey.EXPORT_RECORDS, hotKeys: null, }, updateMultipleRecords: { @@ -244,20 +184,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = { engineComponentKey: EngineComponentKey.MERGE_MULTIPLE_RECORDS, hotKeys: null, }, - exportMultipleRecords: { - universalIdentifier: 'f71f68e5-7b6e-4c03-8161-c48434d7777c', - label: 'Export ${capitalize(objectMetadataItem.labelPlural)}', - icon: 'IconFileExport', - isPinned: false, - position: 16, - shortLabel: 'Export', - availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION, - conditionalAvailabilityExpression: 'numberOfSelectedRecords >= 2', - availabilityObjectMetadataUniversalIdentifier: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.EXPORT_MULTIPLE_RECORDS, - hotKeys: null, - }, importRecords: { universalIdentifier: 'a2dc9de7-4798-422e-bb55-bfad7b9bdbe8', label: 'Import ${capitalize(objectMetadataItem.labelPlural)}',