7338 refactor actionbar and contextmenu to use the context store (#7462)

Closes #7338
This commit is contained in:
Raphaël Bosi
2024-10-10 13:26:19 +02:00
committed by GitHub
parent 54c328a7e6
commit a7d5aa933d
84 changed files with 1481 additions and 954 deletions

View File

@ -0,0 +1,83 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { useActionMenu } from '../useActionMenu';
const openBottomBar = jest.fn();
const closeBottomBar = jest.fn();
const openDropdown = jest.fn();
const closeDropdown = jest.fn();
jest.mock('@/ui/layout/bottom-bar/hooks/useBottomBar', () => ({
useBottomBar: jest.fn(() => ({
openBottomBar: openBottomBar,
closeBottomBar: closeBottomBar,
})),
}));
jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({
useDropdownV2: jest.fn(() => ({
openDropdown: openDropdown,
closeDropdown: closeDropdown,
})),
}));
describe('useActionMenu', () => {
const actionMenuId = 'test-action-menu';
it('should return the correct functions', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
expect(result.current).toHaveProperty('openActionMenuDropdown');
expect(result.current).toHaveProperty('openActionBar');
expect(result.current).toHaveProperty('closeActionBar');
expect(result.current).toHaveProperty('closeActionMenuDropdown');
});
it('should call the correct functions when opening action menu dropdown', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.openActionMenuDropdown();
});
expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`);
expect(openDropdown).toHaveBeenCalledWith(
`action-menu-dropdown-${actionMenuId}`,
);
});
it('should call the correct functions when opening action bar', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.openActionBar();
});
expect(closeDropdown).toHaveBeenCalledWith(
`action-menu-dropdown-${actionMenuId}`,
);
expect(openBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`);
});
it('should call the correct function when closing action menu dropdown', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.closeActionMenuDropdown();
});
expect(closeDropdown).toHaveBeenCalledWith(
`action-menu-dropdown-${actionMenuId}`,
);
});
it('should call the correct function when closing action bar', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.closeActionBar();
});
expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`);
});
});

View File

@ -0,0 +1,32 @@
import { useBottomBar } from '@/ui/layout/bottom-bar/hooks/useBottomBar';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
export const useActionMenu = (actionMenuId: string) => {
const { openDropdown, closeDropdown } = useDropdownV2();
const { openBottomBar, closeBottomBar } = useBottomBar();
const openActionMenuDropdown = () => {
closeBottomBar(`action-bar-${actionMenuId}`);
openDropdown(`action-menu-dropdown-${actionMenuId}`);
};
const openActionBar = () => {
closeDropdown(`action-menu-dropdown-${actionMenuId}`);
openBottomBar(`action-bar-${actionMenuId}`);
};
const closeActionMenuDropdown = () => {
closeDropdown(`action-menu-dropdown-${actionMenuId}`);
};
const closeActionBar = () => {
closeBottomBar(`action-bar-${actionMenuId}`);
};
return {
openActionMenuDropdown,
openActionBar,
closeActionBar,
closeActionMenuDropdown,
};
};

View File

@ -0,0 +1,148 @@
import { useHandleFavoriteButton } from '@/action-menu/hooks/useHandleFavoriteButton';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
import {
displayedExportProgress,
useExportTableData,
} from '@/object-record/record-index/options/hooks/useExportTableData';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import {
IconFileExport,
IconHeart,
IconHeartOff,
IconTrash,
isDefined,
} from 'twenty-ui';
export const useComputeActionsBasedOnContextStore = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);
const { handleFavoriteButtonClick } = useHandleFavoriteButton(
contextStoreTargetedRecordIds,
objectMetadataItem,
);
const baseTableDataParams = {
delayMs: 100,
objectNameSingular: objectMetadataItem.nameSingular,
recordIndexId: objectMetadataItem.namePlural,
};
const { deleteTableData } = useDeleteTableData(baseTableDataParams);
const handleDeleteClick = useCallback(() => {
deleteTableData(contextStoreTargetedRecordIds);
}, [deleteTableData, contextStoreTargetedRecordIds]);
const { progress, download } = useExportTableData({
...baseTableDataParams,
filename: `${objectMetadataItem.nameSingular}.csv`,
});
const isRemoteObject = objectMetadataItem.isRemote;
const numberOfSelectedRecords = contextStoreTargetedRecordIds.length;
const canDelete =
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
const menuActions: ActionMenuEntry[] = useMemo(
() =>
[
{
label: displayedExportProgress(progress),
Icon: IconFileExport,
accent: 'default',
onClick: () => download(),
} satisfies ActionMenuEntry,
canDelete
? ({
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => {
setIsDeleteRecordsModalOpen(true);
},
ConfirmationModal: (
<ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen}
title={`Delete ${numberOfSelectedRecords} ${
numberOfSelectedRecords === 1 ? `record` : 'records'
}`}
subtitle={`Are you sure you want to delete ${
numberOfSelectedRecords === 1
? 'this record'
: 'these records'
}? ${
numberOfSelectedRecords === 1 ? 'It' : 'They'
} can be recovered from the Options menu.`}
onConfirmClick={() => handleDeleteClick()}
deleteButtonText={`Delete ${
numberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`}
/>
),
} satisfies ActionMenuEntry)
: undefined,
].filter(isDefined),
[
download,
progress,
canDelete,
handleDeleteClick,
isDeleteRecordsModalOpen,
numberOfSelectedRecords,
],
);
const hasOnlyOneRecordSelected = contextStoreTargetedRecordIds.length === 1;
const { favorites } = useFavorites();
const isFavorite =
isNonEmptyString(contextStoreTargetedRecordIds[0]) &&
!!favorites?.find(
(favorite) => favorite.recordId === contextStoreTargetedRecordIds[0],
);
return {
availableActionsInContext: [
...menuActions,
...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
? [
{
label: 'Remove from favorites',
Icon: IconHeartOff,
onClick: handleFavoriteButtonClick,
},
]
: []),
...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected
? [
{
label: 'Add to favorites',
Icon: IconHeart,
onClick: handleFavoriteButtonClick,
},
]
: []),
],
};
};

View File

@ -0,0 +1,49 @@
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
export const useHandleFavoriteButton = (
selectedRecordIds: string[],
objectMetadataItem: ObjectMetadataItem,
callback?: () => void,
) => {
const { createFavorite, favorites, deleteFavorite } = useFavorites();
const handleFavoriteButtonClick = useRecoilCallback(
({ snapshot }) =>
() => {
if (selectedRecordIds.length > 1) {
return;
}
const selectedRecordId = selectedRecordIds[0];
const selectedRecord = snapshot
.getLoadable(recordStoreFamilyState(selectedRecordId))
.getValue();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === selectedRecordId,
);
const isFavorite = !!selectedRecordId && !!foundFavorite;
if (isFavorite) {
deleteFavorite(foundFavorite.id);
} else if (isDefined(selectedRecord)) {
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
}
callback?.();
},
[
callback,
createFavorite,
deleteFavorite,
favorites,
objectMetadataItem.nameSingular,
selectedRecordIds,
],
);
return { handleFavoriteButtonClick };
};