diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
index 4d0e22b10..4cc33451e 100644
--- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
+++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
@@ -11,15 +11,15 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
-import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
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 { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
+import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
-import { canPersistViewComponentFamilySelector } from '@/views/states/selectors/canPersistViewComponentFamilySelector';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
@@ -46,11 +46,6 @@ export const UpdateViewButtonGroup = ({
const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
- const canPersistView = useRecoilComponentFamilyValueV2(
- canPersistViewComponentFamilySelector,
- { viewId: currentViewId },
- );
-
const { closeDropdown: closeUpdateViewButtonDropdown } = useDropdown(
UPDATE_VIEW_BUTTON_DROPDOWN_ID,
);
@@ -89,7 +84,16 @@ export const UpdateViewButtonGroup = ({
const { hasFiltersQueryParams } = useViewFromQueryParams();
- const canShowButton = canPersistView && !hasFiltersQueryParams;
+ const { viewFiltersAreDifferentFromRecordFilters } =
+ useAreViewFiltersDifferentFromRecordFilters();
+
+ const { viewSortsAreDifferentFromRecordSorts } =
+ useAreViewSortsDifferentFromRecordSorts();
+
+ const canShowButton =
+ (viewFiltersAreDifferentFromRecordFilters ||
+ viewSortsAreDifferentFromRecordSorts) &&
+ !hasFiltersQueryParams;
if (!canShowButton) {
return <>>;
diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
index d81bacc00..1a915825e 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
@@ -4,10 +4,8 @@ import { ReactNode, useMemo } from 'react';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
-import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
-import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterDropdownButton';
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
@@ -15,14 +13,14 @@ import { EditableSortChip } from '@/views/components/EditableSortChip';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
+import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters';
+import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
+import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
-import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
-import { canPersistViewComponentFamilySelector } from '@/views/states/selectors/canPersistViewComponentFamilySelector';
-import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { isDefined } from 'twenty-ui';
import { VariantFilterChip } from './VariantFilterChip';
@@ -118,13 +116,8 @@ export const ViewBarDetails = ({
const { hasFiltersQueryParams } = useViewFromQueryParams();
- const canPersistView = useRecoilComponentFamilyValueV2(
- canPersistViewComponentFamilySelector,
- { viewId },
- );
-
- const availableFilterDefinitions = useRecoilComponentValueV2(
- availableFilterDefinitionsComponentState,
+ const currentRecordFilters = useRecoilComponentValueV2(
+ currentRecordFiltersComponentState,
);
const availableSortDefinitions = useRecoilComponentValueV2(
@@ -139,35 +132,34 @@ export const ViewBarDetails = ({
viewBarId: viewBarId,
});
const { resetUnsavedViewStates } = useResetUnsavedViewStates();
- const canResetView = canPersistView && !hasFiltersQueryParams;
- const { otherViewFilters, defaultViewFilters } = useMemo(() => {
- if (!currentViewWithCombinedFiltersAndSorts) {
- return {
- otherViewFilters: [],
- defaultViewFilters: [],
- };
- }
+ const { viewFiltersAreDifferentFromRecordFilters } =
+ useAreViewFiltersDifferentFromRecordFilters();
- const otherViewFilters =
- currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
- (viewFilter) =>
- viewFilter.variant &&
- viewFilter.variant !== 'default' &&
- !viewFilter.viewFilterGroupId,
- );
- const defaultViewFilters =
- currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
- (viewFilter) =>
- (!viewFilter.variant || viewFilter.variant === 'default') &&
- !viewFilter.viewFilterGroupId,
- );
+ const { viewSortsAreDifferentFromRecordSorts } =
+ useAreViewSortsDifferentFromRecordSorts();
- return {
- otherViewFilters,
- defaultViewFilters,
- };
- }, [currentViewWithCombinedFiltersAndSorts]);
+ const canResetView =
+ (viewFiltersAreDifferentFromRecordFilters ||
+ viewSortsAreDifferentFromRecordSorts) &&
+ !hasFiltersQueryParams;
+
+ const otherViewFilters = useMemo(() => {
+ return currentRecordFilters.filter(
+ (viewFilter) =>
+ viewFilter.variant &&
+ viewFilter.variant !== 'default' &&
+ !viewFilter.viewFilterGroupId,
+ );
+ }, [currentRecordFilters]);
+
+ const defaultViewFilters = useMemo(() => {
+ return currentRecordFilters.filter(
+ (viewFilter) =>
+ (!viewFilter.variant || viewFilter.variant === 'default') &&
+ !viewFilter.viewFilterGroupId,
+ );
+ }, [currentRecordFilters]);
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
@@ -181,9 +173,9 @@ export const ViewBarDetails = ({
};
const shouldExpandViewBar =
- canPersistView ||
+ viewFiltersAreDifferentFromRecordFilters ||
((currentViewWithCombinedFiltersAndSorts?.viewSorts?.length ||
- currentViewWithCombinedFiltersAndSorts?.viewFilters?.length) &&
+ currentRecordFilters?.length) &&
isViewBarExpanded);
if (!shouldExpandViewBar) {
@@ -201,11 +193,7 @@ export const ViewBarDetails = ({
{otherViewFilters.map((viewFilter) => (
))}
@@ -228,10 +216,7 @@ export const ViewBarDetails = ({
)}
{showAdvancedFilterDropdownButton && }
- {mapViewFiltersToFilters(
- defaultViewFilters,
- availableFilterDefinitions,
- ).map((viewFilter) => (
+ {defaultViewFilters.map((viewFilter) => (
{
+ const { currentView } = useGetCurrentViewOnly();
+ const currentRecordFilters = useRecoilComponentValueV2(
+ currentRecordFiltersComponentState,
+ );
+
+ const viewFiltersAreDifferentFromRecordFilters = useMemo(() => {
+ const currentViewFilters = currentView?.viewFilters ?? [];
+ const viewFiltersFromCurrentRecordFilters = currentRecordFilters.map(
+ mapRecordFilterToViewFilter,
+ );
+
+ const viewFiltersToCreate = getViewFiltersToCreate(
+ currentViewFilters,
+ viewFiltersFromCurrentRecordFilters,
+ );
+
+ const viewFiltersToDelete = getViewFiltersToDelete(
+ currentViewFilters,
+ viewFiltersFromCurrentRecordFilters,
+ );
+
+ const viewFiltersToUpdate = getViewFiltersToUpdate(
+ currentViewFilters,
+ viewFiltersFromCurrentRecordFilters,
+ );
+
+ const filtersHaveChanged =
+ viewFiltersToCreate.length > 0 ||
+ viewFiltersToDelete.length > 0 ||
+ viewFiltersToUpdate.length > 0;
+
+ return filtersHaveChanged;
+ }, [currentRecordFilters, currentView]);
+
+ return { viewFiltersAreDifferentFromRecordFilters };
+};
diff --git a/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts
new file mode 100644
index 000000000..b19896b0e
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts
@@ -0,0 +1,14 @@
+import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
+import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
+import { areViewSortsDifferentFromRecordSortsSelector } from '@/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector';
+
+export const useAreViewSortsDifferentFromRecordSorts = () => {
+ const { currentView } = useGetCurrentViewOnly();
+
+ const viewSortsAreDifferentFromRecordSorts = useRecoilComponentFamilyValueV2(
+ areViewSortsDifferentFromRecordSortsSelector,
+ { viewId: currentView?.id },
+ );
+
+ return { viewSortsAreDifferentFromRecordSorts };
+};
diff --git a/packages/twenty-front/src/modules/views/hooks/useGetCurrentViewOnly.ts b/packages/twenty-front/src/modules/views/hooks/useGetCurrentViewOnly.ts
new file mode 100644
index 000000000..e6fb9bed9
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useGetCurrentViewOnly.ts
@@ -0,0 +1,22 @@
+import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
+import { View } from '@/views/types/View';
+
+import { useMemo } from 'react';
+
+export const useGetCurrentViewOnly = () => {
+ const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
+
+ const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
+
+ const currentView = useMemo(
+ () => views.find((view) => view.id === currentViewId),
+ [views, currentViewId],
+ );
+
+ return {
+ currentView,
+ };
+};
diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
index f2f94e240..aa04c5533 100644
--- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
@@ -3,16 +3,14 @@ import { useRecoilCallback } from 'recoil';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords';
-import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
+import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
-import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
-import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@@ -39,18 +37,6 @@ export const useSaveCurrentViewFiltersAndSorts = (
viewBarComponentId,
);
- const unsavedToDeleteViewFilterIdsCallbackState =
- useRecoilComponentCallbackStateV2(
- unsavedToDeleteViewFilterIdsComponentFamilyState,
- viewBarComponentId,
- );
-
- const unsavedToUpsertViewFiltersCallbackState =
- useRecoilComponentCallbackStateV2(
- unsavedToUpsertViewFiltersComponentFamilyState,
- viewBarComponentId,
- );
-
const unsavedToUpsertViewFilterGroupsCallbackState =
useRecoilComponentCallbackStateV2(
unsavedToUpsertViewFilterGroupsComponentFamilyState,
@@ -69,12 +55,6 @@ export const useSaveCurrentViewFiltersAndSorts = (
deleteViewSortRecords,
} = usePersistViewSortRecords();
- const {
- createViewFilterRecords,
- updateViewFilterRecords,
- deleteViewFilterRecords,
- } = usePersistViewFilterRecords();
-
const {
createViewFilterGroupRecords,
deleteViewFilterGroupRecords,
@@ -130,53 +110,6 @@ export const useSaveCurrentViewFiltersAndSorts = (
],
);
- const saveViewFilters = useRecoilCallback(
- ({ snapshot }) =>
- async (viewId: string) => {
- const unsavedToDeleteViewFilterIds = getSnapshotValue(
- snapshot,
- unsavedToDeleteViewFilterIdsCallbackState({ viewId }),
- );
-
- const unsavedToUpsertViewFilters = getSnapshotValue(
- snapshot,
- unsavedToUpsertViewFiltersCallbackState({ viewId }),
- );
-
- const view = await getViewFromCache(viewId);
-
- if (isUndefinedOrNull(view)) {
- return;
- }
-
- const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
- (viewFilter) =>
- !view.viewFilters.some(
- (viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id,
- ),
- );
-
- const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
- (viewFilter) =>
- view.viewFilters.some(
- (viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id,
- ),
- );
-
- await createViewFilterRecords(viewFiltersToCreate, view);
- await updateViewFilterRecords(viewFiltersToUpdate);
- await deleteViewFilterRecords(unsavedToDeleteViewFilterIds);
- },
- [
- createViewFilterRecords,
- deleteViewFilterRecords,
- getViewFromCache,
- unsavedToDeleteViewFilterIdsCallbackState,
- unsavedToUpsertViewFiltersCallbackState,
- updateViewFilterRecords,
- ],
- );
-
const saveViewFilterGroups = useRecoilCallback(
({ snapshot }) =>
async (viewId: string) => {
@@ -226,6 +159,9 @@ export const useSaveCurrentViewFiltersAndSorts = (
],
);
+ const { saveRecordFiltersToViewFilters } =
+ useSaveRecordFiltersToViewFilters();
+
const saveCurrentViewFilterAndSorts = useRecoilCallback(
({ snapshot }) =>
async (viewIdFromProps?: string) => {
@@ -240,17 +176,18 @@ export const useSaveCurrentViewFiltersAndSorts = (
const viewId = viewIdFromProps ?? currentViewId;
await saveViewFilterGroups(viewId);
- await saveViewFilters(viewId);
await saveViewSorts(viewId);
+ await saveRecordFiltersToViewFilters();
+
resetUnsavedViewStates(viewId);
},
[
currentViewIdCallbackState,
resetUnsavedViewStates,
- saveViewFilters,
saveViewSorts,
saveViewFilterGroups,
+ saveRecordFiltersToViewFilters,
],
);
diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts
new file mode 100644
index 000000000..3a3e8b903
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts
@@ -0,0 +1,79 @@
+import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
+import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
+import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
+import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
+import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
+import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
+import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
+import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-ui';
+
+export const useSaveRecordFiltersToViewFilters = () => {
+ const {
+ createViewFilterRecords,
+ updateViewFilterRecords,
+ deleteViewFilterRecords,
+ } = usePersistViewFilterRecords();
+
+ const { currentView } = useGetCurrentViewOnly();
+
+ const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2(
+ currentRecordFiltersComponentState,
+ );
+
+ const saveRecordFiltersToViewFilters = useRecoilCallback(
+ ({ snapshot }) =>
+ async () => {
+ if (!isDefined(currentView)) {
+ return;
+ }
+
+ const currentViewFilters = currentView?.viewFilters ?? [];
+
+ const currentRecordFilters = getSnapshotValue(
+ snapshot,
+ currentRecordFiltersCallbackState,
+ );
+
+ const newViewFilters = currentRecordFilters.map(
+ mapRecordFilterToViewFilter,
+ );
+
+ const viewFiltersToCreate = getViewFiltersToCreate(
+ currentViewFilters,
+ newViewFilters,
+ );
+
+ const viewFiltersToDelete = getViewFiltersToDelete(
+ currentViewFilters,
+ newViewFilters,
+ );
+
+ const viewFiltersToUpdate = getViewFiltersToUpdate(
+ currentViewFilters,
+ newViewFilters,
+ );
+
+ const viewFilterIdsToDelete = viewFiltersToDelete.map(
+ (viewFilter) => viewFilter.id,
+ );
+
+ await createViewFilterRecords(viewFiltersToCreate, currentView);
+ await updateViewFilterRecords(viewFiltersToUpdate);
+ await deleteViewFilterRecords(viewFilterIdsToDelete);
+ },
+ [
+ createViewFilterRecords,
+ deleteViewFilterRecords,
+ updateViewFilterRecords,
+ currentRecordFiltersCallbackState,
+ currentView,
+ ],
+ );
+
+ return {
+ saveRecordFiltersToViewFilters,
+ };
+};
diff --git a/packages/twenty-front/src/modules/views/states/selectors/canPersistViewComponentFamilySelector.ts b/packages/twenty-front/src/modules/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector.ts
similarity index 59%
rename from packages/twenty-front/src/modules/views/states/selectors/canPersistViewComponentFamilySelector.ts
rename to packages/twenty-front/src/modules/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector.ts
index 51cc6d230..a25d1b397 100644
--- a/packages/twenty-front/src/modules/views/states/selectors/canPersistViewComponentFamilySelector.ts
+++ b/packages/twenty-front/src/modules/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector.ts
@@ -1,35 +1,21 @@
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
-import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
-import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
-export const canPersistViewComponentFamilySelector =
+export const areViewSortsDifferentFromRecordSortsSelector =
createComponentFamilySelectorV2({
- key: 'canPersistViewComponentFamilySelector',
+ key: 'areViewSortsDifferentFromRecordSortsSelector',
get:
({ familyKey, instanceId }) =>
({ get }) => {
return (
- get(
- unsavedToUpsertViewFiltersComponentFamilyState.atomFamily({
- familyKey,
- instanceId,
- }),
- ).length > 0 ||
get(
unsavedToUpsertViewSortsComponentFamilyState.atomFamily({
familyKey,
instanceId,
}),
).length > 0 ||
- get(
- unsavedToDeleteViewFilterIdsComponentFamilyState.atomFamily({
- familyKey,
- instanceId,
- }),
- ).length > 0 ||
get(
unsavedToDeleteViewSortIdsComponentFamilyState.atomFamily({
familyKey,
diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts
new file mode 100644
index 000000000..28db1a0c0
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts
@@ -0,0 +1,89 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+import { areViewFiltersEqual } from '../areViewFiltersEqual';
+
+describe('areViewFiltersEqual', () => {
+ const baseFilter: ViewFilter = {
+ __typename: 'ViewFilter',
+ id: 'filter-1',
+ fieldMetadataId: 'field-1',
+ operand: ViewFilterOperand.Contains,
+ value: 'test',
+ displayValue: 'test',
+ viewFilterGroupId: 'group-1',
+ positionInViewFilterGroup: 0,
+ };
+
+ it('should return true when all comparable properties are equal', () => {
+ const filterA = { ...baseFilter };
+ const filterB = { ...baseFilter };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(true);
+ });
+
+ it('should return false when displayValue is different', () => {
+ const filterA = { ...baseFilter };
+ const filterB = { ...baseFilter, displayValue: 'different' };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+
+ it('should return false when fieldMetadataId is different', () => {
+ const filterA = { ...baseFilter };
+ const filterB = { ...baseFilter, fieldMetadataId: 'field-2' };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+
+ it('should return false when viewFilterGroupId is different', () => {
+ const filterA = { ...baseFilter };
+ const filterB = { ...baseFilter, viewFilterGroupId: 'group-2' };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+
+ it('should return false when operand is different', () => {
+ const filterA = { ...baseFilter };
+ const filterB = {
+ ...baseFilter,
+ operand: ViewFilterOperand.DoesNotContain,
+ };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+
+ it('should return false when positionInViewFilterGroup is different', () => {
+ const filterA = { ...baseFilter };
+ const filterB = { ...baseFilter, positionInViewFilterGroup: 1 };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+
+ it('should return false when value is different', () => {
+ const filterA = { ...baseFilter };
+ const filterB = { ...baseFilter, value: 'different' };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+
+ it('should ignore non-comparable properties', () => {
+ const filterA = { ...baseFilter, id: 'id-1', createdAt: '2023-01-01' };
+ const filterB = { ...baseFilter, id: 'id-2', createdAt: '2023-01-02' };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(true);
+ });
+
+ it('should handle undefined optional properties', () => {
+ const filterA = { ...baseFilter, viewFilterGroupId: undefined };
+ const filterB = { ...baseFilter, viewFilterGroupId: undefined };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(true);
+ });
+
+ it('should handle one filter having optional property and other not', () => {
+ const filterA = { ...baseFilter, viewFilterGroupId: 'group-1' };
+ const filterB = { ...baseFilter, viewFilterGroupId: undefined };
+
+ expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToCreate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToCreate.test.ts
new file mode 100644
index 000000000..5b87e142e
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToCreate.test.ts
@@ -0,0 +1,117 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+import { getViewFiltersToCreate } from '../getViewFiltersToCreate';
+
+describe('getViewFiltersToCreate', () => {
+ const baseFilter: ViewFilter = {
+ __typename: 'ViewFilter',
+ id: 'filter-1',
+ fieldMetadataId: 'field-1',
+ operand: ViewFilterOperand.Contains,
+ value: 'test',
+ displayValue: 'test',
+ viewFilterGroupId: 'group-1',
+ positionInViewFilterGroup: 0,
+ };
+
+ it('should return all filters when current filters array is empty', () => {
+ const currentViewFilters: ViewFilter[] = [];
+ const newViewFilters: ViewFilter[] = [
+ { ...baseFilter },
+ { ...baseFilter, id: 'filter-2', fieldMetadataId: 'field-2' },
+ ];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual(newViewFilters);
+ });
+
+ it('should return empty array when new filters array is empty', () => {
+ const currentViewFilters: ViewFilter[] = [baseFilter];
+ const newViewFilters: ViewFilter[] = [];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return only filters that do not exist in current filters', () => {
+ const existingFilter = { ...baseFilter };
+ const newFilterWithDifferentFieldMetadata = {
+ ...baseFilter,
+ id: 'filter-2',
+ fieldMetadataId: 'field-2',
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+
+ const newViewFilters: ViewFilter[] = [
+ existingFilter,
+ newFilterWithDifferentFieldMetadata,
+ ];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([newFilterWithDifferentFieldMetadata]);
+ });
+
+ it('should handle filters with different viewFilterGroupIds', () => {
+ const existingFilter = { ...baseFilter };
+ const filterWithDifferentGroup = {
+ ...baseFilter,
+ viewFilterGroupId: 'group-2',
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+
+ const newViewFilters: ViewFilter[] = [
+ existingFilter,
+ filterWithDifferentGroup,
+ ];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterWithDifferentGroup]);
+ });
+
+ it('should handle empty arrays for both inputs', () => {
+ const currentViewFilters: ViewFilter[] = [];
+ const newViewFilters: ViewFilter[] = [];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should consider filters with same fieldMetadataId but different viewFilterGroupId as new', () => {
+ const currentViewFilters: ViewFilter[] = [baseFilter];
+ const filterWithSameFieldMetadataIdButDifferentGroup = {
+ ...baseFilter,
+ id: 'filter-2',
+ viewFilterGroupId: 'group-2',
+ };
+ const newViewFilters: ViewFilter[] = [
+ filterWithSameFieldMetadataIdButDifferentGroup,
+ ];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterWithSameFieldMetadataIdButDifferentGroup]);
+ });
+
+ it('should consider filters with same viewFilterGroupId but different fieldMetadataId as new', () => {
+ const currentViewFilters: ViewFilter[] = [baseFilter];
+ const filterWithSameGroupButDifferentFieldMetadata = {
+ ...baseFilter,
+ id: 'filter-2',
+ fieldMetadataId: 'field-2',
+ };
+ const newViewFilters: ViewFilter[] = [
+ filterWithSameGroupButDifferentFieldMetadata,
+ ];
+
+ const result = getViewFiltersToCreate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterWithSameGroupButDifferentFieldMetadata]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToDelete.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToDelete.test.ts
new file mode 100644
index 000000000..0440b5236
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToDelete.test.ts
@@ -0,0 +1,99 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+import { getViewFiltersToDelete } from '../getViewFiltersToDelete';
+
+describe('getViewFiltersToDelete', () => {
+ const baseFilter: ViewFilter = {
+ __typename: 'ViewFilter',
+ id: 'filter-1',
+ fieldMetadataId: 'field-1',
+ operand: ViewFilterOperand.Contains,
+ value: 'test',
+ displayValue: 'test',
+ viewFilterGroupId: 'group-1',
+ positionInViewFilterGroup: 0,
+ };
+
+ it('should return empty array when current filters array is empty', () => {
+ const currentViewFilters: ViewFilter[] = [];
+ const newViewFilters: ViewFilter[] = [baseFilter];
+
+ const result = getViewFiltersToDelete(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return all current filters when new filters array is empty', () => {
+ const existingFilter = { ...baseFilter };
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [];
+
+ const result = getViewFiltersToDelete(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([existingFilter]);
+ });
+
+ it('should return filters that exist in current but not in new filters', () => {
+ const filterToDelete = { ...baseFilter };
+ const filterToKeep = {
+ ...baseFilter,
+ id: 'filter-2',
+ fieldMetadataId: 'field-2',
+ };
+
+ const currentViewFilters: ViewFilter[] = [filterToDelete, filterToKeep];
+ const newViewFilters: ViewFilter[] = [filterToKeep];
+
+ const result = getViewFiltersToDelete(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterToDelete]);
+ });
+
+ it('should handle empty arrays for both inputs', () => {
+ const currentViewFilters: ViewFilter[] = [];
+ const newViewFilters: ViewFilter[] = [];
+
+ const result = getViewFiltersToDelete(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should identify filters to delete based on fieldMetadataId and viewFilterGroupId', () => {
+ const filterInGroup1 = { ...baseFilter };
+ const filterInGroup2 = {
+ ...baseFilter,
+ viewFilterGroupId: 'group-2',
+ };
+ const filterWithDifferentField = {
+ ...baseFilter,
+ fieldMetadataId: 'field-2',
+ };
+
+ const currentViewFilters: ViewFilter[] = [
+ filterInGroup1,
+ filterInGroup2,
+ filterWithDifferentField,
+ ];
+ const newViewFilters: ViewFilter[] = [filterInGroup1];
+
+ const result = getViewFiltersToDelete(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterInGroup2, filterWithDifferentField]);
+ });
+
+ it('should not delete filters that match in both fieldMetadataId and viewFilterGroupId', () => {
+ const existingFilter = { ...baseFilter };
+ const matchingFilter = {
+ ...baseFilter,
+ value: 'different-value',
+ displayValue: 'different-value',
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [matchingFilter];
+
+ const result = getViewFiltersToDelete(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToUpdate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToUpdate.test.ts
new file mode 100644
index 000000000..275f0d6c0
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToUpdate.test.ts
@@ -0,0 +1,133 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+import { getViewFiltersToUpdate } from '../getViewFiltersToUpdate';
+
+describe('getViewFiltersToUpdate', () => {
+ const baseFilter: ViewFilter = {
+ __typename: 'ViewFilter',
+ id: 'filter-1',
+ fieldMetadataId: 'field-1',
+ operand: ViewFilterOperand.Contains,
+ value: 'test',
+ displayValue: 'test',
+ viewFilterGroupId: 'group-1',
+ positionInViewFilterGroup: 0,
+ };
+
+ it('should return empty array when current filters array is empty', () => {
+ const currentViewFilters: ViewFilter[] = [];
+ const newViewFilters: ViewFilter[] = [baseFilter];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return empty array when new filters array is empty', () => {
+ const currentViewFilters: ViewFilter[] = [baseFilter];
+ const newViewFilters: ViewFilter[] = [];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return filters that exist in both arrays but have different values', () => {
+ const existingFilter = { ...baseFilter };
+ const updatedFilter = {
+ ...baseFilter,
+ value: 'updated-value',
+ displayValue: 'updated-value',
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [updatedFilter];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([updatedFilter]);
+ });
+
+ it('should not return filters that exist in both arrays with same values', () => {
+ const existingFilter = { ...baseFilter };
+ const sameFilter = { ...baseFilter };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [sameFilter];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should handle empty arrays for both inputs', () => {
+ const currentViewFilters: ViewFilter[] = [];
+ const newViewFilters: ViewFilter[] = [];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should not update filters with same fieldMetadataId but different viewFilterGroupId', () => {
+ const existingFilter = { ...baseFilter };
+ const filterInDifferentGroup = {
+ ...baseFilter,
+ viewFilterGroupId: 'group-2',
+ value: 'updated-value',
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [filterInDifferentGroup];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should not update filters with same viewFilterGroupId but different fieldMetadataId', () => {
+ const existingFilter = { ...baseFilter };
+ const filterWithDifferentField = {
+ ...baseFilter,
+ fieldMetadataId: 'field-2',
+ value: 'updated-value',
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [filterWithDifferentField];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should update filter when operand changes', () => {
+ const existingFilter = { ...baseFilter };
+ const filterWithNewOperand = {
+ ...baseFilter,
+ operand: ViewFilterOperand.DoesNotContain,
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [filterWithNewOperand];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterWithNewOperand]);
+ });
+
+ it('should update filter when position changes', () => {
+ const existingFilter = { ...baseFilter };
+ const filterWithNewPosition = {
+ ...baseFilter,
+ positionInViewFilterGroup: 1,
+ };
+
+ const currentViewFilters: ViewFilter[] = [existingFilter];
+ const newViewFilters: ViewFilter[] = [filterWithNewPosition];
+
+ const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters);
+
+ expect(result).toEqual([filterWithNewPosition]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts
new file mode 100644
index 000000000..5087bf44a
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts
@@ -0,0 +1,19 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+
+export const areViewFiltersEqual = (
+ viewFilterA: ViewFilter,
+ viewFilterB: ViewFilter,
+) => {
+ const propertiesToCompare: (keyof ViewFilter)[] = [
+ 'fieldMetadataId',
+ 'viewFilterGroupId',
+ 'positionInViewFilterGroup',
+ 'value',
+ 'displayValue',
+ 'operand',
+ ];
+
+ return propertiesToCompare.every(
+ (property) => viewFilterA[property] === viewFilterB[property],
+ );
+};
diff --git a/packages/twenty-front/src/modules/views/utils/getViewFiltersToCreate.ts b/packages/twenty-front/src/modules/views/utils/getViewFiltersToCreate.ts
new file mode 100644
index 000000000..a222a1daf
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/getViewFiltersToCreate.ts
@@ -0,0 +1,21 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { isDefined } from 'twenty-ui';
+
+export const getViewFiltersToCreate = (
+ currentViewFilters: ViewFilter[],
+ newViewFilters: ViewFilter[],
+) => {
+ return newViewFilters.filter((newViewFilter) => {
+ const correspondingViewFilter = currentViewFilters.find(
+ (currentViewFilter) =>
+ currentViewFilter.fieldMetadataId === newViewFilter.fieldMetadataId &&
+ currentViewFilter.viewFilterGroupId === newViewFilter.viewFilterGroupId,
+ );
+
+ const shouldCreateBecauseViewFilterIsNew = !isDefined(
+ correspondingViewFilter,
+ );
+
+ return shouldCreateBecauseViewFilterIsNew;
+ });
+};
diff --git a/packages/twenty-front/src/modules/views/utils/getViewFiltersToDelete.ts b/packages/twenty-front/src/modules/views/utils/getViewFiltersToDelete.ts
new file mode 100644
index 000000000..d6ee34cff
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/getViewFiltersToDelete.ts
@@ -0,0 +1,16 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+
+export const getViewFiltersToDelete = (
+ currentViewFilters: ViewFilter[],
+ newViewFilters: ViewFilter[],
+) => {
+ return currentViewFilters.filter(
+ (currentViewFilter) =>
+ !newViewFilters.some(
+ (newViewFilter) =>
+ newViewFilter.fieldMetadataId === currentViewFilter.fieldMetadataId &&
+ newViewFilter.viewFilterGroupId ===
+ currentViewFilter.viewFilterGroupId,
+ ),
+ );
+};
diff --git a/packages/twenty-front/src/modules/views/utils/getViewFiltersToUpdate.ts b/packages/twenty-front/src/modules/views/utils/getViewFiltersToUpdate.ts
new file mode 100644
index 000000000..d30ee10b2
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/getViewFiltersToUpdate.ts
@@ -0,0 +1,27 @@
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { areViewFiltersEqual } from '@/views/utils/areViewFiltersEqual';
+import { isDefined } from 'twenty-ui';
+
+export const getViewFiltersToUpdate = (
+ currentViewFilters: ViewFilter[],
+ newViewFilters: ViewFilter[],
+) => {
+ return newViewFilters.filter((newViewFilter) => {
+ const correspondingViewFilter = currentViewFilters.find(
+ (currentViewFilter) =>
+ currentViewFilter.fieldMetadataId === newViewFilter.fieldMetadataId &&
+ currentViewFilter.viewFilterGroupId === newViewFilter.viewFilterGroupId,
+ );
+
+ if (!isDefined(correspondingViewFilter)) {
+ return false;
+ }
+
+ const shouldUpdateBecauseViewFilterIsDifferent = !areViewFiltersEqual(
+ newViewFilter,
+ correspondingViewFilter,
+ );
+
+ return shouldUpdateBecauseViewFilterIsDifferent;
+ });
+};
diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts
new file mode 100644
index 000000000..64847493d
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts
@@ -0,0 +1,11 @@
+import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
+import { ViewFilter } from '@/views/types/ViewFilter';
+
+export const mapRecordFilterToViewFilter = (
+ recordFilter: RecordFilter,
+): ViewFilter => {
+ return {
+ __typename: 'ViewFilter',
+ ...recordFilter,
+ } satisfies ViewFilter;
+};