refactor: address review feedback and simplify

Address PR review comments:
- Move isManyToOneRelationField to @/object-metadata/utils (global helper).
  Refactor prefillRecord, sanitizeRecordInput, filterSortableFieldMetadataItems
  and spreadsheetImportHasNestedFields to reuse it.
- Drop narrative/redundant comments on RecordFilter, ViewFilter shape,
  RELATION_SUB_MENU_FIELD_TYPE sentinel and relationTargetFieldMetadataIdUsed
  state — names are self-describing.

Simplifications:
- Unify the two select-filter handlers in AdvancedFilterSubFieldSelectMenu
  behind a single keyword-args entry point.
- Collapse isRelationDrillDown/isCompositeDrillDown flags in
  AdvancedFilterFieldSelectMenu into a single computed sub-menu type.
- Drop the redundant `'relationTargetFieldMetadataId' in viewFilter` check
  in mapViewFiltersToFilters now that ViewFilter shape carries the field.
- Drop the over-engineered useMemo around effectiveFieldMetadataItem in
  ObjectFilterDropdownInnerSelectOperandDropdown — the underlying lookup
  is a cheap dictionary read.
- Reuse the same relationTargetFieldMetadataId for both the filter object
  and the dropdown state setter in useSelectFieldUsedInAdvancedFilterDropdown.
This commit is contained in:
Félix Malfait
2026-05-14 21:28:13 +02:00
parent fd1b51ac03
commit 06f8c8b5a7
19 changed files with 106 additions and 167 deletions

View File

@@ -1,6 +1,7 @@
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { type FieldMetadataType, type RelationType } from '~/generated-metadata/graphql';
type SortableFieldInput = {
isSystem?: boolean | null;
@@ -17,13 +18,9 @@ export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
field.type,
);
const isRelationFieldSortable =
field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE;
return (
!isHiddenSystemField(field) &&
isFieldActive &&
(isFieldTypeSortable || isRelationFieldSortable)
(isFieldTypeSortable || isManyToOneRelationField(field))
);
};

View File

@@ -0,0 +1,10 @@
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
type FieldWithRelation = {
type: FieldMetadataType;
relation?: { type: RelationType } | null;
};
export const isManyToOneRelationField = (field: FieldWithRelation): boolean =>
field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE;

View File

@@ -22,7 +22,7 @@ import {
objectFilterDropdownSubMenuFieldTypeComponentState,
} from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType';
import { isManyToOneRelationField } from '@/object-record/object-filter-dropdown/utils/isManyToOneRelationField';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -122,23 +122,21 @@ export const AdvancedFilterFieldSelectMenu = ({
recordFilterId,
});
const isRelationDrillDown = isManyToOneRelationField(
selectedFieldMetadataItem,
);
const isCompositeDrillDown = isCompositeFilterableFieldType(filterType);
const subMenuType: ObjectFilterDropdownSubMenuFieldType | null =
isManyToOneRelationField(selectedFieldMetadataItem)
? RELATION_SUB_MENU_FIELD_TYPE
: isCompositeFilterableFieldType(filterType)
? filterType
: null;
if (isCompositeDrillDown || isRelationDrillDown) {
const subMenuType = (
isRelationDrillDown ? RELATION_SUB_MENU_FIELD_TYPE : filterType
) as ObjectFilterDropdownSubMenuFieldType;
setObjectFilterDropdownSubMenuFieldType(subMenuType);
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
setObjectFilterDropdownIsSelectingCompositeField(true);
} else {
if (subMenuType === null) {
closeAdvancedFilterFieldSelectDropdown();
return;
}
setObjectFilterDropdownSubMenuFieldType(subMenuType);
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
setObjectFilterDropdownIsSelectingCompositeField(true);
};
const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0;

View File

