Implemented view filter group CRUD hooks and utils (#10551)

This PR implements hooks and utils logic for handling CRUD and view
filter group comparison.

The main hook is useAreViewFilterGroupsDifferentFromRecordFilterGroups,
like view filters and view sorts.

Inside this hook we implement getViewFilterGroupsToCreate,
getViewFilterGroupsToDelete and getViewFilterGroupsToUpdate.

All of those come with their unit tests.

In this PR we also introduce a new util to prevent nasty bugs happening
when we compare undefined === null,

This util is called compareStrictlyExceptForNullAndUndefined and it
should replace every strict equality comparison between values that can
be null or undefined (which we have a lot)

This could be enforced by a custom ESLint rule, the autofix may also be
implemented (maybe the util should be put in twenty-shared ?)
This commit is contained in:
Lucas Bordeau
2025-02-28 13:32:54 +01:00
committed by GitHub
parent b7abaa242c
commit ea1ac3708c
13 changed files with 518 additions and 7 deletions

View File

@ -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],
),
);
};

View File

@ -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],
),
);
};

View File

@ -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],
),
);
};

View File

@ -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;
});
};

View File

@ -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,
),
),
);
};

View File

@ -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;
});
};