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:
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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([]);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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],
|
||||
),
|
||||
);
|
||||
};
|
||||
@ -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],
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@ -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],
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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 = <A, B>(
|
||||
valueA: Nullable<A>,
|
||||
valueB: Nullable<B>,
|
||||
) => {
|
||||
if (!isDefined(valueA) && !isDefined(valueB)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return valueA === valueB;
|
||||
};
|
||||
Reference in New Issue
Block a user