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 { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId';
|
import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId';
|
||||||
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||||
|
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
||||||
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
|
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
|
||||||
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
@ -87,6 +88,9 @@ export const UpdateViewButtonGroup = ({
|
|||||||
|
|
||||||
const { hasFiltersQueryParams } = useViewFromQueryParams();
|
const { hasFiltersQueryParams } = useViewFromQueryParams();
|
||||||
|
|
||||||
|
const { viewFilterGroupsAreDifferentFromRecordFilterGroups } =
|
||||||
|
useAreViewFilterGroupsDifferentFromRecordFilterGroups();
|
||||||
|
|
||||||
const { viewFiltersAreDifferentFromRecordFilters } =
|
const { viewFiltersAreDifferentFromRecordFilters } =
|
||||||
useAreViewFiltersDifferentFromRecordFilters();
|
useAreViewFiltersDifferentFromRecordFilters();
|
||||||
|
|
||||||
@ -95,7 +99,8 @@ export const UpdateViewButtonGroup = ({
|
|||||||
|
|
||||||
const canShowButton =
|
const canShowButton =
|
||||||
(viewFiltersAreDifferentFromRecordFilters ||
|
(viewFiltersAreDifferentFromRecordFilters ||
|
||||||
viewSortsAreDifferentFromRecordSorts) &&
|
viewSortsAreDifferentFromRecordSorts ||
|
||||||
|
viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
|
||||||
!hasFiltersQueryParams;
|
!hasFiltersQueryParams;
|
||||||
|
|
||||||
if (!canShowButton) {
|
if (!canShowButton) {
|
||||||
|
|||||||
@ -24,6 +24,9 @@ import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreVie
|
|||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
|
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 { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
@ -122,6 +125,10 @@ export const ViewBarDetails = ({
|
|||||||
|
|
||||||
const { hasFiltersQueryParams } = useViewFromQueryParams();
|
const { hasFiltersQueryParams } = useViewFromQueryParams();
|
||||||
|
|
||||||
|
const currentRecordFilterGroups = useRecoilComponentValueV2(
|
||||||
|
currentRecordFilterGroupsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const currentRecordFilters = useRecoilComponentValueV2(
|
const currentRecordFilters = useRecoilComponentValueV2(
|
||||||
currentRecordFiltersComponentState,
|
currentRecordFiltersComponentState,
|
||||||
);
|
);
|
||||||
@ -139,6 +146,9 @@ export const ViewBarDetails = ({
|
|||||||
});
|
});
|
||||||
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
|
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
|
||||||
|
|
||||||
|
const { viewFilterGroupsAreDifferentFromRecordFilterGroups } =
|
||||||
|
useAreViewFilterGroupsDifferentFromRecordFilterGroups();
|
||||||
|
|
||||||
const { viewFiltersAreDifferentFromRecordFilters } =
|
const { viewFiltersAreDifferentFromRecordFilters } =
|
||||||
useAreViewFiltersDifferentFromRecordFilters();
|
useAreViewFiltersDifferentFromRecordFilters();
|
||||||
|
|
||||||
@ -147,7 +157,8 @@ export const ViewBarDetails = ({
|
|||||||
|
|
||||||
const canResetView =
|
const canResetView =
|
||||||
(viewFiltersAreDifferentFromRecordFilters ||
|
(viewFiltersAreDifferentFromRecordFilters ||
|
||||||
viewSortsAreDifferentFromRecordSorts) &&
|
viewSortsAreDifferentFromRecordSorts ||
|
||||||
|
viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
|
||||||
!hasFiltersQueryParams;
|
!hasFiltersQueryParams;
|
||||||
|
|
||||||
const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter();
|
const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter();
|
||||||
@ -164,6 +175,9 @@ export const ViewBarDetails = ({
|
|||||||
);
|
);
|
||||||
}, [currentRecordFilters, checkIsSoftDeleteFilter]);
|
}, [currentRecordFilters, checkIsSoftDeleteFilter]);
|
||||||
|
|
||||||
|
const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
|
||||||
|
useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
|
||||||
|
|
||||||
const { applyCurrentViewFiltersToCurrentRecordFilters } =
|
const { applyCurrentViewFiltersToCurrentRecordFilters } =
|
||||||
useApplyCurrentViewFiltersToCurrentRecordFilters();
|
useApplyCurrentViewFiltersToCurrentRecordFilters();
|
||||||
|
|
||||||
@ -173,6 +187,7 @@ export const ViewBarDetails = ({
|
|||||||
const handleCancelClick = () => {
|
const handleCancelClick = () => {
|
||||||
if (isDefined(viewId)) {
|
if (isDefined(viewId)) {
|
||||||
resetUnsavedViewStates(viewId);
|
resetUnsavedViewStates(viewId);
|
||||||
|
applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
|
||||||
applyCurrentViewFiltersToCurrentRecordFilters();
|
applyCurrentViewFiltersToCurrentRecordFilters();
|
||||||
applyCurrentViewSortsToCurrentRecordSorts();
|
applyCurrentViewSortsToCurrentRecordSorts();
|
||||||
toggleSoftDeleteFilterState(false);
|
toggleSoftDeleteFilterState(false);
|
||||||
@ -182,7 +197,10 @@ export const ViewBarDetails = ({
|
|||||||
const shouldExpandViewBar =
|
const shouldExpandViewBar =
|
||||||
viewFiltersAreDifferentFromRecordFilters ||
|
viewFiltersAreDifferentFromRecordFilters ||
|
||||||
viewSortsAreDifferentFromRecordSorts ||
|
viewSortsAreDifferentFromRecordSorts ||
|
||||||
((currentRecordSorts?.length || currentRecordFilters?.length) &&
|
viewFilterGroupsAreDifferentFromRecordFilterGroups ||
|
||||||
|
((currentRecordSorts.length > 0 ||
|
||||||
|
currentRecordFilters.length > 0 ||
|
||||||
|
currentRecordFilterGroups.length > 0) &&
|
||||||
isViewBarExpanded);
|
isViewBarExpanded);
|
||||||
|
|
||||||
if (!shouldExpandViewBar) {
|
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 { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
|
||||||
|
|
||||||
export const areViewFiltersEqual = (
|
export const areViewFiltersEqual = (
|
||||||
viewFilterA: ViewFilter,
|
viewFilterA: ViewFilter,
|
||||||
@ -13,7 +14,10 @@ export const areViewFiltersEqual = (
|
|||||||
'operand',
|
'operand',
|
||||||
];
|
];
|
||||||
|
|
||||||
return propertiesToCompare.every(
|
return propertiesToCompare.every((property) =>
|
||||||
(property) => viewFilterA[property] === viewFilterB[property],
|
compareStrictlyExceptForNullAndUndefined(
|
||||||
|
viewFilterA[property],
|
||||||
|
viewFilterB[property],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ViewSort } from '@/views/types/ViewSort';
|
import { ViewSort } from '@/views/types/ViewSort';
|
||||||
|
import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
|
||||||
|
|
||||||
export const areViewSortsEqual = (viewSortA: ViewSort, viewSortB: ViewSort) => {
|
export const areViewSortsEqual = (viewSortA: ViewSort, viewSortB: ViewSort) => {
|
||||||
const propertiesToCompare: (keyof ViewSort)[] = [
|
const propertiesToCompare: (keyof ViewSort)[] = [
|
||||||
@ -6,7 +7,10 @@ export const areViewSortsEqual = (viewSortA: ViewSort, viewSortB: ViewSort) => {
|
|||||||
'direction',
|
'direction',
|
||||||
];
|
];
|
||||||
|
|
||||||
return propertiesToCompare.every(
|
return propertiesToCompare.every((property) =>
|
||||||
(property) => viewSortA[property] === viewSortB[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