+ isManyToOneRelationField(field) &&
+ field.relation.targetObjectMetadata.nameSingular === 'workspaceMember',
+);
+
+const nonWorkspaceMemberRelationField = opportunity.fields.find(
+ (field) =>
+ isManyToOneRelationField(field) &&
+ field.relation.targetObjectMetadata.nameSingular !== 'workspaceMember',
+);
+
+if (!workspaceMemberRelationField || !nonWorkspaceMemberRelationField) {
+ throw new Error('Missing expected relation fields in opportunity mock');
+}
+
+const BaseWrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: [] });
+
+const CurrentRecordFiltersObserver = () => {
+ const currentRecordFilters = useAtomComponentStateValue(
+ currentRecordFiltersComponentState,
+ );
+
+ return (
+
+ {JSON.stringify(currentRecordFilters)}
+
+ );
+};
+
+const Seed = ({
+ sourceFieldMetadataId,
+ children,
+}: {
+ sourceFieldMetadataId: string;
+ children: React.ReactNode;
+}) => {
+ const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
+ fieldMetadataItemIdUsedInDropdownComponentState,
+ );
+
+ const [isSeeded, setIsSeeded] = useState(false);
+
+ useEffect(() => {
+ setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataId);
+ setIsSeeded(true);
+ }, [sourceFieldMetadataId, setFieldMetadataItemIdUsedInDropdown]);
+
+ return isSeeded ? <>{children}> : null;
+};
+
+const renderSubMenu = (sourceFieldMetadataId: string) => {
+ return render(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+};
+
+describe('AdvancedFilterRelationTargetFieldSelectMenu', () => {
+ it('shows a "filter by record" entry when the relation target is workspaceMember', async () => {
+ const { getByTestId } = renderSubMenu(workspaceMemberRelationField.id);
+
+ await waitFor(() => {
+ expect(getByTestId('select-filter-relation-record')).toBeInTheDocument();
+ });
+ });
+
+ it('does not show the "filter by record" entry for a non-workspaceMember relation', async () => {
+ const { findByText, queryByTestId } = renderSubMenu(
+ nonWorkspaceMemberRelationField.id,
+ );
+
+ await findByText(nonWorkspaceMemberRelationField.label);
+
+ expect(
+ queryByTestId('select-filter-relation-record'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('creates a direct RELATION filter (relationTargetFieldMetadataId null) when the record entry is selected', async () => {
+ const { getByTestId } = renderSubMenu(workspaceMemberRelationField.id);
+
+ const recordEntry = await waitFor(() =>
+ getByTestId('select-filter-relation-record'),
+ );
+
+ fireEvent.click(recordEntry);
+
+ await waitFor(() => {
+ const currentRecordFilters = JSON.parse(
+ getByTestId('current-record-filters').textContent || '[]',
+ );
+
+ expect(currentRecordFilters).toHaveLength(1);
+ expect(currentRecordFilters[0]).toMatchObject({
+ fieldMetadataId: workspaceMemberRelationField.id,
+ type: 'RELATION',
+ relationTargetFieldMetadataId: null,
+ });
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField.ts
index 402b24a9bd..d4f1c6b317 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField.ts
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField.ts
@@ -19,11 +19,6 @@ type ApplyAdvancedFilterSourceFieldParams = {
recordFilterId: string;
};
-// Creates a leaf advanced filter from a source field selection. Composite
-// and many-to-one relation sources are handled by their own specialized
-// hooks (useApplyAdvancedFilterCompositeSubField,
-// useApplyAdvancedFilterRelationTargetField) — callers branch to those
-// before reaching this hook.
export const useApplyAdvancedFilterSourceField = () => {
const setSelectedOperandInDropdown = useSetAtomComponentState(
selectedOperandInDropdownComponentState,
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 459d8e4c32..20baac982c 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
@@ -13,11 +13,13 @@ import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-type
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormRelativeDatePicker } from '@/object-record/record-field/ui/form-types/components/FormRelativeDatePicker';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
+import { FormWorkspaceMemberFilterValueInput } from '@/object-record/record-field/ui/form-types/components/FormWorkspaceMemberFilterValueInput';
import {
type FieldMetadata,
type FieldMultiSelectMetadata,
type FieldSelectMetadata,
} from '@/object-record/record-field/ui/types/FieldMetadata';
+import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
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';
@@ -27,7 +29,7 @@ import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/str
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
import { isObject, isString } from '@sniptt/guards';
import { useContext } from 'react';
-import { FieldMetadataType } from 'twenty-shared/types';
+import { CoreObjectNameSingular, FieldMetadataType } from 'twenty-shared/types';
import { isDefined, type RelativeDateFilter } from 'twenty-shared/utils';
import { parseBooleanFromStringValue } from 'twenty-shared/workflow';
import { type JsonValue } from 'type-fest';
@@ -215,6 +217,25 @@ export const AdvancedFilterSidePanelValueFormInput = ({
metadata: fieldDefinition?.metadata as FieldMetadata,
};
+ const isDirectWorkspaceMemberRelationFilter =
+ !isDefined(recordFilter.relationTargetFieldMetadataId) &&
+ isFieldRelationManyToOne(field) &&
+ field.metadata.relationObjectMetadataNameSingular ===
+ CoreObjectNameSingular.WorkspaceMember;
+
+ if (isDirectWorkspaceMemberRelationFilter) {
+ return (
+
+ );
+ }
+
const shouldUseUTCTimeZone = isWorkflowFindRecords === true;
const timeZone = shouldUseUTCTimeZone ? WORKFLOW_TIMEZONE : undefined;
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
index 8714174662..a2ab1fcda1 100644
--- 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
@@ -1,6 +1,7 @@
import { render, waitFor } from '@testing-library/react';
import { useEffect, useState } from 'react';
+import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
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';
@@ -15,7 +16,7 @@ import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectM
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
jest.mock('@/object-record/record-field/ui/components/FormFieldInput', () => ({
- FormFieldInput: () => null,
+ FormFieldInput: () => ,
}));
jest.mock(
@@ -29,6 +30,15 @@ jest.mock(
}),
);
+jest.mock(
+ '@/object-record/record-field/ui/form-types/components/FormWorkspaceMemberFilterValueInput',
+ () => ({
+ FormWorkspaceMemberFilterValueInput: () => (
+
+ ),
+ }),
+);
+
const INSTANCE_ID = 'advanced-filter-value-test';
const FILTER_ID = 'relation-select-filter';
@@ -43,8 +53,25 @@ const selectField = opportunity.fields.find(
field.type === FieldMetadataType.SELECT && (field.options?.length ?? 0) > 0,
);
-if (!relationField || !selectField) {
- throw new Error('Missing relation or select field in opportunity mock');
+const workspaceMemberRelationField = opportunity.fields.find(
+ (field) =>
+ isManyToOneRelationField(field) &&
+ field.relation.targetObjectMetadata.nameSingular === 'workspaceMember',
+);
+
+const nonWorkspaceMemberRelationField = opportunity.fields.find(
+ (field) =>
+ isManyToOneRelationField(field) &&
+ field.relation.targetObjectMetadata.nameSingular !== 'workspaceMember',
+);
+
+if (
+ !relationField ||
+ !selectField ||
+ !workspaceMemberRelationField ||
+ !nonWorkspaceMemberRelationField
+) {
+ throw new Error('Missing expected fields in opportunity mock');
}
const relationToSelectFilter: RecordFilter = {
@@ -58,9 +85,39 @@ const relationToSelectFilter: RecordFilter = {
label: `${relationField.label} → ${selectField.label}`,
};
+const workspaceMemberRelationFilter: RecordFilter = {
+ id: FILTER_ID,
+ fieldMetadataId: workspaceMemberRelationField.id,
+ relationTargetFieldMetadataId: null,
+ type: FieldMetadataType.RELATION,
+ operand: ViewFilterOperand.IS,
+ value: '',
+ displayValue: '',
+ label: workspaceMemberRelationField.label,
+};
+
+const nonWorkspaceMemberRelationFilter: RecordFilter = {
+ id: FILTER_ID,
+ fieldMetadataId: nonWorkspaceMemberRelationField.id,
+ relationTargetFieldMetadataId: null,
+ type: FieldMetadataType.RELATION,
+ operand: ViewFilterOperand.IS,
+ value: '',
+ displayValue: '',
+ label: nonWorkspaceMemberRelationField.label,
+};
+
const BaseWrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: [] });
-const Seed = ({ children }: { children: React.ReactNode }) => {
+const Seed = ({
+ recordFilter,
+ dropdownFieldMetadataId,
+ children,
+}: {
+ recordFilter: RecordFilter;
+ dropdownFieldMetadataId: string;
+ children: React.ReactNode;
+}) => {
const setCurrentRecordFilters = useSetAtomComponentState(
currentRecordFiltersComponentState,
);
@@ -72,42 +129,66 @@ const Seed = ({ children }: { children: React.ReactNode }) => {
const [isSeeded, setIsSeeded] = useState(false);
useEffect(() => {
- setCurrentRecordFilters([relationToSelectFilter]);
- setFieldMetadataItemIdUsedInDropdown(relationField.id);
+ setCurrentRecordFilters([recordFilter]);
+ setFieldMetadataItemIdUsedInDropdown(dropdownFieldMetadataId);
setIsSeeded(true);
- }, [setCurrentRecordFilters, setFieldMetadataItemIdUsedInDropdown]);
+ }, [
+ recordFilter,
+ dropdownFieldMetadataId,
+ setCurrentRecordFilters,
+ setFieldMetadataItemIdUsedInDropdown,
+ ]);
return isSeeded ? <>{children}> : null;
};
-const Wrapper = () => (
-
-
- {
+ return render(
+
+
-
-
-
-
-
-
-
-
-);
+
+
+
+
+
+
+
+ ,
+ );
+};
describe('AdvancedFilterSidePanelValueFormInput', () => {
it('resolves select options from the relation target field for a relation-traversal filter', async () => {
- const { getByTestId } = render();
+ const { getByTestId } = renderValueInput({
+ recordFilter: relationToSelectFilter,
+ dropdownFieldMetadataId: relationField.id,
+ });
await waitFor(() => {
expect(getByTestId('multiselect-options-count').textContent).toBe(
@@ -115,4 +196,29 @@ describe('AdvancedFilterSidePanelValueFormInput', () => {
);
});
});
+
+ it('renders the workspace member "Me" picker for a direct relation-to-workspaceMember filter', async () => {
+ const { getByTestId } = renderValueInput({
+ recordFilter: workspaceMemberRelationFilter,
+ dropdownFieldMetadataId: workspaceMemberRelationField.id,
+ });
+
+ await waitFor(() => {
+ expect(getByTestId('workspace-member-filter-input')).toBeInTheDocument();
+ });
+ });
+
+ it('falls through to the generic field input for a direct relation to a non-workspaceMember object', async () => {
+ const { getByTestId, queryByTestId } = renderValueInput({
+ recordFilter: nonWorkspaceMemberRelationFilter,
+ dropdownFieldMetadataId: nonWorkspaceMemberRelationField.id,
+ });
+
+ await waitFor(() => {
+ expect(getByTestId('form-field-input')).toBeInTheDocument();
+ });
+ expect(
+ queryByTestId('workspace-member-filter-input'),
+ ).not.toBeInTheDocument();
+ });
});