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 <charles@twenty.com>
This commit is contained in:
+2
-9
@@ -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: (
|
||||
<ActionOpenSidePanelPage
|
||||
page={CommandMenuPages.MergeRecords}
|
||||
pageTitle={msg`Merge records`}
|
||||
pageIcon={IconArrowMerge}
|
||||
/>
|
||||
),
|
||||
component: <MergeMultipleRecordsAction />,
|
||||
},
|
||||
[MultipleRecordsActionKeys.EXPORT]: {
|
||||
type: ActionType.Standard,
|
||||
|
||||
+31
@@ -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 <ActionDisplay onClick={handleClick} />;
|
||||
};
|
||||
+18
@@ -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;
|
||||
};
|
||||
-78
@@ -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;
|
||||
};
|
||||
@@ -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 (
|
||||
<CommandMenuContainer>
|
||||
<CommandMenuContextChipRecordSetterEffect />
|
||||
<CommandMenuPageComponentInstanceContext.Provider
|
||||
value={{ instanceId: commandMenuPageInfo.instanceId }}
|
||||
>
|
||||
|
||||
+6
-26
@@ -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);
|
||||
|
||||
+12
-10
@@ -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,
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+5
-7
@@ -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,
|
||||
|
||||
+46
-26
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
+45
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
+23
-8
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+8
-15
@@ -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 = () => {
|
||||
<RecordComponentInstanceContextsWrapper
|
||||
componentInstanceId={`record-merge-${commandMenuPageInstanceId}`}
|
||||
>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: commandMenuPageInstanceId }}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: commandMenuPageInstanceId }}
|
||||
>
|
||||
<StyledRightDrawerRecord isMobile={isMobile}>
|
||||
<MergeRecordsContainer
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
componentInstanceId={commandMenuPageInstanceId}
|
||||
/>
|
||||
</StyledRightDrawerRecord>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
<StyledRightDrawerRecord isMobile={isMobile}>
|
||||
<MergeRecordsContainer
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
/>
|
||||
</StyledRightDrawerRecord>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</RecordComponentInstanceContextsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -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<string, MorphItem>
|
||||
export const commandMenuNavigationMorphItemsByPageState = createState<
|
||||
Map<string, MorphItem[]>
|
||||
>({
|
||||
key: 'command-menu/commandMenuNavigationMorphItemByPageState',
|
||||
key: 'command-menu/commandMenuNavigationMorphItemsByPageState',
|
||||
defaultValue: new Map(),
|
||||
});
|
||||
-13
@@ -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: [],
|
||||
});
|
||||
-74
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
+2
@@ -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 };
|
||||
|
||||
-3
@@ -60,9 +60,6 @@ const StyledListItem = styled(RecordDetailRecordsListItemContainer)<{
|
||||
}
|
||||
`}
|
||||
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
&:hover {
|
||||
.displayOnHover {
|
||||
opacity: 1;
|
||||
|
||||
+19
-12
@@ -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 (
|
||||
<RightDrawerProvider value={{ isInRightDrawer: true }}>
|
||||
<ShowPageContainer>
|
||||
<StyledShowPageRightContainer>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
value={{ instanceId: instanceId }}
|
||||
>
|
||||
<StyledTabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={componentInstanceId}
|
||||
componentInstanceId={instanceId}
|
||||
/>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
<StyledContentContainer>
|
||||
{activeTabId === MergeRecordsTabId.MERGE_PREVIEW && (
|
||||
<MergePreviewTab objectNameSingular={objectNameSingular} />
|
||||
)}
|
||||
{activeTabId === MergeRecordsTabId.SETTINGS && <MergeSettingsTab />}
|
||||
{activeTabId === MergeRecordsTabId.SETTINGS && (
|
||||
<MergeSettingsTab objectNameSingular={objectNameSingular} />
|
||||
)}
|
||||
{selectedRecords.some((record) => record.id === activeTabId) && (
|
||||
<MergeRecordTab
|
||||
objectNameSingular={objectNameSingular}
|
||||
|
||||
+10
-2
@@ -1,3 +1,4 @@
|
||||
import { useMergePreview } from '@/object-record/record-merge/hooks/useMergePreview';
|
||||
import { useMergeRecordsSettings } from '@/object-record/record-merge/hooks/useMergeRecordsSettings';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import styled from '@emotion/styled';
|
||||
@@ -10,9 +11,16 @@ const StyledSection = styled(Section)`
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
export const MergeSettingsTab = () => {
|
||||
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,
|
||||
|
||||
+34
-14
@@ -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<ObjectRecord | null>(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,
|
||||
};
|
||||
|
||||
+5
-6
@@ -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,
|
||||
|
||||
-7
@@ -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,
|
||||
|
||||
@@ -50,7 +50,6 @@ export const SummaryCard = ({
|
||||
|
||||
const recordIdentifier = useRecoilValue(
|
||||
recordStoreIdentifierFamilySelector({
|
||||
objectNameSingular,
|
||||
recordId: objectRecordId,
|
||||
}),
|
||||
);
|
||||
|
||||
+5
-7
@@ -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);
|
||||
|
||||
|
||||
+37
@@ -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);
|
||||
},
|
||||
});
|
||||
+16
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export const uncapitalize = (text: string) => {
|
||||
return text.charAt(0).toLowerCase() + text.slice(1);
|
||||
};
|
||||
Reference in New Issue
Block a user