diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
index 16a51164a88f..0c884b2035a0 100644
--- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
+++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
@@ -171,15 +171,10 @@ export const ActivityTargetInlineCellEditMode = ({
});
};
- const handleCancel = () => {
- closeEditableField();
- };
-
return (
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts
index 3d06b28fa1f6..0c1cc9a3dae1 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts
@@ -37,6 +37,8 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
relationObjectMetadataNamePlural:
relationObjectMetadataItem?.namePlural ?? '',
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
+ targetFieldMetadataName:
+ field.relationDefinition?.targetFieldMetadata?.name ?? '',
options: field.options,
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
index 42ae3153d060..ce1240e6449c 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
@@ -6,6 +6,7 @@ import { FullNameFieldInput } from '@/object-record/record-field/meta-types/inpu
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
+import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
@@ -14,6 +15,7 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
+import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
@@ -71,7 +73,15 @@ export const FieldInput = ({
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
>
{isFieldRelation(fieldDefinition) ? (
-
+ isFieldRelationFromManyObjects(fieldDefinition) ? (
+
+ ) : (
+
+ )
) : isFieldPhone(fieldDefinition) ||
isFieldDisplayedAsPhone(fieldDefinition) ? (
{
isFieldRelation(fieldDefinition) &&
isFieldRelationValue(valueToPersist);
+ const fieldIsRelationFromManyObjects =
+ isFieldRelationFromManyObjects(
+ fieldDefinition as FieldDefinition,
+ ) && isFieldRelationValue(valueToPersist);
+
const fieldIsText =
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
@@ -111,7 +120,7 @@ export const usePersistField = () => {
isFieldRawJsonValue(valueToPersist);
const isValuePersistable =
- fieldIsRelation ||
+ (fieldIsRelation && !fieldIsRelationFromManyObjects) ||
fieldIsText ||
fieldIsBoolean ||
fieldIsEmail ||
@@ -136,13 +145,14 @@ export const usePersistField = () => {
valueToPersist,
);
- if (fieldIsRelation) {
+ if (fieldIsRelation && !fieldIsRelationFromManyObjects) {
+ const value = valueToPersist as EntityForSelect;
updateRecord?.({
variables: {
where: { id: entityId },
updateOneRecordInput: {
- [fieldName]: valueToPersist,
- [`${fieldName}Id`]: valueToPersist?.id ?? null,
+ [fieldName]: value,
+ [`${fieldName}Id`]: value?.id ?? null,
},
},
});
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx
index 87c14b583862..dace0123781c 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx
@@ -1,6 +1,10 @@
+import { isArray } from '@sniptt/guards';
import { EntityChip } from 'twenty-ui';
+import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
+import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
export const RelationFieldDisplay = () => {
@@ -14,6 +18,12 @@ export const RelationFieldDisplay = () => {
return null;
}
+ if (isArray(fieldValue) && isFieldRelationFromManyObjects(fieldDefinition)) {
+ return (
+
+ );
+ }
+
const recordChipData = generateRecordChipData(fieldValue);
return (
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
new file mode 100644
index 000000000000..d498faaa2966
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
@@ -0,0 +1,37 @@
+import { EntityChip } from 'twenty-ui';
+
+import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
+import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
+import { ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
+import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
+
+export const RelationFromManyFieldDisplay = ({
+ fieldValue,
+}: {
+ fieldValue: ObjectRecord[];
+}) => {
+ const { isFocused } = useFieldFocus();
+ const { generateRecordChipData } = useRelationFieldDisplay();
+
+ const recordChipsData = fieldValue.map((fieldValueItem) =>
+ generateRecordChipData(fieldValueItem),
+ );
+
+ return (
+
+ {recordChipsData.map((record) => {
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx
new file mode 100644
index 000000000000..360abcf4c3b0
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx
@@ -0,0 +1,94 @@
+import { useEffect } from 'react';
+import { Meta, StoryObj } from '@storybook/react';
+import { useSetRecoilState } from 'recoil';
+import { ComponentDecorator } from 'twenty-ui';
+
+import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
+import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
+import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
+import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
+import {
+ RecordFieldValueSelectorContextProvider,
+ useSetRecordValue,
+} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
+import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
+
+import {
+ fieldValue,
+ relationFromManyFieldDisplayMock,
+} from './relationFromManyFieldDisplayMock';
+
+const RelationFieldValueSetterEffect = () => {
+ const setEntity = useSetRecoilState(
+ recordStoreFamilyState(relationFromManyFieldDisplayMock.entityId),
+ );
+
+ const setRelationEntity = useSetRecoilState(
+ recordStoreFamilyState(relationFromManyFieldDisplayMock.relationEntityId),
+ );
+
+ const setRecordValue = useSetRecordValue();
+
+ useEffect(() => {
+ setEntity(relationFromManyFieldDisplayMock.entityValue);
+ setRelationEntity(relationFromManyFieldDisplayMock.relationFieldValue);
+
+ setRecordValue(
+ relationFromManyFieldDisplayMock.entityValue.id,
+ relationFromManyFieldDisplayMock.entityValue,
+ );
+ setRecordValue(
+ relationFromManyFieldDisplayMock.relationFieldValue.id,
+ relationFromManyFieldDisplayMock.relationFieldValue,
+ );
+ }, [setEntity, setRelationEntity, setRecordValue]);
+
+ return null;
+};
+
+const meta: Meta = {
+ title: 'UI/Data/Field/Display/RelationFromManyFieldDisplay',
+ decorators: [
+ MemoryRouterDecorator,
+ (Story) => (
+
+ ,
+ hotkeyScope: 'hotkey-scope',
+ }}
+ >
+
+
+
+
+ ),
+ ComponentDecorator,
+ ],
+ component: RelationFromManyFieldDisplay,
+ argTypes: { value: { control: 'date' } },
+ args: { fieldValue: fieldValue },
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Performance = getProfilingStory({
+ componentName: 'RelationFromManyFieldDisplay',
+ averageThresholdInMs: 0.5,
+ numberOfRuns: 20,
+ numberOfTestsPerRun: 100,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/relationFromManyFieldDisplayMock.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/relationFromManyFieldDisplayMock.ts
new file mode 100644
index 000000000000..cbc63cb40be5
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/relationFromManyFieldDisplayMock.ts
@@ -0,0 +1,172 @@
+import { FieldMetadataType } from '~/generated-metadata/graphql';
+
+export const fieldValue = [
+ {
+ __typename: 'Company',
+ domainName: 'google.com',
+ xLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ name: 'Google',
+ annualRecurringRevenue: {
+ __typename: 'Currency',
+ amountMicros: null,
+ currencyCode: '',
+ },
+ employees: null,
+ accountOwnerId: null,
+ address: '',
+ idealCustomerProfile: false,
+ createdAt: '2024-05-01T13:16:29.046Z',
+ id: '20202020-c21e-4ec2-873b-de4264d89025',
+ position: 6,
+ updatedAt: '2024-05-01T13:16:29.046Z',
+ linkedinLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ },
+ {
+ __typename: 'Company',
+ domainName: 'airbnb.com',
+ xLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ name: 'Airbnb',
+ annualRecurringRevenue: {
+ __typename: 'Currency',
+ amountMicros: null,
+ currencyCode: '',
+ },
+ employees: null,
+ accountOwnerId: null,
+ address: '',
+ idealCustomerProfile: false,
+ createdAt: '2024-05-01T13:16:29.046Z',
+ id: '20202020-171e-4bcc-9cf7-43448d6fb278',
+ position: 6,
+ updatedAt: '2024-05-01T13:16:29.046Z',
+ linkedinLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ },
+];
+
+export const relationFromManyFieldDisplayMock = {
+ entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
+ relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
+ entityValue: {
+ __typename: 'Person',
+ asd: '',
+ city: 'Seattle',
+ jobTitle: '',
+ name: {
+ __typename: 'FullName',
+ firstName: 'Lorie',
+ lastName: 'Vladim',
+ },
+ createdAt: '2024-05-01T13:16:29.046Z',
+ company: {
+ __typename: 'Company',
+ domainName: 'google.com',
+ xLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ name: 'Google',
+ annualRecurringRevenue: {
+ __typename: 'Currency',
+ amountMicros: null,
+ currencyCode: '',
+ },
+ employees: null,
+ accountOwnerId: null,
+ address: '',
+ idealCustomerProfile: false,
+ createdAt: '2024-05-01T13:16:29.046Z',
+ id: '20202020-c21e-4ec2-873b-de4264d89025',
+ position: 6,
+ updatedAt: '2024-05-01T13:16:29.046Z',
+ linkedinLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ },
+ id: '20202020-2d40-4e49-8df4-9c6a049191df',
+ email: 'lorie.vladim@google.com',
+ phone: '+33788901235',
+ linkedinLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ xLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ tEst: '',
+ position: 15,
+ },
+ relationFieldValue: {
+ __typename: 'Company',
+ domainName: 'microsoft.com',
+ xLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ name: 'Microsoft',
+ annualRecurringRevenue: {
+ __typename: 'Currency',
+ amountMicros: null,
+ currencyCode: '',
+ },
+ employees: null,
+ accountOwnerId: null,
+ address: '',
+ idealCustomerProfile: false,
+ createdAt: '2024-05-01T13:16:29.046Z',
+ id: '20202020-ed89-413a-b31a-962986e67bb4',
+ position: 4,
+ updatedAt: '2024-05-01T13:16:29.046Z',
+ linkedinLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ },
+ fieldDefinition: {
+ fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059',
+ label: 'Company',
+ metadata: {
+ fieldName: 'company',
+ placeHolder: 'Company',
+ relationType: 'FROM_MANY_OBJECTS',
+ relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25',
+ relationObjectMetadataNameSingular: 'company',
+ relationObjectMetadataNamePlural: 'companies',
+ objectMetadataNameSingular: 'person',
+ options: null,
+ },
+ type: FieldMetadataType.Relation,
+ iconName: 'IconUsers',
+ defaultValue: null,
+ editButtonIcon: {
+ propTypes: {},
+ },
+ position: 3,
+ size: 100,
+ isLabelIdentifier: false,
+ isVisible: true,
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts
index f61b22df666d..980f4c050205 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts
@@ -5,14 +5,16 @@ import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButto
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { FieldRelationValue } from '@/object-record/record-field/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
+import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldRelation } from '../../types/guards/isFieldRelation';
-// TODO: we will be able to type more precisely when we will have custom field and custom entities support
-export const useRelationField = () => {
+export const useRelationField = <
+ T extends EntityForSelect | EntityForSelect[],
+>() => {
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
const button = useGetButtonIcon();
@@ -24,11 +26,11 @@ export const useRelationField = () => {
const fieldName = fieldDefinition.metadata.fieldName;
- const [fieldValue, setFieldValue] = useRecoilState(
+ const [fieldValue, setFieldValue] = useRecoilState>(
recordStoreFamilySelector({ recordId: entityId, fieldName }),
);
- const { getDraftValueSelector } = useRecordFieldInput(
+ const { getDraftValueSelector } = useRecordFieldInput>(
`${entityId}-${fieldName}`,
);
const draftValue = useRecoilValue(getDraftValueSelector());
@@ -41,5 +43,6 @@ export const useRelationField = () => {
initialSearchValue,
setFieldValue,
maxWidth: button && maxWidth ? maxWidth - 28 : maxWidth,
+ entityId,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx
index ec80fd26c1df..ed752bef9130 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx
@@ -24,7 +24,7 @@ export const RelationFieldInput = ({
onCancel,
}: RelationFieldInputProps) => {
const { fieldDefinition, initialSearchValue, fieldValue } =
- useRelationField();
+ useRelationField();
const persistField = usePersistField();
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationManyFieldInput.tsx
new file mode 100644
index 000000000000..9e3629528463
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationManyFieldInput.tsx
@@ -0,0 +1,82 @@
+import { useMemo } from 'react';
+
+import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { useUpdateRelationManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput';
+import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
+import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
+import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
+import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
+import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
+
+import { useRelationField } from '../../hooks/useRelationField';
+
+export const RelationManyFieldInput = ({
+ relationPickerScopeId = 'relation-picker',
+}: {
+ relationPickerScopeId?: string;
+}) => {
+ const { closeInlineCell: closeEditableField } = useInlineCell();
+
+ const { fieldDefinition, fieldValue } = useRelationField();
+ const { entities, relationPickerSearchFilter } =
+ useRelationPickerEntitiesOptions({
+ relationObjectNameSingular:
+ fieldDefinition.metadata.relationObjectMetadataNameSingular,
+ relationPickerScopeId,
+ });
+
+ const { setRelationPickerSearchFilter } = useRelationPicker({
+ relationPickerScopeId,
+ });
+
+ const { handleChange } = useUpdateRelationManyFieldInput({ entities });
+
+ const { objectMetadataItem } = useObjectMetadataItem({
+ objectNameSingular:
+ fieldDefinition.metadata.relationObjectMetadataNameSingular,
+ });
+ const allRecords = useMemo(
+ () => [
+ ...entities.entitiesToSelect.map((entity) => {
+ const { record, ...recordIdentifier } = entity;
+ return {
+ objectMetadataItem: objectMetadataItem,
+ record: record,
+ recordIdentifier: recordIdentifier,
+ };
+ }),
+ ],
+ [entities.entitiesToSelect, objectMetadataItem],
+ );
+
+ const selectedRecords = useMemo(
+ () =>
+ allRecords.filter(
+ (entity) =>
+ fieldValue?.some((f) => {
+ return f.id === entity.recordIdentifier.id;
+ }),
+ ),
+ [allRecords, fieldValue],
+ );
+
+ return (
+ <>
+
+ {
+ closeEditableField();
+ }}
+ onChange={handleChange}
+ />
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx
new file mode 100644
index 000000000000..b768056cb87f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx
@@ -0,0 +1,86 @@
+import { useEffect } from 'react';
+import { Meta, StoryObj } from '@storybook/react';
+import { useSetRecoilState } from 'recoil';
+
+import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
+import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+import { FieldMetadataType } from '~/generated/graphql';
+import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
+import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
+import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import {
+ mockDefaultWorkspace,
+ mockedWorkspaceMemberData,
+} from '~/testing/mock-data/users';
+
+import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
+
+const RelationWorkspaceSetterEffect = () => {
+ const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
+ const setCurrentWorkspaceMember = useSetRecoilState(
+ currentWorkspaceMemberState,
+ );
+
+ useEffect(() => {
+ setCurrentWorkspace(mockDefaultWorkspace);
+ setCurrentWorkspaceMember(mockedWorkspaceMemberData);
+ }, [setCurrentWorkspace, setCurrentWorkspaceMember]);
+
+ return <>>;
+};
+
+const RelationManyFieldInputWithContext = () => {
+ const setHotKeyScope = useSetHotkeyScope();
+
+ useEffect(() => {
+ setHotKeyScope('hotkey-scope');
+ }, [setHotKeyScope]);
+
+ return (
+
+ );
+};
+const meta: Meta = {
+ title: 'UI/Data/Field/Input/RelationManyFieldInput',
+ component: RelationManyFieldInputWithContext,
+ args: {},
+ decorators: [ObjectMetadataItemsDecorator, SnackBarDecorator],
+ parameters: {
+ clearMocks: true,
+ msw: graphqlMocks,
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ decorators: [ComponentWithRecoilScopeDecorator],
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput.tsx
new file mode 100644
index 000000000000..a0d274418af5
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput.tsx
@@ -0,0 +1,52 @@
+import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
+import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
+import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
+import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
+import { isDefined } from '~/utils/isDefined';
+
+export const useUpdateRelationManyFieldInput = ({
+ entities,
+}: {
+ entities: EntitiesForMultipleEntitySelect;
+}) => {
+ const { fieldDefinition, fieldValue, setFieldValue, entityId } =
+ useRelationField();
+
+ const { updateOneRecord } = useUpdateOneRecord({
+ objectNameSingular:
+ fieldDefinition.metadata.relationObjectMetadataNameSingular,
+ });
+
+ const fieldName = fieldDefinition.metadata.targetFieldMetadataName;
+
+ const handleChange = (
+ objectRecord: ObjectRecordForSelect | null,
+ isSelected: boolean,
+ ) => {
+ const entityToAddOrRemove = entities.entitiesToSelect.find(
+ (entity) => entity.id === objectRecord?.recordIdentifier.id,
+ );
+
+ const updatedFieldValue = isSelected
+ ? [...(fieldValue ?? []), entityToAddOrRemove]
+ : (fieldValue ?? []).filter(
+ (value) => value.id !== objectRecord?.recordIdentifier.id,
+ );
+ setFieldValue(
+ updatedFieldValue.filter((value) =>
+ isDefined(value),
+ ) as EntityForSelect[],
+ );
+ if (isDefined(objectRecord)) {
+ updateOneRecord({
+ idToUpdate: objectRecord.record?.id,
+ updateOneRecordInput: {
+ [`${fieldName}Id`]: isSelected ? entityId : null,
+ },
+ });
+ }
+ };
+
+ return { handleChange };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts
index 76a232a142b2..0282ccd02f86 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts
@@ -18,6 +18,7 @@ import {
FieldTextValue,
FieldUUidValue,
} from '@/object-record/record-field/types/FieldMetadata';
+import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
export type FieldTextDraftValue = string;
export type FieldNumberDraftValue = string;
@@ -78,7 +79,9 @@ export type FieldInputDraftValue = FieldValue extends FieldTextValue
? FieldSelectDraftValue
: FieldValue extends FieldMultiSelectValue
? FieldMultiSelectDraftValue
- : FieldValue extends FieldRelationValue
+ : FieldValue extends
+ | FieldRelationValue
+ | FieldRelationValue
? FieldRelationDraftValue
: FieldValue extends FieldAddressValue
? FieldAddressDraftValue
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
index 8a287bd6af1a..f54794613652 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
@@ -106,6 +106,7 @@ export type FieldRelationMetadata = {
relationObjectMetadataNamePlural: string;
relationObjectMetadataNameSingular: string;
relationType?: FieldDefinitionRelationType;
+ targetFieldMetadataName?: string;
useEditButton?: boolean;
};
@@ -173,7 +174,8 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number];
export type FieldSelectValue = string | null;
export type FieldMultiSelectValue = string[] | null;
-export type FieldRelationValue = EntityForSelect | null;
+export type FieldRelationValue =
+ T | null;
// See https://zod.dev/?id=json-type
type Literal = string | number | boolean | null;
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts
new file mode 100644
index 000000000000..03b56892afd6
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts
@@ -0,0 +1,10 @@
+import { FieldMetadataType } from '~/generated-metadata/graphql';
+
+import { FieldDefinition } from '../FieldDefinition';
+import { FieldRelationMetadata } from '../FieldMetadata';
+
+export const isFieldRelationFromManyObjects = (
+ field: Pick, 'type' | 'metadata'>,
+): field is FieldDefinition =>
+ field.type === FieldMetadataType.Relation &&
+ field.metadata.relationType === 'FROM_MANY_OBJECTS';
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts
index 879437f9164c..1919c493e327 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts
@@ -1,9 +1,13 @@
import { isNull, isObject, isUndefined } from '@sniptt/guards';
+import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
+
import { FieldRelationValue } from '../FieldMetadata';
// TODO: add zod
-export const isFieldRelationValue = (
+export const isFieldRelationValue = <
+ T extends EntityForSelect | EntityForSelect[],
+>(
fieldValue: unknown,
-): fieldValue is FieldRelationValue =>
+): fieldValue is FieldRelationValue =>
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx
new file mode 100644
index 000000000000..049176e6cb08
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx
@@ -0,0 +1,163 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
+import { useDebouncedCallback } from 'use-debounce';
+
+import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
+import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
+import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
+import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
+import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
+import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
+import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
+import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
+import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
+import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import { isDefined } from '~/utils/isDefined';
+
+export const StyledSelectableItem = styled(SelectableItem)`
+ height: 100%;
+ width: 100%;
+`;
+export const MultiRecordSelect = ({
+ onChange,
+ onSubmit,
+ selectedObjectRecords,
+ allRecords,
+ loading,
+ searchFilter,
+ setSearchFilter,
+}: {
+ onChange?: (
+ changedRecordForSelect: ObjectRecordForSelect,
+ newSelectedValue: boolean,
+ ) => void;
+ onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
+ selectedObjectRecords: ObjectRecordForSelect[];
+ allRecords: ObjectRecordForSelect[];
+ loading: boolean;
+ searchFilter: string;
+ setSearchFilter: (searchFilter: string) => void;
+}) => {
+ const containerRef = useRef(null);
+
+ const [internalSelectedRecords, setInternalSelectedRecords] = useState<
+ ObjectRecordForSelect[]
+ >([]);
+
+ useEffect(() => {
+ if (!loading) {
+ setInternalSelectedRecords(selectedObjectRecords);
+ }
+ }, [selectedObjectRecords, loading]);
+
+ const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
+ leading: true,
+ });
+
+ const handleFilterChange = (event: React.ChangeEvent) => {
+ debouncedSetSearchFilter(event.currentTarget.value);
+ };
+
+ const handleSelectChange = (
+ changedRecordForSelect: ObjectRecordForSelect,
+ newSelectedValue: boolean,
+ ) => {
+ const newSelectedRecords = newSelectedValue
+ ? [...internalSelectedRecords, changedRecordForSelect]
+ : internalSelectedRecords.filter(
+ (selectedRecord) =>
+ selectedRecord.record.id !== changedRecordForSelect.record.id,
+ );
+
+ setInternalSelectedRecords(newSelectedRecords);
+
+ onChange?.(changedRecordForSelect, newSelectedValue);
+ };
+
+ const entitiesInDropdown = useMemo(
+ () =>
+ [...(allRecords ?? [])].filter((entity) =>
+ isNonEmptyString(entity.recordIdentifier.id),
+ ),
+ [allRecords],
+ );
+
+ const selectableItemIds = entitiesInDropdown.map(
+ (entity) => entity.record.id,
+ );
+
+ return (
+ <>
+ {
+ onSubmit?.(internalSelectedRecords);
+ }}
+ />
+
+
+
+
+ {loading ? (
+
+ ) : (
+ <>
+ {
+ const recordIsSelected = internalSelectedRecords?.some(
+ (selectedRecord) => selectedRecord.record.id === recordId,
+ );
+
+ const correspondingRecordForSelect = entitiesInDropdown?.find(
+ (entity) => entity.record.id === recordId,
+ );
+
+ if (isDefined(correspondingRecordForSelect)) {
+ handleSelectChange(
+ correspondingRecordForSelect,
+ !recordIsSelected,
+ );
+ }
+ }}
+ >
+ {entitiesInDropdown?.map((objectRecordForSelect) => (
+
+ handleSelectChange(
+ objectRecordForSelect,
+ newSelectedValue,
+ )
+ }
+ selected={internalSelectedRecords?.some(
+ (selectedRecord) => {
+ return (
+ selectedRecord.record.id ===
+ objectRecordForSelect.record.id
+ );
+ },
+ )}
+ />
+ ))}
+
+ {entitiesInDropdown?.length === 0 && (
+
+ )}
+ >
+ )}
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx
index 44932a8ea774..659461a30d57 100644
--- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx
@@ -1,37 +1,18 @@
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useMemo, useState } from 'react';
import styled from '@emotion/styled';
-import { isNonEmptyString } from '@sniptt/guards';
-import { useDebouncedCallback } from 'use-debounce';
-import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
-import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
-import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
+import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
import {
ObjectRecordForSelect,
SelectedObjectRecordId,
useMultiObjectSearch,
} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
-import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
-import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
-import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
-import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
-import { isDefined } from '~/utils/isDefined';
export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
width: 100%;
`;
-
-export type EntitiesForMultipleObjectRecordSelect = {
- filteredSelectedObjectRecords: ObjectRecordForSelect[];
- objectRecordsToSelect: ObjectRecordForSelect[];
- loading: boolean;
-};
-
export const MultipleObjectRecordSelect = ({
onChange,
onSubmit,
@@ -41,12 +22,9 @@ export const MultipleObjectRecordSelect = ({
changedRecordForSelect: ObjectRecordForSelect,
newSelectedValue: boolean,
) => void;
- onCancel?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
selectedObjectRecordIds: SelectedObjectRecordId[];
}) => {
- const containerRef = useRef(null);
-
const [searchFilter, setSearchFilter] = useState('');
const {
@@ -73,122 +51,18 @@ export const MultipleObjectRecordSelect = ({
[selectedObjectRecords, selectedObjectRecordIds],
);
- const [internalSelectedRecords, setInternalSelectedRecords] = useState<
- ObjectRecordForSelect[]
- >([]);
-
- useEffect(() => {
- if (!loading) {
- setInternalSelectedRecords(selectedObjectRecordsForSelect);
- }
- }, [selectedObjectRecordsForSelect, loading]);
-
- const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
- leading: true,
- });
-
- const handleFilterChange = (event: React.ChangeEvent) => {
- debouncedSetSearchFilter(event.currentTarget.value);
- };
-
- const handleSelectChange = (
- changedRecordForSelect: ObjectRecordForSelect,
- newSelectedValue: boolean,
- ) => {
- const newSelectedRecords = newSelectedValue
- ? [...internalSelectedRecords, changedRecordForSelect]
- : internalSelectedRecords.filter(
- (selectedRecord) =>
- selectedRecord.record.id !== changedRecordForSelect.record.id,
- );
-
- setInternalSelectedRecords(newSelectedRecords);
-
- onChange?.(changedRecordForSelect, newSelectedValue);
- };
-
- const entitiesInDropdown = useMemo(
- () =>
- [
+ return (
+ isNonEmptyString(entity.recordIdentifier.id)),
- [filteredSelectedObjectRecords, objectRecordsToSelect],
- );
-
- const selectableItemIds = entitiesInDropdown.map(
- (entity) => entity.record.id,
- );
-
- return (
- <>
- {
- onSubmit?.(internalSelectedRecords);
- }}
- />
-
-
-
-
- {loading ? (
-
- ) : (
- <>
- {
- const recordIsSelected = internalSelectedRecords?.some(
- (selectedRecord) => selectedRecord.record.id === recordId,
- );
-
- const correspondingRecordForSelect = entitiesInDropdown?.find(
- (entity) => entity.record.id === recordId,
- );
-
- if (isDefined(correspondingRecordForSelect)) {
- handleSelectChange(
- correspondingRecordForSelect,
- !recordIsSelected,
- );
- }
- }}
- >
- {entitiesInDropdown?.map((objectRecordForSelect) => (
-
- handleSelectChange(
- objectRecordForSelect,
- newSelectedValue,
- )
- }
- selected={internalSelectedRecords?.some(
- (selectedRecord) => {
- return (
- selectedRecord.record.id ===
- objectRecordForSelect.record.id
- );
- },
- )}
- />
- ))}
-
- {entitiesInDropdown?.length === 0 && (
-
- )}
- >
- )}
-
-
- >
+ ]}
+ loading={loading}
+ searchFilter={searchFilter}
+ setSearchFilter={setSearchFilter}
+ />
);
};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx
index 09752cef8b5e..70c777c7bfd3 100644
--- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx
@@ -1,12 +1,9 @@
-import { useRecoilValue } from 'recoil';
-
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
import {
SingleEntitySelectMenuItems,
SingleEntitySelectMenuItemsProps,
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
-import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
-import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
+import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { isDefined } from '~/utils/isDefined';
@@ -44,32 +41,16 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
relationPickerScopeId,
});
- const { searchQueryState, relationPickerSearchFilterState } =
- useRelationPickerScopedStates({
- relationPickerScopedId: relationPickerScopeId,
+ const { entities, relationPickerSearchFilter } =
+ useRelationPickerEntitiesOptions({
+ relationObjectNameSingular,
+ relationPickerScopeId,
+ selectedRelationRecordIds,
+ excludedRelationRecordIds,
});
- const searchQuery = useRecoilValue(searchQueryState);
- const relationPickerSearchFilter = useRecoilValue(
- relationPickerSearchFilterState,
- );
-
const showCreateButton = isDefined(onCreate);
- const entities = useFilteredSearchEntityQuery({
- filters: [
- {
- fieldNames:
- searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
- filter: relationPickerSearchFilter,
- },
- ],
- orderByField: 'createdAt',
- selectedIds: selectedRelationRecordIds,
- excludeEntityIds: excludedRelationRecordIds,
- objectNameSingular: relationObjectNameSingular,
- });
-
let onCreateWithInput = undefined;
if (isDefined(onCreate)) {
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts
index b327099bb099..3e74f0edafbf 100644
--- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts
@@ -1,4 +1,4 @@
-import { useRecoilState, useSetRecoilState } from 'recoil';
+import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
@@ -28,6 +28,10 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
relationPickerSearchFilterState,
);
+ const relationPickerSearchFilter = useRecoilValue(
+ relationPickerSearchFilterState,
+ );
+
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
useRecoilState(relationPickerPreselectedIdState);
@@ -37,5 +41,6 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
setRelationPickerSearchFilter,
relationPickerPreselectedId,
setRelationPickerPreselectedId,
+ relationPickerSearchFilter,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions.ts
new file mode 100644
index 000000000000..9eb5f990c622
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions.ts
@@ -0,0 +1,41 @@
+import { useRecoilValue } from 'recoil';
+
+import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
+import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
+
+export const useRelationPickerEntitiesOptions = ({
+ relationObjectNameSingular,
+ relationPickerScopeId = 'relation-picker',
+ selectedRelationRecordIds = [],
+ excludedRelationRecordIds = [],
+}: {
+ relationObjectNameSingular: string;
+ relationPickerScopeId?: string;
+ selectedRelationRecordIds?: string[];
+ excludedRelationRecordIds?: string[];
+}) => {
+ const { searchQueryState, relationPickerSearchFilterState } =
+ useRelationPickerScopedStates({
+ relationPickerScopedId: relationPickerScopeId,
+ });
+ const relationPickerSearchFilter = useRecoilValue(
+ relationPickerSearchFilterState,
+ );
+
+ const searchQuery = useRecoilValue(searchQueryState);
+ const entities = useFilteredSearchEntityQuery({
+ filters: [
+ {
+ fieldNames:
+ searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
+ filter: relationPickerSearchFilter,
+ },
+ ],
+ orderByField: 'createdAt',
+ selectedIds: selectedRelationRecordIds,
+ excludeEntityIds: excludedRelationRecordIds,
+ objectNameSingular: relationObjectNameSingular,
+ });
+
+ return { entities, relationPickerSearchFilter };
+};
diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts
index 57c0d193c2cf..fce402359687 100644
--- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts
+++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts
@@ -3,6 +3,7 @@ import { isString } from '@sniptt/guards';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
+import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
@@ -30,7 +31,7 @@ export const sanitizeRecordInput = ({
if (
fieldMetadataItem.type === FieldMetadataType.Relation &&
- isFieldRelationValue(fieldValue)
+ isFieldRelationValue(fieldValue)
) {
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(