diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx index 89243ead97c5..dfa609fc0547 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx @@ -1,51 +1,91 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +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 { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useCallback, useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { IconTrash } from 'twenty-ui'; +import { IconTrash, isDefined } from 'twenty-ui'; export const DeleteRecordsActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = + useState(false); + + const { resetTableRowSelection } = useRecordTable({ + recordTableId: objectMetadataItem.namePlural, + }); + + const { deleteManyRecords } = useDeleteManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const { favorites, deleteFavorite } = useFavorites(); + + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, + ); + + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const graphqlFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, + const { fetchAllRecordIds } = useFetchAllRecordIds({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: graphqlFilter, }); - const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = - useState(false); + const handleDeleteClick = useCallback(async () => { + const recordIdsToDelete = await fetchAllRecordIds(); - const { deleteTableData } = useDeleteTableData({ - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', - }); + resetTableRowSelection(); - const handleDeleteClick = useCallback(() => { - deleteTableData(contextStoreTargetedRecordIds); - }, [deleteTableData, contextStoreTargetedRecordIds]); + for (const recordIdToDelete of recordIdsToDelete) { + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === recordIdToDelete, + ); - const isRemoteObject = objectMetadataItem?.isRemote ?? false; + if (foundFavorite !== undefined) { + deleteFavorite(foundFavorite.id); + } + } + + await deleteManyRecords(recordIdsToDelete, { + delayInMsBetweenRequests: 50, + }); + }, [ + deleteFavorite, + deleteManyRecords, + favorites, + fetchAllRecordIds, + resetTableRowSelection, + ]); - const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; + const isRemoteObject = objectMetadataItem.isRemote; const canDelete = - !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; + !isRemoteObject && + isDefined(contextStoreNumberOfSelectedRecords) && + contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT && + contextStoreNumberOfSelectedRecords > 0; useEffect(() => { if (canDelete) { @@ -62,17 +102,19 @@ export const DeleteRecordsActionEffect = ({ handleDeleteClick()} deleteButtonText={`Delete ${ - numberOfSelectedRecords > 1 ? 'Records' : 'Record' + contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' }`} /> ), @@ -80,14 +122,18 @@ export const DeleteRecordsActionEffect = ({ } else { removeActionMenuEntry('delete'); } + + return () => { + removeActionMenuEntry('delete'); + }; }, [ - canDelete, addActionMenuEntry, - removeActionMenuEntry, - isDeleteRecordsModalOpen, - numberOfSelectedRecords, + canDelete, + contextStoreNumberOfSelectedRecords, handleDeleteClick, + isDeleteRecordsModalOpen, position, + removeActionMenuEntry, ]); return null; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx index d7b50ddaf0d3..bd5ce07cf817 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx @@ -1,38 +1,27 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { displayedExportProgress, - useExportTableData, -} from '@/object-record/record-index/options/hooks/useExportTableData'; + useExportRecordData, +} from '@/action-menu/hooks/useExportRecordData'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; import { IconFileExport } from 'twenty-ui'; export const ExportRecordsActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, - ); - - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - - const baseTableDataParams = { + const { progress, download } = useExportRecordData({ delayMs: 100, - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', - }; - - const { progress, download } = useExportTableData({ - ...baseTableDataParams, - filename: `${objectMetadataItem?.nameSingular}.csv`, + objectMetadataItem, + recordIndexId: objectMetadataItem.namePlural, + filename: `${objectMetadataItem.nameSingular}.csv`, }); useEffect(() => { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx index e9767b034203..572bc239395a 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx @@ -1,8 +1,7 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; @@ -10,30 +9,28 @@ import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui'; export const ManageFavoritesActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, - ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, ); const { favorites, createFavorite, deleteFavorite } = useFavorites(); - const selectedRecordId = contextStoreTargetedRecordIds[0]; + const selectedRecordId = + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds[0] + : undefined; const selectedRecord = useRecoilValue( - recordStoreFamilyState(selectedRecordId), + recordStoreFamilyState(selectedRecordId ?? ''), ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - const foundFavorite = favorites?.find( (favorite) => favorite.recordId === selectedRecordId, ); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx index 69bfd3305094..ad47a1ee179f 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx @@ -1,13 +1,22 @@ import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect]; -export const MultipleRecordsActionMenuEntriesSetter = () => { +export const MultipleRecordsActionMenuEntriesSetter = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { return ( <> {actionEffects.map((ActionEffect, index) => ( - + ))} ); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx index 75267e445d49..acf4a9bed732 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx @@ -1,20 +1,44 @@ import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter'; import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useRecoilValue } from 'recoil'; export const RecordActionMenuEntriesSetter = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); - if (contextStoreTargetedRecordIds.length === 0) { + const contextStoreCurrentObjectMetadataId = useRecoilValue( + contextStoreCurrentObjectMetadataIdState, + ); + + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: contextStoreCurrentObjectMetadataId ?? '', + }); + + if (!objectMetadataItem) { + throw new Error( + `Object metadata item not found for id ${contextStoreCurrentObjectMetadataId}`, + ); + } + + if (!contextStoreNumberOfSelectedRecords) { return null; } - if (contextStoreTargetedRecordIds.length === 1) { - return ; + if (contextStoreNumberOfSelectedRecords === 1) { + return ( + + ); } - return ; + return ( + + ); }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx index 4b61fa58eadb..9c4b1d528f21 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx @@ -1,8 +1,13 @@ import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -export const SingleRecordActionMenuEntriesSetter = () => { +export const SingleRecordActionMenuEntriesSetter = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { const actionEffects = [ ManageFavoritesActionEffect, ExportRecordsActionEffect, @@ -11,7 +16,11 @@ export const SingleRecordActionMenuEntriesSetter = () => { return ( <> {actionEffects.map((ActionEffect, index) => ( - + ))} ); diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx new file mode 100644 index 000000000000..92cda27cc9d8 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx @@ -0,0 +1,30 @@ +import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; +import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; +import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { useRecoilValue } from 'recoil'; + +export const ActionMenu = ({ actionMenuId }: { actionMenuId: string }) => { + const contextStoreCurrentObjectMetadataId = useRecoilValue( + contextStoreCurrentObjectMetadataIdState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + + + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx index 258683347919..2fd2937408c0 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx @@ -4,7 +4,7 @@ import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry' import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -19,8 +19,8 @@ const StyledLabel = styled.div` `; export const ActionMenuBar = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( @@ -42,9 +42,7 @@ export const ActionMenuBar = () => { scope: ActionBarHotkeyScope.ActionBar, }} > - - {contextStoreTargetedRecordIds.length} selected: - + {contextStoreNumberOfSelectedRecords} selected: {actionMenuEntries.map((entry, index) => ( ))} diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx index 18ebdac7667e..c05df9b758a2 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx @@ -64,7 +64,7 @@ export const ActionMenuDropdown = () => { return ( { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( @@ -26,17 +26,17 @@ export const ActionMenuEffect = () => { ); useEffect(() => { - if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) { + if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) { // We only handle opening the ActionMenuBar here, not the Dropdown. // The Dropdown is already managed by sync handlers for events like // right-click to open and click outside to close. openActionBar(); } - if (contextStoreTargetedRecordIds.length === 0) { + if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) { closeActionBar(); } }, [ - contextStoreTargetedRecordIds, + contextStoreNumberOfSelectedRecords, openActionBar, closeActionBar, isDropdownOpen, diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx index 34d709d1685d..b34462d8fb3c 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx @@ -5,7 +5,8 @@ import { RecoilRoot } from 'recoil'; import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { userEvent, waitFor, within } from '@storybook/test'; import { IconCheckbox, IconTrash } from 'twenty-ui'; @@ -20,7 +21,11 @@ const meta: Meta = { (Story) => ( { - set(contextStoreTargetedRecordIdsState, ['1', '2', '3']); + set(contextStoreTargetedRecordsRuleState, { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }); + set(contextStoreNumberOfSelectedRecordsState, 3); set( actionMenuEntriesComponentState.atomFamily({ instanceId: 'story-action-menu', diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts similarity index 99% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts rename to packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts index 0494fd32f023..65fa9ba2e29c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts @@ -7,7 +7,7 @@ import { displayedExportProgress, download, generateCsv, -} from '../useExportTableData'; +} from '../useExportRecordData'; jest.useFakeTimers(); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts similarity index 91% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts rename to packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts index 532b8e0aa59b..8fa6d6f5981c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts @@ -4,10 +4,11 @@ import { useMemo } from 'react'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport'; + import { - useTableData, - UseTableDataOptions, -} from '@/object-record/record-index/options/hooks/useTableData'; + UseRecordDataOptions, + useRecordData, +} from '@/object-record/record-index/options/hooks/useRecordData'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; @@ -134,21 +135,22 @@ const downloader = (mimeType: string, generator: GenerateExport) => { export const csvDownloader = downloader('text/csv', generateCsv); -type UseExportTableDataOptions = Omit & { +type UseExportTableDataOptions = Omit & { filename: string; }; -export const useExportTableData = ({ +export const useExportRecordData = ({ delayMs, filename, maximumRequests = 100, - objectNameSingular, + objectMetadataItem, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, viewType, }: UseExportTableDataOptions) => { - const { processRecordsForCSVExport } = - useProcessRecordsForCSVExport(objectNameSingular); + const { processRecordsForCSVExport } = useProcessRecordsForCSVExport( + objectMetadataItem.nameSingular, + ); const downloadCsv = useMemo( () => @@ -160,10 +162,10 @@ export const useExportTableData = ({ [filename, processRecordsForCSVExport], ); - const { getTableData: download, progress } = useTableData({ + const { getTableData: download, progress } = useRecordData({ delayMs, maximumRequests, - objectNameSingular, + objectMetadataItem, pageSize, recordIndexId, callback: downloadCsv, diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts new file mode 100644 index 000000000000..fb1b3544d320 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const contextStoreNumberOfSelectedRecordsState = createState({ + key: 'contextStoreNumberOfSelectedRecordsState', + defaultValue: 0, +}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts deleted file mode 100644 index df0c3451172c..000000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreTargetedRecordIdsState = createState({ - key: 'contextStoreTargetedRecordIdsState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts new file mode 100644 index 000000000000..7f71377c3186 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts @@ -0,0 +1,26 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { createState } from 'twenty-ui'; + +type ContextStoreTargetedRecordsRuleSelectionMode = { + mode: 'selection'; + selectedRecordIds: string[]; +}; + +type ContextStoreTargetedRecordsRuleExclusionMode = { + mode: 'exclusion'; + excludedRecordIds: string[]; + filters: Filter[]; +}; + +export type ContextStoreTargetedRecordsRule = + | ContextStoreTargetedRecordsRuleSelectionMode + | ContextStoreTargetedRecordsRuleExclusionMode; + +export const contextStoreTargetedRecordsRuleState = + createState({ + key: 'contextStoreTargetedRecordsRuleState', + defaultValue: { + mode: 'selection', + selectedRecordIds: [], + }, + }); diff --git a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts new file mode 100644 index 000000000000..689d7287d4da --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts @@ -0,0 +1,77 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +describe('computeContextStoreFilters', () => { + const personObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + )!; + + it('should work for selection mode', () => { + const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }; + + const filters = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + personObjectMetadataItem, + ); + + expect(filters).toEqual({ + id: { + in: ['1', '2', '3'], + }, + }); + }); + + it('should work for exclusion mode', () => { + const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { + mode: 'exclusion', + filters: [ + { + id: 'name-filter', + variant: 'default', + fieldMetadataId: personObjectMetadataItem.fields.find( + (field) => field.name === 'name', + )!.id, + value: 'John', + displayValue: 'John', + displayAvatarUrl: undefined, + operand: ViewFilterOperand.Contains, + definition: { + fieldMetadataId: personObjectMetadataItem.fields.find( + (field) => field.name === 'name', + )!.id, + label: 'Name', + iconName: 'person', + type: 'TEXT', + }, + }, + ], + excludedRecordIds: ['1', '2', '3'], + }; + + const filters = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + personObjectMetadataItem, + ); + + expect(filters).toEqual({ + and: [ + { + name: { + ilike: '%John%', + }, + }, + { + not: { + id: { + in: ['1', '2', '3'], + }, + }, + }, + ], + }); + }); +}); diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts new file mode 100644 index 000000000000..26727fbc26ee --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -0,0 +1,42 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; + +export const computeContextStoreFilters = ( + contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, + objectMetadataItem: ObjectMetadataItem, +) => { + let queryFilter: RecordGqlOperationFilter | undefined; + + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { + queryFilter = makeAndFilterVariables([ + turnFiltersIntoQueryFilter( + contextStoreTargetedRecordsRule.filters, + objectMetadataItem?.fields ?? [], + ), + contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 + ? { + not: { + id: { + in: contextStoreTargetedRecordsRule.excludedRecordIds, + }, + }, + } + : undefined, + ]); + } + if (contextStoreTargetedRecordsRule.mode === 'selection') { + queryFilter = + contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 + ? { + id: { + in: contextStoreTargetedRecordsRule.selectedRecordIds, + }, + } + : undefined; + } + + return queryFilter; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts index ceff2e45541a..01cdbc405d6d 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts @@ -33,16 +33,11 @@ describe('useObjectMetadataItemById', () => { expect(objectMetadataItem?.id).toBe(opportunityObjectMetadata.id); }); - it('should return null when invalid ID is provided', async () => { - const { result } = renderHook( - () => useObjectMetadataItemById({ objectId: 'invalid-id' }), - { + it('should throw an error when invalid ID is provided', async () => { + expect(() => + renderHook(() => useObjectMetadataItemById({ objectId: 'invalid-id' }), { wrapper: Wrapper, - }, - ); - - const { objectMetadataItem } = result.current; - - expect(objectMetadataItem).toBeNull(); + }), + ).toThrow(`Object metadata item not found for id invalid-id`); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts index 72c559364226..1783ea61fd1b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts @@ -6,7 +6,7 @@ import { isDefined } from '~/utils/isDefined'; export const useObjectMetadataItemById = ({ objectId, }: { - objectId: string | null; + objectId: string; }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -15,9 +15,7 @@ export const useObjectMetadataItemById = ({ ); if (!isDefined(objectMetadataItem)) { - return { - objectMetadataItem: null, - }; + throw new Error(`Object metadata item not found for id ${objectId}`); } return { diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 01ca2843c3bb..592f2d7b4af4 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -66,7 +66,7 @@ export const RecordBoard = () => { useListenClickOutsideByClassName({ classNames: ['record-board-card'], - excludeClassNames: ['bottom-bar', 'context-menu'], + excludeClassNames: ['bottom-bar', 'action-menu-dropdown'], callback: resetRecordSelection, }); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnFiltersIntoQueryFilter.test.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnFiltersIntoQueryFilter.test.ts index 6486ca29b92e..e8778a3f89bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnFiltersIntoQueryFilter.test.ts @@ -1,5 +1,5 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getCompaniesMock } from '~/testing/mock-data/companies'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; @@ -16,7 +16,7 @@ const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); -describe('turnObjectDropdownFilterIntoQueryFilter', () => { +describe('turnFiltersIntoQueryFilter', () => { it('should work as expected for single filter', () => { const companyMockNameFieldMetadataId = companyMockObjectMetadataItem.fields.find( @@ -37,7 +37,7 @@ describe('turnObjectDropdownFilterIntoQueryFilter', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [nameFilter], companyMockObjectMetadataItem.fields, ); @@ -88,7 +88,7 @@ describe('turnObjectDropdownFilterIntoQueryFilter', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [nameFilter, employeesFilter], companyMockObjectMetadataItem.fields, ); @@ -173,7 +173,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ addressFilterContains, addressFilterDoesNotContain, @@ -554,7 +554,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ phonesFilterContains, phonesFilterDoesNotContain, @@ -754,7 +754,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ emailsFilterContains, emailsFilterDoesNotContain, @@ -908,7 +908,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ dateFilterIsAfter, dateFilterIsBefore, @@ -1023,7 +1023,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ employeesFilterIsGreaterThan, employeesFilterIsLessThan, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts similarity index 99% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts index 345421f7ce95..0e3c69d7c0b8 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts @@ -31,7 +31,7 @@ import { z } from 'zod'; // TODO: break this down into smaller functions and make the whole thing immutable // Especially applyEmptyFilters -export const turnObjectDropdownFilterIntoQueryFilter = ( +export const turnFiltersIntoQueryFilter = ( rawUIFilters: Filter[], fields: Pick[], ): RecordGqlOperationFilter | undefined => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx index 354abcf09dab..9e8358f9e096 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx @@ -2,8 +2,7 @@ import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -121,32 +120,23 @@ export const RecordIndexBoardDataLoaderEffect = ({ const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, + const setContextStoreTargetedRecords = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); - const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, - ); - - useEffect(() => { - setContextStoreTargetedRecordIds(selectedRecordIds); - }, [selectedRecordIds, setContextStoreTargetedRecordIds]); - useEffect(() => { - setContextStoreTargetedRecordIds(selectedRecordIds); - setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: selectedRecordIds, + }); return () => { - setContextStoreTargetedRecordIds([]); - setContextStoreCurrentObjectMetadataItem(null); + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: [], + }); }; - }, [ - objectMetadataItem?.id, - selectedRecordIds, - setContextStoreCurrentObjectMetadataItem, - setContextStoreTargetedRecordIds, - ]); + }, [selectedRecordIds, setContextStoreTargetedRecords]); return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 9aecee3e6160..a28d2f26ac08 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -22,12 +22,8 @@ import { RecordFieldValueSelectorContextProvider } from '@/object-record/record- import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; -import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; -import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; -import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; -import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; -import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect'; -import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenu } from '@/action-menu/components/ActionMenu'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewField } from '@/views/types/ViewField'; @@ -106,6 +102,10 @@ export const RecordIndexContainer = () => { [columnDefinitions, setTableColumns], ); + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, + ); + return ( @@ -119,7 +119,7 @@ export const RecordIndexContainer = () => { optionsDropdownButton={ } @@ -135,6 +135,13 @@ export const RecordIndexContainer = () => { setRecordIndexFilters( mapViewFiltersToFilters(view.viewFilters, filterDefinitions), ); + setContextStoreTargetedRecordsRule((prev) => ({ + ...prev, + filters: mapViewFiltersToFilters( + view.viewFilters, + filterDefinitions, + ), + })); setTableSorts( mapViewSortsToSorts(view.viewSorts, sortDefinitions), ); @@ -179,15 +186,7 @@ export const RecordIndexContainer = () => { /> )} - - - - - - - + diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx new file mode 100644 index 000000000000..2a538c542af1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -0,0 +1,66 @@ +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { useContext, useEffect } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; + +export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = + () => { + const setContextStoreNumberOfSelectedRecords = useSetRecoilState( + contextStoreNumberOfSelectedRecordsState, + ); + + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); + + const { objectNamePlural } = useContext(RecordIndexRootPropsContext); + + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const findManyRecordsParams = useFindManyParams( + objectMetadataItem?.nameSingular ?? '', + objectMetadataItem?.namePlural ?? '', + ); + + const { totalCount } = useFindManyRecords({ + ...findManyRecordsParams, + recordGqlFields: { + id: true, + }, + filter: computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, + ), + limit: 1, + skip: contextStoreTargetedRecordsRule.mode === 'selection', + }); + + useEffect(() => { + if (contextStoreTargetedRecordsRule.mode === 'selection') { + setContextStoreNumberOfSelectedRecords( + contextStoreTargetedRecordsRule.selectedRecordIds.length, + ); + } + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { + setContextStoreNumberOfSelectedRecords(totalCount ?? 0); + } + }, [ + contextStoreTargetedRecordsRule, + setContextStoreNumberOfSelectedRecords, + totalCount, + ]); + + return null; + }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx new file mode 100644 index 000000000000..c94611836a1b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx @@ -0,0 +1,31 @@ +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useContext, useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +export const RecordIndexContainerContextStoreObjectMetadataEffect = () => { + const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + const { objectNamePlural } = useContext(RecordIndexRootPropsContext); + + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + useEffect(() => { + setContextStoreCurrentObjectMetadataItem(objectMetadataItem.id); + + return () => { + setContextStoreCurrentObjectMetadataItem(null); + }; + }, [objectMetadataItem.id, setContextStoreCurrentObjectMetadataItem]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index da407d91e5a9..ba541ca1a67c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -1,8 +1,7 @@ import { useContext, useEffect } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; @@ -24,18 +23,12 @@ export const RecordIndexTableContainerEffect = () => { selectedRowIdsSelector, setOnToggleColumnFilter, setOnToggleColumnSort, + hasUserSelectedAllRowsState, + unselectedRowIdsSelector, } = useRecordTable({ recordTableId: recordIndexId, }); - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, - ); - - const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, - ); - const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -50,8 +43,6 @@ export const RecordIndexTableContainerEffect = () => { setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const handleToggleColumnFilter = useHandleToggleColumnFilter({ objectNameSingular, viewBarId, @@ -82,19 +73,38 @@ export const RecordIndexTableContainerEffect = () => { ); }, [setRecordCountInCurrentView, setOnEntityCountChange]); + const setContextStoreTargetedRecords = useSetRecoilState( + contextStoreTargetedRecordsRuleState, + ); + const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); + const unselectedRowIds = useRecoilValue(unselectedRowIdsSelector()); + useEffect(() => { - setContextStoreTargetedRecordIds(selectedRowIds); - setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); + if (hasUserSelectedAllRows) { + setContextStoreTargetedRecords({ + mode: 'exclusion', + excludedRecordIds: unselectedRowIds, + filters: [], + }); + } else { + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: selectedRowIds, + }); + } return () => { - setContextStoreTargetedRecordIds([]); - setContextStoreCurrentObjectMetadataItem(null); + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: [], + }); }; }, [ - objectMetadataItem?.id, + hasUserSelectedAllRows, selectedRowIds, - setContextStoreCurrentObjectMetadataItem, - setContextStoreTargetedRecordIds, + setContextStoreTargetedRecords, + unselectedRowIds, ]); return <>; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index 297f1dcf8088..3f5ce71ed09b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -44,7 +44,7 @@ export const useLoadRecordIndexBoard = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); - const requestFilters = turnObjectDropdownFilterIntoQueryFilter( + const requestFilters = turnFiltersIntoQueryFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 02485fe0d78b..e77545fdcf6f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; @@ -35,7 +35,7 @@ export const useLoadRecordIndexBoardColumn = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); - const requestFilters = turnObjectDropdownFilterIntoQueryFilter( + const requestFilters = turnFiltersIntoQueryFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index df178df4c4fd..f39ebc77915b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -5,7 +5,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; @@ -27,7 +27,7 @@ export const useFindManyParams = ( const tableFilters = useRecoilValue(tableFiltersState); const tableSorts = useRecoilValue(tableSortsState); - const filter = turnObjectDropdownFilterIntoQueryFilter( + const filter = turnFiltersIntoQueryFilter( tableFilters, objectMetadataItem?.fields ?? [], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx index 25e53d8cc5b1..3c2f5b2bae3e 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx @@ -1,3 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton'; import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; @@ -7,13 +8,13 @@ import { ViewType } from '@/views/types/ViewType'; type RecordIndexOptionsDropdownProps = { viewType: ViewType; - objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; recordIndexId: string; }; export const RecordIndexOptionsDropdown = ({ recordIndexId, - objectNameSingular, + objectMetadataItem, viewType, }: RecordIndexOptionsDropdownProps) => { return ( @@ -26,7 +27,7 @@ export const RecordIndexOptionsDropdown = ({ dropdownComponents={ } diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index a884eda8582b..9396d30da07c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -14,10 +14,12 @@ import { import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; + import { displayedExportProgress, - useExportTableData, -} from '@/object-record/record-index/options/hooks/useExportTableData'; + useExportRecordData, +} from '@/action-menu/hooks/useExportRecordData'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; @@ -44,14 +46,14 @@ type RecordIndexOptionsMenu = 'fields' | 'hiddenFields'; type RecordIndexOptionsDropdownContentProps = { recordIndexId: string; - objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; viewType: ViewType; }; export const RecordIndexOptionsDropdownContent = ({ viewType, recordIndexId, - objectNameSingular, + objectMetadataItem, }: RecordIndexOptionsDropdownContentProps) => { const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); @@ -68,7 +70,7 @@ export const RecordIndexOptionsDropdownContent = ({ }; const { objectNamePlural } = useObjectNamePluralFromSingular({ - objectNameSingular: objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, }); const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { @@ -92,7 +94,7 @@ export const RecordIndexOptionsDropdownContent = ({ const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, viewBarId: recordIndexId, }); @@ -104,7 +106,7 @@ export const RecordIndexOptionsDropdownContent = ({ isCompactModeActive, setAndPersistIsCompactModeActive, } = useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }); @@ -126,12 +128,12 @@ export const RecordIndexOptionsDropdownContent = ({ : handleColumnVisibilityChange; const { openObjectRecordsSpreasheetImportDialog } = - useOpenObjectRecordsSpreasheetImportDialog(objectNameSingular); + useOpenObjectRecordsSpreasheetImportDialog(objectMetadataItem.nameSingular); - const { progress, download } = useExportTableData({ + const { progress, download } = useExportRecordData({ delayMs: 100, - filename: `${objectNameSingular}.csv`, - objectNameSingular, + filename: `${objectMetadataItem.nameSingular}.csv`, + objectMetadataItem, recordIndexId, viewType, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx similarity index 86% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx rename to packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx index aa9f392782f0..9747c2c4e9e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx @@ -1,6 +1,6 @@ import { renderHook, waitFor } from '@testing-library/react'; import { act } from 'react'; -import { percentage, sleep, useTableData } from '../useTableData'; +import { percentage, sleep, useRecordData } from '../useRecordData'; import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -11,7 +11,7 @@ import { ViewType } from '@/views/types/ViewType'; import { MockedResponse } from '@apollo/client/testing'; import gql from 'graphql-tag'; import { useRecoilValue } from 'recoil'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getJestMetadataAndApolloMocksAndContextStoreWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const defaultResponseData = { @@ -127,9 +127,16 @@ const mocks: MockedResponse[] = [ }, ]; -const WrapperWithResponse = getJestMetadataAndApolloMocksWrapper({ - apolloMocks: mocks, -}); +const WrapperWithResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper( + { + apolloMocks: mocks, + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreCurrentObjectMetadataNameSingular: 'person', + }, +); const graphqlEmptyResponse = [ { @@ -145,28 +152,41 @@ const graphqlEmptyResponse = [ }, ]; -const WrapperWithEmptyResponse = getJestMetadataAndApolloMocksWrapper({ - apolloMocks: graphqlEmptyResponse, -}); +const WrapperWithEmptyResponse = + getJestMetadataAndApolloMocksAndContextStoreWrapper({ + apolloMocks: graphqlEmptyResponse, + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreCurrentObjectMetadataNameSingular: 'person', + }); -describe('useTableData', () => { +describe('useRecordData', () => { const recordIndexId = 'people'; - const objectNameSingular = 'person'; + const objectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + ); + if (!objectMetadataItem) { + throw new Error('Object metadata item not found'); + } describe('data fetching', () => { it('should handle no records', async () => { const callback = jest.fn(); const { result } = renderHook( () => - useTableData({ + useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, pageSize: 30, callback, delayMs: 0, viewType: ViewType.Kanban, }), - { wrapper: WrapperWithEmptyResponse }, + { + wrapper: WrapperWithEmptyResponse, + }, ); await act(async () => { @@ -182,9 +202,9 @@ describe('useTableData', () => { const callback = jest.fn(); const { result } = renderHook( () => - useTableData({ + useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, callback, pageSize: 30, @@ -211,9 +231,9 @@ describe('useTableData', () => { recordIndexId, ); return { - tableData: useTableData({ + tableData: useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, callback, pageSize: 30, maximumRequests: 100, @@ -223,7 +243,7 @@ describe('useTableData', () => { useRecordBoardHook: useRecordBoard(recordIndexId), kanbanFieldName: useRecoilValue(kanbanFieldNameState), kanbanData: useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }), @@ -304,9 +324,9 @@ describe('useTableData', () => { recordIndexId, ); return { - tableData: useTableData({ + tableData: useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, callback, pageSize: 30, maximumRequests: 100, @@ -316,7 +336,7 @@ describe('useTableData', () => { setKanbanFieldName: useRecordBoard(recordIndexId), kanbanFieldName: useRecoilValue(kanbanFieldNameState), kanbanData: useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }), diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts deleted file mode 100644 index 345e11453892..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; -import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData'; -import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; - -type UseDeleteTableDataOptions = Pick< - UseTableDataOptions, - 'objectNameSingular' | 'recordIndexId' ->; - -export const useDeleteTableData = ({ - objectNameSingular, - recordIndexId, -}: UseDeleteTableDataOptions) => { - const { resetTableRowSelection } = useRecordTable({ - recordTableId: recordIndexId, - }); - - const { deleteManyRecords } = useDeleteManyRecords({ - objectNameSingular, - }); - const { favorites, deleteFavorite } = useFavorites(); - - const deleteRecords = async (recordIdsToDelete: string[]) => { - resetTableRowSelection(); - - for (const recordIdToDelete of recordIdsToDelete) { - const foundFavorite = favorites?.find( - (favorite) => favorite.recordId === recordIdToDelete, - ); - - if (foundFavorite !== undefined) { - deleteFavorite(foundFavorite.id); - } - } - - await deleteManyRecords(recordIdsToDelete, { - delayInMsBetweenRequests: 50, - }); - }; - - return { deleteTableData: deleteRecords }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts similarity index 71% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts rename to packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts index 98294115c5d2..7c65a53105a9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts @@ -1,18 +1,21 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { ViewType } from '@/views/types/ViewType'; -import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -21,10 +24,10 @@ export const percentage = (part: number, whole: number): number => { return Math.round((part / whole) * 100); }; -export type UseTableDataOptions = { +export type UseRecordDataOptions = { delayMs: number; maximumRequests?: number; - objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; pageSize?: number; recordIndexId: string; callback: ( @@ -40,15 +43,15 @@ type ExportProgress = { displayType: 'percentage' | 'number'; }; -export const useTableData = ({ +export const useRecordData = ({ + objectMetadataItem, delayMs, maximumRequests = 100, - objectNameSingular, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, callback, viewType = ViewType.Table, -}: UseTableDataOptions) => { +}: UseRecordDataOptions) => { const [isDownloading, setIsDownloading] = useState(false); const [inflight, setInflight] = useState(false); const [pageCount, setPageCount] = useState(0); @@ -57,15 +60,10 @@ export const useTableData = ({ }); const [previousRecordCount, setPreviousRecordCount] = useState(0); - const { - visibleTableColumnsSelector, - selectedRowIdsSelector, - tableRowIdsState, - hasUserSelectedAllRowsState, - } = useRecordTableStates(recordIndexId); + const { visibleTableColumnsSelector } = useRecordTableStates(recordIndexId); const { hiddenBoardFields } = useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }); @@ -76,61 +74,21 @@ export const useTableData = ({ (column) => column.metadata.fieldName === kanbanFieldMetadataName, ); const columns = useRecoilValue(visibleTableColumnsSelector()); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - - const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); - const tableRowIds = useRecoilValue(tableRowIdsState); - // user has checked select all and then unselected some rows - const userHasUnselectedSomeRows = - hasUserSelectedAllRows && selectedRowIds.length < tableRowIds.length; - - const hasSelectedRows = - selectedRowIds.length > 0 && - !(hasUserSelectedAllRows && selectedRowIds.length === tableRowIds.length); + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); - const unselectedRowIds = useMemo( - () => - userHasUnselectedSomeRows - ? tableRowIds.filter((id) => !selectedRowIds.includes(id)) - : [], - [userHasUnselectedSomeRows, tableRowIds, selectedRowIds], + const queryFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, ); const findManyRecordsParams = useFindManyParams( - objectNameSingular, + objectMetadataItem.nameSingular, recordIndexId, ); - const selectedFindManyParams = { - ...findManyRecordsParams, - filter: { - ...findManyRecordsParams.filter, - id: { - in: selectedRowIds, - }, - }, - }; - - const unselectedFindManyParams = { - ...findManyRecordsParams, - filter: { - ...findManyRecordsParams.filter, - not: { - id: { - in: unselectedRowIds, - }, - }, - }, - }; - - const usedFindManyParams = - hasSelectedRows && !userHasUnselectedSomeRows - ? selectedFindManyParams - : userHasUnselectedSomeRows - ? unselectedFindManyParams - : findManyRecordsParams; - const { findManyRecords, totalCount, @@ -138,7 +96,8 @@ export const useTableData = ({ fetchMoreRecordsWithPagination, loading, } = useLazyFindManyRecords({ - ...usedFindManyParams, + ...findManyRecordsParams, + filter: queryFilter, limit: pageSize, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx index 0dff4b429dca..828897b3bb1e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx @@ -1,41 +1,22 @@ import { Key } from 'ts-key-enum'; -import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; -import { - ClickOutsideMode, - useListenClickOutsideByClassName, -} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; type RecordTableInternalEffectProps = { recordTableId: string; - tableBodyRef: React.RefObject; }; export const RecordTableInternalEffect = ({ recordTableId, - tableBodyRef, }: RecordTableInternalEffectProps) => { const { leaveTableFocus, resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({ recordTableId }); useMapKeyboardToSoftFocus(); - const { useListenClickOutside } = useClickOutsideListener( - SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID, - ); - - useListenClickOutside({ - refs: [tableBodyRef], - callback: () => { - leaveTableFocus(); - }, - mode: ClickOutsideMode.compareHTMLRef, - }); - useScopedHotkeys( [Key.Escape], () => { @@ -46,9 +27,9 @@ export const RecordTableInternalEffect = ({ useListenClickOutsideByClassName({ classNames: ['entity-table-cell'], - excludeClassNames: ['bottom-bar', 'context-menu'], + excludeClassNames: ['bottom-bar', 'action-menu-dropdown'], callback: () => { - resetTableRowSelection(); + leaveTableFocus(); }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index b7a46e64829d..43bc9c76cf2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -87,10 +87,7 @@ export const RecordTableWithWrappers = ({ onDragSelectionChange={setRowSelected} /> - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index 0a52eb083239..fc3f386432aa 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -6,21 +6,19 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV import { TableHotkeyScope } from '../../types/TableHotkeyScope'; +import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode'; import { useDisableSoftFocus } from './useDisableSoftFocus'; -import { useSetHasUserSelectedAllRows } from './useSetAllRowSelectedState'; export const useLeaveTableFocus = (recordTableId?: string) => { const disableSoftFocus = useDisableSoftFocus(recordTableId); const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode(recordTableId); - const setHasUserSelectedAllRows = useSetHasUserSelectedAllRows(recordTableId); - - const selectAllRows = useSetHasUserSelectedAllRows(recordTableId); - const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); + const resetTableRowSelection = useResetTableRowSelection(recordTableId); + return useRecoilCallback( ({ snapshot }) => () => { @@ -33,6 +31,8 @@ export const useLeaveTableFocus = (recordTableId?: string) => { .getLoadable(currentHotkeyScopeState) .getValue(); + resetTableRowSelection(); + if (!isSoftFocusActive) { return; } @@ -43,15 +43,12 @@ export const useLeaveTableFocus = (recordTableId?: string) => { closeCurrentCellInEditMode(); disableSoftFocus(); - setHasUserSelectedAllRows(false); - selectAllRows(false); }, [ closeCurrentCellInEditMode, disableSoftFocus, isSoftFocusActiveState, - selectAllRows, - setHasUserSelectedAllRows, + resetTableRowSelection, ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts index 106b1174de02..af9bc7f80209 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts @@ -19,6 +19,7 @@ import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-t import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; +import { unselectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; @@ -134,6 +135,10 @@ export const useRecordTableStates = (recordTableId?: string) => { selectedRowIdsComponentSelector, scopeId, ), + unselectedRowIdsSelector: extractComponentReadOnlySelector( + unselectedRowIdsComponentSelector, + scopeId, + ), visibleTableColumnsSelector: extractComponentReadOnlySelector( visibleTableColumnsComponentSelector, scopeId, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 79deb4693a2c..3bb5dc6ea08a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -46,17 +46,16 @@ export const useSetRecordTableData = ({ const recordIds = newRecords.map((record) => record.id); if (!isDeeplyEqual(currentRowIds, recordIds)) { - set(tableRowIdsState, recordIds); - } - - if (hasUserSelectedAllRows) { - for (const rowId of recordIds) { - set(isRowSelectedFamilyState(rowId), true); + if (hasUserSelectedAllRows) { + for (const rowId of recordIds) { + set(isRowSelectedFamilyState(rowId), true); + } } - } - set(numberOfTableRowsState, totalCount ?? 0); - onEntityCountChange(totalCount); + set(tableRowIdsState, recordIds); + set(numberOfTableRowsState, totalCount ?? 0); + onEntityCountChange(totalCount); + } }, [ numberOfTableRowsState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 6cad63df5448..ffd741ec7e75 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -42,6 +42,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { isRecordTableInitialLoadingState, tableLastRowVisibleState, selectedRowIdsSelector, + unselectedRowIdsSelector, onToggleColumnFilterState, onToggleColumnSortState, pendingRecordIdState, @@ -223,6 +224,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { setSoftFocusPosition, isSomeCellInEditModeState, selectedRowIdsSelector, + unselectedRowIdsSelector, setHasUserSelectedAllRows, setOnToggleColumnFilter, setOnToggleColumnSort, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx index 459211537244..597d38e61dc7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx @@ -1,9 +1,7 @@ import styled from '@emotion/styled'; import { useCallback, useContext } from 'react'; -import { useRecoilValue } from 'recoil'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; import { Checkbox } from '@/ui/input/components/Checkbox'; @@ -21,19 +19,16 @@ const StyledContainer = styled.div` export const RecordTableCellCheckbox = () => { const { isSelected } = useContext(RecordTableRowContext); - const { recordId } = useContext(RecordTableRowContext); - const { isRowSelectedFamilyState } = useRecordTableStates(); const { setCurrentRowSelected } = useSetCurrentRowSelected(); - const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId)); const handleClick = useCallback(() => { - setCurrentRowSelected(!currentRowSelected); - }, [currentRowSelected, setCurrentRowSelected]); + setCurrentRowSelected(!isSelected); + }, [isSelected, setCurrentRowSelected]); return ( - + ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx index 5bcfd65d67cd..912789351078 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx @@ -37,7 +37,6 @@ export const RecordTableHeaderCheckboxColumn = () => { setHasUserSelectedAllRows(true); selectAllRows(); } else { - setHasUserSelectedAllRows(false); resetTableRowSelection(); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts new file mode 100644 index 000000000000..37621eacc0fa --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts @@ -0,0 +1,23 @@ +import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; +import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; + +export const unselectedRowIdsComponentSelector = + createComponentReadOnlySelector({ + key: 'unselectedRowIdsComponentSelector', + get: + ({ scopeId }) => + ({ get }) => { + const rowIds = get(tableRowIdsComponentState({ scopeId })); + + return rowIds.filter( + (rowId) => + get( + isRowSelectedComponentFamilyState({ + scopeId, + familyKey: rowId, + }), + ) === false, + ); + }, + }); diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx index 1ce2f5affe9c..6c538fe36209 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx @@ -1,11 +1,9 @@ import styled from '@emotion/styled'; -import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { ViewType } from '@/views/types/ViewType'; const StyledContainer = styled.div` display: flex; @@ -26,13 +24,7 @@ export const SignInBackgroundMockContainer = () => { {}} - optionsDropdownButton={ - - } + optionsDropdownButton={<>} /> { + + diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx index a9a5514e285d..080b6d5a48d4 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx @@ -1,5 +1,6 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; @@ -10,8 +11,8 @@ export const RecordShowPageContextStoreEffect = ({ }: { recordId: string; }) => { - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); const setContextStoreCurrentObjectMetadataId = useSetRecoilState( @@ -24,19 +25,32 @@ export const RecordShowPageContextStoreEffect = ({ objectNameSingular: objectNameSingular ?? '', }); + const setContextStoreNumberOfSelectedRecords = useSetRecoilState( + contextStoreNumberOfSelectedRecordsState, + ); + useEffect(() => { - setContextStoreTargetedRecordIds([recordId]); + setContextStoreTargetedRecordsRule({ + mode: 'selection', + selectedRecordIds: [recordId], + }); setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); + setContextStoreNumberOfSelectedRecords(1); return () => { - setContextStoreTargetedRecordIds([]); + setContextStoreTargetedRecordsRule({ + mode: 'selection', + selectedRecordIds: [], + }); setContextStoreCurrentObjectMetadataId(null); + setContextStoreNumberOfSelectedRecords(0); }; }, [ recordId, - setContextStoreTargetedRecordIds, + setContextStoreTargetedRecordsRule, setContextStoreCurrentObjectMetadataId, objectMetadataItem?.id, + setContextStoreNumberOfSelectedRecords, ]); return null; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx deleted file mode 100644 index e40a00da25ee..000000000000 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; - -export const RecordShowPageEffect = ({ recordId }: { recordId: string }) => { - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, - ); - - useEffect(() => { - setContextStoreTargetedRecordIds([recordId]); - }, [recordId, setContextStoreTargetedRecordIds]); - - return null; -}; diff --git a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx new file mode 100644 index 000000000000..866ebe143e2e --- /dev/null +++ b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx @@ -0,0 +1,49 @@ +import { ReactNode, useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; + +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { + ContextStoreTargetedRecordsRule, + contextStoreTargetedRecordsRuleState, +} from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; + +export const JestContextStoreSetter = ({ + contextStoreTargetedRecordsRule = { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreCurrentObjectMetadataNameSingular = '', + children, +}: { + contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; + contextStoreCurrentObjectMetadataNameSingular?: string; + children: ReactNode; +}) => { + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, + ); + const setContextStoreCurrentObjectMetadataId = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: contextStoreCurrentObjectMetadataNameSingular, + }); + + const contextStoreCurrentObjectMetadataId = objectMetadataItem.id; + + const [isLoaded, setIsLoaded] = useState(false); + useEffect(() => { + setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule); + setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId); + setIsLoaded(true); + }, [ + setContextStoreTargetedRecordsRule, + setContextStoreCurrentObjectMetadataId, + contextStoreTargetedRecordsRule, + contextStoreCurrentObjectMetadataId, + ]); + + return isLoaded ? <>{children} : null; +}; diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx new file mode 100644 index 000000000000..e674d4282114 --- /dev/null +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx @@ -0,0 +1,37 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { MockedResponse } from '@apollo/client/testing'; +import { ReactNode } from 'react'; +import { MutableSnapshot } from 'recoil'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; + +export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ + apolloMocks, + onInitializeRecoilSnapshot, + contextStoreTargetedRecordsRule, + contextStoreCurrentObjectMetadataNameSingular, +}: { + apolloMocks: + | readonly MockedResponse, Record>[] + | undefined; + onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void; + contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; + contextStoreCurrentObjectMetadataNameSingular?: string; +}) => { + const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks, + onInitializeRecoilSnapshot, + }); + return ({ children }: { children: ReactNode }) => ( + + + {children} + + + ); +};