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 3573d83674..a81a0d873d 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 80d0877d04..c6ffcfd584 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 2c18e3fb3a..9b0d08d5b3 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 5302570c3a..81c207b9a0 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 0000000000..7a4b9fb174 --- /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 0000000000..36c138d3d9 --- /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 0000000000..35de0eb7e4 --- /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 0000000000..372972f3e1 --- /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 c5a6083e17..00efe5de9d 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 692cc3ec1b..edaf730594 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 f0b7528332..f9ef88a2e0 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,