diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
index db57a613e..d25355bf1 100644
--- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
+++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
@@ -16,6 +16,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
+import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
@@ -87,6 +88,9 @@ export const UpdateViewButtonGroup = ({
const { hasFiltersQueryParams } = useViewFromQueryParams();
+ const { viewFilterGroupsAreDifferentFromRecordFilterGroups } =
+ useAreViewFilterGroupsDifferentFromRecordFilterGroups();
+
const { viewFiltersAreDifferentFromRecordFilters } =
useAreViewFiltersDifferentFromRecordFilters();
@@ -95,7 +99,8 @@ export const UpdateViewButtonGroup = ({
const canShowButton =
(viewFiltersAreDifferentFromRecordFilters ||
- viewSortsAreDifferentFromRecordSorts) &&
+ viewSortsAreDifferentFromRecordSorts ||
+ viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
!hasFiltersQueryParams;
if (!canShowButton) {
diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
index 4fbf82de5..cb2fde916 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
@@ -24,6 +24,9 @@ import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreVie
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
+import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
+import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
+import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
import { t } from '@lingui/core/macro';
import { isNonEmptyArray } from '@sniptt/guards';
@@ -122,6 +125,10 @@ export const ViewBarDetails = ({
const { hasFiltersQueryParams } = useViewFromQueryParams();
+ const currentRecordFilterGroups = useRecoilComponentValueV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
@@ -139,6 +146,9 @@ export const ViewBarDetails = ({
});
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
+ const { viewFilterGroupsAreDifferentFromRecordFilterGroups } =
+ useAreViewFilterGroupsDifferentFromRecordFilterGroups();
+
const { viewFiltersAreDifferentFromRecordFilters } =
useAreViewFiltersDifferentFromRecordFilters();
@@ -147,7 +157,8 @@ export const ViewBarDetails = ({
const canResetView =
(viewFiltersAreDifferentFromRecordFilters ||
- viewSortsAreDifferentFromRecordSorts) &&
+ viewSortsAreDifferentFromRecordSorts ||
+ viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
!hasFiltersQueryParams;
const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter();
@@ -164,6 +175,9 @@ export const ViewBarDetails = ({
);
}, [currentRecordFilters, checkIsSoftDeleteFilter]);
+ const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
+ useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
@@ -173,6 +187,7 @@ export const ViewBarDetails = ({
const handleCancelClick = () => {
if (isDefined(viewId)) {
resetUnsavedViewStates(viewId);
+ applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
applyCurrentViewFiltersToCurrentRecordFilters();
applyCurrentViewSortsToCurrentRecordSorts();
toggleSoftDeleteFilterState(false);
@@ -182,7 +197,10 @@ export const ViewBarDetails = ({
const shouldExpandViewBar =
viewFiltersAreDifferentFromRecordFilters ||
viewSortsAreDifferentFromRecordSorts ||
- ((currentRecordSorts?.length || currentRecordFilters?.length) &&
+ viewFilterGroupsAreDifferentFromRecordFilterGroups ||
+ ((currentRecordSorts.length > 0 ||
+ currentRecordFilters.length > 0 ||
+ currentRecordFilterGroups.length > 0) &&
isViewBarExpanded);
if (!shouldExpandViewBar) {
diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups.test.tsx
new file mode 100644
index 000000000..0a7c90b38
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups.test.tsx
@@ -0,0 +1,205 @@
+import { renderHook } from '@testing-library/react';
+
+import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
+import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
+import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
+import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
+import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
+import { View } from '@/views/types/View';
+import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
+import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
+import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
+import { ViewType } from '@/views/types/ViewType';
+import { mapViewFilterGroupLogicalOperatorToRecordFilterGroupLogicalOperator } from '@/views/utils/mapViewFilterGroupLogicalOperatorToRecordFilterGroupLogicalOperator';
+import { act } from 'react';
+import { isDefined } from 'twenty-shared';
+import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
+
+const mockObjectMetadataItemNameSingular = 'company';
+
+describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
+ const mockObjectMetadataItem = generatedMockObjectMetadataItems.find(
+ (item) => item.nameSingular === mockObjectMetadataItemNameSingular,
+ );
+
+ if (!isDefined(mockObjectMetadataItem)) {
+ throw new Error(
+ 'Missing mock object metadata item with name singular "company"',
+ );
+ }
+
+ const mockViewFilterGroup: ViewFilterGroup = {
+ __typename: 'ViewFilterGroup',
+ id: 'filter-group-1',
+ logicalOperator: ViewFilterGroupLogicalOperator.AND,
+ viewId: 'view-1',
+ parentViewFilterGroupId: null,
+ positionInViewFilterGroup: 0,
+ };
+
+ const mockView: View = {
+ id: 'view-1',
+ name: 'Test View',
+ objectMetadataId: mockObjectMetadataItem.id,
+ viewFilters: [],
+ viewFilterGroups: [mockViewFilterGroup],
+ type: ViewType.Table,
+ key: null,
+ isCompact: false,
+ openRecordIn: ViewOpenRecordInType.SIDE_PANEL,
+ viewFields: [],
+ viewGroups: [],
+ viewSorts: [],
+ kanbanFieldMetadataId: '',
+ kanbanAggregateOperation: AGGREGATE_OPERATIONS.count,
+ icon: '',
+ kanbanAggregateOperationFieldMetadataId: '',
+ position: 0,
+ __typename: 'View',
+ };
+
+ it('should apply view filter groups from current view', () => {
+ const { result } = renderHook(
+ () => {
+ const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
+ useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+
+ const currentRecordFilterGroups = useRecoilComponentValueV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
+ return {
+ applyCurrentViewFilterGroupsToCurrentRecordFilterGroups,
+ currentRecordFilterGroups,
+ };
+ },
+ {
+ wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({
+ apolloMocks: [],
+ componentInstanceId: 'instanceId',
+ contextStoreCurrentObjectMetadataNameSingular:
+ mockObjectMetadataItemNameSingular,
+ onInitializeRecoilSnapshot: (snapshot) => {
+ snapshot.set(
+ contextStoreCurrentViewIdComponentState.atomFamily({
+ instanceId: 'instanceId',
+ }),
+ mockView.id,
+ );
+ snapshot.set(prefetchViewsState, [mockView]);
+ },
+ }),
+ },
+ );
+
+ act(() => {
+ result.current.applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+ });
+
+ const expectedRecordFilterGroups: RecordFilterGroup[] = [
+ {
+ id: mockViewFilterGroup.id,
+ logicalOperator:
+ mapViewFilterGroupLogicalOperatorToRecordFilterGroupLogicalOperator({
+ viewFilterGroupLogicalOperator: mockViewFilterGroup.logicalOperator,
+ }),
+ parentRecordFilterGroupId: mockViewFilterGroup.parentViewFilterGroupId,
+ positionInRecordFilterGroup:
+ mockViewFilterGroup.positionInViewFilterGroup,
+ },
+ ];
+
+ expect(result.current.currentRecordFilterGroups).toEqual(
+ expectedRecordFilterGroups,
+ );
+ });
+
+ it('should not apply view filter groups when current view is not found', () => {
+ const { result } = renderHook(
+ () => {
+ const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
+ useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+
+ const currentRecordFilterGroups = useRecoilComponentValueV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
+ return {
+ applyCurrentViewFilterGroupsToCurrentRecordFilterGroups,
+ currentRecordFilterGroups,
+ };
+ },
+ {
+ wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({
+ apolloMocks: [],
+ componentInstanceId: 'instanceId',
+ contextStoreCurrentObjectMetadataNameSingular:
+ mockObjectMetadataItemNameSingular,
+ onInitializeRecoilSnapshot: (snapshot) => {
+ snapshot.set(
+ contextStoreCurrentViewIdComponentState.atomFamily({
+ instanceId: 'instanceId',
+ }),
+ mockView.id,
+ );
+
+ snapshot.set(prefetchViewsState, []);
+ },
+ }),
+ },
+ );
+
+ act(() => {
+ result.current.applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+ });
+
+ expect(result.current.currentRecordFilterGroups).toEqual([]);
+ });
+
+ it('should handle view with empty view filter groups', () => {
+ const { result } = renderHook(
+ () => {
+ const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
+ useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+
+ const currentRecordFilterGroups = useRecoilComponentValueV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
+ return {
+ applyCurrentViewFilterGroupsToCurrentRecordFilterGroups,
+ currentRecordFilterGroups,
+ };
+ },
+ {
+ wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({
+ apolloMocks: [],
+ componentInstanceId: 'instanceId',
+ contextStoreCurrentObjectMetadataNameSingular:
+ mockObjectMetadataItemNameSingular,
+ onInitializeRecoilSnapshot: (snapshot) => {
+ snapshot.set(
+ contextStoreCurrentViewIdComponentState.atomFamily({
+ instanceId: 'instanceId',
+ }),
+ mockView.id,
+ );
+
+ snapshot.set(prefetchViewsState, [
+ { ...mockView, viewFilterGroups: [] },
+ ]);
+ },
+ }),
+ },
+ );
+
+ act(() => {
+ result.current.applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
+ });
+
+ expect(result.current.currentRecordFilterGroups).toEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups.ts b/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups.ts
new file mode 100644
index 000000000..0eecb7bd6
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups.ts
@@ -0,0 +1,67 @@
+import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
+import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
+import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
+import { useRecoilCallback } from 'recoil';
+
+import { isDefined } from 'twenty-shared';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+
+export const useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups =
+ () => {
+ const currentViewId = useRecoilComponentValueV2(
+ contextStoreCurrentViewIdComponentState,
+ );
+
+ const setCurrentRecordFilterGroups = useSetRecoilComponentStateV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
+ const currentRecordFilterGroupsCallbackState =
+ useRecoilComponentCallbackStateV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
+ const applyCurrentViewFilterGroupsToCurrentRecordFilterGroups =
+ useRecoilCallback(
+ ({ snapshot }) =>
+ () => {
+ const currentView = snapshot
+ .getLoadable(
+ prefetchViewFromViewIdFamilySelector({
+ viewId: currentViewId ?? '',
+ }),
+ )
+ .getValue();
+
+ if (isDefined(currentView)) {
+ const currentRecordFilterGroups = snapshot
+ .getLoadable(currentRecordFilterGroupsCallbackState)
+ .getValue();
+
+ const newRecordFilterGroups =
+ mapViewFilterGroupsToRecordFilterGroups(
+ currentView.viewFilterGroups ?? [],
+ );
+
+ if (
+ !isDeeplyEqual(currentRecordFilterGroups, newRecordFilterGroups)
+ ) {
+ setCurrentRecordFilterGroups(newRecordFilterGroups);
+ }
+ }
+ },
+ [
+ currentViewId,
+ setCurrentRecordFilterGroups,
+ currentRecordFilterGroupsCallbackState,
+ ],
+ );
+
+ return {
+ applyCurrentViewFilterGroupsToCurrentRecordFilterGroups,
+ };
+ };
diff --git a/packages/twenty-front/src/modules/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups.ts b/packages/twenty-front/src/modules/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups.ts
new file mode 100644
index 000000000..0e64b950d
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups.ts
@@ -0,0 +1,51 @@
+import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
+import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
+import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
+import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
+import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
+import { isDefined } from 'twenty-shared';
+
+export const useAreViewFilterGroupsDifferentFromRecordFilterGroups = () => {
+ const { currentView } = useGetCurrentViewOnly();
+
+ const currentRecordFilterGroups = useRecoilComponentValueV2(
+ currentRecordFilterGroupsComponentState,
+ );
+
+ const currentViewFilterGroups = currentView?.viewFilterGroups ?? [];
+
+ const viewFilterGroupsFromCurrentRecordFilterGroups = isDefined(currentView)
+ ? currentRecordFilterGroups.map((recordFilterGroup) =>
+ mapRecordFilterGroupToViewFilterGroup({
+ recordFilterGroup,
+ view: currentView,
+ }),
+ )
+ : [];
+
+ const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
+ currentViewFilterGroups,
+ viewFilterGroupsFromCurrentRecordFilterGroups,
+ );
+
+ const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
+ currentViewFilterGroups,
+ viewFilterGroupsFromCurrentRecordFilterGroups,
+ );
+
+ const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
+ currentViewFilterGroups,
+ viewFilterGroupsFromCurrentRecordFilterGroups,
+ );
+
+ const viewFilterGroupsAreDifferentFromRecordFilterGroups =
+ viewFilterGroupsToCreate.length > 0 ||
+ viewFilterGroupsToDelete.length > 0 ||
+ viewFilterGroupsToUpdate.length > 0;
+
+ return {
+ viewFilterGroupsAreDifferentFromRecordFilterGroups,
+ };
+};
diff --git a/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts
new file mode 100644
index 000000000..9f47fac16
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/areViewFilterGroupsEqual.ts
@@ -0,0 +1,21 @@
+import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
+
+export const areViewFilterGroupsEqual = (
+ viewFilterGroupA: ViewFilterGroup,
+ viewFilterGroupB: ViewFilterGroup,
+) => {
+ const propertiesToCompare: (keyof ViewFilterGroup)[] = [
+ 'positionInViewFilterGroup',
+ 'logicalOperator',
+ 'parentViewFilterGroupId',
+ 'viewId',
+ ];
+
+ return propertiesToCompare.every((property) =>
+ compareStrictlyExceptForNullAndUndefined(
+ viewFilterGroupA[property],
+ viewFilterGroupB[property],
+ ),
+ );
+};
diff --git a/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts
index 5087bf44a..2ceee15f0 100644
--- a/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts
+++ b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts
@@ -1,4 +1,5 @@
import { ViewFilter } from '@/views/types/ViewFilter';
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
export const areViewFiltersEqual = (
viewFilterA: ViewFilter,
@@ -13,7 +14,10 @@ export const areViewFiltersEqual = (
'operand',
];
- return propertiesToCompare.every(
- (property) => viewFilterA[property] === viewFilterB[property],
+ return propertiesToCompare.every((property) =>
+ compareStrictlyExceptForNullAndUndefined(
+ viewFilterA[property],
+ viewFilterB[property],
+ ),
);
};
diff --git a/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts
index d028c2375..6f807446e 100644
--- a/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts
+++ b/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts
@@ -1,4 +1,5 @@
import { ViewSort } from '@/views/types/ViewSort';
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
export const areViewSortsEqual = (viewSortA: ViewSort, viewSortB: ViewSort) => {
const propertiesToCompare: (keyof ViewSort)[] = [
@@ -6,7 +7,10 @@ export const areViewSortsEqual = (viewSortA: ViewSort, viewSortB: ViewSort) => {
'direction',
];
- return propertiesToCompare.every(
- (property) => viewSortA[property] === viewSortB[property],
+ return propertiesToCompare.every((property) =>
+ compareStrictlyExceptForNullAndUndefined(
+ viewSortA[property],
+ viewSortB[property],
+ ),
);
};
diff --git a/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToCreate.ts b/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToCreate.ts
new file mode 100644
index 000000000..f82ef4854
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToCreate.ts
@@ -0,0 +1,24 @@
+import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
+import { isDefined } from 'twenty-shared';
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
+
+export const getViewFilterGroupsToCreate = (
+ currentViewFilterGroups: ViewFilterGroup[],
+ newViewFilterGroups: ViewFilterGroup[],
+) => {
+ return newViewFilterGroups.filter((newViewFilterGroup) => {
+ const correspondingViewFilterGroup = currentViewFilterGroups.find(
+ (currentViewFilterGroup) =>
+ compareStrictlyExceptForNullAndUndefined(
+ currentViewFilterGroup.id,
+ newViewFilterGroup.id,
+ ),
+ );
+
+ const shouldCreateBecauseViewFilterGroupIsNew = !isDefined(
+ correspondingViewFilterGroup,
+ );
+
+ return shouldCreateBecauseViewFilterGroupIsNew;
+ });
+};
diff --git a/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToDelete.ts b/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToDelete.ts
new file mode 100644
index 000000000..d038f78bc
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToDelete.ts
@@ -0,0 +1,17 @@
+import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
+
+export const getViewFilterGroupsToDelete = (
+ currentViewFilterGroups: ViewFilterGroup[],
+ newViewFilterGroups: ViewFilterGroup[],
+) => {
+ return currentViewFilterGroups.filter(
+ (currentViewFilterGroup) =>
+ !newViewFilterGroups.some((newViewFilterGroup) =>
+ compareStrictlyExceptForNullAndUndefined(
+ newViewFilterGroup.id,
+ currentViewFilterGroup.id,
+ ),
+ ),
+ );
+};
diff --git a/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToUpdate.ts b/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToUpdate.ts
new file mode 100644
index 000000000..06e0f13d2
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/getViewFilterGroupsToUpdate.ts
@@ -0,0 +1,31 @@
+import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
+import { areViewFilterGroupsEqual } from '@/views/utils/areViewFilterGroupsEqual';
+import { isDefined } from 'twenty-shared';
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
+
+export const getViewFilterGroupsToUpdate = (
+ currentViewFilterGroups: ViewFilterGroup[],
+ newViewFilterGroups: ViewFilterGroup[],
+) => {
+ return newViewFilterGroups.filter((newViewFilterGroup) => {
+ const correspondingViewFilterGroup = currentViewFilterGroups.find(
+ (currentViewFilterGroup) =>
+ compareStrictlyExceptForNullAndUndefined(
+ currentViewFilterGroup.id,
+ newViewFilterGroup.id,
+ ),
+ );
+
+ if (!isDefined(correspondingViewFilterGroup)) {
+ return false;
+ }
+
+ const shouldUpdateBecauseViewFilterGroupIsDifferent =
+ !areViewFilterGroupsEqual(
+ newViewFilterGroup,
+ correspondingViewFilterGroup,
+ );
+
+ return shouldUpdateBecauseViewFilterGroupIsDifferent;
+ });
+};
diff --git a/packages/twenty-front/src/utils/__tests__/compareStrictlyExceptForNullAndUndefined.test.ts b/packages/twenty-front/src/utils/__tests__/compareStrictlyExceptForNullAndUndefined.test.ts
new file mode 100644
index 000000000..08e2529e0
--- /dev/null
+++ b/packages/twenty-front/src/utils/__tests__/compareStrictlyExceptForNullAndUndefined.test.ts
@@ -0,0 +1,49 @@
+import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
+
+describe('compareStrictlyExceptForNullAndUndefined', () => {
+ it('should return true for undefined === null', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(undefined, null)).toBe(
+ true,
+ );
+ });
+
+ it('should return true for null === undefined', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(null, undefined)).toBe(
+ true,
+ );
+ });
+
+ it('should return true for undefined === undefined', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(undefined, undefined)).toBe(
+ true,
+ );
+ });
+
+ it('should return true for null === null', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(null, null)).toBe(true);
+ });
+
+ it('should return true for 2 === 2', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(2, 2)).toBe(true);
+ });
+
+ it('should return false for 2 === 3', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(2, 3)).toBe(false);
+ });
+
+ it('should return false for undefined === 2', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(undefined, 2)).toBe(false);
+ });
+
+ it('should return false for null === 2', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(null, 2)).toBe(false);
+ });
+
+ it('should return false for 2 === "2"', () => {
+ expect(compareStrictlyExceptForNullAndUndefined(2, '2')).toBe(false);
+ });
+
+ it('should return true for "2" === "2"', () => {
+ expect(compareStrictlyExceptForNullAndUndefined('2', '2')).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/utils/compareStrictlyExceptForNullAndUndefined.ts b/packages/twenty-front/src/utils/compareStrictlyExceptForNullAndUndefined.ts
new file mode 100644
index 000000000..509fee39e
--- /dev/null
+++ b/packages/twenty-front/src/utils/compareStrictlyExceptForNullAndUndefined.ts
@@ -0,0 +1,15 @@
+import { isDefined } from 'twenty-shared';
+import { Nullable } from 'twenty-ui';
+
+// TODO: we should create a custom eslint rule that enforces the use of this function
+// instead of using the `===` operator where a and b are | undefined | null
+export const compareStrictlyExceptForNullAndUndefined = (
+ valueA: Nullable,
+ valueB: Nullable,
+) => {
+ if (!isDefined(valueA) && !isDefined(valueB)) {
+ return true;
+ }
+
+ return valueA === valueB;
+};