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);
+};