7338 refactor actionbar and contextmenu to use the context store (#7462)
Closes #7338
This commit is contained in:
@ -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}`);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user