From 694553608b262ca608dbdfef0d06d75cbc7b9517 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 26 Feb 2025 16:24:11 +0100 Subject: [PATCH] Implements record filter group upsert and remove (#10514) This PR implements a parallel code path to upsert and remove record filter groups. Those record filter groups aren't keeping track of the view id but since they are in a view context, it's implicit that the advanced filter feature can keep track of view for record filter groups. We'll need record filter group for an upcoming feature without views, so we need to abstract record filter group from view. We have viewFilterGroup.id === recordFilterGroup.id, so it's easy to map each other. --- .../AdvancedFilterAddFilterRuleSelect.tsx | 20 +++- .../AdvancedFilterLogicalOperatorDropdown.tsx | 13 +++ .../AdvancedFilterRuleOptionsDropdown.tsx | 4 + .../components/AdvancedFilterButton.tsx | 8 ++ .../useRemoveRecordFilterGroup.test.tsx | 104 ++++++++++++++++++ .../useUpsertRecordFilterGroup.test.tsx | 94 ++++++++++++++++ .../hooks/useRemoveRecordFilterGroup.ts | 59 ++++++++++ .../hooks/useUpsertRecordFilterGroup.ts | 63 +++++++++++ .../__tests__/useRemoveRecordFilter.test.tsx | 25 ++--- .../__tests__/useUpsertRecordFilter.test.tsx | 24 ++-- .../AdvancedFilterDropdownButton.tsx | 4 + 11 files changed, 391 insertions(+), 27 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useRemoveRecordFilterGroup.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useUpsertRecordFilterGroup.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup.ts diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx index 3573d8367..a81a0d873 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx @@ -2,6 +2,9 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup'; +import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup'; +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -31,6 +34,8 @@ export const AdvancedFilterAddFilterRuleSelect = ({ const { currentViewId } = useGetCurrentView(); const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup(); + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); + const { upsertRecordFilter } = useUpsertRecordFilter(); const newPositionInViewFilterGroup = lastChildPosition + 1; @@ -108,15 +113,26 @@ export const AdvancedFilterAddFilterRuleSelect = ({ throw new Error('Missing view id'); } - const newViewFilterGroup = { - id: v4(), + const newRecordFilterGroupId = v4(); + + const newViewFilterGroup: ViewFilterGroup = { + __typename: 'ViewFilterGroup', + id: newRecordFilterGroupId, viewId: currentViewId, logicalOperator: ViewFilterGroupLogicalOperator.AND, parentViewFilterGroupId: viewFilterGroup.id, positionInViewFilterGroup: newPositionInViewFilterGroup, }; + const newRecordFilterGroup: RecordFilterGroup = { + id: newRecordFilterGroupId, + logicalOperator: RecordFilterGroupLogicalOperator.AND, + parentRecordFilterGroupId: viewFilterGroup.id, + positionInRecordFilterGroup: newPositionInViewFilterGroup, + }; + upsertCombinedViewFilterGroup(newViewFilterGroup); + upsertRecordFilterGroup(newRecordFilterGroup); const defaultFieldMetadataItem = getDefaultFieldMetadataItem(); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx index 80d0877d0..c6ffcfd58 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx @@ -1,5 +1,7 @@ import { ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS } from '@/object-record/advanced-filter/constants/AdvancedFilterLogicalOperatorOptions'; import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup'; +import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup'; +import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; import { Select } from '@/ui/input/components/Select'; import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; @@ -12,12 +14,23 @@ export const AdvancedFilterLogicalOperatorDropdown = ({ viewFilterGroup, }: AdvancedFilterLogicalOperatorDropdownProps) => { const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup(); + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); const handleChange = (value: ViewFilterGroupLogicalOperator) => { upsertCombinedViewFilterGroup({ ...viewFilterGroup, logicalOperator: value, }); + + upsertRecordFilterGroup({ + id: viewFilterGroup.id, + parentRecordFilterGroupId: viewFilterGroup.parentViewFilterGroupId, + positionInRecordFilterGroup: viewFilterGroup.positionInViewFilterGroup, + logicalOperator: + value === ViewFilterGroupLogicalOperator.AND + ? RecordFilterGroupLogicalOperator.AND + : RecordFilterGroupLogicalOperator.OR, + }); }; return ( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown.tsx index 2c18e3fb3..9b0d08d5b 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown.tsx @@ -2,6 +2,7 @@ import { AdvancedFilterRuleOptionsDropdownButton } from '@/object-record/advance import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup'; import { useDeleteCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup'; +import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup'; import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; @@ -30,6 +31,7 @@ export const AdvancedFilterRuleOptionsDropdown = ({ const { removeRecordFilter } = useRemoveRecordFilter(); const { deleteCombinedViewFilterGroup } = useDeleteCombinedViewFilterGroup(); + const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); const { currentViewFilterGroup, childViewFiltersAndViewFilterGroups } = useCurrentViewViewFilterGroup({ @@ -56,9 +58,11 @@ export const AdvancedFilterRuleOptionsDropdown = ({ isDefined(currentRecordFilter?.viewFilterGroupId) ) { deleteCombinedViewFilterGroup(currentRecordFilter.viewFilterGroupId); + removeRecordFilterGroup(currentRecordFilter.viewFilterGroupId); } } else if (isDefined(currentViewFilterGroup)) { deleteCombinedViewFilterGroup(currentViewFilterGroup.id); + removeRecordFilterGroup(currentViewFilterGroup.id); // TODO: This is a temporary fix view filter group will be removed soon. const childViewFilters = childViewFiltersAndViewFilterGroups.filter( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx index 5302570c3..81c207b9a 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx @@ -3,6 +3,8 @@ import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-met import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; +import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup'; +import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; @@ -58,6 +60,7 @@ export const AdvancedFilterButton = () => { useGetCurrentView(); const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup(); + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); const { upsertRecordFilter } = useUpsertRecordFilter(); @@ -96,6 +99,11 @@ export const AdvancedFilterButton = () => { upsertCombinedViewFilterGroup(newViewFilterGroup); + upsertRecordFilterGroup({ + id: newViewFilterGroup.id, + logicalOperator: RecordFilterGroupLogicalOperator.AND, + }); + const defaultFieldMetadataItem = availableFieldMetadataItemsForFilter.find( (fieldMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useRemoveRecordFilterGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useRemoveRecordFilterGroup.test.tsx new file mode 100644 index 000000000..7a4b9fb17 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useRemoveRecordFilterGroup.test.tsx @@ -0,0 +1,104 @@ +import { renderHook } from '@testing-library/react'; + +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { act } from 'react'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { useRemoveRecordFilterGroup } from '../useRemoveRecordFilterGroup'; +import { useUpsertRecordFilterGroup } from '../useUpsertRecordFilterGroup'; + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + +describe('useRemoveRecordFilterGroup', () => { + it('should remove an existing record filter group', () => { + const { result } = renderHook( + () => { + const currentRecordFilterGroups = useRecoilComponentValueV2( + currentRecordFilterGroupsComponentState, + ); + + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); + const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); + + return { + upsertRecordFilterGroup, + removeRecordFilterGroup, + currentRecordFilterGroups, + }; + }, + { + wrapper: Wrapper, + }, + ); + + const mockRecordFilterGroup: RecordFilterGroup = { + id: 'record-filter-group-1', + logicalOperator: RecordFilterGroupLogicalOperator.AND, + parentRecordFilterGroupId: null, + }; + + act(() => { + result.current.upsertRecordFilterGroup(mockRecordFilterGroup); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(1); + expect(result.current.currentRecordFilterGroups[0]).toEqual( + mockRecordFilterGroup, + ); + + act(() => { + result.current.removeRecordFilterGroup(mockRecordFilterGroup.id); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(0); + }); + + it('should not modify record filter groups when trying to remove a non-existent record filter group', () => { + const { result } = renderHook( + () => { + const currentRecordFilterGroups = useRecoilComponentValueV2( + currentRecordFilterGroupsComponentState, + ); + + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); + const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); + + return { + upsertRecordFilterGroup, + removeRecordFilterGroup, + currentRecordFilterGroups, + }; + }, + { + wrapper: Wrapper, + }, + ); + + const mockRecordFilterGroup: RecordFilterGroup = { + id: 'record-filter-group-1', + logicalOperator: RecordFilterGroupLogicalOperator.AND, + parentRecordFilterGroupId: null, + }; + + act(() => { + result.current.upsertRecordFilterGroup(mockRecordFilterGroup); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(1); + + act(() => { + result.current.removeRecordFilterGroup( + 'non-existent-record-filter-group-id', + ); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(1); + expect(result.current.currentRecordFilterGroups[0]).toEqual( + mockRecordFilterGroup, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useUpsertRecordFilterGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useUpsertRecordFilterGroup.test.tsx new file mode 100644 index 000000000..36c138d3d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/__tests__/useUpsertRecordFilterGroup.test.tsx @@ -0,0 +1,94 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; + +import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { RecordFilterGroupLogicalOperator } from '@/object-record/record-filter-group/types/RecordFilterGroupLogicalOperator'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + +describe('useUpsertRecordFilterGroup', () => { + it('should add a new record filter group', () => { + const { result } = renderHook( + () => { + const currentRecordFilterGroups = useRecoilComponentValueV2( + currentRecordFilterGroupsComponentState, + ); + + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); + + return { upsertRecordFilterGroup, currentRecordFilterGroups }; + }, + { + wrapper: Wrapper, + }, + ); + + const mockRecordFilterGroup: RecordFilterGroup = { + id: 'record-filter-group-1', + logicalOperator: RecordFilterGroupLogicalOperator.AND, + parentRecordFilterGroupId: null, + }; + + act(() => { + result.current.upsertRecordFilterGroup(mockRecordFilterGroup); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(1); + expect(result.current.currentRecordFilterGroups[0]).toEqual( + mockRecordFilterGroup, + ); + }); + + it('should update an existing record filter group', () => { + const { result } = renderHook( + () => { + const currentRecordFilterGroups = useRecoilComponentValueV2( + currentRecordFilterGroupsComponentState, + ); + + const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup(); + + return { upsertRecordFilterGroup, currentRecordFilterGroups }; + }, + { + wrapper: Wrapper, + }, + ); + + const mockInitialRecordFilterGroup: RecordFilterGroup = { + id: 'record-filter-group-1', + logicalOperator: RecordFilterGroupLogicalOperator.AND, + parentRecordFilterGroupId: null, + }; + + const mockUpdatedRecordFilterGroup: RecordFilterGroup = { + id: 'record-filter-group-1', + logicalOperator: RecordFilterGroupLogicalOperator.OR, + parentRecordFilterGroupId: null, + }; + + act(() => { + result.current.upsertRecordFilterGroup(mockInitialRecordFilterGroup); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(1); + expect(result.current.currentRecordFilterGroups[0]).toEqual( + mockInitialRecordFilterGroup, + ); + + act(() => { + result.current.upsertRecordFilterGroup(mockUpdatedRecordFilterGroup); + }); + + expect(result.current.currentRecordFilterGroups).toHaveLength(1); + expect(result.current.currentRecordFilterGroups[0]).toEqual( + mockUpdatedRecordFilterGroup, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup.ts b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup.ts new file mode 100644 index 000000000..35de0eb7e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup.ts @@ -0,0 +1,59 @@ +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; + +export const useRemoveRecordFilterGroup = () => { + const currentRecordFilterGroupsCallbackState = + useRecoilComponentCallbackStateV2(currentRecordFilterGroupsComponentState); + + const removeRecordFilterGroup = useRecoilCallback( + ({ set, snapshot }) => + (recordFilterGroupIdToRemove: string) => { + const currentRecordFilterGroups = getSnapshotValue( + snapshot, + currentRecordFilterGroupsCallbackState, + ); + + const hasFoundRecordFilterGroupInCurrentRecordFilterGroups = + currentRecordFilterGroups.some( + (existingRecordFilterGroup) => + existingRecordFilterGroup.id === recordFilterGroupIdToRemove, + ); + + if (hasFoundRecordFilterGroupInCurrentRecordFilterGroups) { + set( + currentRecordFilterGroupsCallbackState, + (currentRecordFilterGroups) => { + const newCurrentRecordFilterGroups = [ + ...currentRecordFilterGroups, + ]; + + const indexOfRecordFilterGroupToRemove = + newCurrentRecordFilterGroups.findIndex( + (existingRecordFilterGroup) => + existingRecordFilterGroup.id === + recordFilterGroupIdToRemove, + ); + + if (indexOfRecordFilterGroupToRemove === -1) { + return newCurrentRecordFilterGroups; + } + + newCurrentRecordFilterGroups.splice( + indexOfRecordFilterGroupToRemove, + 1, + ); + + return newCurrentRecordFilterGroups; + }, + ); + } + }, + [currentRecordFilterGroupsCallbackState], + ); + + return { + removeRecordFilterGroup, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup.ts b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup.ts new file mode 100644 index 000000000..372972f3e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup.ts @@ -0,0 +1,63 @@ +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; + +export const useUpsertRecordFilterGroup = () => { + const currentRecordFilterGroupsCallbackState = + useRecoilComponentCallbackStateV2(currentRecordFilterGroupsComponentState); + + const upsertRecordFilterGroup = useRecoilCallback( + ({ set, snapshot }) => + (recordFilterGroupToSet: RecordFilterGroup) => { + const currentRecordFilterGroups = getSnapshotValue( + snapshot, + currentRecordFilterGroupsCallbackState, + ); + + const hasFoundRecordFilterGroupInCurrentRecordFilterGroups = + currentRecordFilterGroups.some( + (existingRecordFilterGroup) => + existingRecordFilterGroup.id === recordFilterGroupToSet.id, + ); + + if (!hasFoundRecordFilterGroupInCurrentRecordFilterGroups) { + set(currentRecordFilterGroupsCallbackState, [ + ...currentRecordFilterGroups, + recordFilterGroupToSet, + ]); + } else { + set( + currentRecordFilterGroupsCallbackState, + (currentRecordFilterGroups) => { + const newCurrentRecordFilterGroups = [ + ...currentRecordFilterGroups, + ]; + + const indexOfRecordFilterGroupToUpdate = + newCurrentRecordFilterGroups.findIndex( + (existingRecordFilterGroup) => + existingRecordFilterGroup.id === recordFilterGroupToSet.id, + ); + + if (indexOfRecordFilterGroupToUpdate === -1) { + return newCurrentRecordFilterGroups; + } + + newCurrentRecordFilterGroups[indexOfRecordFilterGroupToUpdate] = { + ...recordFilterGroupToSet, + }; + + return newCurrentRecordFilterGroups; + }, + ); + } + }, + [currentRecordFilterGroupsCallbackState], + ); + + return { + upsertRecordFilterGroup, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useRemoveRecordFilter.test.tsx b/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useRemoveRecordFilter.test.tsx index c5a6083e1..00efe5de9 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useRemoveRecordFilter.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useRemoveRecordFilter.test.tsx @@ -1,10 +1,10 @@ import { renderHook } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { act } from 'react'; import { FieldMetadataType } from '~/generated/graphql'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { useRemoveRecordFilter } from '../useRemoveRecordFilter'; @@ -15,7 +15,7 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ }); describe('useRemoveRecordFilter', () => { - it('should remove an existing filter', () => { + it('should remove an existing record filter', () => { const { result } = renderHook( () => { const currentRecordFilters = useRecoilComponentValueV2( @@ -36,7 +36,7 @@ describe('useRemoveRecordFilter', () => { }, ); - const filter: RecordFilter = { + const mockRecordFilter: RecordFilter = { id: 'filter-1', fieldMetadataId: 'field-1', value: 'test-value', @@ -46,17 +46,15 @@ describe('useRemoveRecordFilter', () => { type: FieldMetadataType.TEXT, }; - // First add a filter act(() => { - result.current.upsertRecordFilter(filter); + result.current.upsertRecordFilter(mockRecordFilter); }); expect(result.current.currentRecordFilters).toHaveLength(1); - expect(result.current.currentRecordFilters[0]).toEqual(filter); + expect(result.current.currentRecordFilters[0]).toEqual(mockRecordFilter); - // Then remove it act(() => { - result.current.removeRecordFilter(filter.fieldMetadataId); + result.current.removeRecordFilter(mockRecordFilter.fieldMetadataId); }); expect(result.current.currentRecordFilters).toHaveLength(0); @@ -81,7 +79,7 @@ describe('useRemoveRecordFilter', () => { }, ); - const filter: RecordFilter = { + const mockRecordFilter: RecordFilter = { id: 'filter-1', fieldMetadataId: 'field-1', value: 'test-value', @@ -91,20 +89,17 @@ describe('useRemoveRecordFilter', () => { type: FieldMetadataType.TEXT, }; - // Add a filter act(() => { - result.current.upsertRecordFilter(filter); + result.current.upsertRecordFilter(mockRecordFilter); }); expect(result.current.currentRecordFilters).toHaveLength(1); - // Try to remove a non-existent filter act(() => { - result.current.removeRecordFilter('non-existent-field'); + result.current.removeRecordFilter('non-existent-field-metadata-id'); }); - // Filter list should remain unchanged expect(result.current.currentRecordFilters).toHaveLength(1); - expect(result.current.currentRecordFilters[0]).toEqual(filter); + expect(result.current.currentRecordFilters[0]).toEqual(mockRecordFilter); }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useUpsertRecordFilter.test.tsx b/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useUpsertRecordFilter.test.tsx index 692cc3ec1..edaf73059 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useUpsertRecordFilter.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/__tests__/useUpsertRecordFilter.test.tsx @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { act } from 'react'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; @@ -30,7 +30,7 @@ describe('useUpsertRecordFilter', () => { }, ); - const newFilter: RecordFilter = { + const mockNewRecordFilter: RecordFilter = { id: 'filter-1', fieldMetadataId: 'field-1', value: 'test-value', @@ -41,11 +41,11 @@ describe('useUpsertRecordFilter', () => { }; act(() => { - result.current.upsertRecordFilter(newFilter); + result.current.upsertRecordFilter(mockNewRecordFilter); }); expect(result.current.currentRecordFilters).toHaveLength(1); - expect(result.current.currentRecordFilters[0]).toEqual(newFilter); + expect(result.current.currentRecordFilters[0]).toEqual(mockNewRecordFilter); }); it('should update an existing filter when fieldMetadataId exists', () => { @@ -64,7 +64,7 @@ describe('useUpsertRecordFilter', () => { }, ); - const initialFilter: RecordFilter = { + const mockInitialRecordFilter: RecordFilter = { id: 'filter-1', fieldMetadataId: 'field-1', value: 'initial-value', @@ -74,7 +74,7 @@ describe('useUpsertRecordFilter', () => { type: FieldMetadataType.TEXT, }; - const updatedFilter: RecordFilter = { + const mockUpdatedRecordFilter: RecordFilter = { id: 'filter-1', fieldMetadataId: 'field-1', value: 'updated-value', @@ -85,17 +85,21 @@ describe('useUpsertRecordFilter', () => { }; act(() => { - result.current.upsertRecordFilter(initialFilter); + result.current.upsertRecordFilter(mockInitialRecordFilter); }); expect(result.current.currentRecordFilters).toHaveLength(1); - expect(result.current.currentRecordFilters[0]).toEqual(initialFilter); + expect(result.current.currentRecordFilters[0]).toEqual( + mockInitialRecordFilter, + ); act(() => { - result.current.upsertRecordFilter(updatedFilter); + result.current.upsertRecordFilter(mockUpdatedRecordFilter); }); expect(result.current.currentRecordFilters).toHaveLength(1); - expect(result.current.currentRecordFilters[0]).toEqual(updatedFilter); + expect(result.current.currentRecordFilters[0]).toEqual( + mockUpdatedRecordFilter, + ); }); }); diff --git a/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx index f0b752833..f9ef88a2e 100644 --- a/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx @@ -4,6 +4,7 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { AdvancedFilterRootLevelViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup'; import { useDeleteCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup'; +import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup'; import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -14,6 +15,7 @@ import { isDefined } from 'twenty-shared'; export const AdvancedFilterDropdownButton = () => { const { deleteCombinedViewFilterGroup } = useDeleteCombinedViewFilterGroup(); + const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); @@ -43,6 +45,7 @@ export const AdvancedFilterDropdownButton = () => { for (const viewFilterGroupId of viewFilterGroupIds) { await deleteCombinedViewFilterGroup(viewFilterGroupId); + removeRecordFilterGroup(viewFilterGroupId); } for (const recordFilterId of advancedRecordFilterIds) { @@ -50,6 +53,7 @@ export const AdvancedFilterDropdownButton = () => { } }, [ advancedRecordFilterIds, + removeRecordFilterGroup, removeRecordFilter, deleteCombinedViewFilterGroup, currentViewWithCombinedFiltersAndSorts?.viewFilterGroups,