Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 97eeb23e09 fix: gracefully handle view filters referencing deleted field metadata
https://sonarly.com/issue/18698?type=bug

When a view filter references a field metadata ID that no longer exists in the workspace's metadata, `getRecordFilterFieldMetadataItem` throws an unhandled error during React render, crashing the entire `/objects/people` page.

Fix: Changed `getRecordFilterFieldMetadataItem` from throwing when a field metadata ID is not found to returning `undefined`. Added an explicit `FieldMetadataItem | undefined` return type annotation.

Updated both callers (`isRecordFilterAboutSoftDelete` and `isSeeDeletedRecordsFilter` in `useCheckIsSoftDeleteFilter`) to handle the `undefined` case by returning `false` — meaning filters that reference deleted/missing field metadata are simply treated as non-soft-delete filters rather than crashing the page.

This also fixes the `hasAnySoftDeleteFilterOnViewComponentSelector` Jotai selector, which calls `isRecordFilterAboutSoftDelete` and was also crashing (visible in the breadcrumbs via `useSetAdvancedFilterDropdownAllRowsStates`).

Added a test for `getRecordFilterFieldMetadataItem` covering the found, not-found, and empty metadata cases.

The fix uses `isDefined` from `twenty-shared/utils` which is the team's standard null-check utility, already used throughout the codebase.
2026-03-26 14:14:21 +00:00
4 changed files with 71 additions and 11 deletions
@@ -4,6 +4,7 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { getRecordFilterFieldMetadataItem } from '@/object-record/record-filter/utils/getRecordFilterFieldMetadataItem';
import { isRecordFilterAboutSoftDelete as isRecordFilterAboutSoftDeleteUtil } from '@/object-record/record-filter/utils/isRecordFilterAboutSoftDelete';
import { isDefined } from 'twenty-shared/utils';
export const useCheckIsSoftDeleteFilter = () => {
const { objectMetadataItems } = useObjectMetadataItems();
@@ -21,6 +22,10 @@ export const useCheckIsSoftDeleteFilter = () => {
objectMetadataItems,
});
if (!isDefined(foundFieldMetadataItem)) {
return false;
}
const isNotEmptyFilter =
recordFilter.operand === RecordFilterOperand.IS_NOT_EMPTY;
@@ -0,0 +1,58 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getRecordFilterFieldMetadataItem } from '@/object-record/record-filter/utils/getRecordFilterFieldMetadataItem';
const mockFieldMetadataItem = {
id: 'field-1',
name: 'deletedAt',
type: 'DATE_TIME',
label: 'Deleted At',
};
const mockObjectMetadataItems = [
{
fields: [mockFieldMetadataItem],
},
] as unknown as EnrichedObjectMetadataItem[];
const baseRecordFilter: RecordFilter = {
id: 'filter-1',
fieldMetadataId: 'field-1',
value: '',
displayValue: '',
type: 'DATE_TIME',
operand: 'is',
label: 'Deleted At',
};
describe('getRecordFilterFieldMetadataItem', () => {
it('should return the field metadata item when found', () => {
const result = getRecordFilterFieldMetadataItem({
recordFilter: baseRecordFilter,
objectMetadataItems: mockObjectMetadataItems,
});
expect(result).toEqual(mockFieldMetadataItem);
});
it('should return undefined when field metadata id does not exist', () => {
const result = getRecordFilterFieldMetadataItem({
recordFilter: {
...baseRecordFilter,
fieldMetadataId: 'non-existent-field-id',
},
objectMetadataItems: mockObjectMetadataItems,
});
expect(result).toBeUndefined();
});
it('should return undefined when object metadata items is empty', () => {
const result = getRecordFilterFieldMetadataItem({
recordFilter: baseRecordFilter,
objectMetadataItems: [],
});
expect(result).toBeUndefined();
});
});
@@ -1,6 +1,6 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isDefined } from 'twenty-shared/utils';
export const getRecordFilterFieldMetadataItem = ({
recordFilter,
@@ -8,21 +8,13 @@ export const getRecordFilterFieldMetadataItem = ({
}: {
recordFilter: RecordFilter;
objectMetadataItems: EnrichedObjectMetadataItem[];
}) => {
}): FieldMetadataItem | undefined => {
const allFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
);
const foundFieldMetadataItem = allFieldMetadataItems.find(
return allFieldMetadataItems.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFilter.fieldMetadataId,
);
if (!isDefined(foundFieldMetadataItem)) {
throw new Error(
`Field metadata item not found for field metadata id: ${recordFilter.fieldMetadataId}`,
);
}
return foundFieldMetadataItem;
};
@@ -4,6 +4,7 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { getRecordFilterFieldMetadataItem } from '@/object-record/record-filter/utils/getRecordFilterFieldMetadataItem';
import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-shared/utils';
export const isRecordFilterAboutSoftDelete = ({
recordFilter,
@@ -17,6 +18,10 @@ export const isRecordFilterAboutSoftDelete = ({
objectMetadataItems,
});
if (!isDefined(foundFieldMetadataItem)) {
return false;
}
const valueIsNotEmptyFilter =
(recordFilter.operand === RecordFilterOperand.IS &&
!isEmpty(recordFilter.value)) ||