mirror of
https://github.com/twentyhq/twenty.git
synced 2026-05-24 16:32:28 -04:00
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:
@@ -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))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user