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 0a0e7aa7ad3b..c8095bd72808 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,10 +1,13 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords'; import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; 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'; @@ -39,12 +42,23 @@ export const DeleteRecordsActionEffect = ({ const { favorites, deleteFavorite } = useFavorites(); - const { totalCount: numberOfSelectedRecords, fetchAllRecordIds } = - useContextStoreSelectedRecords({ - recordGqlFields: { - id: true, - }, - }); + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, + ); + + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); + + const filter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem ?? undefined, + ); + + const { fetchAllRecordIds } = useFetchAllRecordIds({ + objectNameSingular: objectMetadataItem?.nameSingular ?? '', + filter, + }); const handleDeleteClick = useCallback(async () => { const recordIdsToDelete = await fetchAllRecordIds(); @@ -76,9 +90,9 @@ export const DeleteRecordsActionEffect = ({ const canDelete = !isRemoteObject && - isDefined(numberOfSelectedRecords) && - numberOfSelectedRecords < DELETE_MAX_COUNT && - numberOfSelectedRecords > 0; + isDefined(contextStoreNumberOfSelectedRecords) && + contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT && + contextStoreNumberOfSelectedRecords > 0; useEffect(() => { if (canDelete) { @@ -95,17 +109,19 @@ export const DeleteRecordsActionEffect = ({ handleDeleteClick()} deleteButtonText={`Delete ${ - numberOfSelectedRecords > 1 ? 'Records' : 'Record' + contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' }`} /> ), @@ -120,9 +136,9 @@ export const DeleteRecordsActionEffect = ({ }, [ addActionMenuEntry, canDelete, + contextStoreNumberOfSelectedRecords, handleDeleteClick, isDeleteRecordsModalOpen, - numberOfSelectedRecords, position, removeActionMenuEntry, ]); 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 942d6b7dc5bc..925d6ac227bb 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,6 +1,6 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -15,8 +15,8 @@ export const ManageFavoritesActionEffect = ({ }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecords = useRecoilValue( - contextStoreTargetedRecordsState, + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, ); const contextStoreCurrentObjectMetadataId = useRecoilValue( contextStoreCurrentObjectMetadataIdState, @@ -25,9 +25,9 @@ export const ManageFavoritesActionEffect = ({ const { favorites, createFavorite, deleteFavorite } = useFavorites(); const selectedRecordId = - contextStoreTargetedRecords.selectedRecordIds === 'all' - ? '' - : contextStoreTargetedRecords.selectedRecordIds[0]; + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds[0] + : ''; const selectedRecord = useRecoilValue( recordStoreFamilyState(selectedRecordId), 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 d12f63e0120d..826bdcb28b7f 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,15 +1,18 @@ import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter'; import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; -import { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { useRecoilValue } from 'recoil'; export const RecordActionMenuEntriesSetter = () => { - const { totalCount } = useContextStoreSelectedRecords({ limit: 1 }); + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, + ); - if (!totalCount) { + if (!contextStoreNumberOfSelectedRecords) { return null; } - if (totalCount === 1) { + if (contextStoreNumberOfSelectedRecords === 1) { return ; } 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 5e41f2845d26..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,10 +4,11 @@ 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 { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords'; +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'; +import { useRecoilValue } from 'recoil'; const StyledLabel = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; @@ -18,10 +19,9 @@ const StyledLabel = styled.div` `; export const ActionMenuBar = () => { - const { totalCount: numberOfSelectedRecords } = - useContextStoreSelectedRecords({ - limit: 1, - }); + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, + ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( ActionMenuComponentInstanceContext, @@ -42,7 +42,7 @@ export const ActionMenuBar = () => { scope: ActionBarHotkeyScope.ActionBar, }} > - {numberOfSelectedRecords} selected: + {contextStoreNumberOfSelectedRecords} selected: {actionMenuEntries.map((entry, index) => ( ))} diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx index dbc0d9c09333..89ba0f003ea7 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx @@ -1,7 +1,6 @@ import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; -import { isOneRecordOrMoreSelected } from '@/context-store/utils/isOneRecordOrMoreSelected'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; @@ -9,12 +8,10 @@ import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; export const ActionMenuEffect = () => { - const contextStoreTargetedRecords = useRecoilValue( - contextStoreTargetedRecordsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); - const selectedRecords = contextStoreTargetedRecords.selectedRecordIds; - const actionMenuId = useAvailableComponentInstanceIdOrThrow( ActionMenuComponentInstanceContext, ); @@ -29,21 +26,20 @@ export const ActionMenuEffect = () => { ); useEffect(() => { - if (isOneRecordOrMoreSelected(selectedRecords) && !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 (!isOneRecordOrMoreSelected(selectedRecords) && isDropdownOpen) { + if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) { closeActionBar(); } }, [ - contextStoreTargetedRecords, + contextStoreNumberOfSelectedRecords, openActionBar, closeActionBar, isDropdownOpen, - selectedRecords, ]); return null; 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 962046e083dc..cdf60bef13bd 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,7 @@ 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 { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { userEvent, waitFor, within } from '@storybook/test'; import { IconCheckbox, IconTrash } from 'twenty-ui'; @@ -20,9 +20,9 @@ const meta: Meta = { (Story) => ( { - set(contextStoreTargetedRecordsState, { + set(contextStoreTargetedRecordsRuleState, { + mode: 'selection', selectedRecordIds: ['1', '2', '3'], - excludedRecordIds: [], }); set( actionMenuEntriesComponentState.atomFamily({ diff --git a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreSelectedRecords.ts b/packages/twenty-front/src/modules/context-store/hooks/useContextStoreSelectedRecords.ts deleted file mode 100644 index 3ff0c371cee7..000000000000 --- a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreSelectedRecords.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordsFiltersState } from '@/context-store/states/contextStoreTargetedRecordsFilters'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; -import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; -import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; -import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; -import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; -import { useRecoilValue } from 'recoil'; - -const getFilterForSelectedRecords = ( - selectedRecordIds: string[] | 'all', - excludedRecordIds: string[], - queryFilter: any, -) => { - if (selectedRecordIds === 'all') { - if (excludedRecordIds.length > 0) { - return makeAndFilterVariables([ - queryFilter, - { - not: { - id: { - in: excludedRecordIds, - }, - }, - }, - ]); - } - return queryFilter; - } - - return makeAndFilterVariables([ - queryFilter, - { - id: { - in: selectedRecordIds, - }, - }, - ]); -}; - -export const useContextStoreSelectedRecords = ({ - limit = undefined, - recordGqlFields, -}: { - limit?: number; - recordGqlFields?: RecordGqlFields; -}) => { - const contextStoreTargetedRecords = useRecoilValue( - contextStoreTargetedRecordsState, - ); - - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, - ); - - const contextStoreTargetedRecordsFilters = useRecoilValue( - contextStoreTargetedRecordsFiltersState, - ); - - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - const queryFilter = turnFiltersIntoQueryFilter( - contextStoreTargetedRecordsFilters, - objectMetadataItem?.fields ?? [], - ); - - const { selectedRecordIds, excludedRecordIds } = contextStoreTargetedRecords; - - // Determine if we should skip the query based on the following conditions: - // 1. We're not selecting all records (selectedRecordIds !== 'all') - // 2. Either: - // a) No specific records are selected (selectedRecordIds.length === 0) - // b) We're only requesting the 'id' field (which we already have) - const isOnlyRequestingId = - Object.keys(recordGqlFields ?? {}).length === 1 && - recordGqlFields?.id === true; - - const skip = - selectedRecordIds !== 'all' && - (selectedRecordIds.length === 0 || isOnlyRequestingId); - - const filter = getFilterForSelectedRecords( - selectedRecordIds, - excludedRecordIds, - queryFilter, - ); - - const findManyRecordsParams = useFindManyParams( - objectMetadataItem?.nameSingular ?? '', - objectMetadataItem?.namePlural ?? '', - ); - - const result = useFindManyRecords({ - ...findManyRecordsParams, - recordGqlFields, - filter, - limit, - skip, - }); - - const { fetchAllRecordIds } = useFetchAllRecordIds({ - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - filter, - limit, - }); - - return { - ...result, - totalCount: skip ? selectedRecordIds.length : result.totalCount, - records: skip ? selectedRecordIds.map((id) => ({ id })) : result.records, - fetchAllRecordIds, - }; -}; 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/contextStoreTargetedRecordsFilters.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsFilters.ts deleted file mode 100644 index 8d081e942a53..000000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsFilters.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { createState } from 'twenty-ui'; - -export const contextStoreTargetedRecordsFiltersState = createState({ - key: 'contextStoreTargetedRecordsFiltersState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsState.ts index bbd45c61df6d..7f71377c3186 100644 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsState.ts +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsState.ts @@ -1,12 +1,26 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { createState } from 'twenty-ui'; -export const contextStoreTargetedRecordsState = createState<{ - selectedRecordIds: 'all' | string[]; +type ContextStoreTargetedRecordsRuleSelectionMode = { + mode: 'selection'; + selectedRecordIds: string[]; +}; + +type ContextStoreTargetedRecordsRuleExclusionMode = { + mode: 'exclusion'; excludedRecordIds: string[]; -}>({ - key: 'contextStoreTargetedRecordsState', - defaultValue: { - selectedRecordIds: [], - excludedRecordIds: [], - }, -}); + 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/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts new file mode 100644 index 000000000000..e093c96e014e --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -0,0 +1,42 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsState'; +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/context-store/utils/isOneRecordOrMoreSelected.ts b/packages/twenty-front/src/modules/context-store/utils/isOneRecordOrMoreSelected.ts deleted file mode 100644 index 2da564a9a2fb..000000000000 --- a/packages/twenty-front/src/modules/context-store/utils/isOneRecordOrMoreSelected.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const isOneRecordOrMoreSelected = ( - selectedRecordIds: 'all' | string[], -): boolean => { - return selectedRecordIds === 'all' || selectedRecordIds.length >= 1; -}; 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 07ec3b0495d6..c67260eab4ec 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,7 +2,7 @@ import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -121,19 +121,19 @@ export const RecordIndexBoardDataLoaderEffect = ({ const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); const setContextStoreTargetedRecords = useSetRecoilState( - contextStoreTargetedRecordsState, + contextStoreTargetedRecordsRuleState, ); useEffect(() => { setContextStoreTargetedRecords({ + mode: 'selection', selectedRecordIds: selectedRecordIds, - excludedRecordIds: [], }); return () => { setContextStoreTargetedRecords({ + mode: 'selection', selectedRecordIds: [], - excludedRecordIds: [], }); }; }, [selectedRecordIds, setContextStoreTargetedRecords]); 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 d6afaa0eaecf..a1ffc9ed9e6f 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 @@ -23,7 +23,7 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; import { ActionMenu } from '@/action-menu/components/ActionMenu'; -import { contextStoreTargetedRecordsFiltersState } from '@/context-store/states/contextStoreTargetedRecordsFilters'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewField } from '@/views/types/ViewField'; @@ -102,8 +102,8 @@ export const RecordIndexContainer = () => { [columnDefinitions, setTableColumns], ); - const setContextStoreTargetedRecordsFilters = useSetRecoilState( - contextStoreTargetedRecordsFiltersState, + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); return ( @@ -135,9 +135,13 @@ export const RecordIndexContainer = () => { setRecordIndexFilters( mapViewFiltersToFilters(view.viewFilters, filterDefinitions), ); - setContextStoreTargetedRecordsFilters( - mapViewFiltersToFilters(view.viewFilters, filterDefinitions), - ); + setContextStoreTargetedRecordsRule((prev) => ({ + ...prev, + filters: mapViewFiltersToFilters( + view.viewFilters, + filterDefinitions, + ), + })); setTableSorts( mapViewSortsToSorts(view.viewSorts, sortDefinitions), ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreEffect.tsx similarity index 94% rename from packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerEffect.tsx rename to packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreEffect.tsx index 9a1eb8d8dc81..d465eb1b6e86 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreEffect.tsx @@ -5,7 +5,7 @@ import { RecordIndexRootPropsContext } from '@/object-record/record-index/contex import { useContext, useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; -export const RecordIndexContainerEffect = () => { +export const RecordIndexContainerContextStoreEffect = () => { const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( contextStoreCurrentObjectMetadataIdState, ); 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 8b009aca28ee..2e621d2771ff 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,7 +1,7 @@ import { useContext, useEffect } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; @@ -74,25 +74,30 @@ export const RecordIndexTableContainerEffect = () => { }, [setRecordCountInCurrentView, setOnEntityCountChange]); const setContextStoreTargetedRecords = useSetRecoilState( - contextStoreTargetedRecordsState, + contextStoreTargetedRecordsRuleState, ); const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); const unselectedRowIds = useRecoilValue(unselectedRowIdsSelector()); useEffect(() => { - setContextStoreTargetedRecords({ - selectedRecordIds: - selectedRowIds.length !== 1 && hasUserSelectedAllRows - ? 'all' - : selectedRowIds, - excludedRecordIds: unselectedRowIds, - }); + if (hasUserSelectedAllRows) { + setContextStoreTargetedRecords({ + mode: 'exclusion', + excludedRecordIds: unselectedRowIds, + filters: [], + }); + } else { + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: selectedRowIds, + }); + } return () => { setContextStoreTargetedRecords({ + mode: 'selection', selectedRecordIds: [], - excludedRecordIds: [], }); }; }, [ diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx index e6b8d38349f6..7f53bc472ad0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx @@ -130,12 +130,11 @@ const mocks: MockedResponse[] = [ const WrapperWithResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper( { apolloMocks: mocks, - contextStoreTargetedRecords: { - selectedRecordIds: 'all', - excludedRecordIds: [], + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], }, contextStoreCurrentObjectMetadataNameSingular: 'person', - contextStoreTargetedRecordsFilters: [], }, ); @@ -156,12 +155,11 @@ const graphqlEmptyResponse = [ const WrapperWithEmptyResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper({ apolloMocks: graphqlEmptyResponse, - contextStoreTargetedRecords: { - selectedRecordIds: 'all', - excludedRecordIds: [], + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], }, contextStoreCurrentObjectMetadataNameSingular: 'person', - contextStoreTargetedRecordsFilters: [], }); describe('useRecordData', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts index 51868976a005..e260938e3999 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts @@ -8,16 +8,14 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordsFiltersState } from '@/context-store/states/contextStoreTargetedRecordsFilters'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; 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 { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { ViewType } from '@/views/types/ViewType'; export const sleep = (ms: number) => @@ -78,29 +76,23 @@ export const useRecordData = ({ ); const columns = useRecoilValue(visibleTableColumnsSelector()); - const contextStoreTargetedRecords = useRecoilValue( - contextStoreTargetedRecordsState, + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, ); const contextStoreCurrentObjectMetadataId = useRecoilValue( contextStoreCurrentObjectMetadataIdState, ); - const contextStoreTargetedRecordsFilters = useRecoilValue( - contextStoreTargetedRecordsFiltersState, - ); - const { objectMetadataItem } = useObjectMetadataItemById({ objectId: contextStoreCurrentObjectMetadataId, }); - const queryFilter = turnFiltersIntoQueryFilter( - contextStoreTargetedRecordsFilters, - objectMetadataItem?.fields ?? [], + const queryFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem ?? undefined, ); - const { selectedRecordIds, excludedRecordIds } = contextStoreTargetedRecords; - const findManyRecordsParams = useFindManyParams( objectNameSingular, recordIndexId, @@ -114,26 +106,7 @@ export const useRecordData = ({ loading, } = useLazyFindManyRecords({ ...findManyRecordsParams, - filter: makeAndFilterVariables([ - queryFilter, - selectedRecordIds !== 'all' - ? selectedRecordIds.length === 0 - ? undefined - : { - id: { - in: selectedRecordIds, - }, - } - : excludedRecordIds.length > 0 - ? { - not: { - id: { - in: excludedRecordIds, - }, - }, - } - : undefined, - ]), + filter: queryFilter, limit: pageSize, }); diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 0473832f5d2f..11e60dece444 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer'; -import { RecordIndexContainerEffect } from '@/object-record/record-index/components/RecordIndexContainerEffect'; +import { RecordIndexContainerContextStoreEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreEffect'; import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick'; @@ -72,7 +72,7 @@ export const RecordIndexPage = () => { - + diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx index d55e1ce554f2..57af590aa6a0 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx @@ -1,5 +1,5 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; @@ -10,8 +10,8 @@ export const RecordShowPageContextStoreEffect = ({ }: { recordId: string; }) => { - const setcontextStoreTargetedRecords = useSetRecoilState( - contextStoreTargetedRecordsState, + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); const setContextStoreCurrentObjectMetadataId = useSetRecoilState( @@ -25,22 +25,22 @@ export const RecordShowPageContextStoreEffect = ({ }); useEffect(() => { - setcontextStoreTargetedRecords({ + setContextStoreTargetedRecordsRule({ + mode: 'selection', selectedRecordIds: [recordId], - excludedRecordIds: [], }); setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); return () => { - setcontextStoreTargetedRecords({ + setContextStoreTargetedRecordsRule({ + mode: 'selection', selectedRecordIds: [], - excludedRecordIds: [], }); setContextStoreCurrentObjectMetadataId(null); }; }, [ recordId, - setcontextStoreTargetedRecords, + setContextStoreTargetedRecordsRule, setContextStoreCurrentObjectMetadataId, objectMetadataItem?.id, ]); diff --git a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx index 4d2e83a39e32..39ba6e36e545 100644 --- a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx @@ -2,36 +2,30 @@ import { ReactNode, useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordsFiltersState } from '@/context-store/states/contextStoreTargetedRecordsFilters'; -import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState'; +import { + ContextStoreTargetedRecordsRule, + contextStoreTargetedRecordsRuleState, +} from '@/context-store/states/contextStoreTargetedRecordsState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; export const JestContextStoreSetter = ({ - contextStoreTargetedRecords = { - selectedRecordIds: 'all', - excludedRecordIds: [], + contextStoreTargetedRecordsRule = { + mode: 'selection', + selectedRecordIds: [], }, contextStoreCurrentObjectMetadataNameSingular = '', - contextStoreTargetedRecordsFilters = [], children, }: { - contextStoreTargetedRecords?: { - selectedRecordIds: string[] | 'all'; - excludedRecordIds: string[]; - }; + contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; contextStoreCurrentObjectMetadataNameSingular?: string; - contextStoreTargetedRecordsFilters?: []; children: ReactNode; }) => { - const setContextStoreTargetedRecords = useSetRecoilState( - contextStoreTargetedRecordsState, + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); const setContextStoreCurrentObjectMetadataId = useSetRecoilState( contextStoreCurrentObjectMetadataIdState, ); - const setContextStoreTargetedRecordsFilters = useSetRecoilState( - contextStoreTargetedRecordsFiltersState, - ); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular: contextStoreCurrentObjectMetadataNameSingular, @@ -41,17 +35,14 @@ export const JestContextStoreSetter = ({ const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { - setContextStoreTargetedRecords(contextStoreTargetedRecords); + setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule); setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId); - setContextStoreTargetedRecordsFilters(contextStoreTargetedRecordsFilters); setIsLoaded(true); }, [ - setContextStoreTargetedRecords, + setContextStoreTargetedRecordsRule, setContextStoreCurrentObjectMetadataId, - setContextStoreTargetedRecordsFilters, - contextStoreTargetedRecords, + contextStoreTargetedRecordsRule, contextStoreCurrentObjectMetadataId, - contextStoreTargetedRecordsFilters, ]); return isLoaded ? <>{children} : null; diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx index 3e9a588094cf..241a76203c63 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx @@ -1,3 +1,4 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsState'; import { MockedResponse } from '@apollo/client/testing'; import { ReactNode } from 'react'; import { MutableSnapshot } from 'recoil'; @@ -7,20 +8,15 @@ import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ apolloMocks, onInitializeRecoilSnapshot, - contextStoreTargetedRecords, + contextStoreTargetedRecordsRule, contextStoreCurrentObjectMetadataNameSingular, - contextStoreTargetedRecordsFilters, }: { apolloMocks: | readonly MockedResponse, Record>[] | undefined; onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void; - contextStoreTargetedRecords?: { - selectedRecordIds: string[] | 'all'; - excludedRecordIds: string[]; - }; + contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; contextStoreCurrentObjectMetadataNameSingular?: string; - contextStoreTargetedRecordsFilters?: []; }) => { const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks, @@ -29,11 +25,10 @@ export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ return ({ children }: { children: ReactNode }) => ( {children}