From 504eaa5600ba88739541668a7baf373c0aeecd37 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:00:49 +0200
Subject: [PATCH] Fix relation-traversal filters showing no options on select
fields (#21616)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Relation-traversal filters (e.g. *Company → Industry*) on
`SELECT`/`MULTI_SELECT` fields rendered an empty option picker, so the
filter couldn't be configured.
The value inputs resolved the select options from the **source relation
field** (which has no options) instead of the **relation target field**.
Fixed in both filter UIs:
- Dashboard chart filters and workflow "Find Records"
(`AdvancedFilterSidePanelValueFormInput`)
- Record-index simple & advanced filters and the role-permission filter
builder (`ObjectFilterDropdownOptionSelect`)
Both now resolve the value-input field and its options from
`relationTargetFieldMetadataId` when a filter traverses a relation,
falling back to the source field otherwise.
## Before
## After
---
.../AdvancedFilterSidePanelValueFormInput.tsx | 21 +++-
...ncedFilterSidePanelValueFormInput.test.tsx | 118 ++++++++++++++++++
.../ObjectFilterDropdownOptionSelect.tsx | 16 ++-
.../ObjectFilterDropdownOptionSelect.test.tsx | 118 ++++++++++++++++++
4 files changed, 269 insertions(+), 4 deletions(-)
create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/__tests__/AdvancedFilterSidePanelValueFormInput.test.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__tests__/ObjectFilterDropdownOptionSelect.test.tsx
diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelValueFormInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelValueFormInput.tsx
index c86d1feb58..837f928317 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelValueFormInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelValueFormInput.tsx
@@ -1,3 +1,4 @@
+import { fieldMetadataItemByIdSelector } from '@/object-metadata/states/fieldMetadataItemByIdSelector';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { AdvancedFilterSidePanelValueFormCompositeFieldInput } from '@/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelValueFormCompositeFieldInput';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
@@ -20,6 +21,7 @@ import {
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
@@ -91,10 +93,23 @@ export const AdvancedFilterSidePanelValueFormInput = ({
dropdownInstanceId,
);
- const fieldDefinition = fieldMetadataItemUsedInDropdown
+ const {
+ foundFieldMetadataItem: relationTargetFieldMetadataItem,
+ foundObjectMetadataItem: relationTargetObjectMetadataItem,
+ } = useAtomFamilySelectorValue(fieldMetadataItemByIdSelector, {
+ fieldMetadataItemId: recordFilter?.relationTargetFieldMetadataId ?? '',
+ });
+
+ const fieldMetadataItemForValueInput =
+ relationTargetFieldMetadataItem ?? fieldMetadataItemUsedInDropdown;
+
+ const objectMetadataItemForValueInput =
+ relationTargetObjectMetadataItem ?? objectMetadataItem;
+
+ const fieldDefinition = fieldMetadataItemForValueInput
? formatFieldMetadataItemAsFieldDefinition({
- field: fieldMetadataItemUsedInDropdown,
- objectMetadataItem: objectMetadataItem,
+ field: fieldMetadataItemForValueInput,
+ objectMetadataItem: objectMetadataItemForValueInput,
})
: null;
diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/__tests__/AdvancedFilterSidePanelValueFormInput.test.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/__tests__/AdvancedFilterSidePanelValueFormInput.test.tsx
new file mode 100644
index 0000000000..8714174662
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/side-panel/components/__tests__/AdvancedFilterSidePanelValueFormInput.test.tsx
@@ -0,0 +1,118 @@
+import { render, waitFor } from '@testing-library/react';
+import { useEffect, useState } from 'react';
+
+import { AdvancedFilterSidePanelValueFormInput } from '@/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelValueFormInput';
+import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
+import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId';
+import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
+import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
+import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
+import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
+import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
+import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
+import { FieldMetadataType, ViewFilterOperand } from 'twenty-shared/types';
+import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
+import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
+
+jest.mock('@/object-record/record-field/ui/components/FormFieldInput', () => ({
+ FormFieldInput: () => null,
+}));
+
+jest.mock(
+ '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput',
+ () => ({
+ FormMultiSelectFieldInput: ({
+ options,
+ }: {
+ options: { value: string }[];
+ }) =>
{options.length}
,
+ }),
+);
+
+const INSTANCE_ID = 'advanced-filter-value-test';
+const FILTER_ID = 'relation-select-filter';
+
+const opportunity = getMockObjectMetadataItemOrThrow('opportunity');
+
+const relationField = opportunity.fields.find(
+ (field) => field.type === FieldMetadataType.RELATION,
+);
+
+const selectField = opportunity.fields.find(
+ (field) =>
+ field.type === FieldMetadataType.SELECT && (field.options?.length ?? 0) > 0,
+);
+
+if (!relationField || !selectField) {
+ throw new Error('Missing relation or select field in opportunity mock');
+}
+
+const relationToSelectFilter: RecordFilter = {
+ id: FILTER_ID,
+ fieldMetadataId: relationField.id,
+ relationTargetFieldMetadataId: selectField.id,
+ type: FieldMetadataType.SELECT,
+ operand: ViewFilterOperand.IS,
+ value: '',
+ displayValue: '',
+ label: `${relationField.label} → ${selectField.label}`,
+};
+
+const BaseWrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: [] });
+
+const Seed = ({ children }: { children: React.ReactNode }) => {
+ const setCurrentRecordFilters = useSetAtomComponentState(
+ currentRecordFiltersComponentState,
+ );
+ const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
+ fieldMetadataItemIdUsedInDropdownComponentState,
+ getAdvancedFilterObjectFilterDropdownComponentInstanceId(FILTER_ID),
+ );
+
+ const [isSeeded, setIsSeeded] = useState(false);
+
+ useEffect(() => {
+ setCurrentRecordFilters([relationToSelectFilter]);
+ setFieldMetadataItemIdUsedInDropdown(relationField.id);
+ setIsSeeded(true);
+ }, [setCurrentRecordFilters, setFieldMetadataItemIdUsedInDropdown]);
+
+ return isSeeded ? <>{children}> : null;
+};
+
+const Wrapper = () => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+describe('AdvancedFilterSidePanelValueFormInput', () => {
+ it('resolves select options from the relation target field for a relation-traversal filter', async () => {
+ const { getByTestId } = render();
+
+ await waitFor(() => {
+ expect(getByTestId('multiselect-options-count').textContent).toBe(
+ String(selectField.options?.length),
+ );
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx
index 39e2e32f84..6947b25111 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Key } from 'ts-key-enum';
+import { fieldMetadataItemByIdSelector } from '@/object-metadata/states/fieldMetadataItemByIdSelector';
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { useOptionsForSelect } from '@/object-record/object-filter-dropdown/hooks/useOptionsForSelect';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -19,6 +20,7 @@ import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotke
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { MAX_OPTIONS_TO_DISPLAY } from 'twenty-shared/constants';
@@ -76,9 +78,21 @@ export const ObjectFilterDropdownOptionSelect = ({
componentInstanceId,
);
+ const { foundFieldMetadataItem: relationTargetFieldMetadataItem } =
+ useAtomFamilySelectorValue(fieldMetadataItemByIdSelector, {
+ fieldMetadataItemId:
+ objectFilterDropdownCurrentRecordFilter?.relationTargetFieldMetadataId ??
+ '',
+ });
+
const fieldMetaDataId = fieldMetadataItemUsedInDropdown?.id ?? '';
- const { selectOptions } = useOptionsForSelect(fieldMetaDataId);
+ const { selectOptions: sourceFieldSelectOptions } =
+ useOptionsForSelect(fieldMetaDataId);
+
+ const selectOptions = isDefined(relationTargetFieldMetadataItem)
+ ? relationTargetFieldMetadataItem.options
+ : sourceFieldSelectOptions;
const [selectableOptions, setSelectableOptions] = useState<
SelectOptionForFilter[]
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__tests__/ObjectFilterDropdownOptionSelect.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__tests__/ObjectFilterDropdownOptionSelect.test.tsx
new file mode 100644
index 0000000000..b961d84910
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__tests__/ObjectFilterDropdownOptionSelect.test.tsx
@@ -0,0 +1,118 @@
+import { render } from '@testing-library/react';
+import { useEffect, useState } from 'react';
+
+import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect';
+import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
+import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
+import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
+import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
+import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
+import { FieldMetadataType, ViewFilterOperand } from 'twenty-shared/types';
+import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
+import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
+
+jest.mock(
+ '@/object-record/object-filter-dropdown/hooks/useOptionsForSelect',
+ () => ({
+ useOptionsForSelect: () => ({ selectOptions: [] }),
+ }),
+);
+
+jest.mock('@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement', () => ({
+ useHotkeysOnFocusedElement: () => undefined,
+}));
+
+jest.mock('@/ui/layout/dropdown/hooks/useCloseDropdown', () => ({
+ useCloseDropdown: () => ({ closeDropdown: jest.fn() }),
+}));
+
+jest.mock('@/ui/layout/selectable-list/hooks/useSelectableList', () => ({
+ useSelectableList: () => ({ resetSelectedItem: jest.fn() }),
+}));
+
+jest.mock(
+ '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue',
+ () => ({
+ useApplyObjectFilterDropdownFilterValue: () => ({
+ applyObjectFilterDropdownFilterValue: jest.fn(),
+ }),
+ }),
+);
+
+const INSTANCE_ID = 'object-filter-dropdown-option-select-test';
+
+const opportunity = getMockObjectMetadataItemOrThrow('opportunity');
+
+const relationField = opportunity.fields.find(
+ (field) => field.type === FieldMetadataType.RELATION,
+);
+
+const selectField = opportunity.fields.find(
+ (field) =>
+ field.type === FieldMetadataType.SELECT && (field.options?.length ?? 0) > 0,
+);
+
+if (!relationField || !selectField) {
+ throw new Error('Missing relation or select field in opportunity mock');
+}
+
+const relationToSelectFilter: RecordFilter = {
+ id: 'relation-select-filter',
+ fieldMetadataId: relationField.id,
+ relationTargetFieldMetadataId: selectField.id,
+ type: FieldMetadataType.SELECT,
+ operand: ViewFilterOperand.IS,
+ value: '',
+ displayValue: '',
+ label: `${relationField.label} → ${selectField.label}`,
+};
+
+const BaseWrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: [] });
+
+const Seed = ({ children }: { children: React.ReactNode }) => {
+ const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
+ fieldMetadataItemIdUsedInDropdownComponentState,
+ );
+ const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
+ objectFilterDropdownCurrentRecordFilterComponentState,
+ );
+
+ const [isSeeded, setIsSeeded] = useState(false);
+
+ useEffect(() => {
+ setFieldMetadataItemIdUsedInDropdown(relationField.id);
+ setObjectFilterDropdownCurrentRecordFilter(relationToSelectFilter);
+ setIsSeeded(true);
+ }, [
+ setFieldMetadataItemIdUsedInDropdown,
+ setObjectFilterDropdownCurrentRecordFilter,
+ ]);
+
+ return isSeeded ? <>{children}> : null;
+};
+
+const Wrapper = () => (
+
+
+
+
+
+
+
+);
+
+describe('ObjectFilterDropdownOptionSelect', () => {
+ it('renders the relation target field options for a relation-traversal filter', async () => {
+ const { findByText, getByText } = render();
+
+ const [firstOption, ...otherOptions] = selectField.options ?? [];
+
+ await findByText(firstOption.label);
+
+ for (const option of otherOptions) {
+ expect(getByText(option.label)).toBeInTheDocument();
+ }
+ });
+});