diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 7ee649b5c5ca..c02633c4c993 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -360,6 +360,7 @@ export enum FieldMetadataType {
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
+ Emails = 'EMAILS',
FullName = 'FULL_NAME',
Link = 'LINK',
Links = 'LINKS',
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index d9823a7969d9..39201910cafc 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -265,6 +265,7 @@ export enum FieldMetadataType {
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
+ Emails = 'EMAILS',
FullName = 'FULL_NAME',
Link = 'LINK',
Links = 'LINKS',
diff --git a/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts b/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts
index 9271c4d2b5e7..2e7713d96fc0 100644
--- a/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts
+++ b/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts
@@ -9,6 +9,7 @@ export const SORTABLE_FIELD_METADATA_TYPES = [
FieldMetadataType.Select,
FieldMetadataType.Phone,
FieldMetadataType.Email,
+ FieldMetadataType.Emails,
FieldMetadataType.FullName,
FieldMetadataType.Rating,
FieldMetadataType.Currency,
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts
index 7fa1fae41dc4..e8aef6f8ff11 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts
@@ -26,6 +26,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
FieldMetadataType.DateTime,
FieldMetadataType.Text,
FieldMetadataType.Email,
+ FieldMetadataType.Emails,
FieldMetadataType.Number,
FieldMetadataType.Link,
FieldMetadataType.Links,
@@ -77,6 +78,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
return 'CURRENCY';
case FieldMetadataType.Email:
return 'EMAIL';
+ case FieldMetadataType.Emails:
+ return 'EMAILS';
case FieldMetadataType.Phone:
return 'PHONE';
case FieldMetadataType.Relation:
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getOrderByForFieldMetadataType.ts b/packages/twenty-front/src/modules/object-metadata/utils/getOrderByForFieldMetadataType.ts
index 0e54e820e27d..1803bc9c5db8 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getOrderByForFieldMetadataType.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getOrderByForFieldMetadataType.ts
@@ -1,7 +1,10 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
-import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
+import {
+ FieldEmailsValue,
+ FieldLinksValue,
+} from '@/object-record/record-field/types/FieldMetadata';
import { OrderBy } from '@/types/OrderBy';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -43,6 +46,14 @@ export const getOrderByForFieldMetadataType = (
} satisfies { [key in keyof FieldLinksValue]?: OrderBy },
},
];
+ case FieldMetadataType.Emails:
+ return [
+ {
+ [field.name]: {
+ primaryEmail: direction ?? 'AscNullsLast',
+ } satisfies { [key in keyof FieldEmailsValue]?: OrderBy },
+ },
+ ];
default:
return [
{
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
index 7f46e887d6e2..9984d62ffdcf 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
@@ -156,5 +156,13 @@ ${mapObjectMetadataToGraphQLQuery({
}`;
}
+ if (fieldType === FieldMetadataType.Emails) {
+ return `${field.name}
+{
+ primaryEmail
+ additionalEmails
+}`;
+ }
+
return '';
};
diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts
index 29658b5cb8bc..38038ee0562e 100644
--- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts
@@ -94,6 +94,10 @@ export type ActorFilter = {
name?: StringFilter;
};
+export type EmailsFilter = {
+ primaryEmail?: StringFilter;
+};
+
export type LeafFilter =
| UUIDFilter
| StringFilter
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx
index f795fdddbc29..91e31b5671ef 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx
@@ -60,6 +60,7 @@ export const MultipleFiltersDropdownContent = ({
{[
'TEXT',
'EMAIL',
+ 'EMAILS',
'PHONE',
'FULL_NAME',
'LINK',
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts
index 36ba51c2096e..875148acf06b 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts
@@ -2,6 +2,7 @@ export type FilterType =
| 'TEXT'
| 'PHONE'
| 'EMAIL'
+ | 'EMAILS'
| 'DATE_TIME'
| 'DATE'
| 'NUMBER'
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts
index a7400625ead0..8265e54a9fe3 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts
@@ -15,6 +15,7 @@ export const getOperandsForFilterType = (
switch (filterType) {
case 'TEXT':
case 'EMAIL':
+ case 'EMAILS':
case 'FULL_NAME':
case 'ADDRESS':
case 'PHONE':
diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
index 6f648f145c77..613eeb72f071 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
@@ -2,6 +2,7 @@ import { useContext } from 'react';
import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay';
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
+import { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
@@ -10,6 +11,7 @@ import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-type
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
+import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
@@ -100,5 +102,7 @@ export const FieldDisplay = () => {
) : isFieldActor(fieldDefinition) ? (
+ ) : isFieldEmails(fieldDefinition) ? (
+
) : null;
};
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 bea16402ae2a..16555c0d1cf8 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
@@ -2,6 +2,7 @@ import { useContext } from 'react';
import { AddressFieldInput } from '@/object-record/record-field/meta-types/input/components/AddressFieldInput';
import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
+import { EmailsFieldInput } from '@/object-record/record-field/meta-types/input/components/EmailsFieldInput';
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
@@ -11,6 +12,7 @@ import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
+import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
@@ -103,6 +105,8 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
+ ) : isFieldEmails(fieldDefinition) ? (
+
) : isFieldFullName(fieldDefinition) ? (
{
const fieldIsEmail =
isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist);
+ const fieldIsEmails =
+ isFieldEmails(fieldDefinition) && isFieldEmailsValue(valueToPersist);
+
const fieldIsDateTime =
isFieldDateTime(fieldDefinition) &&
isFieldDateTimeValue(valueToPersist);
@@ -119,6 +124,7 @@ export const usePersistField = () => {
fieldIsText ||
fieldIsBoolean ||
fieldIsEmail ||
+ fieldIsEmails ||
fieldIsRating ||
fieldIsNumber ||
fieldIsDateTime ||
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/EmailsFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/EmailsFieldDisplay.tsx
new file mode 100644
index 000000000000..b2a6fc724024
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/EmailsFieldDisplay.tsx
@@ -0,0 +1,8 @@
+import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
+import { EmailsDisplay } from '@/ui/field/display/components/EmailsDisplay';
+
+export const EmailsFieldDisplay = () => {
+ const { fieldValue } = useEmailsField();
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts
new file mode 100644
index 000000000000..ff723f0048ac
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts
@@ -0,0 +1,53 @@
+import { useContext } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
+
+import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
+import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
+import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
+import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
+import { emailsSchema } from '@/object-record/record-field/types/guards/isFieldEmailsValue';
+import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
+
+import { FieldContext } from '../../contexts/FieldContext';
+import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
+
+export const useEmailsField = () => {
+ const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
+
+ assertFieldMetadata(FieldMetadataType.Emails, isFieldEmails, fieldDefinition);
+
+ const fieldName = fieldDefinition.metadata.fieldName;
+
+ const [fieldValue, setFieldValue] = useRecoilState(
+ recordStoreFamilySelector({
+ recordId,
+ fieldName: fieldName,
+ }),
+ );
+
+ const { setDraftValue, getDraftValueSelector } =
+ useRecordFieldInput(`${recordId}-${fieldName}`);
+
+ const draftValue = useRecoilValue(getDraftValueSelector());
+
+ const persistField = usePersistField();
+
+ const persistEmailsField = (nextValue: FieldEmailsValue) => {
+ try {
+ persistField(emailsSchema.parse(nextValue));
+ } catch {
+ return;
+ }
+ };
+
+ return {
+ fieldDefinition,
+ fieldValue,
+ draftValue,
+ setDraftValue,
+ setFieldValue,
+ hotkeyScope,
+ persistEmailsField,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsFieldDisplay.ts
new file mode 100644
index 000000000000..c3a66facf23f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsFieldDisplay.ts
@@ -0,0 +1,23 @@
+import { useContext } from 'react';
+
+import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
+
+import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
+import { FieldContext } from '../../contexts/FieldContext';
+
+export const useEmailsFieldDisplay = () => {
+ const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
+
+ const fieldName = fieldDefinition.metadata.fieldName;
+
+ const fieldValue = useRecordFieldValue(
+ recordId,
+ fieldName,
+ );
+
+ return {
+ fieldDefinition,
+ fieldValue,
+ hotkeyScope,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx
new file mode 100644
index 000000000000..c3d90e7b1bbb
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx
@@ -0,0 +1,57 @@
+import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
+import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem';
+import { useMemo } from 'react';
+import { isDefined } from 'twenty-ui';
+import { MultiItemFieldInput } from './MultiItemFieldInput';
+
+type EmailsFieldInputProps = {
+ onCancel?: () => void;
+};
+
+export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
+ const { persistEmailsField, hotkeyScope, fieldValue } = useEmailsField();
+
+ const emails = useMemo(
+ () =>
+ [
+ fieldValue?.primaryEmail ? fieldValue?.primaryEmail : null,
+ ...(fieldValue?.additionalEmails ?? []),
+ ].filter(isDefined),
+ [fieldValue?.primaryEmail, fieldValue?.additionalEmails],
+ );
+
+ const handlePersistEmails = (updatedEmails: string[]) => {
+ const [nextPrimaryEmail, ...nextAdditionalEmails] = updatedEmails;
+ persistEmailsField({
+ primaryEmail: nextPrimaryEmail ?? '',
+ additionalEmails: nextAdditionalEmails,
+ });
+ };
+
+ return (
+ (
+
+ )}
+ hotkeyScope={hotkeyScope}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem.tsx
new file mode 100644
index 000000000000..f55b67854f2d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem.tsx
@@ -0,0 +1,32 @@
+import { EmailDisplay } from '@/ui/field/display/components/EmailDisplay';
+import { MultiItemFieldMenuItem } from './MultiItemFieldMenuItem';
+
+type EmailsFieldMenuItemProps = {
+ dropdownId: string;
+ isPrimary?: boolean;
+ onEdit?: () => void;
+ onSetAsPrimary?: () => void;
+ onDelete?: () => void;
+ email: string;
+};
+
+export const EmailsFieldMenuItem = ({
+ dropdownId,
+ isPrimary,
+ onEdit,
+ onSetAsPrimary,
+ onDelete,
+ email,
+}: EmailsFieldMenuItemProps) => {
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx
index dfb3adf03803..66ed0055d8f0 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx
@@ -1,28 +1,9 @@
-import styled from '@emotion/styled';
-import { useMemo, useRef, useState } from 'react';
-import { Key } from 'ts-key-enum';
-import { IconCheck, IconPlus } from 'twenty-ui';
-
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem';
-import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
-import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
-import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
-import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
-import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
-import { moveArrayItem } from '~/utils/array/moveArrayItem';
-import { toSpliced } from '~/utils/array/toSpliced';
-import { isDefined } from '~/utils/isDefined';
+import { useMemo } from 'react';
+import { isDefined } from 'twenty-ui';
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
-
-const StyledDropdownMenu = styled(DropdownMenu)`
- left: -1px;
- position: absolute;
- top: -1px;
-`;
+import { MultiItemFieldInput } from './MultiItemFieldInput';
type LinksFieldInputProps = {
onCancel?: () => void;
@@ -31,8 +12,6 @@ type LinksFieldInputProps = {
export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();
- const containerRef = useRef(null);
-
const links = useMemo<{ url: string; label: string }[]>(
() =>
[
@@ -51,158 +30,44 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
],
);
- const handleDropdownClose = () => {
- onCancel?.();
- };
-
- useListenClickOutside({
- refs: [containerRef],
- callback: handleDropdownClose,
- });
-
- useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
-
- const [isInputDisplayed, setIsInputDisplayed] = useState(false);
- const [inputValue, setInputValue] = useState('');
- const [linkToEditIndex, setLinkToEditIndex] = useState(-1);
- const isAddingNewLink = linkToEditIndex === -1;
-
- const handleAddButtonClick = () => {
- setLinkToEditIndex(-1);
- setIsInputDisplayed(true);
- };
-
- const handleEditButtonClick = (index: number) => {
- setLinkToEditIndex(index);
- setInputValue(links[index].url);
- setIsInputDisplayed(true);
- };
-
- const urlInputValidation = inputValue
- ? absoluteUrlSchema.safeParse(inputValue)
- : null;
-
- const handleSubmitInput = () => {
- if (!urlInputValidation?.success) return;
-
- const validatedInputValue = urlInputValidation.data;
-
- // Don't persist if value hasn't changed.
- if (
- !isAddingNewLink &&
- validatedInputValue === links[linkToEditIndex].url
- ) {
- setIsInputDisplayed(false);
- setInputValue('');
- return;
- }
-
- const linkValue = { label: '', url: validatedInputValue };
- const nextLinks = isAddingNewLink
- ? [...links, linkValue]
- : toSpliced(links, linkToEditIndex, 1, linkValue);
- const [nextPrimaryLink, ...nextSecondaryLinks] = nextLinks;
-
+ const handlePersistLinks = (
+ updatedLinks: { url: string; label: string }[],
+ ) => {
+ const [nextPrimaryLink, ...nextSecondaryLinks] = updatedLinks;
persistLinksField({
- primaryLinkUrl: nextPrimaryLink.url ?? '',
- primaryLinkLabel: nextPrimaryLink.label ?? '',
+ primaryLinkUrl: nextPrimaryLink?.url ?? '',
+ primaryLinkLabel: nextPrimaryLink?.label ?? '',
secondaryLinks: nextSecondaryLinks,
});
- setIsInputDisplayed(false);
- setInputValue('');
- };
-
- const handleSetPrimaryLink = (index: number) => {
- const nextLinks = moveArrayItem(links, { fromIndex: index, toIndex: 0 });
- const [nextPrimaryLink, ...nextSecondaryLinks] = nextLinks;
-
- persistLinksField({
- primaryLinkUrl: nextPrimaryLink.url ?? '',
- primaryLinkLabel: nextPrimaryLink.label ?? '',
- secondaryLinks: nextSecondaryLinks,
- });
- };
-
- const handleDeleteLink = (index: number) => {
- const hasOnlyOneLastLink = links.length === 1;
-
- if (hasOnlyOneLastLink) {
- persistLinksField({
- primaryLinkUrl: '',
- primaryLinkLabel: '',
- secondaryLinks: null,
- });
-
- handleDropdownClose();
-
- return;
- }
-
- const isRemovingPrimary = index === 0;
- if (isRemovingPrimary) {
- const [, nextPrimaryLink, ...nextSecondaryLinks] = links;
-
- persistLinksField({
- primaryLinkUrl: nextPrimaryLink.url ?? '',
- primaryLinkLabel: nextPrimaryLink.label ?? '',
- secondaryLinks: nextSecondaryLinks,
- });
-
- return;
- }
-
- persistLinksField({
- ...fieldValue,
- secondaryLinks: toSpliced(fieldValue.secondaryLinks ?? [], index - 1, 1),
- });
};
return (
-
- {!!links.length && (
- <>
-
- {links.map(({ label, url }, index) => (
- handleEditButtonClick(index)}
- onSetAsPrimary={() => handleSetPrimaryLink(index)}
- onDelete={() => handleDeleteLink(index)}
- url={url}
- />
- ))}
-
-
- >
- )}
- {isInputDisplayed || !links.length ? (
- setInputValue(event.target.value)}
- onEnter={handleSubmitInput}
- rightComponent={
-
- }
+ absoluteUrlSchema.safeParse(input).success}
+ formatInput={(input) => ({ url: input, label: '' })}
+ renderItem={({
+ value: link,
+ index,
+ handleEdit,
+ handleSetPrimary,
+ handleDelete,
+ }) => (
+
- ) : (
-
-
-
)}
-
+ hotkeyScope={hotkeyScope}
+ />
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx
index 9cc08ac9f294..fbc74d34fdef 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx
@@ -1,19 +1,5 @@
-import styled from '@emotion/styled';
-import { useEffect, useState } from 'react';
-import {
- IconBookmark,
- IconBookmarkPlus,
- IconComponent,
- IconDotsVertical,
- IconPencil,
- IconTrash,
-} from 'twenty-ui';
-
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
-import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import { MultiItemFieldMenuItem } from './MultiItemFieldMenuItem';
type LinksFieldMenuItemProps = {
dropdownId: string;
@@ -25,12 +11,6 @@ type LinksFieldMenuItemProps = {
url: string;
};
-const StyledIconBookmark = styled(IconBookmark)`
- color: ${({ theme }) => theme.font.color.light};
- height: ${({ theme }) => theme.icon.size.sm}px;
- width: ${({ theme }) => theme.icon.size.sm}px;
-`;
-
export const LinksFieldMenuItem = ({
dropdownId,
isPrimary,
@@ -40,76 +20,15 @@ export const LinksFieldMenuItem = ({
onDelete,
url,
}: LinksFieldMenuItemProps) => {
- const [isHovered, setIsHovered] = useState(false);
- const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
-
- const handleMouseEnter = () => setIsHovered(true);
- const handleMouseLeave = () => setIsHovered(false);
-
- const handleDeleteClick = () => {
- setIsHovered(false);
- onDelete?.();
- };
-
- // Make sure dropdown closes on unmount.
- useEffect(() => {
- if (isDropdownOpen) {
- return () => closeDropdown();
- }
- }, [closeDropdown, isDropdownOpen]);
-
return (
- }
- isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
- iconButtons={[
- {
- Wrapper: isHovered
- ? ({ iconButton }) => (
-
- {!isPrimary && (
-
- )}
-
-
-
- }
- />
- )
- : undefined,
- Icon:
- isPrimary && !isHovered
- ? (StyledIconBookmark as IconComponent)
- : IconDotsVertical,
- accent: 'tertiary',
- onClick: isHovered ? () => {} : undefined,
- },
- ]}
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx
new file mode 100644
index 000000000000..b223abfcff13
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx
@@ -0,0 +1,155 @@
+import styled from '@emotion/styled';
+import { useRef, useState } from 'react';
+import { Key } from 'ts-key-enum';
+import { IconCheck, IconPlus } from 'twenty-ui';
+
+import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
+import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
+import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
+import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
+import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
+import { moveArrayItem } from '~/utils/array/moveArrayItem';
+import { toSpliced } from '~/utils/array/toSpliced';
+
+const StyledDropdownMenu = styled(DropdownMenu)`
+ left: -1px;
+ position: absolute;
+ top: -1px;
+`;
+
+type MultiItemFieldInputProps = {
+ items: T[];
+ onPersist: (updatedItems: T[]) => void;
+ onCancel?: () => void;
+ placeholder: string;
+ validateInput?: (input: string) => boolean;
+ formatInput?: (input: string) => T;
+ renderItem: (props: {
+ value: T;
+ index: number;
+ handleEdit: () => void;
+ handleSetPrimary: () => void;
+ handleDelete: () => void;
+ }) => React.ReactNode;
+ hotkeyScope: string;
+};
+
+export const MultiItemFieldInput = ({
+ items,
+ onPersist,
+ onCancel,
+ placeholder,
+ validateInput,
+ formatInput,
+ renderItem,
+ hotkeyScope,
+}: MultiItemFieldInputProps) => {
+ const containerRef = useRef(null);
+
+ const handleDropdownClose = () => {
+ onCancel?.();
+ };
+
+ useListenClickOutside({
+ refs: [containerRef],
+ callback: handleDropdownClose,
+ });
+
+ useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
+
+ const [isInputDisplayed, setIsInputDisplayed] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+ const [itemToEditIndex, setItemToEditIndex] = useState(-1);
+ const isAddingNewItem = itemToEditIndex === -1;
+
+ const handleAddButtonClick = () => {
+ setItemToEditIndex(-1);
+ setIsInputDisplayed(true);
+ };
+
+ const handleEditButtonClick = (index: number) => {
+ setItemToEditIndex(index);
+ setInputValue((items[index] as unknown as string) || '');
+ setIsInputDisplayed(true);
+ };
+
+ const handleSubmitInput = () => {
+ if (validateInput !== undefined && !validateInput(inputValue)) return;
+
+ const newItem = formatInput
+ ? formatInput(inputValue)
+ : (inputValue as unknown as T);
+
+ if (!isAddingNewItem && newItem === items[itemToEditIndex]) {
+ setIsInputDisplayed(false);
+ setInputValue('');
+ return;
+ }
+
+ const updatedItems = isAddingNewItem
+ ? [...items, newItem]
+ : toSpliced(items, itemToEditIndex, 1, newItem);
+
+ onPersist(updatedItems);
+ setIsInputDisplayed(false);
+ setInputValue('');
+ };
+
+ const handleSetPrimaryItem = (index: number) => {
+ const updatedItems = moveArrayItem(items, { fromIndex: index, toIndex: 0 });
+ onPersist(updatedItems);
+ };
+
+ const handleDeleteItem = (index: number) => {
+ const updatedItems = toSpliced(items, index, 1);
+ onPersist(updatedItems);
+ };
+
+ return (
+
+ {!!items.length && (
+ <>
+
+ {items.map((item, index) =>
+ renderItem({
+ value: item,
+ index,
+ handleEdit: () => handleEditButtonClick(index),
+ handleSetPrimary: () => handleSetPrimaryItem(index),
+ handleDelete: () => handleDeleteItem(index),
+ }),
+ )}
+
+
+ >
+ )}
+ {isInputDisplayed || !items.length ? (
+ setInputValue(event.target.value)}
+ onEnter={handleSubmitInput}
+ rightComponent={
+
+ }
+ />
+ ) : (
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx
new file mode 100644
index 000000000000..95fcf4e5a943
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx
@@ -0,0 +1,110 @@
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
+import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import styled from '@emotion/styled';
+import { useEffect, useState } from 'react';
+import {
+ IconBookmark,
+ IconBookmarkPlus,
+ IconComponent,
+ IconDotsVertical,
+ IconPencil,
+ IconTrash,
+} from 'twenty-ui';
+
+type MultiItemFieldMenuItemProps = {
+ dropdownId: string;
+ isPrimary?: boolean;
+ value: T;
+ onEdit?: () => void;
+ onSetAsPrimary?: () => void;
+ onDelete?: () => void;
+ DisplayComponent: React.ComponentType<{ value: T }>;
+};
+
+const StyledIconBookmark = styled(IconBookmark)`
+ color: ${({ theme }) => theme.font.color.light};
+ height: ${({ theme }) => theme.icon.size.sm}px;
+ width: ${({ theme }) => theme.icon.size.sm}px;
+`;
+
+export const MultiItemFieldMenuItem = ({
+ dropdownId,
+ isPrimary,
+ value,
+ onEdit,
+ onSetAsPrimary,
+ onDelete,
+ DisplayComponent,
+}: MultiItemFieldMenuItemProps) => {
+ const [isHovered, setIsHovered] = useState(false);
+ const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
+
+ const handleMouseEnter = () => setIsHovered(true);
+ const handleMouseLeave = () => setIsHovered(false);
+
+ const handleDeleteClick = () => {
+ setIsHovered(false);
+ onDelete?.();
+ };
+
+ useEffect(() => {
+ if (isDropdownOpen) {
+ return () => closeDropdown();
+ }
+ }, [closeDropdown, isDropdownOpen]);
+
+ return (
+ }
+ isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
+ iconButtons={[
+ {
+ Wrapper: isHovered
+ ? ({ iconButton }) => (
+
+ {!isPrimary && (
+
+ )}
+
+
+
+ }
+ />
+ )
+ : undefined,
+ Icon:
+ isPrimary && !isHovered
+ ? (StyledIconBookmark as IconComponent)
+ : IconDotsVertical,
+ accent: 'tertiary',
+ onClick: isHovered ? () => {} : undefined,
+ },
+ ]}
+ />
+ );
+};
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 55a25af1c4c9..b3196f1c702f 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
@@ -5,6 +5,7 @@ import {
FieldBooleanValue,
FieldCurrencyValue,
FieldDateTimeValue,
+ FieldEmailsValue,
FieldEmailValue,
FieldFullNameValue,
FieldJsonValue,
@@ -26,6 +27,10 @@ export type FieldNumberDraftValue = string;
export type FieldDateTimeDraftValue = string;
export type FieldPhoneDraftValue = string;
export type FieldEmailDraftValue = string;
+export type FieldEmailsDraftValue = {
+ primaryEmail: string;
+ additionalEmails: string[] | null;
+};
export type FieldSelectDraftValue = string;
export type FieldMultiSelectDraftValue = string[];
export type FieldRelationDraftValue = string;
@@ -72,28 +77,30 @@ export type FieldInputDraftValue = FieldValue extends FieldTextValue
? FieldPhoneDraftValue
: FieldValue extends FieldEmailValue
? FieldEmailDraftValue
- : FieldValue extends FieldLinkValue
- ? FieldLinkDraftValue
- : FieldValue extends FieldLinksValue
- ? FieldLinksDraftValue
- : FieldValue extends FieldCurrencyValue
- ? FieldCurrencyDraftValue
- : FieldValue extends FieldFullNameValue
- ? FieldFullNameDraftValue
- : FieldValue extends FieldRatingValue
- ? FieldRatingValue
- : FieldValue extends FieldSelectValue
- ? FieldSelectDraftValue
- : FieldValue extends FieldMultiSelectValue
- ? FieldMultiSelectDraftValue
- : FieldValue extends FieldRelationToOneValue
- ? FieldRelationDraftValue
- : FieldValue extends FieldRelationFromManyValue
- ? FieldRelationManyDraftValue
- : FieldValue extends FieldAddressValue
- ? FieldAddressDraftValue
- : FieldValue extends FieldJsonValue
- ? FieldJsonDraftValue
- : FieldValue extends FieldActorValue
- ? FieldActorDraftValue
- : never;
+ : FieldValue extends FieldEmailsValue
+ ? FieldEmailsDraftValue
+ : FieldValue extends FieldLinkValue
+ ? FieldLinkDraftValue
+ : FieldValue extends FieldLinksValue
+ ? FieldLinksDraftValue
+ : FieldValue extends FieldCurrencyValue
+ ? FieldCurrencyDraftValue
+ : FieldValue extends FieldFullNameValue
+ ? FieldFullNameDraftValue
+ : FieldValue extends FieldRatingValue
+ ? FieldRatingValue
+ : FieldValue extends FieldSelectValue
+ ? FieldSelectDraftValue
+ : FieldValue extends FieldMultiSelectValue
+ ? FieldMultiSelectDraftValue
+ : FieldValue extends FieldRelationToOneValue
+ ? FieldRelationDraftValue
+ : FieldValue extends FieldRelationFromManyValue
+ ? FieldRelationManyDraftValue
+ : FieldValue extends FieldAddressValue
+ ? FieldAddressDraftValue
+ : FieldValue extends FieldJsonValue
+ ? FieldJsonDraftValue
+ : FieldValue extends FieldActorValue
+ ? FieldActorDraftValue
+ : never;
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 7777c10c091d..367f14680570 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
@@ -72,6 +72,11 @@ export type FieldEmailMetadata = {
fieldName: string;
};
+export type FieldEmailsMetadata = {
+ objectMetadataNameSingular?: string;
+ fieldName: string;
+};
+
export type FieldPhoneMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
@@ -180,6 +185,10 @@ export type FieldBooleanValue = boolean;
export type FieldPhoneValue = string;
export type FieldEmailValue = string;
+export type FieldEmailsValue = {
+ primaryEmail: string;
+ additionalEmails: string[] | null;
+};
export type FieldLinkValue = { url: string; label: string };
export type FieldLinksValue = {
primaryLinkLabel: string;
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts
index 8445a468e028..9c94d9b666a7 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts
@@ -9,6 +9,7 @@ import {
FieldDateMetadata,
FieldDateTimeMetadata,
FieldEmailMetadata,
+ FieldEmailsMetadata,
FieldFullNameMetadata,
FieldLinkMetadata,
FieldLinksMetadata,
@@ -38,35 +39,37 @@ type AssertFieldMetadataFunction = <
? FieldDateMetadata
: E extends 'EMAIL'
? FieldEmailMetadata
- : E extends 'SELECT'
- ? FieldSelectMetadata
- : E extends 'MULTI_SELECT'
- ? FieldMultiSelectMetadata
- : E extends 'RATING'
- ? FieldRatingMetadata
- : E extends 'LINK'
- ? FieldLinkMetadata
- : E extends 'LINKS'
- ? FieldLinksMetadata
- : E extends 'NUMBER'
- ? FieldNumberMetadata
- : E extends 'PHONE'
- ? FieldPhoneMetadata
- : E extends 'RELATION'
- ? FieldRelationMetadata
- : E extends 'TEXT'
- ? FieldTextMetadata
- : E extends 'UUID'
- ? FieldUuidMetadata
- : E extends 'ADDRESS'
- ? FieldAddressMetadata
- : E extends 'RAW_JSON'
- ? FieldRawJsonMetadata
- : E extends 'RICH_TEXT'
- ? FieldTextMetadata
- : E extends 'ACTOR'
- ? FieldActorMetadata
- : never,
+ : E extends 'EMAILS'
+ ? FieldEmailsMetadata
+ : E extends 'SELECT'
+ ? FieldSelectMetadata
+ : E extends 'MULTI_SELECT'
+ ? FieldMultiSelectMetadata
+ : E extends 'RATING'
+ ? FieldRatingMetadata
+ : E extends 'LINK'
+ ? FieldLinkMetadata
+ : E extends 'LINKS'
+ ? FieldLinksMetadata
+ : E extends 'NUMBER'
+ ? FieldNumberMetadata
+ : E extends 'PHONE'
+ ? FieldPhoneMetadata
+ : E extends 'RELATION'
+ ? FieldRelationMetadata
+ : E extends 'TEXT'
+ ? FieldTextMetadata
+ : E extends 'UUID'
+ ? FieldUuidMetadata
+ : E extends 'ADDRESS'
+ ? FieldAddressMetadata
+ : E extends 'RAW_JSON'
+ ? FieldRawJsonMetadata
+ : E extends 'RICH_TEXT'
+ ? FieldTextMetadata
+ : E extends 'ACTOR'
+ ? FieldActorMetadata
+ : never,
>(
fieldType: E,
fieldTypeGuard: (
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmails.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmails.ts
new file mode 100644
index 000000000000..434586c2bf8d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmails.ts
@@ -0,0 +1,9 @@
+import { FieldMetadataType } from '~/generated-metadata/graphql';
+
+import { FieldDefinition } from '../FieldDefinition';
+import { FieldEmailsMetadata, FieldMetadata } from '../FieldMetadata';
+
+export const isFieldEmails = (
+ field: Pick, 'type'>,
+): field is FieldDefinition =>
+ field.type === FieldMetadataType.Emails;
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmailsValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmailsValue.ts
new file mode 100644
index 000000000000..04fa3c9a16e2
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmailsValue.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
+
+export const emailsSchema = z.object({
+ primaryEmail: z.string(),
+ additionalEmails: z.array(z.string()).nullable(),
+}) satisfies z.ZodType;
+
+export const isFieldEmailsValue = (
+ fieldValue: unknown,
+): fieldValue is FieldEmailsValue => emailsSchema.safeParse(fieldValue).success;
diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx
index d8ea93496ffe..b100872b5470 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx
@@ -3,6 +3,7 @@ import { IconComponent, IconPencil } from 'twenty-ui';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
+import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
@@ -29,7 +30,8 @@ export const getFieldButtonIcon = (
(isFieldRelation(fieldDefinition) &&
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
'workspaceMember') ||
- isFieldLinks(fieldDefinition)
+ isFieldLinks(fieldDefinition) ||
+ isFieldEmails(fieldDefinition)
) {
return IconPencil;
}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts
index 1cb5296c40ad..905afb646a25 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts
@@ -12,6 +12,8 @@ import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail';
+import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
+import { isFieldEmailsValue } from '@/object-record/record-field/types/guards/isFieldEmailsValue';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
@@ -120,6 +122,12 @@ export const isFieldValueEmpty = ({
return !isFieldActorValue(fieldValue) || isValueEmpty(fieldValue.name);
}
+ if (isFieldEmails(fieldDefinition)) {
+ return (
+ !isFieldEmailsValue(fieldValue) || isValueEmpty(fieldValue.primaryEmail)
+ );
+ }
+
throw new Error(
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
);
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
index bf0b4a545de0..a60d907d685d 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
@@ -8,6 +8,7 @@ import {
BooleanFilter,
CurrencyFilter,
DateFilter,
+ EmailsFilter,
FloatFilter,
FullNameFilter,
LinksFilter,
@@ -268,6 +269,18 @@ export const isRecordMatchingFilter = ({
})
);
}
+ case FieldMetadataType.Emails: {
+ const emailsFilter = filterValue as EmailsFilter;
+
+ if (emailsFilter.primaryEmail === undefined) {
+ return false;
+ }
+
+ return isMatchingStringFilter({
+ stringFilter: emailsFilter.primaryEmail,
+ value: record[filterKey].primaryEmail,
+ });
+ }
case FieldMetadataType.Relation: {
throw new Error(
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts
index 159a064cf471..6d93ceba9e1d 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts
@@ -5,6 +5,7 @@ import {
AddressFilter,
CurrencyFilter,
DateFilter,
+ EmailsFilter,
FloatFilter,
RecordGqlOperationFilter,
RelationFilter,
@@ -229,6 +230,22 @@ const applyEmptyFilters = (
],
};
break;
+ case 'EMAILS':
+ emptyRecordFilter = {
+ or: [
+ {
+ [correspondingField.name]: {
+ primaryEmail: { ilike: '' },
+ } as EmailsFilter,
+ },
+ {
+ [correspondingField.name]: {
+ primaryEmail: { is: 'NULL' },
+ } as EmailsFilter,
+ },
+ ],
+ };
+ break;
default:
throw new Error(`Unsupported empty filter type ${filterType}`);
}
@@ -806,6 +823,51 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
);
}
break;
+ case 'EMAILS':
+ switch (rawUIFilter.operand) {
+ case ViewFilterOperand.Contains:
+ objectRecordFilters.push({
+ or: [
+ {
+ [correspondingField.name]: {
+ primaryEmail: {
+ ilike: `%${rawUIFilter.value}%`,
+ },
+ } as EmailsFilter,
+ },
+ ],
+ });
+ break;
+ case ViewFilterOperand.DoesNotContain:
+ objectRecordFilters.push({
+ and: [
+ {
+ not: {
+ [correspondingField.name]: {
+ primaryEmail: {
+ ilike: `%${rawUIFilter.value}%`,
+ },
+ } as EmailsFilter,
+ },
+ },
+ ],
+ });
+ break;
+ case ViewFilterOperand.IsEmpty:
+ case ViewFilterOperand.IsNotEmpty:
+ applyEmptyFilters(
+ rawUIFilter.operand,
+ correspondingField,
+ objectRecordFilters,
+ rawUIFilter.definition.type,
+ );
+ break;
+ default:
+ throw new Error(
+ `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
+ );
+ }
+ break;
default:
throw new Error('Unknown filter type');
}
diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts
index 95747a055586..f9d6be3208c0 100644
--- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts
+++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts
@@ -12,6 +12,9 @@ export const generateEmptyFieldValue = (
case FieldMetadataType.Text: {
return '';
}
+ case FieldMetadataType.Emails: {
+ return { primaryEmail: '', additionalEmails: null };
+ }
case FieldMetadataType.Link: {
return {
label: '',
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts
index 8a190997a37f..3272ad990fa1 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts
@@ -99,6 +99,11 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = {
Icon: IconRelationManyToMany,
},
[FieldMetadataType.Email]: { label: 'Email', Icon: IconMail },
+ [FieldMetadataType.Emails]: {
+ label: 'Emails',
+ Icon: IconMail,
+ exampleValue: { primaryEmail: 'john@twenty.com' },
+ },
[FieldMetadataType.Phone]: {
label: 'Phone',
Icon: IconPhone,
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
index d0e6604d143b..65f2505bc47c 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
@@ -85,6 +85,7 @@ const previewableTypes = [
FieldMetadataType.Currency,
FieldMetadataType.Date,
FieldMetadataType.DateTime,
+ FieldMetadataType.Emails,
FieldMetadataType.FullName,
FieldMetadataType.Link,
FieldMetadataType.Links,
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx
new file mode 100644
index 000000000000..8b7e67780a2a
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx
@@ -0,0 +1,53 @@
+import { useMemo } from 'react';
+import { THEME_COMMON } from 'twenty-ui';
+
+import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
+import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
+import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
+import styled from '@emotion/styled';
+import { isDefined } from '~/utils/isDefined';
+
+type EmailsDisplayProps = {
+ value?: FieldEmailsValue;
+ isFocused?: boolean;
+};
+
+const themeSpacing = THEME_COMMON.spacingMultiplicator;
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeSpacing * 1}px;
+ justify-content: flex-start;
+
+ max-width: 100%;
+
+ overflow: hidden;
+
+ width: 100%;
+`;
+
+export const EmailsDisplay = ({ value, isFocused }: EmailsDisplayProps) => {
+ const emails = useMemo(
+ () =>
+ [
+ value?.primaryEmail ? value.primaryEmail : null,
+ ...(value?.additionalEmails ?? []),
+ ].filter(isDefined),
+ [value?.primaryEmail, value?.additionalEmails],
+ );
+
+ return isFocused ? (
+
+ {emails.map((email, index) => (
+
+ ))}
+
+ ) : (
+
+ {emails.map((email, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
index faedb92ce8be..a05e522a3bfc 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
@@ -216,7 +216,10 @@ export const SettingsObjectFieldEdit = () => {
{
FieldMetadataType.Numeric,
FieldMetadataType.RichText,
FieldMetadataType.Actor,
+ FieldMetadataType.Email,
] as const
).filter(isDefined);
diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts
index 1a20e09c9c55..ab348dd176f8 100644
--- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts
+++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts
@@ -205,12 +205,19 @@ const fieldActorMock = {
name: '',
},
};
+const fieldEmailsMock = {
+ name: 'fieldEmails',
+ type: FieldMetadataType.EMAILS,
+ isNullable: false,
+ defaultValue: [{ primaryEmail: '', additionalEmails: {} }],
+};
export const fields = [
fieldUuidMock,
fieldTextMock,
fieldPhoneMock,
fieldEmailMock,
+ fieldEmailsMock,
fieldDateTimeMock,
fieldDateMock,
fieldBooleanMock,
diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts
index b82e5b05c6c6..c96998bed59f 100644
--- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts
+++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts
@@ -144,5 +144,13 @@ export const mapFieldMetadataToGraphqlQuery = (
name
}
`;
+ } else if (fieldType === FieldMetadataType.EMAILS) {
+ return `
+ ${field.name}
+ {
+ primaryEmail
+ additionalEmails
+ }
+ `;
}
};
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
index 3275b8392d01..e481a2343859 100644
--- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
@@ -147,6 +147,17 @@ describe('computeSchemaComponents', () => {
},
type: 'object',
},
+ fieldEmails: {
+ properties: {
+ primaryEmail: {
+ type: 'string',
+ },
+ additionalEmails: {
+ type: 'object',
+ },
+ },
+ type: 'object',
+ },
},
},
'ObjectName with Relations': {
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
index bd7248cc0939..cc5c91065f35 100644
--- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
+++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
@@ -72,6 +72,7 @@ const getSchemaComponentsProperties = (
case FieldMetadataType.FULL_NAME:
case FieldMetadataType.ADDRESS:
case FieldMetadataType.ACTOR:
+ case FieldMetadataType.EMAILS:
itemProperty = {
type: 'object',
properties: compositeTypeDefinitions
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts
new file mode 100644
index 000000000000..3cee1f44127a
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts
@@ -0,0 +1,26 @@
+import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
+
+import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
+
+export const emailsCompositeType: CompositeType = {
+ type: FieldMetadataType.EMAILS,
+ properties: [
+ {
+ name: 'primaryEmail',
+ type: FieldMetadataType.TEXT,
+ hidden: false,
+ isRequired: false,
+ },
+ {
+ name: 'additionalEmails',
+ type: FieldMetadataType.RAW_JSON,
+ hidden: false,
+ isRequired: false,
+ },
+ ],
+};
+
+export type EmailsMetadata = {
+ primaryEmail: string;
+ additionalEmails: string[] | null;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts
index 7f1a2f129b86..991618f8cdde 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts
@@ -4,6 +4,7 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
import { actorCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { addressCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
import { currencyCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
+import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type';
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
@@ -23,4 +24,5 @@ export const compositeTypeDefinitions = new Map<
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
[FieldMetadataType.ADDRESS, addressCompositeType],
[FieldMetadataType.ACTOR, actorCompositeType],
+ [FieldMetadataType.EMAILS, emailsCompositeType],
]);
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
index 15f565d73b9c..8a03f6b54bcf 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
@@ -175,3 +175,13 @@ export class FieldMetadataDefaultActor {
@IsString()
name: string;
}
+
+export class FieldMetadataDefaultValueEmails {
+ @ValidateIf((_object, value) => value !== null)
+ @IsQuotedString()
+ primaryEmail: string | null;
+
+ @ValidateIf((_object, value) => value !== null)
+ @IsObject()
+ additionalEmails: string[] | null;
+}
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
index 09f1fad3314a..263790c7ca7d 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
@@ -26,6 +26,7 @@ export enum FieldMetadataType {
TEXT = 'TEXT',
PHONE = 'PHONE',
EMAIL = 'EMAIL',
+ EMAILS = 'EMAILS',
DATE_TIME = 'DATE_TIME',
DATE = 'DATE',
BOOLEAN = 'BOOLEAN',
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts
index 6c428c468120..ede0e64e9f79 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts
@@ -143,6 +143,13 @@ export class FieldMetadataService extends TypeOrmQueryService(
fieldMetadataInput,
objectMetadata,
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts
index 82fd946614e4..d6e9e700215c 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts
@@ -4,6 +4,7 @@ import {
FieldMetadataDefaultValueBoolean,
FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDateTime,
+ FieldMetadataDefaultValueEmails,
FieldMetadataDefaultValueFullName,
FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueLinks,
@@ -27,6 +28,7 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
+ [FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails;
[FieldMetadataType.DATE_TIME]:
| FieldMetadataDefaultValueDateTime
| FieldMetadataDefaultValueNowFunction;
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts
index b6e0b1b4be36..b031e4884675 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts
@@ -10,6 +10,11 @@ export function generateDefaultValue(
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
return "''";
+ case FieldMetadataType.EMAILS:
+ return {
+ primaryEmail: "''",
+ additionalEmails: null,
+ };
case FieldMetadataType.FULL_NAME:
return {
firstName: "''",
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts
index e795d79a5abf..437310003d2c 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts
@@ -8,7 +8,8 @@ export const isCompositeFieldMetadataType = (
| FieldMetadataType.FULL_NAME
| FieldMetadataType.ADDRESS
| FieldMetadataType.LINKS
- | FieldMetadataType.ACTOR => {
+ | FieldMetadataType.ACTOR
+ | FieldMetadataType.EMAILS => {
return [
FieldMetadataType.LINK,
FieldMetadataType.CURRENCY,
@@ -16,5 +17,6 @@ export const isCompositeFieldMetadataType = (
FieldMetadataType.ADDRESS,
FieldMetadataType.LINKS,
FieldMetadataType.ACTOR,
+ FieldMetadataType.EMAILS,
].includes(type);
};
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
index 7087cfb5c70e..ce254ffd7d7d 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
@@ -13,6 +13,7 @@ import {
FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDate,
FieldMetadataDefaultValueDateTime,
+ FieldMetadataDefaultValueEmails,
FieldMetadataDefaultValueFullName,
FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueLinks,
@@ -53,6 +54,7 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
[FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor],
+ [FieldMetadataType.EMAILS]: [FieldMetadataDefaultValueEmails],
};
type ValidationResult = {
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts
index 2a6ed45b0fcd..dc913233c3ca 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts
@@ -23,7 +23,8 @@ export type CompositeFieldMetadataType =
| FieldMetadataType.CURRENCY
| FieldMetadataType.FULL_NAME
| FieldMetadataType.LINK
- | FieldMetadataType.LINKS;
+ | FieldMetadataType.LINKS
+ | FieldMetadataType.EMAILS;
@Injectable()
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory {
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
index 8c16ab64cb0c..d495051d703d 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
@@ -97,6 +97,10 @@ export class WorkspaceMigrationFactory {
],
[FieldMetadataType.LINKS, { factory: this.compositeColumnActionFactory }],
[FieldMetadataType.ACTOR, { factory: this.compositeColumnActionFactory }],
+ [
+ FieldMetadataType.EMAILS,
+ { factory: this.compositeColumnActionFactory },
+ ],
]);
}