From e613b15c5a66d81ae99b0a63069c063a55dbce65 Mon Sep 17 00:00:00 2001 From: Ranjeet Baraik <84462743+anadi45@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:33:11 +0530 Subject: [PATCH] fix: duplicate merge button bug (#15284) Fixes - https://github.com/twentyhq/twenty/issues/15263 - Replaced `useLoadSelectedRecordsInContextStore` with `useLoadMergeRecords` in `useOpenMergeRecordsPageInCommandMenu` for improved functionality. - Updated `useMergePreview`, `useMergeRecordsActions`, and `useMergeRecordsSettings` to utilize `mergeRecordsState` instead of the deprecated context store hook. - Cleaned up imports and ensured consistency across merge-related hooks. https://github.com/user-attachments/assets/453539c9-7f2b-4e8c-bfa1-3ceebca07081 --------- Co-authored-by: Charles Bochet --- .../constants/DefaultRecordActionsConfig.tsx | 11 +-- .../components/MergeMultipleRecordsAction.tsx | 31 ++++++++ .../hooks/useSelectedRecordIds.tsx | 18 +++++ ...mmandMenuContextChipRecordSetterEffect.tsx | 78 ------------------- .../components/CommandMenuRouter.tsx | 2 - ...MenuCloseAnimationCompleteCleanup.test.tsx | 32 ++------ .../useOpenRecordInCommandMenu.test.tsx | 22 +++--- .../__tests__/useWorkflowCommandMenu.test.tsx | 8 +- ...ommandMenuCloseAnimationCompleteCleanup.ts | 12 ++- .../hooks/useCommandMenuContextChips.tsx | 72 ++++++++++------- .../hooks/useCommandMenuHistory.ts | 21 ++--- ...ndMenuUpdateNavigationMorphItemsByPage.tsx | 45 +++++++++++ .../hooks/useNavigateCommandMenu.ts | 6 +- .../useOpenMergeRecordsPageInCommandMenu.tsx | 31 ++++++-- .../hooks/useOpenRecordInCommandMenu.ts | 10 +-- .../components/CommandMenuMergeRecordPage.tsx | 23 ++---- ...andMenuNavigationMorphItemsByPageState.ts} | 6 +- .../commandMenuNavigationRecordsState.ts | 13 ---- .../useLoadSelectedRecordsInContextStore.tsx | 74 ------------------ .../hooks/useMergeManyRecords.ts | 7 +- .../RecordDetailRecordsListItemContainer.tsx | 2 + .../RecordDetailRelationRecordsListItem.tsx | 3 - .../components/MergeRecordsContainer.tsx | 31 +++++--- .../components/MergeSettingsTab.tsx | 12 ++- .../record-merge/hooks/useMergePreview.ts | 48 ++++++++---- .../hooks/useMergeRecordsActions.ts | 11 ++- .../hooks/useMergeRecordsSettings.ts | 7 -- .../record-show/components/SummaryCard.tsx | 1 - .../recordStoreIdentifierSelector.ts | 12 ++- .../recordStoreIdentifiersSelector.ts | 37 +++++++++ .../selectors/recordStoreRecordsSelector.ts | 16 ++++ packages/twenty-shared/src/utils/index.ts | 1 + .../strings/__tests__/uncapitalize.test.ts | 10 +++ .../src/utils/strings/uncapitalize.ts | 3 + 34 files changed, 366 insertions(+), 350 deletions(-) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MergeMultipleRecordsAction.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIds.tsx delete mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipRecordSetterEffect.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx rename packages/twenty-front/src/modules/command-menu/states/{commandMenuNavigationMorphItemsState.ts => commandMenuNavigationMorphItemsByPageState.ts} (50%) delete mode 100644 packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationRecordsState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifiersSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreRecordsSelector.ts create mode 100644 packages/twenty-shared/src/utils/strings/__tests__/uncapitalize.test.ts create mode 100644 packages/twenty-shared/src/utils/strings/uncapitalize.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx index 28b4c9af685..959441dbaac 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx @@ -1,8 +1,8 @@ import { ActionLink } from '@/action-menu/actions/components/ActionLink'; -import { ActionOpenSidePanelPage } from '@/action-menu/actions/components/ActionOpenSidePanelPage'; import { DeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction'; import { DestroyMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction'; import { ExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/components/ExportMultipleRecordsAction'; +import { MergeMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/components/MergeMultipleRecordsAction'; import { RestoreMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction'; import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys'; import { CreateNewTableRecordNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/CreateNewTableRecordNoSelectionRecordAction'; @@ -25,7 +25,6 @@ import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig'; import { ActionScope } from '@/action-menu/actions/types/ActionScope'; import { ActionType } from '@/action-menu/actions/types/ActionType'; import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; -import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { BACKEND_BATCH_REQUEST_MAX_COUNT } from '@/object-record/constants/BackendBatchRequestMaxCount'; @@ -199,13 +198,7 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< Boolean(objectPermissions.canDestroyObjectRecords) && numberOfSelectedRecords <= MUTATION_MAX_MERGE_RECORDS, availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION], - component: ( - - ), + component: , }, [MultipleRecordsActionKeys.EXPORT]: { type: ActionType.Standard, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MergeMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MergeMultipleRecordsAction.tsx new file mode 100644 index 00000000000..0f46c5f2144 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MergeMultipleRecordsAction.tsx @@ -0,0 +1,31 @@ +import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDisplay'; +import { useSelectedRecordIds } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIds'; +import { useOpenMergeRecordsPageInCommandMenu } from '@/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu'; +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; + +export const MergeMultipleRecordsAction = () => { + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const contextStoreCurrentViewId = useRecoilComponentValue( + contextStoreCurrentViewIdComponentState, + ); + + if (!contextStoreCurrentViewId) { + throw new Error('Current view ID is not defined'); + } + const selectedRecordIds = useSelectedRecordIds(); + + const { openMergeRecordsPageInCommandMenu } = + useOpenMergeRecordsPageInCommandMenu({ + objectNameSingular: objectMetadataItem.nameSingular, + objectRecordIds: selectedRecordIds, + }); + + const handleClick = () => { + openMergeRecordsPageInCommandMenu(); + }; + + return ; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIds.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIds.tsx new file mode 100644 index 00000000000..1f44b822d58 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIds.tsx @@ -0,0 +1,18 @@ +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; + +export const useSelectedRecordIds = () => { + const contextStoreTargetedRecordsRule = useRecoilComponentValue( + contextStoreTargetedRecordsRuleComponentState, + ); + + if ( + contextStoreTargetedRecordsRule.mode === 'exclusion' || + (contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length === 0) + ) { + return []; + } + + return contextStoreTargetedRecordsRule.selectedRecordIds; +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipRecordSetterEffect.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipRecordSetterEffect.tsx deleted file mode 100644 index 8ae24efb78b..00000000000 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipRecordSetterEffect.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; -import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -export const CommandMenuContextChipRecordSetterEffect = () => { - const commandMenuNavigationMorphItemByPage = useRecoilValue( - commandMenuNavigationMorphItemByPageState, - ); - - const setCommandMenuNavigationRecords = useSetRecoilState( - commandMenuNavigationRecordsState, - ); - - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - - const commandMenuNavigationStack = useRecoilValue( - commandMenuNavigationStackState, - ); - - const apolloCoreClient = useApolloCoreClient(); - - useEffect(() => { - if (commandMenuNavigationStack.length > 1) { - const morphItems = Array.from( - commandMenuNavigationMorphItemByPage.values(), - ); - - const records = morphItems - .map((morphItem) => { - const objectMetadataItem = objectMetadataItems.find( - ({ id }) => id === morphItem.objectMetadataId, - ); - - if (!objectMetadataItem) { - return null; - } - - const record = getRecordFromCache({ - recordId: morphItem.recordId, - cache: apolloCoreClient.cache, - objectMetadataItems, - objectMetadataItem, - objectPermissionsByObjectMetadataId, - }); - - if (!record) { - return null; - } - - return { - objectMetadataItem, - record, - }; - }) - .filter(isDefined); - - setCommandMenuNavigationRecords(records); - } - }, [ - apolloCoreClient.cache, - commandMenuNavigationMorphItemByPage, - commandMenuNavigationStack, - commandMenuNavigationStack.length, - objectMetadataItems, - setCommandMenuNavigationRecords, - objectPermissionsByObjectMetadataId, - ]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx index cd43aeec879..7a238216ada 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx @@ -1,6 +1,5 @@ import { ActionMenuContextProvider } from '@/action-menu/contexts/ActionMenuContextProvider'; import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer'; -import { CommandMenuContextChipRecordSetterEffect } from '@/command-menu/components/CommandMenuContextChipRecordSetterEffect'; import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar'; import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; @@ -31,7 +30,6 @@ export const CommandMenuRouter = () => { return ( - diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx index 01e6a1b6770..095e20884c4 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx @@ -7,8 +7,7 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId'; import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId'; import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; -import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; @@ -73,11 +72,9 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { const commandMenuNavigationStack = useRecoilValue( commandMenuNavigationStackState, ); - const commandMenuNavigationRecords = useRecoilValue( - commandMenuNavigationRecordsState, - ); - const commandMenuNavigationMorphItemByPage = useRecoilValue( - commandMenuNavigationMorphItemByPageState, + + const commandMenuNavigationMorphItemsByPage = useRecoilValue( + commandMenuNavigationMorphItemsByPageState, ); const hasUserSelectedCommand = useRecoilValue( hasUserSelectedCommandState, @@ -97,9 +94,7 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { const setCommandMenuNavigationStack = useSetRecoilState( commandMenuNavigationStackState, ); - const setCommandMenuNavigationRecords = useSetRecoilState( - commandMenuNavigationRecordsState, - ); + const setHasUserSelectedCommand = useSetRecoilState( hasUserSelectedCommandState, ); @@ -115,8 +110,7 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { isCommandMenuOpened, commandMenuSearch, commandMenuNavigationStack, - commandMenuNavigationRecords, - commandMenuNavigationMorphItemByPage, + commandMenuNavigationMorphItemsByPage, hasUserSelectedCommand, isCommandMenuClosing, viewableRecordId, @@ -125,7 +119,6 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { setIsCommandMenuOpened, setCommandMenuSearch, setCommandMenuNavigationStack, - setCommandMenuNavigationRecords, setHasUserSelectedCommand, setIsCommandMenuClosing, setViewableRecordId, @@ -158,12 +151,6 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { pageId: '1', }, ]); - result.current.setCommandMenuNavigationRecords([ - { - objectMetadataItem: { id: '1', nameSingular: 'Record' } as any, - record: { id: '1' } as any, - }, - ]); result.current.setHasUserSelectedCommand(true); result.current.setIsCommandMenuClosing(true); result.current.setViewableRecordId('record-123'); @@ -185,12 +172,6 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { pageId: '1', }, ]); - expect(result.current.commandMenuNavigationRecords).toEqual([ - { - objectMetadataItem: { id: '1', nameSingular: 'Record' } as any, - record: { id: '1' } as any, - }, - ]); expect(result.current.hasUserSelectedCommand).toBe(true); expect(result.current.isCommandMenuClosing).toBe(true); expect(result.current.viewableRecordId).toBe('record-123'); @@ -208,7 +189,6 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => { expect(result.current.isCommandMenuOpened).toBe(false); expect(result.current.commandMenuSearch).toBe(''); expect(result.current.commandMenuNavigationStack).toEqual([]); - expect(result.current.commandMenuNavigationRecords).toEqual([]); expect(result.current.hasUserSelectedCommand).toBe(false); expect(result.current.isCommandMenuClosing).toBe(false); expect(result.current.viewableRecordId).toBe(null); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx index d0a984c8c82..afe012bf6e4 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx @@ -6,7 +6,7 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; @@ -54,8 +54,8 @@ const renderHooks = () => { const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const commandMenuPage = useRecoilValue(commandMenuPageState); - const commandMenuNavigationMorphItemByPage = useRecoilValue( - commandMenuNavigationMorphItemByPageState, + const commandMenuNavigationMorphItemsByPage = useRecoilValue( + commandMenuNavigationMorphItemsByPageState, ); const viewableRecordId = useRecoilComponentValue( @@ -88,7 +88,7 @@ const renderHooks = () => { openRecordInCommandMenu, viewableRecordId, commandMenuPage, - commandMenuNavigationMorphItemByPage, + commandMenuNavigationMorphItemsByPage, viewableRecordNameSingular, currentObjectMetadataItemId, targetedRecordsRule, @@ -134,13 +134,15 @@ describe('useOpenRecordInCommandMenu', () => { expect(result.current.numberOfSelectedRecords).toBe(1); expect(result.current.currentViewType).toBe(ContextStoreViewType.ShowPage); - expect(result.current.commandMenuNavigationMorphItemByPage.size).toBe(1); + expect(result.current.commandMenuNavigationMorphItemsByPage.size).toBe(1); expect( - result.current.commandMenuNavigationMorphItemByPage.get('mocked-uuid'), - ).toEqual({ - objectMetadataId: personMockObjectMetadataItem.id, - recordId, - }); + result.current.commandMenuNavigationMorphItemsByPage.get('mocked-uuid'), + ).toEqual([ + { + objectMetadataId: personMockObjectMetadataItem.id, + recordId, + }, + ]); expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ page: CommandMenuPages.ViewRecord, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx index 8ee72637a7d..3e95ea1d43c 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx @@ -6,7 +6,7 @@ import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState'; import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; @@ -68,8 +68,8 @@ const renderHooks = () => { openWorkflowViewStepInCommandMenu, } = useWorkflowCommandMenu(); const commandMenuPage = useRecoilValue(commandMenuPageState); - const commandMenuNavigationMorphItemByPage = useRecoilValue( - commandMenuNavigationMorphItemByPageState, + const commandMenuNavigationMorphItemsByPage = useRecoilValue( + commandMenuNavigationMorphItemsByPageState, ); const viewableRecordId = useRecoilComponentValue( @@ -116,7 +116,7 @@ const renderHooks = () => { workflowVersionId, viewableRecordId, commandMenuPage, - commandMenuNavigationMorphItemByPage, + commandMenuNavigationMorphItemsByPage, viewableRecordNameSingular, currentObjectMetadataItemId, targetedRecordsRule, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts index dea70b558e6..ccfcbd2b7ae 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts @@ -3,8 +3,7 @@ import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/con import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId'; import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId'; import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; -import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; @@ -91,8 +90,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => { }); set(isCommandMenuOpenedState, false); set(commandMenuSearchState, ''); - set(commandMenuNavigationMorphItemByPageState, new Map()); - set(commandMenuNavigationRecordsState, []); + set(commandMenuNavigationMorphItemsByPageState, new Map()); set(commandMenuNavigationStackState, []); resetSelectedItem(); set(hasUserSelectedCommandState, false); @@ -106,14 +104,14 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => { WorkflowServerlessFunctionTabId.CODE, ); - for (const [pageId, morphItem] of snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) + for (const [pageId, morphItems] of snapshot + .getLoadable(commandMenuNavigationMorphItemsByPageState) .getValue()) { set( activeTabIdComponentState.atomFamily({ instanceId: getShowPageTabListComponentId({ pageId, - targetObjectId: morphItem.recordId, + targetObjectId: morphItems[0].recordId, }), }), null, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx index 77708400d4e..eca4b5ebbec 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx @@ -1,10 +1,11 @@ import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars'; import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; -import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { recordStoreIdentifiersFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifiersSelector'; +import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useMemo } from 'react'; @@ -24,16 +25,31 @@ export const useCommandMenuContextChips = () => { commandMenuNavigationStackState, ); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const { navigateCommandMenuHistory } = useCommandMenuHistory(); const theme = useTheme(); - const commandMenuNavigationMorphItemByPage = useRecoilValue( - commandMenuNavigationMorphItemByPageState, + const commandMenuNavigationMorphItemsByPage = useRecoilValue( + commandMenuNavigationMorphItemsByPageState, ); - const commandMenuNavigationRecords = useRecoilValue( - commandMenuNavigationRecordsState, + const allRecordIds = Array.from( + commandMenuNavigationMorphItemsByPage.entries(), + ).flatMap(([, morphItems]) => + morphItems.map((morphItem) => morphItem.recordId), + ); + + const recordIdentifiers = useRecoilValue( + recordStoreIdentifiersFamilySelector({ + recordIds: allRecordIds, + }), + ); + const records = useRecoilValue( + recordStoreRecordsSelector({ + recordIds: allRecordIds, + }), ); const contextChips = useMemo(() => { @@ -51,32 +67,34 @@ export const useCommandMenuContextChips = () => { if (isRecordPage && !isLastChip) { const commandMenuNavigationMorphItem = - commandMenuNavigationMorphItemByPage.get(page.pageId); + commandMenuNavigationMorphItemsByPage.get(page.pageId)?.[0]; if (!isDefined(commandMenuNavigationMorphItem?.recordId)) { return null; } - const objectMetadataItem = commandMenuNavigationRecords.find( - ({ objectMetadataItem }) => - objectMetadataItem.id === - commandMenuNavigationMorphItem.objectMetadataId, - )?.objectMetadataItem; + const objectMetadataItem = objectMetadataItems.find( + (item) => + item.id === commandMenuNavigationMorphItem.objectMetadataId, + ); - const record = commandMenuNavigationRecords.find( - ({ record }) => - record.id === commandMenuNavigationMorphItem.recordId, - )?.record; + const recordIdentifier = recordIdentifiers.find( + (recordIdentifier) => + recordIdentifier.id === commandMenuNavigationMorphItem.recordId, + ); - if (!isDefined(objectMetadataItem) || !isDefined(record)) { + const record = records.find( + (record) => record.id === commandMenuNavigationMorphItem.recordId, + ); + + if ( + !isDefined(objectMetadataItem) || + !isDefined(recordIdentifier) || + !isDefined(record) + ) { return null; } - const name = getObjectRecordIdentifier({ - objectMetadataItem, - record, - }).name; - return { page, Icons: [ @@ -85,7 +103,7 @@ export const useCommandMenuContextChips = () => { record={record} />, ], - text: name, + text: recordIdentifier.name, onClick: () => { navigateCommandMenuHistory(index); }, @@ -119,10 +137,12 @@ export const useCommandMenuContextChips = () => { }) .filter(isDefined); }, [ - commandMenuNavigationMorphItemByPage, - commandMenuNavigationRecords, + commandMenuNavigationMorphItemsByPage, commandMenuNavigationStack, navigateCommandMenuHistory, + objectMetadataItems, + recordIdentifiers, + records, theme.font.color.tertiary, theme.icon.size.sm, ]); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts index c9002abd744..a76c39521b3 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts @@ -1,13 +1,14 @@ import { useRecoilCallback } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { isNonEmptyArray } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; export const useCommandMenuHistory = () => { @@ -39,7 +40,7 @@ export const useCommandMenuHistory = () => { set(commandMenuNavigationStackState, newNavigationStack); const currentMorphItems = snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) + .getLoadable(commandMenuNavigationMorphItemsByPageState) .getValue(); if (currentNavigationStack.length > 0) { @@ -48,15 +49,15 @@ export const useCommandMenuHistory = () => { if (isDefined(removedItem)) { const newMorphItems = new Map(currentMorphItems); newMorphItems.delete(removedItem.pageId); - set(commandMenuNavigationMorphItemByPageState, newMorphItems); + set(commandMenuNavigationMorphItemsByPageState, newMorphItems); - const morphItem = currentMorphItems.get(removedItem.pageId); - if (isDefined(morphItem)) { + const morphItems = currentMorphItems.get(removedItem.pageId); + if (isNonEmptyArray(morphItems)) { set( activeTabIdComponentState.atomFamily({ instanceId: getShowPageTabListComponentId({ pageId: removedItem.pageId, - targetObjectId: morphItem.recordId, + targetObjectId: morphItems[0].recordId, }), }), null, @@ -96,16 +97,16 @@ export const useCommandMenuHistory = () => { instanceId: newNavigationStackItem.pageId, }); const currentMorphItems = snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) + .getLoadable(commandMenuNavigationMorphItemsByPageState) .getValue(); - for (const [pageId, morphItem] of currentMorphItems.entries()) { + for (const [pageId, morphItems] of currentMorphItems.entries()) { if (!newNavigationStack.some((item) => item.pageId === pageId)) { set( activeTabIdComponentState.atomFamily({ instanceId: getShowPageTabListComponentId({ pageId, - targetObjectId: morphItem.recordId, + targetObjectId: morphItems[0].recordId, }), }), null, @@ -119,7 +120,7 @@ export const useCommandMenuHistory = () => { ), ); - set(commandMenuNavigationMorphItemByPageState, newMorphItems); + set(commandMenuNavigationMorphItemsByPageState, newMorphItems); set(hasUserSelectedCommandState, false); }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx new file mode 100644 index 00000000000..bc1b248ea7f --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx @@ -0,0 +1,45 @@ +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; +import { isNonEmptyArray } from '@sniptt/guards'; +import { useRecoilCallback } from 'recoil'; + +type UpdateNavigationMorphItemsByPageParams = { + pageId: string; + objectMetadataId: string; + objectRecordIds: string[]; +}; + +export const useCommandMenuUpdateNavigationMorphItemsByPage = () => { + const updateCommandMenuNavigationMorphItemsByPage = useRecoilCallback( + ({ set, snapshot }) => + async ({ + pageId, + objectMetadataId, + objectRecordIds, + }: UpdateNavigationMorphItemsByPageParams) => { + const currentMorphItems = snapshot + .getLoadable(commandMenuNavigationMorphItemsByPageState) + .getValue(); + + const currentMorphItemsForPage = currentMorphItems.get(pageId); + + const newMorphItems = [ + ...(isNonEmptyArray(currentMorphItemsForPage) + ? currentMorphItemsForPage + : []), + ...objectRecordIds.map((recordId) => ({ + objectMetadataId, + recordId, + })), + ]; + + const newMorphItemsMap = new Map(currentMorphItems); + newMorphItemsMap.set(pageId, newMorphItems); + set(commandMenuNavigationMorphItemsByPageState, newMorphItemsMap); + }, + [], + ); + + return { + updateCommandMenuNavigationMorphItemsByPage, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts index 4ac6ec84df6..76fd761c78a 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts @@ -2,8 +2,7 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId'; import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; -import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; @@ -122,8 +121,7 @@ export const useNavigateCommandMenu = () => { }, ]); - set(commandMenuNavigationRecordsState, []); - set(commandMenuNavigationMorphItemByPageState, new Map()); + set(commandMenuNavigationMorphItemsByPageState, new Map()); } else { set(commandMenuNavigationStackState, [ ...currentNavigationStack, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx index 2ef26c25d55..04e5198ef8a 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx @@ -1,7 +1,9 @@ +import { useCommandMenuUpdateNavigationMorphItemsByPage } from '@/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage'; import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useLoadSelectedRecordsInContextStore } from '@/object-record/hooks/useLoadSelectedRecordsInContextStore'; +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { msg, t } from '@lingui/core/macro'; import { IconArrowMerge } from 'twenty-ui/display'; @@ -19,21 +21,34 @@ export const useOpenMergeRecordsPageInCommandMenu = ({ }); const { navigateCommandMenu } = useNavigateCommandMenu(); + const { updateCommandMenuNavigationMorphItemsByPage } = + useCommandMenuUpdateNavigationMorphItemsByPage(); - const { loadSelectedRecordsInContextStore } = - useLoadSelectedRecordsInContextStore({ - objectNameSingular, - objectRecordIds, - objectMetadataItemId: objectMetadataItem.id, - }); + const { upsertRecordsInStore } = useUpsertRecordsInStore(); + + const { findManyRecordsLazy } = useLazyFindManyRecords({ + objectNameSingular, + filter: { + id: { + in: objectRecordIds, + }, + }, + }); const openMergeRecordsPageInCommandMenu = async () => { - await loadSelectedRecordsInContextStore(); + await updateCommandMenuNavigationMorphItemsByPage({ + pageId: CommandMenuPages.MergeRecords, + objectMetadataId: objectMetadataItem.id, + objectRecordIds, + }); + const { records } = await findManyRecordsLazy(); + upsertRecordsInStore(records ?? []); navigateCommandMenu({ page: CommandMenuPages.MergeRecords, pageTitle: t(msg`Merge records`), pageIcon: IconArrowMerge, + pageId: CommandMenuPages.MergeRecords, }); }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts index 8c5ced59677..8d0a66392b8 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts @@ -1,7 +1,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; @@ -142,7 +142,7 @@ export const useOpenRecordInCommandMenu = () => { ); const currentMorphItems = snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) + .getLoadable(commandMenuNavigationMorphItemsByPageState) .getValue(); const morphItemToAdd = { @@ -150,10 +150,10 @@ export const useOpenRecordInCommandMenu = () => { recordId, }; - const newMorphItems = new Map(currentMorphItems); - newMorphItems.set(pageComponentInstanceId, morphItemToAdd); + const newMorphItemsMap = new Map(currentMorphItems); + newMorphItemsMap.set(pageComponentInstanceId, [morphItemToAdd]); - set(commandMenuNavigationMorphItemByPageState, newMorphItems); + set(commandMenuNavigationMorphItemsByPageState, newMorphItemsMap); const Icon = objectMetadataItem?.icon ? getIcon(objectMetadataItem.icon) diff --git a/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx index 3c9ebef8fca..ea97c77ab63 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx @@ -1,8 +1,6 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; -import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { MergeRecordsContainer } from '@/object-record/record-merge/components/MergeRecordsContainer'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -36,20 +34,15 @@ export const CommandMenuMergeRecordPage = () => { - - - - - - - + + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationMorphItemsState.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationMorphItemsByPageState.ts similarity index 50% rename from packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationMorphItemsState.ts rename to packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationMorphItemsByPageState.ts index 5162c2e15ad..0709e3c6f36 100644 --- a/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationMorphItemsState.ts +++ b/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationMorphItemsByPageState.ts @@ -1,9 +1,9 @@ import { type MorphItem } from '@/object-record/multiple-objects/types/MorphItem'; import { createState } from 'twenty-ui/utilities'; -export const commandMenuNavigationMorphItemByPageState = createState< - Map +export const commandMenuNavigationMorphItemsByPageState = createState< + Map >({ - key: 'command-menu/commandMenuNavigationMorphItemByPageState', + key: 'command-menu/commandMenuNavigationMorphItemsByPageState', defaultValue: new Map(), }); diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationRecordsState.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationRecordsState.ts deleted file mode 100644 index 26053a8922f..00000000000 --- a/packages/twenty-front/src/modules/command-menu/states/commandMenuNavigationRecordsState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { createState } from 'twenty-ui/utilities'; - -export const commandMenuNavigationRecordsState = createState< - { - objectMetadataItem: ObjectMetadataItem; - record: ObjectRecord; - }[] ->({ - key: 'command-menu/commandMenuNavigationRecordsState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx b/packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx deleted file mode 100644 index b4c05b367d8..00000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; -import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; -import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; -import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; -import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { useRecoilCallback } from 'recoil'; - -type UseLoadSelectedRecordsInContextStoreProps = { - objectNameSingular: string; - objectRecordIds: string[]; - objectMetadataItemId: string; -}; - -export const useLoadSelectedRecordsInContextStore = ({ - objectNameSingular, - objectRecordIds, - objectMetadataItemId, -}: UseLoadSelectedRecordsInContextStoreProps) => { - const { upsertRecordsInStore } = useUpsertRecordsInStore(); - - const { findManyRecordsLazy } = useLazyFindManyRecords({ - objectNameSingular, - filter: { - id: { - in: objectRecordIds, - }, - }, - }); - - const loadSelectedRecordsInContextStore = useRecoilCallback( - ({ set }) => { - return async () => { - set( - contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({ - instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, - }), - objectMetadataItemId, - ); - - set( - contextStoreTargetedRecordsRuleComponentState.atomFamily({ - instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, - }), - { - mode: 'selection', - selectedRecordIds: objectRecordIds, - }, - ); - - set( - contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ - instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, - }), - objectRecordIds.length, - ); - - const { records } = await findManyRecordsLazy(); - - upsertRecordsInStore(records ?? []); - }; - }, - [ - objectRecordIds, - objectMetadataItemId, - findManyRecordsLazy, - upsertRecordsInStore, - ], - ); - - return { - loadSelectedRecordsInContextStore, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts index 1e77c6b269e..6c022e00b30 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts @@ -103,12 +103,11 @@ export const useMergeManyRecords = < if (!preview) { await refetchAggregateQueries(); + registerObjectOperation(objectNameSingular, { + type: 'merge-records', + }); } - registerObjectOperation(objectNameSingular, { - type: 'merge-records', - }); - return mergedObject.data?.[mutationResponseField] ?? null; } catch (error) { setLoading(false); diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListItemContainer.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListItemContainer.tsx index 99a76403585..7586427243c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListItemContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListItemContainer.tsx @@ -6,6 +6,8 @@ const StyledListItem = styled.div` gap: ${({ theme }) => theme.spacing(1)}; display: flex; height: ${({ theme }) => theme.spacing(10)}; + padding-left: ${({ theme }) => theme.spacing(3)}; + padding-right: ${({ theme }) => theme.spacing(2)}; `; export { StyledListItem as RecordDetailRecordsListItemContainer }; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx index 1346356b9f7..359c1dd7a76 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx @@ -60,9 +60,6 @@ const StyledListItem = styled(RecordDetailRecordsListItemContainer)<{ } `} - padding-left: ${({ theme }) => theme.spacing(3)}; - padding-right: ${({ theme }) => theme.spacing(2)}; - &:hover { .displayOnHover { opacity: 1; diff --git a/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordsContainer.tsx index 7188c766326..2c10579cd3f 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordsContainer.tsx @@ -7,8 +7,10 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useMergeRecordsSettings } from '@/object-record/record-merge/hooks/useMergeRecordsSettings'; +import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; +import { useMergePreview } from '@/object-record/record-merge/hooks/useMergePreview'; import { MergeRecordsTabId } from '@/object-record/record-merge/types/MergeRecordsTabId'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useMergeRecordsContainerTabs } from '../hooks/useMergeRecordsContainerTabs'; import { MergePreviewTab } from './MergePreviewTab'; import { MergeRecordTab } from './MergeRecordTab'; @@ -37,41 +39,46 @@ const StyledContentContainer = styled.div` `; type MergeRecordsContainerProps = { - componentInstanceId: string; objectNameSingular: string; }; export const MergeRecordsContainer = ({ - componentInstanceId, objectNameSingular, }: MergeRecordsContainerProps) => { - const { selectedRecords } = useMergeRecordsSettings(); - - const activeTabId = useRecoilComponentValue( - activeTabIdComponentState, - componentInstanceId, - ); + const { selectedRecords } = useMergePreview({ + objectNameSingular, + }); const { tabs } = useMergeRecordsContainerTabs(selectedRecords); + const instanceId = useAvailableComponentInstanceIdOrThrow( + CommandMenuPageComponentInstanceContext, + ); + const activeTabId = useRecoilComponentValue( + activeTabIdComponentState, + instanceId, + ); + return ( {activeTabId === MergeRecordsTabId.MERGE_PREVIEW && ( )} - {activeTabId === MergeRecordsTabId.SETTINGS && } + {activeTabId === MergeRecordsTabId.SETTINGS && ( + + )} {selectedRecords.some((record) => record.id === activeTabId) && ( { - const { mergeSettings, updatePriorityRecordIndex, selectedRecords } = +export const MergeSettingsTab = ({ + objectNameSingular, +}: { + objectNameSingular: string; +}) => { + const { mergeSettings, updatePriorityRecordIndex } = useMergeRecordsSettings(); + const { selectedRecords } = useMergePreview({ + objectNameSingular, + }); const priorityOptions = selectedRecords.map((_, index) => ({ value: index, diff --git a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergePreview.ts b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergePreview.ts index 357121a3a18..f3039428331 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergePreview.ts +++ b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergePreview.ts @@ -1,11 +1,12 @@ -import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { useMergeManyRecords } from '@/object-record/hooks/useMergeManyRecords'; import { useMergeRecordRelationships } from '@/object-record/record-merge/hooks/useMergeRecordRelationships'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { useEffect, useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { isMergeInProgressState } from '../states/mergeInProgressState'; import { mergeSettingsState } from '../states/mergeSettingsState'; @@ -19,18 +20,29 @@ export const useMergePreview = ({ const [mergePreviewRecord, setMergePreviewRecord] = useState(null); const [isGeneratingPreview, setIsGeneratingPreview] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); const mergeSettings = useRecoilValue(mergeSettingsState); const isMergeInProgress = useRecoilValue(isMergeInProgressState); - const { records: selectedRecords } = useFindManyRecordsSelectedInContextStore( - { - limit: 10, - }, - ); const { mergeManyRecords } = useMergeManyRecords({ objectNameSingular, }); + + const commandMenuNavigationMorphItemsByPage = useRecoilValue( + commandMenuNavigationMorphItemsByPageState, + ); + + const selectedRecordIds = + commandMenuNavigationMorphItemsByPage + .get(CommandMenuPages.MergeRecords) + ?.map((morphItem) => morphItem.recordId) ?? []; + const selectedRecords = useRecoilValue( + recordStoreRecordsSelector({ + recordIds: selectedRecordIds, + }), + ); + const { upsertRecordsInStore } = useUpsertRecordsInStore(); const { isLoading: isLoadingRelationships } = useMergeRecordRelationships({ @@ -41,7 +53,9 @@ export const useMergePreview = ({ useEffect(() => { const fetchPreview = async () => { - if (selectedRecords.length < 2 || isMergeInProgress) return; + if (selectedRecords.length < 2 || isMergeInProgress || isInitialized) + return; + setIsGeneratingPreview(true); try { const previewRecord = await mergeManyRecords({ @@ -49,28 +63,34 @@ export const useMergePreview = ({ mergeSettings, preview: true, }); - if (!previewRecord) { setMergePreviewRecord(null); return; } - setMergePreviewRecord(previewRecord); upsertRecordsInStore([previewRecord]); } catch { setMergePreviewRecord(null); } finally { setIsGeneratingPreview(false); + setIsInitialized(true); } }; if (selectedRecords.length > 0 && !isMergeInProgress) { fetchPreview(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedRecords, mergeSettings]); + }, [ + selectedRecords, + mergeSettings, + isMergeInProgress, + mergeManyRecords, + upsertRecordsInStore, + isInitialized, + ]); return { + selectedRecords, mergePreviewRecord, isGeneratingPreview: isGeneratingPreview || isLoadingRelationships, }; diff --git a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts index e686c761f3e..7c6b10f59b5 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts @@ -2,8 +2,8 @@ import { useLingui } from '@lingui/react/macro'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; import { useMergeManyRecords } from '@/object-record/hooks/useMergeManyRecords'; +import { useMergePreview } from '@/object-record/record-merge/hooks/useMergePreview'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { AppPath } from 'twenty-shared/types'; import { useNavigateApp } from '~/hooks/useNavigateApp'; @@ -18,11 +18,10 @@ export const useMergeRecordsActions = ({ objectNameSingular, }: UseMergeRecordsActionsProps) => { const mergeSettings = useRecoilValue(mergeSettingsState); - const { records: selectedRecords } = useFindManyRecordsSelectedInContextStore( - { - limit: 10, - }, - ); + + const { selectedRecords } = useMergePreview({ + objectNameSingular, + }); const { mergeManyRecords, loading: isMerging } = useMergeManyRecords({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsSettings.ts b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsSettings.ts index b252bfd8122..0d7033d91bd 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsSettings.ts +++ b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsSettings.ts @@ -1,16 +1,10 @@ import { useRecoilState } from 'recoil'; -import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; import { type MergeManySettings } from '@/object-record/hooks/useMergeManyRecords'; import { mergeSettingsState } from '../states/mergeSettingsState'; export const useMergeRecordsSettings = () => { const [mergeSettings, setMergeSettings] = useRecoilState(mergeSettingsState); - const { records: selectedRecords } = useFindManyRecordsSelectedInContextStore( - { - limit: 10, - }, - ); const updateMergeSettings = (settings: MergeManySettings) => { setMergeSettings(settings); @@ -24,7 +18,6 @@ export const useMergeRecordsSettings = () => { }; return { - selectedRecords, mergeSettings, updateMergeSettings, updatePriorityRecordIndex, diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx index 74ebfa2e100..597aba92b94 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx @@ -50,7 +50,6 @@ export const SummaryCard = ({ const recordIdentifier = useRecoilValue( recordStoreIdentifierFamilySelector({ - objectNameSingular, recordId: objectRecordId, }), ); diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts index 24502a28810..93ecf694871 100644 --- a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts @@ -3,19 +3,17 @@ import { selectorFamily } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { uncapitalize } from 'twenty-shared/utils'; export const recordStoreIdentifierFamilySelector = selectorFamily({ key: 'recordStoreIdentifierFamilySelector', get: - ({ - recordId, - objectNameSingular, - }: { - recordId: string; - objectNameSingular: string; - }) => + ({ recordId }: { recordId: string }) => ({ get }) => { const recordFromStore = get(recordStoreFamilyState(recordId)); + const objectNameSingular = uncapitalize( + recordFromStore?.__typename ?? '', + ); const objectMetadataItems = get(objectMetadataItemsState); diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifiersSelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifiersSelector.ts new file mode 100644 index 00000000000..438362b140b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifiersSelector.ts @@ -0,0 +1,37 @@ +import { selectorFamily } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { isDefined, uncapitalize } from 'twenty-shared/utils'; + +export const recordStoreIdentifiersFamilySelector = selectorFamily({ + key: 'recordStoreIdentifiersFamilySelector', + get: + ({ recordIds }: { recordIds: string[] }) => + ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + + return recordIds + .map((recordId) => { + const recordFromStore = get(recordStoreFamilyState(recordId)); + const objectNameSingular = uncapitalize( + recordFromStore?.__typename ?? '', + ); + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === objectNameSingular, + ); + + if (!objectMetadataItem || !recordFromStore) { + return null; + } + + return getObjectRecordIdentifier({ + objectMetadataItem: objectMetadataItem, + record: recordFromStore, + }); + }) + .filter(isDefined); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreRecordsSelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreRecordsSelector.ts new file mode 100644 index 00000000000..3224d57e9f5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreRecordsSelector.ts @@ -0,0 +1,16 @@ +import { selectorFamily } from 'recoil'; + +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { isDefined } from 'twenty-shared/utils'; + +export const recordStoreRecordsSelector = selectorFamily({ + key: 'recordStoreRecordsSelector', + get: + ({ recordIds }: { recordIds: string[] }) => + ({ get }) => { + const records = recordIds + .map((recordId) => get(recordStoreFamilyState(recordId))) + .filter(isDefined); + return records; + }, +}); diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index b25f0066bca..c021fff9738 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -85,6 +85,7 @@ export { safeParseRelativeDateFilterValue } from './safeParseRelativeDateFilterV export { getGenericOperationName } from './sentry/getGenericOperationName'; export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromCode'; export { capitalize } from './strings/capitalize'; +export { uncapitalize } from './strings/uncapitalize'; export type { TipTapMarkType, TipTapNodeType, diff --git a/packages/twenty-shared/src/utils/strings/__tests__/uncapitalize.test.ts b/packages/twenty-shared/src/utils/strings/__tests__/uncapitalize.test.ts new file mode 100644 index 00000000000..fd6cffafcc8 --- /dev/null +++ b/packages/twenty-shared/src/utils/strings/__tests__/uncapitalize.test.ts @@ -0,0 +1,10 @@ +import { uncapitalize } from '@/utils/strings/uncapitalize'; +describe('uncapitalize', () => { + it('should uncapitalize a string', () => { + expect(uncapitalize('Test')).toBe('test'); + }); + + it('should return an empty string if input is an empty string', () => { + expect(uncapitalize('')).toBe(''); + }); +}); diff --git a/packages/twenty-shared/src/utils/strings/uncapitalize.ts b/packages/twenty-shared/src/utils/strings/uncapitalize.ts new file mode 100644 index 00000000000..55270f071aa --- /dev/null +++ b/packages/twenty-shared/src/utils/strings/uncapitalize.ts @@ -0,0 +1,3 @@ +export const uncapitalize = (text: string) => { + return text.charAt(0).toLowerCase() + text.slice(1); +};