Fix view filter/sort deletion (#19567)

fixes https://github.com/twentyhq/twenty/issues/19543

+ bonus bug : when deleting an advanced filter, it triggers a destroy
which cascade-deletes associated view filters. Then, view filters
deletion throws.
This commit is contained in:
Etienne
2026-04-14 11:43:52 +02:00
committed by GitHub
parent eb13378760
commit 9c07ecd363
8 changed files with 230 additions and 247 deletions
@@ -29,7 +29,7 @@ export const useSaveRecordTableWidgetFiltersToView = (
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
} = usePerformViewFilterGroupAPIPersist();
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
@@ -142,7 +142,7 @@ export const useSaveRecordTableWidgetFiltersToView = (
})),
);
await performViewFilterGroupAPIDelete(
await performViewFilterGroupAPIDestroy(
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
);
}, [
@@ -155,7 +155,7 @@ export const useSaveRecordTableWidgetFiltersToView = (
performViewFilterAPIDelete,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
]);
return { saveRecordTableWidgetFiltersToView };
@@ -39,7 +39,7 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
} = usePerformViewFilterGroupAPIPersist();
const {
@@ -251,7 +251,7 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
},
})),
);
await performViewFilterGroupAPIDelete(
await performViewFilterGroupAPIDestroy(
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
);
@@ -297,7 +297,7 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
performViewFilterAPIDelete,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
@@ -104,11 +104,11 @@ export const usePerformViewFilterGroupAPIPersist = () => {
[apolloClient],
);
const performViewFilterGroupAPIDelete = useCallback(
(viewFilterGroupIdsToDelete: string[]) => {
if (!viewFilterGroupIdsToDelete.length) return;
const performViewFilterGroupAPIDestroy = useCallback(
(viewFilterGroupIdsToDestroy: string[]) => {
if (!viewFilterGroupIdsToDestroy.length) return;
return Promise.all(
viewFilterGroupIdsToDelete.map((viewFilterGroupId) =>
viewFilterGroupIdsToDestroy.map((viewFilterGroupId) =>
apolloClient.mutate<{ destroyViewFilterGroup: ViewFilterGroup }>({
mutation: DESTROY_VIEW_FILTER_GROUP,
variables: {
@@ -124,6 +124,6 @@ export const usePerformViewFilterGroupAPIPersist = () => {
return {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewFilterGroupAPIDestroy,
};
};
@@ -1,14 +1,10 @@
import { useSaveAnyFieldFilterToView } from '@/views/hooks/useSaveAnyFieldFilterToView';
import { useSaveRecordFilterGroupsToViewFilterGroups } from '@/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups';
import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters';
import { useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters } from '@/views/hooks/useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters';
import { useSaveRecordSortsToViewSorts } from '@/views/hooks/useSaveRecordSortsToViewSorts';
export const useSaveCurrentViewFiltersAndSorts = () => {
const { saveRecordFilterGroupsToViewFilterGroups } =
useSaveRecordFilterGroupsToViewFilterGroups();
const { saveRecordFiltersToViewFilters } =
useSaveRecordFiltersToViewFilters();
const { saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters } =
useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters();
const { saveRecordSortsToViewSorts } = useSaveRecordSortsToViewSorts();
@@ -16,8 +12,7 @@ export const useSaveCurrentViewFiltersAndSorts = () => {
const saveCurrentViewFilterAndSorts = async () => {
await saveRecordSortsToViewSorts();
await saveRecordFilterGroupsToViewFilterGroups();
await saveRecordFiltersToViewFilters();
await saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters();
await saveAnyFieldFilterToView();
};
@@ -1,86 +0,0 @@
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
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 { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
} = usePerformViewFilterGroupAPIPersist();
const { currentView } = useGetCurrentViewOnly();
const currentRecordFilterGroupsCallbackState =
useAtomComponentStateCallbackState(currentRecordFilterGroupsComponentState);
const store = useStore();
const saveRecordFilterGroupsToViewFilterGroups = useCallback(async () => {
if (!canPersistChanges || !isDefined(currentView)) {
return;
}
const currentViewFilterGroups = currentView?.viewFilterGroups ?? [];
const currentRecordFilterGroups = store.get(
currentRecordFilterGroupsCallbackState,
);
const newViewFilterGroups = currentRecordFilterGroups.map(
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: currentView,
}),
);
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupIdsToDelete = viewFilterGroupsToDelete.map(
(viewFilterGroup) => viewFilterGroup.id,
);
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterGroupAPIDelete(viewFilterGroupIdsToDelete);
}, [
canPersistChanges,
currentView,
store,
currentRecordFilterGroupsCallbackState,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
]);
return {
saveRecordFilterGroupsToViewFilterGroups,
};
};
@@ -0,0 +1,211 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
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 { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters =
() => {
const { canPersistChanges } = useCanPersistViewChanges();
const { currentView } = useGetCurrentViewOnly();
const store = useStore();
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDestroy,
} = usePerformViewFilterGroupAPIPersist();
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDestroy,
} = usePerformViewFilterAPIPersist();
const currentRecordFilterGroupsCallbackState =
useAtomComponentStateCallbackState(
currentRecordFilterGroupsComponentState,
);
const currentRecordFiltersCallbackState =
useAtomComponentStateCallbackState(currentRecordFiltersComponentState);
const saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters =
useCallback(async () => {
if (!canPersistChanges || !isDefined(currentView)) {
return;
}
const currentViewFilterGroups = currentView.viewFilterGroups ?? [];
const currentRecordFilterGroups = store.get(
currentRecordFilterGroupsCallbackState,
);
const newViewFilterGroups = currentRecordFilterGroups.map(
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: currentView,
}),
);
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupIdsToDestroy = viewFilterGroupsToDelete.map(
(viewFilterGroup) => viewFilterGroup.id,
);
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterGroupAPIDestroy(viewFilterGroupIdsToDestroy);
// Mirror the DB cascade: remove cascade-deleted viewFilters from the store
if (viewFilterGroupIdsToDestroy.length > 0) {
const destroyedIdsSet = new Set(viewFilterGroupIdsToDestroy);
store.set(metadataStoreState.atomFamily('viewFilters'), (prev) => ({
...prev,
current: (prev.current as FlatViewFilter[]).filter(
(viewFilter) =>
!isDefined(viewFilter.viewFilterGroupId) ||
!destroyedIdsSet.has(viewFilter.viewFilterGroupId),
),
}));
}
const currentViewFilters = currentView.viewFilters ?? [];
const currentRecordFilters = store.get(
currentRecordFiltersCallbackState,
);
const newViewFilters = currentRecordFilters.map(
mapRecordFilterToViewFilter,
);
const viewFiltersToCreate = getViewFiltersToCreate(
currentViewFilters,
newViewFilters,
);
const viewFiltersToDelete = getViewFiltersToDelete(
currentViewFilters,
newViewFilters,
).filter(
(viewFilter) =>
!isDefined(viewFilter.viewFilterGroupId) ||
!viewFilterGroupIdsToDestroy.includes(viewFilter.viewFilterGroupId),
);
const viewFiltersToUpdate = getViewFiltersToUpdate(
currentViewFilters,
newViewFilters,
);
const createViewFilterInputs = viewFiltersToCreate.map(
(viewFilter) => ({
input: {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: currentView.id,
value: viewFilter.value,
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
},
}),
);
const updateViewFilterInputs = viewFiltersToUpdate.map(
(viewFilter) => ({
input: {
id: viewFilter.id,
update: {
value: viewFilter.value,
operand: viewFilter.operand,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
},
},
}),
);
const destroyViewFilterInputs = viewFiltersToDelete.map(
(viewFilter) => ({
input: {
id: viewFilter.id,
},
}),
);
const createResult = await performViewFilterAPICreate(
createViewFilterInputs,
);
if (createResult.status === 'failed') {
return;
}
const updateResult = await performViewFilterAPIUpdate(
updateViewFilterInputs,
);
if (updateResult.status === 'failed') {
return;
}
const deleteResult = await performViewFilterAPIDestroy(
destroyViewFilterInputs,
);
if (deleteResult.status === 'failed') {
return;
}
}, [
canPersistChanges,
currentView,
store,
currentRecordFilterGroupsCallbackState,
currentRecordFiltersCallbackState,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDestroy,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDestroy,
]);
return {
saveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters,
};
};
@@ -1,137 +0,0 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
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 { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFiltersToViewFilters = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
} = usePerformViewFilterAPIPersist();
const contextStoreCurrentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
);
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
currentRecordFiltersComponentState,
);
const store = useStore();
const saveRecordFiltersToViewFilters = useCallback(async () => {
const views = store.get(viewsSelector.atom);
const currentView = views.find(
(view) => view.id === contextStoreCurrentViewId,
);
if (!isDefined(currentView)) {
return;
}
if (!canPersistChanges || !isDefined(currentView)) {
return;
}
const currentViewFilters = currentView?.viewFilters ?? [];
const currentRecordFilters = store.get(currentRecordFiltersCallbackState);
const newViewFilters = currentRecordFilters.map(
mapRecordFilterToViewFilter,
);
const viewFiltersToCreate = getViewFiltersToCreate(
currentViewFilters,
newViewFilters,
);
const viewFiltersToDelete = getViewFiltersToDelete(
currentViewFilters,
newViewFilters,
);
const viewFiltersToUpdate = getViewFiltersToUpdate(
currentViewFilters,
newViewFilters,
);
const createViewFilterInputs = viewFiltersToCreate.map((viewFilter) => ({
input: {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: currentView.id,
value: viewFilter.value,
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
},
}));
const updateViewFilterInputs = viewFiltersToUpdate.map((viewFilter) => ({
input: {
id: viewFilter.id,
update: {
value: viewFilter.value,
operand: viewFilter.operand,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
},
},
}));
const deleteViewFilterInputs = viewFiltersToDelete.map((viewFilter) => ({
input: {
id: viewFilter.id,
},
}));
const createResult = await performViewFilterAPICreate(
createViewFilterInputs,
);
if (createResult.status === 'failed') {
return;
}
const updateResult = await performViewFilterAPIUpdate(
updateViewFilterInputs,
);
if (updateResult.status === 'failed') {
return;
}
const deleteResult = await performViewFilterAPIDelete(
deleteViewFilterInputs,
);
if (deleteResult.status === 'failed') {
return;
}
}, [
store,
canPersistChanges,
currentRecordFiltersCallbackState,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
contextStoreCurrentViewId,
]);
return {
saveRecordFiltersToViewFilters,
};
};
@@ -16,7 +16,7 @@ export const useSaveRecordSortsToViewSorts = () => {
const {
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
performViewSortAPIDestroy,
} = usePerformViewSortAPIPersist();
const { currentView } = useGetCurrentViewOnly();
@@ -71,7 +71,7 @@ export const useSaveRecordSortsToViewSorts = () => {
},
}));
const deleteViewSortInputs = viewSortsToDelete.map((viewSort) => ({
const destroyViewSortInputs = viewSortsToDelete.map((viewSort) => ({
input: {
id: viewSort.id,
},
@@ -87,7 +87,7 @@ export const useSaveRecordSortsToViewSorts = () => {
return;
}
const deleteResult = await performViewSortAPIDelete(deleteViewSortInputs);
const deleteResult = await performViewSortAPIDestroy(destroyViewSortInputs);
if (deleteResult.status === 'failed') {
return;
}
@@ -98,7 +98,7 @@ export const useSaveRecordSortsToViewSorts = () => {
currentRecordSortsCallbackState,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
performViewSortAPIDestroy,
]);
return {