From 858ae62b67b64cfb2289327d06d047fb55000eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 15 May 2026 11:09:10 +0200 Subject: [PATCH] refactor(filters): split advanced-filter sub-menu, derive relation target on demand - RecordFilter only carries relationTargetFieldMetadataId; derive label/type at render - Split AdvancedFilterSubFieldSelectMenu into Composite + RelationTargetField components - Dedicated objectFilterDropdownIsSelectingRelationTargetField signal (no RELATION sentinel) - mapViewFiltersToFilters takes a single fieldMetadataItems list - Server view-query-params and common-group-by stop embedding resolved relation target - Lift focus-stack push out of useSelectFieldUsedInAdvancedFilterDropdown --- ...ncedFilterCompositeSubFieldSelectMenu.tsx} | 58 ++----- ...vancedFilterFieldSelectDropdownContent.tsx | 35 ++-- .../AdvancedFilterFieldSelectMenu.tsx | 62 +++++-- .../AdvancedFilterRelationSubMenu.tsx | 97 ----------- ...cedFilterRelationTargetFieldSelectMenu.tsx | 151 ++++++++++++++++++ .../components/AdvancedFilterValueInput.tsx | 2 +- ...SelectFieldUsedInAdvancedFilterDropdown.ts | 36 +---- .../constants/RelationSubMenuFieldType.ts | 1 - ...ectingRelationTargetFieldComponentState.ts | 9 ++ .../hooks/useGetRecordFilterDisplayValue.ts | 3 +- .../ObjectFilterDropdownSubMenuFieldType.ts | 5 +- .../record-filter/types/RecordFilter.ts | 12 +- .../hooks/useLoadRecordIndexStates.ts | 4 - ...ordLevelPermissionFieldSelectFieldMenu.tsx | 34 +++- ...LevelPermissionFieldSelectSubFieldMenu.tsx | 6 +- .../RecordTableSettingsFilters.tsx | 5 +- ...leSettingsFiltersInitializeStateEffect.tsx | 7 - ...ViewFiltersToCurrentRecordFilters.test.tsx | 2 +- .../views/hooks/useMapViewFiltersToFilters.ts | 12 +- .../useSetEditableFilterChipDropdownStates.ts | 2 +- .../utils/__tests__/viewMapFunctions.test.ts | 2 +- .../utils/mapRecordFilterToViewFilter.ts | 3 +- .../views/utils/mapViewFiltersToFilters.ts | 29 ++-- .../common-group-by-query-runner.service.ts | 23 +-- .../services/view-query-params.service.ts | 21 +-- ...RecordFilterGroupIntoGqlOperationFilter.ts | 8 +- .../turnRecordFilterIntoGqlOperationFilter.ts | 25 +-- 27 files changed, 317 insertions(+), 337 deletions(-) rename packages/twenty-front/src/modules/object-record/advanced-filter/components/{AdvancedFilterSubFieldSelectMenu.tsx => AdvancedFilterCompositeSubFieldSelectMenu.tsx} (83%) delete mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationSubMenu.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState.ts diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx similarity index 83% rename from packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx rename to packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx index 5fc3a0af8e3..923726f6d19 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx @@ -1,16 +1,6 @@ -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; - -import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; -import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { isDefined } from 'twenty-shared/utils'; - -import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { AdvancedFilterRelationSubMenu } from '@/object-record/advanced-filter/components/AdvancedFilterRelationSubMenu'; import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown'; -import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; @@ -23,21 +13,26 @@ import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/Com import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; +import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; +import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; import { IconChevronLeft, useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -type AdvancedFilterSubFieldSelectMenuProps = { +type AdvancedFilterCompositeSubFieldSelectMenuProps = { recordFilterId: string; }; -export const AdvancedFilterSubFieldSelectMenu = ({ +export const AdvancedFilterCompositeSubFieldSelectMenu = ({ recordFilterId, -}: AdvancedFilterSubFieldSelectMenuProps) => { +}: AdvancedFilterCompositeSubFieldSelectMenuProps) => { const { getIcon } = useIcons(); const fieldMetadataItemUsedInDropdown = useAtomComponentSelectorValue( @@ -63,20 +58,14 @@ export const AdvancedFilterSubFieldSelectMenu = ({ const handleSelectFilter = ({ fieldMetadataItem, subFieldName, - relationTargetFieldMetadataItem, }: { fieldMetadataItem: FieldMetadataItem; subFieldName?: CompositeFieldSubFieldName | null; - relationTargetFieldMetadataItem?: FieldMetadataItem | null; }) => { selectFieldUsedInAdvancedFilterDropdown({ fieldMetadataItemId: fieldMetadataItem.id, recordFilterId, subFieldName, - relationTargetFieldMetadataItem, - // The value picker that opens next is for a sub-field or target - // field, not the source — its dropdown manages its own focus. - skipFocusPush: true, }); closeAdvancedFilterFieldSelectDropdown(); @@ -99,35 +88,8 @@ export const AdvancedFilterSubFieldSelectMenu = ({ return null; } - if ( - objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE && - isDefined(fieldMetadataItemUsedInDropdown) && - isManyToOneRelationField(fieldMetadataItemUsedInDropdown) - ) { - return ( - - ); - } - - const compositeSubMenuFieldType = - objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE - ? null - : objectFilterDropdownSubMenuFieldType; - - if (!isDefined(compositeSubMenuFieldType)) { - return null; - } - const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ - compositeSubMenuFieldType + objectFilterDropdownSubMenuFieldType ].subFields .filter((subField) => subField.isFilterable === true) .map((subField) => subField.subFieldName); @@ -216,7 +178,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({ }); }} text={getCompositeSubFieldLabel( - compositeSubMenuFieldType, + objectFilterDropdownSubMenuFieldType, subFieldName, )} LeftIcon={getIcon( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownContent.tsx index 0c37640650d..12f3539d717 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownContent.tsx @@ -1,7 +1,9 @@ +import { AdvancedFilterCompositeSubFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu'; import { AdvancedFilterFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu'; -import { AdvancedFilterSubFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu'; +import { AdvancedFilterRelationTargetFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; -import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; +import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; type AdvancedFilterFieldSelectDropdownContentProps = { recordFilterId: string; @@ -10,16 +12,29 @@ type AdvancedFilterFieldSelectDropdownContentProps = { export const AdvancedFilterFieldSelectDropdownContent = ({ recordFilterId, }: AdvancedFilterFieldSelectDropdownContentProps) => { - const [objectFilterDropdownIsSelectingCompositeField] = useAtomComponentState( + const isSelectingCompositeField = useAtomComponentStateValue( objectFilterDropdownIsSelectingCompositeFieldComponentState, ); - const shouldShowCompositeSelectionSubMenu = - objectFilterDropdownIsSelectingCompositeField; - - return shouldShowCompositeSelectionSubMenu ? ( - - ) : ( - + const isSelectingRelationTargetField = useAtomComponentStateValue( + objectFilterDropdownIsSelectingRelationTargetFieldComponentState, ); + + if (isSelectingRelationTargetField) { + return ( + + ); + } + + if (isSelectingCompositeField) { + return ( + + ); + } + + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx index 48409771e40..b742e6a2f80 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx @@ -16,9 +16,8 @@ import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/co import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; -import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType'; +import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; -import { type ObjectFilterDropdownSubMenuFieldType } from '@/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType'; import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType'; import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField'; import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; @@ -26,6 +25,8 @@ import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/h import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { useLingui } from '@lingui/react/macro'; @@ -102,10 +103,17 @@ export const AdvancedFilterFieldSelectMenu = ({ objectFilterDropdownIsSelectingCompositeFieldComponentState, ); + const [, setObjectFilterDropdownIsSelectingRelationTargetField] = + useAtomComponentState( + objectFilterDropdownIsSelectingRelationTargetFieldComponentState, + ); + const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState( fieldMetadataItemIdUsedInDropdownComponentState, ); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const handleFieldMetadataItemSelect = ( selectedFieldMetadataItem: FieldMetadataItem, ) => { @@ -115,30 +123,50 @@ export const AdvancedFilterFieldSelectMenu = ({ selectedFieldMetadataItem.type, ); - const subMenuType: ObjectFilterDropdownSubMenuFieldType | null = - isManyToOneRelationField(selectedFieldMetadataItem) - ? RELATION_SUB_MENU_FIELD_TYPE - : isCompositeFilterableFieldType(filterType) - ? filterType - : null; + const isRelationTraversalField = isManyToOneRelationField( + selectedFieldMetadataItem, + ); + + const compositeSubMenuFieldType = + !isRelationTraversalField && isCompositeFilterableFieldType(filterType) + ? filterType + : null; - // Sub-menus (composite or relation traversal) drive their own focus - // scope; pushing the source field id on the focus stack would shadow - // their selectable list hotkeys. selectFieldUsedInAdvancedFilterDropdown({ fieldMetadataItemId: selectedFieldMetadataItem.id, recordFilterId, - skipFocusPush: subMenuType !== null, }); - if (subMenuType === null) { - closeAdvancedFilterFieldSelectDropdown(); + if (isRelationTraversalField) { + setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); + setObjectFilterDropdownIsSelectingRelationTargetField(true); return; } - setObjectFilterDropdownSubMenuFieldType(subMenuType); - setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); - setObjectFilterDropdownIsSelectingCompositeField(true); + if (compositeSubMenuFieldType !== null) { + setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); + setObjectFilterDropdownSubMenuFieldType(compositeSubMenuFieldType); + setObjectFilterDropdownIsSelectingCompositeField(true); + return; + } + + // Leaf RELATION/SELECT fields open a value picker keyed by the source + // field id — push it on the focus stack so the picker's keyboard + // hotkeys are active when it opens. + if ( + selectedFieldMetadataItem.type === 'RELATION' || + selectedFieldMetadataItem.type === 'SELECT' + ) { + pushFocusItemToFocusStack({ + focusId: selectedFieldMetadataItem.id, + component: { + type: FocusComponentType.DROPDOWN, + instanceId: selectedFieldMetadataItem.id, + }, + }); + } + + closeAdvancedFilterFieldSelectDropdown(); }; const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationSubMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationSubMenu.tsx deleted file mode 100644 index 6fb0b2ed04a..00000000000 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationSubMenu.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; -import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems'; -import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; -import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; -import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; -import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; -import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { IconChevronLeft, useIcons } from 'twenty-ui/display'; -import { MenuItem } from 'twenty-ui/navigation'; - -type AdvancedFilterRelationSubMenuProps = { - recordFilterId: string; - relationFieldMetadataItem: FieldMetadataItem; - targetObjectMetadataId: string; - onBack: () => void; - onSelectTargetField: (params: { - fieldMetadataItem: FieldMetadataItem; - relationTargetFieldMetadataItem: FieldMetadataItem; - }) => void; -}; - -export const AdvancedFilterRelationSubMenu = ({ - recordFilterId, - relationFieldMetadataItem, - targetObjectMetadataId, - onBack, - onSelectTargetField, -}: AdvancedFilterRelationSubMenuProps) => { - const { getIcon } = useIcons(); - - const { advancedFilterFieldSelectDropdownId } = - useAdvancedFilterFieldSelectDropdown(recordFilterId); - - const selectedItemId = useAtomComponentStateValue( - selectedItemIdComponentState, - advancedFilterFieldSelectDropdownId, - ); - - const { filterableFieldMetadataItems: relationTargetFields } = - useFilterableFieldMetadataItems(targetObjectMetadataId); - - const selectableItemIdArray = relationTargetFields.map((field) => field.id); - - return ( - - - } - > - {relationFieldMetadataItem.label} - - - - {relationTargetFields.map((targetField, index) => ( - { - onSelectTargetField({ - fieldMetadataItem: relationFieldMetadataItem, - relationTargetFieldMetadataItem: targetField, - }); - }} - > - { - onSelectTargetField({ - fieldMetadataItem: relationFieldMetadataItem, - relationTargetFieldMetadataItem: targetField, - }); - }} - text={targetField.label} - LeftIcon={getIcon(targetField.icon)} - /> - - ))} - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu.tsx new file mode 100644 index 00000000000..15727c3a569 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu.tsx @@ -0,0 +1,151 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField'; +import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; +import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; +import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState'; +import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; +import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { isDefined } from 'twenty-shared/utils'; +import { IconChevronLeft, useIcons } from 'twenty-ui/display'; +import { MenuItem } from 'twenty-ui/navigation'; + +type AdvancedFilterRelationTargetFieldSelectMenuProps = { + recordFilterId: string; +}; + +export const AdvancedFilterRelationTargetFieldSelectMenu = ({ + recordFilterId, +}: AdvancedFilterRelationTargetFieldSelectMenuProps) => { + const { getIcon } = useIcons(); + + const sourceFieldMetadataItem = useAtomComponentSelectorValue( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + + const setIsSelectingRelationTargetField = useSetAtomComponentState( + objectFilterDropdownIsSelectingRelationTargetFieldComponentState, + ); + + const { closeAdvancedFilterFieldSelectDropdown } = + useAdvancedFilterFieldSelectDropdown(recordFilterId); + + const { selectFieldUsedInAdvancedFilterDropdown } = + useSelectFieldUsedInAdvancedFilterDropdown(); + + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + + const { advancedFilterFieldSelectDropdownId } = + useAdvancedFilterFieldSelectDropdown(recordFilterId); + + const selectedItemId = useAtomComponentStateValue( + selectedItemIdComponentState, + advancedFilterFieldSelectDropdownId, + ); + + const targetObjectMetadataId = + isDefined(sourceFieldMetadataItem) && + isManyToOneRelationField(sourceFieldMetadataItem) + ? sourceFieldMetadataItem.relation.targetObjectMetadata.id + : null; + + const { filterableFieldMetadataItems: relationTargetFields } = + useFilterableFieldMetadataItems(targetObjectMetadataId ?? ''); + + if ( + !isDefined(sourceFieldMetadataItem) || + !isManyToOneRelationField(sourceFieldMetadataItem) + ) { + return null; + } + + const handleSubMenuBack = () => { + setIsSelectingRelationTargetField(false); + }; + + const handleSelectTargetField = ( + relationTargetFieldMetadataItem: FieldMetadataItem, + ) => { + selectFieldUsedInAdvancedFilterDropdown({ + fieldMetadataItemId: sourceFieldMetadataItem.id, + recordFilterId, + relationTargetFieldMetadataItem, + }); + + // Leaf RELATION/SELECT target fields open a value picker keyed by the + // target field id — push it on the focus stack so the picker's + // keyboard hotkeys are active when it opens. + if ( + relationTargetFieldMetadataItem.type === 'RELATION' || + relationTargetFieldMetadataItem.type === 'SELECT' + ) { + pushFocusItemToFocusStack({ + focusId: relationTargetFieldMetadataItem.id, + component: { + type: FocusComponentType.DROPDOWN, + instanceId: relationTargetFieldMetadataItem.id, + }, + }); + } + + setIsSelectingRelationTargetField(false); + closeAdvancedFilterFieldSelectDropdown(); + }; + + const selectableItemIdArray = relationTargetFields.map((field) => field.id); + + return ( + + + } + > + {sourceFieldMetadataItem.label} + + + + {relationTargetFields.map((targetField, index) => ( + { + handleSelectTargetField(targetField); + }} + > + { + handleSelectTargetField(targetField); + }} + text={targetField.label} + LeftIcon={getIcon(targetField.icon)} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx index 7a98fed6057..90efd0e171a 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInput.tsx @@ -82,7 +82,7 @@ export const AdvancedFilterValueInput = ({ setObjectFilterDropdownCurrentRecordFilter(recordFilter); setFieldMetadataItemIdUsedInDropdown(recordFilter.fieldMetadataId); setRelationTargetFieldMetadataIdUsedInDropdown( - recordFilter.relationTargetField?.id ?? null, + recordFilter.relationTargetFieldMetadataId ?? null, ); }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts index 130909244be..bd964992612 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown.ts @@ -15,8 +15,6 @@ import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object- import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField'; import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; -import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; -import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils'; @@ -26,10 +24,6 @@ type SelectFilterParams = { recordFilterId: string; subFieldName?: CompositeFieldSubFieldName | null | undefined; relationTargetFieldMetadataItem?: FieldMetadataItem | null | undefined; - // Set when the next step is another dropdown that manages its own focus - // (e.g. composite or relation-traversal sub-menus). The default push of - // the source field id on the focus stack would shadow it. - skipFocusPush?: boolean; }; export const useSelectFieldUsedInAdvancedFilterDropdown = () => { @@ -49,8 +43,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { currentRecordFiltersComponentState, ); - const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); - const { getFieldMetadataItemByIdOrThrow } = useGetFieldMetadataItemByIdOrThrow(); @@ -75,7 +67,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { recordFilterId, subFieldName, relationTargetFieldMetadataItem, - skipFocusPush, }: SelectFilterParams) => { setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId); @@ -86,20 +77,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { return; } - if ( - skipFocusPush !== true && - (fieldMetadataItem.type === 'RELATION' || - fieldMetadataItem.type === 'SELECT') - ) { - pushFocusItemToFocusStack({ - focusId: fieldMetadataItem.id, - component: { - type: FocusComponentType.DROPDOWN, - instanceId: fieldMetadataItem.id, - }, - }); - } - const isRelationTraversal = isDefined(relationTargetFieldMetadataItem); const filterType = getFilterTypeFromFieldType( @@ -157,13 +134,8 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { ? `${fieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}` : fieldMetadataItem.label; - const relationTargetField = isRelationTraversal - ? { - id: relationTargetFieldMetadataItem.id, - name: relationTargetFieldMetadataItem.name, - type: relationTargetFieldMetadataItem.type, - label: relationTargetFieldMetadataItem.label, - } + const relationTargetFieldMetadataId = isRelationTraversal + ? relationTargetFieldMetadataItem.id : null; const newAdvancedFilter = { @@ -178,12 +150,12 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => { type: filterType, label, subFieldName: subFieldNameToUse, - relationTargetField, + relationTargetFieldMetadataId, } satisfies RecordFilter; setSubFieldNameUsedInDropdown(subFieldNameToUse); setRelationTargetFieldMetadataIdUsedInDropdown( - relationTargetField?.id ?? null, + relationTargetFieldMetadataId, ); setObjectFilterDropdownSearchInput(''); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType.ts deleted file mode 100644 index 2173a9fae80..00000000000 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType.ts +++ /dev/null @@ -1 +0,0 @@ -export const RELATION_SUB_MENU_FIELD_TYPE = 'RELATION' as const; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState.ts new file mode 100644 index 00000000000..29664c96beb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState.ts @@ -0,0 +1,9 @@ +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState'; + +export const objectFilterDropdownIsSelectingRelationTargetFieldComponentState = + createAtomComponentState({ + key: 'objectFilterDropdownIsSelectingRelationTargetFieldComponentState', + defaultValue: false, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts index 88760ec9414..c71bf010c0e 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts @@ -156,7 +156,8 @@ export const useGetRecordFilterDisplayValue = () => { } const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow( - recordFilter.relationTargetField?.id ?? recordFilter.fieldMetadataId, + recordFilter.relationTargetFieldMetadataId ?? + recordFilter.fieldMetadataId, ); const fieldMetadataItemOptions = fieldMetadataItem.options; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType.ts index 8650220a01a..c4deab78ec3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType.ts @@ -1,6 +1,3 @@ -import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType'; import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType'; -export type ObjectFilterDropdownSubMenuFieldType = - | CompositeFilterableFieldType - | typeof RELATION_SUB_MENU_FIELD_TYPE; +export type ObjectFilterDropdownSubMenuFieldType = CompositeFilterableFieldType; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts index dd6786b86c9..bd0fbb7701c 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts @@ -1,7 +1,6 @@ import { type FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; import { - type FieldMetadataType, type FilterableAndTSVectorFieldType, type ViewFilterOperand, } from 'twenty-shared/types'; @@ -25,16 +24,7 @@ export type RecordFilter = { positionInRecordFilterGroup?: number | null; label: string; subFieldName?: CompositeFieldSubFieldName | null | undefined; - // Resolved at filter construction time (frontend mapping or chip edit) so - // the GraphQL builder doesn't need a cross-object field lookup. Null for - // non-relation filters; also null when the target field can no longer be - // resolved (e.g. it was deleted). - relationTargetField?: { - id: string; - name: string; - type: FieldMetadataType; - label: string; - } | null; + relationTargetFieldMetadataId?: string | null; rlsDynamicValue?: RLSDynamicValue | null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index 927677f92d5..e25d22cb318 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -31,7 +31,6 @@ import { hasInitializedCurrentRecordFieldsComponentFamilyState } from '@/views/s import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState'; import { hasInitializedCurrentRecordSortsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordSortsComponentFamilyState'; import { type View } from '@/views/types/View'; -import { getFilterableFields } from '@/views/utils/getFilterableFields'; import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField'; import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions'; import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups'; @@ -129,13 +128,11 @@ export const useLoadRecordIndexStates = () => { .map(mapViewFieldToRecordField) .filter(isDefined); - const allFilterableFields = getFilterableFields(objectMetadataItem); const flattenedFieldMetadataItems = store.get( flattenedFieldMetadataItemsSelector.atom, ); const recordFilters = mapViewFiltersToFilters( view.viewFilters, - allFilterableFields, flattenedFieldMetadataItems, ); @@ -145,7 +142,6 @@ export const useLoadRecordIndexStates = () => { const contextStoreFilters = mapViewFiltersToFilters( view.viewFilters, - filterableFieldMetadataItems, flattenedFieldMetadataItems, ); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx index 3cb09b297d9..f722314d4b9 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx @@ -24,6 +24,8 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { useContext } from 'react'; @@ -89,6 +91,8 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF fieldMetadataItemIdUsedInDropdownComponentState, ); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const handleFieldSelect = ( selectedFieldMetadataItem: FieldMetadataItem, ) => { @@ -102,13 +106,31 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF setObjectFilterDropdownSubMenuFieldType(filterType); setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); setObjectFilterDropdownIsSelectingCompositeField(true); - } else { - selectFieldUsedInAdvancedFilterDropdown({ - fieldMetadataItemId: selectedFieldMetadataItem.id, - recordFilterId, - }); - closeAdvancedFilterFieldSelectDropdown(); + return; } + + selectFieldUsedInAdvancedFilterDropdown({ + fieldMetadataItemId: selectedFieldMetadataItem.id, + recordFilterId, + }); + + // RELATION/SELECT leaf fields open a value picker keyed by the + // source field id — push it on the focus stack so the picker's + // keyboard hotkeys are active when it opens. + if ( + selectedFieldMetadataItem.type === FieldMetadataType.RELATION || + selectedFieldMetadataItem.type === FieldMetadataType.SELECT + ) { + pushFocusItemToFocusStack({ + focusId: selectedFieldMetadataItem.id, + component: { + type: FocusComponentType.DROPDOWN, + instanceId: selectedFieldMetadataItem.id, + }, + }); + } + + closeAdvancedFilterFieldSelectDropdown(); }; const selectableItemIdArray = filteredFieldMetadataItems.map( diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx index b31b179f5ae..9ec48ac47c1 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx @@ -13,7 +13,6 @@ import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-f import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown'; import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; -import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField'; @@ -95,10 +94,7 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectS advancedFilterFieldSelectDropdownId, ); - if ( - !isDefined(objectFilterDropdownSubMenuFieldType) || - objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE - ) { + if (!isDefined(objectFilterDropdownSubMenuFieldType)) { return null; } diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx index daa3d4d1019..15c99172931 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx @@ -71,10 +71,7 @@ export const RecordTableSettingsFilters = ({ isWorkflowFindRecords={false} onUpdate={handleFilterUpdate} /> - + diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect.tsx index 5b3cd9cf0c0..887042fc1f9 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect.tsx @@ -1,5 +1,4 @@ import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; -import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { useSetAdvancedFilterDropdownStates } from '@/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; @@ -7,7 +6,6 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { type View } from '@/views/types/View'; -import { getFilterableFields } from '@/views/utils/getFilterableFields'; import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { useEffect, useState } from 'react'; @@ -15,12 +13,10 @@ import { isDefined } from 'twenty-shared/utils'; type RecordTableSettingsFiltersInitializeStateEffectProps = { view: View; - objectMetadataItem: EnrichedObjectMetadataItem; }; export const RecordTableSettingsFiltersInitializeStateEffect = ({ view, - objectMetadataItem, }: RecordTableSettingsFiltersInitializeStateEffectProps) => { const setCurrentRecordFilters = useSetAtomComponentState( currentRecordFiltersComponentState, @@ -60,10 +56,8 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({ return; } - const filterableFields = getFilterableFields(objectMetadataItem); const recordFilters = mapViewFiltersToFilters( view.viewFilters, - filterableFields, flattenedFieldMetadataItems, ); @@ -78,7 +72,6 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({ setHasInitializedFilters(true); }, [ view, - objectMetadataItem, flattenedFieldMetadataItems, hasInitializedFilters, stateAlreadyHasFilters, diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx index f271a1dfa23..bed324298fb 100644 --- a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx @@ -125,7 +125,7 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => { label: mockFieldMetadataItem.label, type: getFilterTypeFromFieldType(mockFieldMetadataItem.type), subFieldName: null, - relationTargetField: null, + relationTargetFieldMetadataId: null, } satisfies RecordFilter, ]); }); diff --git a/packages/twenty-front/src/modules/views/hooks/useMapViewFiltersToFilters.ts b/packages/twenty-front/src/modules/views/hooks/useMapViewFiltersToFilters.ts index 844f2f49763..07bbc8af9f7 100644 --- a/packages/twenty-front/src/modules/views/hooks/useMapViewFiltersToFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/useMapViewFiltersToFilters.ts @@ -1,14 +1,10 @@ import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; -import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { type ViewFilter as GqlViewFilter } from '~/generated-metadata/graphql'; import { type ViewFilter } from '@/views/types/ViewFilter'; -import { getFilterableFields } from '@/views/utils/getFilterableFields'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; export const useMapViewFiltersToFilters = () => { - const { objectMetadataItem } = useRecordIndexContextOrThrow(); - const flattenedFieldMetadataItems = useAtomStateValue( flattenedFieldMetadataItemsSelector, ); @@ -16,13 +12,7 @@ export const useMapViewFiltersToFilters = () => { const mapViewFiltersToRecordFilters = ( viewFilters: ViewFilter[] | GqlViewFilter[], ) => { - const filterableFieldMetadataItems = - getFilterableFields(objectMetadataItem); - return mapViewFiltersToFilters( - viewFilters, - filterableFieldMetadataItems, - flattenedFieldMetadataItems, - ); + return mapViewFiltersToFilters(viewFilters, flattenedFieldMetadataItems); }; return { mapViewFiltersToRecordFilters }; diff --git a/packages/twenty-front/src/modules/views/hooks/useSetEditableFilterChipDropdownStates.ts b/packages/twenty-front/src/modules/views/hooks/useSetEditableFilterChipDropdownStates.ts index 843d5344eef..391dd32fc85 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSetEditableFilterChipDropdownStates.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSetEditableFilterChipDropdownStates.ts @@ -67,7 +67,7 @@ export const useSetEditableFilterChipDropdownStates = () => { recordFilterId: recordFilter.id, }), }), - recordFilter.relationTargetField?.id ?? null, + recordFilter.relationTargetFieldMetadataId ?? null, ); }, [store, filterableFieldMetadataItems], diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts index 8fa0538ae7a..fde0083b11a 100644 --- a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts +++ b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts @@ -44,7 +44,7 @@ describe('mapViewFiltersToFilters', () => { positionInRecordFilterGroup: undefined, recordFilterGroupId: undefined, subFieldName: undefined, - relationTargetField: null, + relationTargetFieldMetadataId: null, }, ]; expect( diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts index ac6d9ccd449..53d53e4d5bd 100644 --- a/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts +++ b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts @@ -13,6 +13,7 @@ export const mapRecordFilterToViewFilter = ( positionInViewFilterGroup: recordFilter.positionInRecordFilterGroup, viewFilterGroupId: recordFilter.recordFilterGroupId, subFieldName: recordFilter.subFieldName, - relationTargetFieldMetadataId: recordFilter.relationTargetField?.id ?? null, + relationTargetFieldMetadataId: + recordFilter.relationTargetFieldMetadataId ?? null, }; }; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts index 718fc019f86..337387afc78 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts @@ -14,25 +14,24 @@ import { type ViewFilter } from '@/views/types/ViewFilter'; export const mapViewFiltersToFilters = ( viewFilters: ViewFilter[] | GqlViewFilter[], - availableFieldMetadataItems: FieldMetadataItem[], - allFieldMetadataItems: FieldMetadataItem[] = availableFieldMetadataItems, + fieldMetadataItems: FieldMetadataItem[], ): RecordFilter[] => { return viewFilters .map((viewFilter) => { - const availableFieldMetadataItem = availableFieldMetadataItems.find( + const sourceFieldMetadataItem = fieldMetadataItems.find( (fieldMetadataItem) => fieldMetadataItem.id === viewFilter.fieldMetadataId, ); - if (!isDefined(availableFieldMetadataItem)) { - // Todo: we we don't throw an error yet as we have race condition on view change + if (!isDefined(sourceFieldMetadataItem)) { + // Todo: we don't throw an error yet as we have race condition on view change return undefined; } const relationTargetFieldMetadataItem = isDefined( viewFilter.relationTargetFieldMetadataId, ) - ? allFieldMetadataItems.find( + ? fieldMetadataItems.find( (fieldMetadataItem) => fieldMetadataItem.id === viewFilter.relationTargetFieldMetadataId, ) @@ -40,13 +39,13 @@ export const mapViewFiltersToFilters = ( const filterType = isDefined(relationTargetFieldMetadataItem) ? getFilterTypeFromFieldType(relationTargetFieldMetadataItem.type) - : getFilterTypeFromFieldType(availableFieldMetadataItem.type); + : getFilterTypeFromFieldType(sourceFieldMetadataItem.type); - const label = isSystemSearchVectorField(availableFieldMetadataItem.name) + const label = isSystemSearchVectorField(sourceFieldMetadataItem.name) ? 'Search' : isDefined(relationTargetFieldMetadataItem) - ? `${availableFieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}` - : availableFieldMetadataItem.label; + ? `${sourceFieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}` + : sourceFieldMetadataItem.label; const operand = viewFilter.operand; @@ -66,14 +65,8 @@ export const mapViewFiltersToFilters = ( label, type: filterType, subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName, - relationTargetField: isDefined(relationTargetFieldMetadataItem) - ? { - id: relationTargetFieldMetadataItem.id, - name: relationTargetFieldMetadataItem.name, - type: relationTargetFieldMetadataItem.type, - label: relationTargetFieldMetadataItem.label, - } - : null, + relationTargetFieldMetadataId: + viewFilter.relationTargetFieldMetadataId ?? null, } satisfies RecordFilter; }) .filter(isDefined); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index 72fa9e98e5c..c139786ea8e 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -222,34 +222,17 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic ); } - const relationTargetField = isDefined( - viewFilter.relationTargetFieldMetadataId, - ) - ? findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: viewFilter.relationTargetFieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }) - : null; - return { id: viewFilter.id, fieldMetadataId: viewFilter.fieldMetadataId, value: convertViewFilterValueToString(viewFilter.value), - type: getFilterTypeFromFieldType( - relationTargetField?.type ?? fieldMetadata.type, - ), + type: getFilterTypeFromFieldType(fieldMetadata.type), operand: viewFilter.operand, recordFilterGroupId: viewFilter.viewFilterGroupId, positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup, subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName, - relationTargetField: isDefined(relationTargetField) - ? { - id: relationTargetField.id, - name: relationTargetField.name, - type: relationTargetField.type, - label: relationTargetField.label, - } - : null, + relationTargetFieldMetadataId: + viewFilter.relationTargetFieldMetadataId ?? null, }; }); diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts index 28fd4cde760..b50809a9048 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts @@ -82,31 +82,16 @@ export class ViewQueryParamsService { if (!field) return null; - const relationTargetField = isDefined( - viewFilter.relationTargetFieldMetadataId, - ) - ? findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: viewFilter.relationTargetFieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }) - : null; - return { id: viewFilter.id, fieldMetadataId: viewFilter.fieldMetadataId, value: viewFilter.value ?? '', - type: relationTargetField?.type ?? field.type, + type: field.type, recordFilterGroupId: viewFilter.viewFilterGroupId, operand: viewFilter.operand, subFieldName: viewFilter.subFieldName, - relationTargetField: isDefined(relationTargetField) - ? { - id: relationTargetField.id, - name: relationTargetField.name, - type: relationTargetField.type, - label: relationTargetField.label, - } - : null, + relationTargetFieldMetadataId: + viewFilter.relationTargetFieldMetadataId ?? null, } as RecordFilter; }) .filter(isDefined); diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts index f2fe240ca45..8d7f0810ca0 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts @@ -1,6 +1,5 @@ import { type CompositeFieldSubFieldName, - type FieldMetadataType, type FilterableAndTSVectorFieldType, type PartialFieldMetadataItem, RecordFilterGroupLogicalOperator, @@ -20,12 +19,7 @@ export type RecordFilter = { recordFilterGroupId?: string | null; operand: ViewFilterOperand; subFieldName?: CompositeFieldSubFieldName | null | undefined; - // Resolved at filter construction time so traversal doesn't need a - // cross-object field lookup downstream. - relationTargetField?: - | { id: string; name: string; type: FieldMetadataType; label: string } - | null - | undefined; + relationTargetFieldMetadataId?: string | null | undefined; }; export type RecordFilterGroup = { diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index c08dee7547b..aa607ce277d 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -89,25 +89,28 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ // Must run before the emptiness shortcut so an "is empty" filter on // `company.name` is evaluated against `Company.name`, not the FK column. - // If `relationTargetField` is missing the filter is dropped, not fed - // through the legacy relation-by-record path — that path parses `value` - // as a UUID list and would silently mishandle a target-field value like - // "Acme", risking broadening of destructive operations. + // If the target field can't be resolved the filter is dropped rather than + // fed to the per-type switch as a RELATION (which would parse `value` as + // a UUID list and silently mishandle target-field values like "Acme"). if ( correspondingFieldMetadataItem.type === FieldMetadataType.RELATION && - isDefined(recordFilter.relationTargetField) + isDefined(recordFilter.relationTargetFieldMetadataId) ) { - const targetField = recordFilter.relationTargetField; + const targetFieldMetadataItem = fieldMetadataItems.find( + (field) => field.id === recordFilter.relationTargetFieldMetadataId, + ); + + if (!isDefined(targetFieldMetadataItem)) { + return; + } const innerFilter = turnRecordFilterIntoRecordGqlOperationFilter({ recordFilter: { ...recordFilter, - fieldMetadataId: targetField.id, - relationTargetField: null, + fieldMetadataId: targetFieldMetadataItem.id, + relationTargetFieldMetadataId: null, }, - // Inject the target so the recursive lookup succeeds even when the - // caller only supplied the current object's fields. - fieldMetadataItems: [...fieldMetadataItems, targetField], + fieldMetadataItems, filterValueDependencies, });