@@ -15,7 +15,7 @@ import {
objectFilterDropdownSubMenuFieldTypeComponentState,
} from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { isManyToOneRelationField } from '@/object-record/object-filter-dropdown/utils/isManyToOneRelationField';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable';
@@ -62,8 +62,6 @@ export const AdvancedFilterSubFieldSelectMenu = ({
const { selectFieldUsedInAdvancedFilterDropdown } =
useSelectFieldUsedInAdvancedFilterDropdown();
// Hooks must run unconditionally — pull target object's filterable fields
// even when we're in composite mode (the empty list is harmless).
const isRelationSubMenu =
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE &&
isDefined(fieldMetadataItemUsedInDropdown) &&
@@ -76,31 +74,20 @@ export const AdvancedFilterSubFieldSelectMenu = ({
const { filterableFieldMetadataItems: relationTargetFields } =
useFilterableFieldMetadataItems(targetObjectMetadataId);
const handleSelectCompositeFilter = (
selectedFieldMetadataItem: FieldMetadataItem | null | undefined,
subFieldName?: CompositeFieldSubFieldName | null | undefined,
) => {
if (!isDefined(selectedFieldMetadataItem)) {
return;
}
const handleSelectFilter = ({
fieldMetadataItem,
subFieldName,
relationTargetFieldMetadataItem,
}: {
fieldMetadataItem: FieldMetadataItem;
subFieldName?: CompositeFieldSubFieldName | null;
relationTargetFieldMetadataItem?: FieldMetadataItem | null;
}) => {
selectFieldUsedInAdvancedFilterDropdown({
fieldMetadataItemId: selectedFieldMetadataItem.id,
fieldMetadataItemId: fieldMetadataItem.id,
recordFilterId,
subFieldName,
});
closeAdvancedFilterFieldSelectDropdown();
};
const handleSelectRelationTargetFilter = (
relationFieldMetadataItem: FieldMetadataItem,
targetFieldMetadataItem: FieldMetadataItem,
) => {
selectFieldUsedInAdvancedFilterDropdown({
fieldMetadataItemId: relationFieldMetadataItem.id,
recordFilterId,
relationTargetFieldMetadataItem: targetFieldMetadataItem,
relationTargetFieldMetadataItem,
});
closeAdvancedFilterFieldSelectDropdown();
@@ -150,10 +137,10 @@ export const AdvancedFilterSubFieldSelectMenu = ({
itemId={targetField.id}
key={`select-filter-relation-${index}`}
onEnter={() => {
handleSelectRelationTargetFilter(
fieldMetadataItemUsedInDropdown,
targetField,
);
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
relationTargetFieldMetadataItem: targetField,
});
}}
>
<MenuItem
@@ -161,10 +148,10 @@ export const AdvancedFilterSubFieldSelectMenu = ({
key={`select-filter-relation-${index}`}
testId={`select-filter-relation-${index}`}
onClick={() => {
handleSelectRelationTargetFilter(
fieldMetadataItemUsedInDropdown,
targetField,
);
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
relationTargetFieldMetadataItem: targetField,
});
}}
text={targetField.label}
LeftIcon={getIcon(targetField.icon)}
@@ -227,36 +214,42 @@ export const AdvancedFilterSubFieldSelectMenu = ({
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
>
{compositeFieldTypeIsFilterableByAnySubField && (
<SelectableListItem
itemId="-1"
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectCompositeFilter(fieldMetadataItemUsedInDropdown);
}}
>
<MenuItem
{compositeFieldTypeIsFilterableByAnySubField &&
isDefined(fieldMetadataItemUsedInDropdown) && (
<SelectableListItem
itemId="-1"
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
focused={selectedItemId === '-1'}
onClick={() => {
handleSelectCompositeFilter(fieldMetadataItemUsedInDropdown);
onEnter={() => {
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
});
}}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
text={t`Any ${fieldLabel} field`}
/>
</SelectableListItem>
)}
>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
focused={selectedItemId === '-1'}
onClick={() => {
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
});
}}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
text={t`Any ${fieldLabel} field`}
/>
</SelectableListItem>
)}
{subFieldsAreFilterable &&
isDefined(fieldMetadataItemUsedInDropdown) &&
subFieldNames.map((subFieldName, index) => (
<SelectableListItem
itemId={subFieldName}
key={`select-filter-${index}`}
onEnter={() => {
handleSelectCompositeFilter(
fieldMetadataItemUsedInDropdown,
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
subFieldName,
);
});
}}
>
<MenuItem
@@ -264,12 +257,10 @@ export const AdvancedFilterSubFieldSelectMenu = ({
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
if (isDefined(fieldMetadataItemUsedInDropdown)) {
handleSelectCompositeFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
subFieldName,
});
}}
text={getCompositeSubFieldLabel(
compositeSubMenuFieldType,
@@ -277,7 +268,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
)}
LeftIcon={getIcon(
ICON_NAME_BY_SUB_FIELD[subFieldName] ??
fieldMetadataItemUsedInDropdown?.icon,
fieldMetadataItemUsedInDropdown.icon,
)}
/>
</SelectableListItem>

View File

@@ -25,10 +25,6 @@ type SelectFilterParams = {
fieldMetadataItemId: string;
recordFilterId: string;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
// When the user drilled into a MANY_TO_ONE relation and picked a field on
// the target object, this carries that target field. The stored filter
// then operates against the target field's type (so operand picker /
// value input reflect the target, not the relation itself).
relationTargetFieldMetadataItem?: FieldMetadataItem | null | undefined;
};
@@ -155,6 +151,10 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
? `${fieldMetadataItem.label}${relationTargetFieldMetadataItem.label}`
: fieldMetadataItem.label;
const relationTargetFieldMetadataId = isRelationTraversal
? relationTargetFieldMetadataItem.id
: null;
const newAdvancedFilter = {
id: recordFilterId,
fieldMetadataId: fieldMetadataItem.id,
@@ -167,14 +167,12 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
type: filterType,
label,
subFieldName: subFieldNameToUse,
relationTargetFieldMetadataId: isRelationTraversal
? relationTargetFieldMetadataItem.id
: null,
relationTargetFieldMetadataId,
} satisfies RecordFilter;
setSubFieldNameUsedInDropdown(subFieldNameToUse);
setRelationTargetFieldMetadataIdUsedInDropdown(
isRelationTraversal ? relationTargetFieldMetadataItem.id : null,
relationTargetFieldMetadataId,
);
setObjectFilterDropdownSearchInput('');

View File

@@ -1,7 +1,7 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isManyToOneRelationField } from '@/object-record/object-filter-dropdown/utils/isManyToOneRelationField';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { DATE_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/object-filter-dropdown/constants/DatePickerDropdownContentWidth';
@@ -44,22 +42,13 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
const { getFieldMetadataItemByIdOrThrow } =
useGetFieldMetadataItemByIdOrThrow();
// For relation traversal the operand list must match the target field's
// type, not the relation field's. Looked up through the global metadata
// store so it works across objects.
const effectiveFieldMetadataItem = useMemo(
() =>
isDefined(relationTargetFieldMetadataIdUsedInDropdown)
? getFieldMetadataItemByIdOrThrow(
relationTargetFieldMetadataIdUsedInDropdown,
).fieldMetadataItem
: fieldMetadataItemUsedInDropdown,
[
relationTargetFieldMetadataIdUsedInDropdown,
fieldMetadataItemUsedInDropdown,
getFieldMetadataItemByIdOrThrow,
],
);
const effectiveFieldMetadataItem = isDefined(
relationTargetFieldMetadataIdUsedInDropdown,
)
? getFieldMetadataItemByIdOrThrow(
relationTargetFieldMetadataIdUsedInDropdown,
).fieldMetadataItem
: fieldMetadataItemUsedInDropdown;
const operandsForFilterType = isDefined(effectiveFieldMetadataItem)
? getRecordFilterOperands({

View File

@@ -2,8 +2,6 @@ import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/ob
import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
// Sentinel for the relation-traversal mode of the shared sub-menu state,
// distinguished from `CompositeFilterableFieldType`.
export const RELATION_SUB_MENU_FIELD_TYPE = 'RELATION' as const;
export type ObjectFilterDropdownSubMenuFieldType =

View File

@@ -1,10 +1,6 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
// Set when the in-flight filter is a one-hop relation traversal: holds the
// metadata id of the field on the related object that the filter is being
// applied to (e.g. Company.name's field id when filtering People by company
// name). Null for direct-field and composite-sub-field filters.
export const relationTargetFieldMetadataIdUsedInDropdownComponentState =
createAtomComponentState<string | null>({
key: 'relationTargetFieldMetadataIdUsedInDropdownComponentState',

View File

@@ -1,9 +0,0 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
// MANY_TO_ONE is the only relation type the backend can currently traverse in
// filters (ONE_TO_MANY would need EXISTS-subquery support), so it's also the
// only one we expose as a drill-down target in the filter dropdown.
export const isManyToOneRelationField = (field: FieldMetadataItem): boolean =>
field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE;

View File

@@ -24,11 +24,7 @@ export type RecordFilter = {
positionInRecordFilterGroup?: number | null;
label: string;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
// For one-hop relation traversal: when `fieldMetadataId` references a
// MANY_TO_ONE relation field, this is the metadata id of the field on the
// target object whose value the filter compares against. Null otherwise.
relationTargetFieldMetadataId?: string | null;
// RLS-specific: when set, filter compares against current user's field value
rlsDynamicValue?: RLSDynamicValue | null;
};

View File

@@ -2,6 +2,7 @@ import { isUndefined } from '@sniptt/guards';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
import {
@@ -26,10 +27,7 @@ export const prefillRecord = <T extends ObjectRecord>({
const fieldValue = isUndefined(inputValue)
? generateEmptyFieldValue({ fieldMetadataItem })
: inputValue;
if (
fieldMetadataItem.type === FieldMetadataType.RELATION &&
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
) {
if (isManyToOneRelationField(fieldMetadataItem)) {
const joinColumnName = computeRelationGqlFieldJoinColumnName({
name: fieldMetadataItem.name,
});

View File

@@ -1,4 +1,5 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
@@ -63,8 +64,7 @@ export const sanitizeRecordInput = ({
if (
isDefined(fieldMetadataItem) &&
fieldMetadataItem.type === FieldMetadataType.RELATION &&
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE &&
isManyToOneRelationField(fieldMetadataItem) &&
!isDefined(recordInput[fieldMetadataItem.name]?.connect?.where)
) {
return undefined;

View File

@@ -97,9 +97,6 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectS
advancedFilterFieldSelectDropdownId,
);
// The RELATION sentinel is exclusive to the filter dropdown's
// relation-traversal flow — role-permission sub-fields only ever drill
// into composite fields, so bail out early in that case.
if (
!isDefined(objectFilterDropdownSubMenuFieldType) ||
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE

View File

@@ -1,12 +1,10 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
export const hasNestedFields = (fieldMetadata: FieldMetadataItem) => {
return (
(fieldMetadata.type === FieldMetadataType.RELATION &&
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE) ||
isManyToOneRelationField(fieldMetadata) ||
isCompositeFieldType(fieldMetadata.type)
);
};

View File

@@ -15,9 +15,6 @@ import { type ViewFilter } from '@/views/types/ViewFilter';
export const mapViewFiltersToFilters = (
viewFilters: ViewFilter[] | GqlViewFilter[],
availableFieldMetadataItems: FieldMetadataItem[],
// All field metadata items across every object, used to resolve relation
// traversal targets that live on a different object than the source field.
// Defaults to `availableFieldMetadataItems` for non-traversal callers.
allFieldMetadataItems: FieldMetadataItem[] = availableFieldMetadataItems,
): RecordFilter[] => {
return viewFilters
@@ -32,12 +29,8 @@ export const mapViewFiltersToFilters = (
return undefined;
}
// The codegen-generated `GqlViewFilter` and the local `ViewFilter`
// type don't both expose this field; `in` narrows safely across both.
const relationTargetFieldMetadataId =
'relationTargetFieldMetadataId' in viewFilter
? (viewFilter.relationTargetFieldMetadataId ?? null)
: null;
viewFilter.relationTargetFieldMetadataId ?? null;
const relationTargetFieldMetadataItem = isDefined(
relationTargetFieldMetadataId,
@@ -48,9 +41,6 @@ export const mapViewFiltersToFilters = (
)
: undefined;
// For relation traversal, the operand picker / value input must match
// the target field's type, and the label must reflect both hops so the
// filter is recognizable in the UI (e.g. "Company → Name").
const filterType = isDefined(relationTargetFieldMetadataItem)
? getFilterTypeFromFieldType(relationTargetFieldMetadataItem.type)
: getFilterTypeFromFieldType(availableFieldMetadataItem.type);

View File

@@ -64,17 +64,13 @@ export class ViewFilterEntity
@Column({ nullable: true, type: 'text', default: null })
subFieldName: string | null;
// Stable pointer to a field on the related object for one-hop relation
// traversal filters: when `fieldMetadataId` is a MANY_TO_ONE relation, this
// is the field on the target object whose value is being compared. Null for
// direct-field and composite-sub-field filters.
@Column({ nullable: true, type: 'uuid', default: null })
relationTargetFieldMetadataId: string | null;
// ON DELETE SET NULL keeps the row when the target field is deleted, but
// the operand/value stay shaped for the original target. The load path
// therefore drops any filter where `relationTargetFieldMetadataId` has
// become null after a non-null persisted value (deferred to caller).
// ON DELETE SET NULL keeps the row when the target field is deleted: the
// load path drops any filter where relationTargetFieldMetadataId was set
// but is now null, because operand/value are still shaped for the original
// target.
@ManyToOne(() => FieldMetadataEntity, {
onDelete: 'SET NULL',
nullable: true,

View File

@@ -19,9 +19,6 @@ export type RecordFilter = {
recordFilterGroupId?: string | null;
operand: ViewFilterOperand;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
// For one-hop relation traversal: when `fieldMetadataId` references a
// MANY_TO_ONE relation field, this is the metadata id of the field on the
// target object whose value the filter compares against. Null otherwise.
relationTargetFieldMetadataId?: string | null | undefined;
};

View File

@@ -87,8 +87,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
return;
}
// Relation traversal: the filter applies to a field on the related object.
// Run this BEFORE the emptiness shortcut so an "is empty" filter on
// Must run before the emptiness shortcut so an "is empty" filter on
// `company.name` is evaluated against `Company.name`, not the FK column.
if (
correspondingFieldMetadataItem.type === FieldMetadataType.RELATION &&
@@ -117,8 +116,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
[correspondingFieldMetadataItem.name]: innerFilter,
} as RecordGqlOperationFilter;
}
// Target field not in the provided fieldMetadataItems — fall through to
// the legacy relation-by-record path so the filter still works.
// Target not in fieldMetadataItems — fall through to legacy
// relation-by-record matching.
}
const shouldComputeEmptinessFilter = checkIfShouldComputeEmptinessFilter({