Skip to content

Commit

Permalink
Add composite Emails field and forbid creation of Email field type (#…
Browse files Browse the repository at this point in the history
…6689)

### Description

1. 
   - We are introducing new field type(Emails)


   - We are Forbiding creation of Email field


   - We Added support for filtering and sorting on Emails field


- We are using the same display mode as used on the Links field type
(chips), check the Domain field of the Company object


   - We are also using the same logic of the link when editing the field

   \
   How To Test\
   Follow the below steps for testing locally:\
   1. Checkout to TWENTY-6261\
2. Reset database using "npx nx database:reset twenty-server" command\
   3. Run both the backend and frontend app\
4. Go to Settings/Data model and choose one of the standard objects like
people\
   5. Click on Add Field button and choose Emails as the field type

   \
   ### Refs

   #6261\
   \
   ### Demo

    \

<https://www.loom.com/share/22979acac8134ed390fef93cc56fe07c?sid=adafba94-840d-4f01-872c-dc9ec256d987>

Co-authored-by: gitstart-twenty <[email protected]>
  • Loading branch information
gitstart-app[bot] and gitstart-twenty authored Aug 29, 2024
1 parent c87ccfa commit 7a9a43b
Show file tree
Hide file tree
Showing 52 changed files with 866 additions and 318 deletions.
1 change: 1 addition & 0 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export enum FieldMetadataType {
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
Emails = 'EMAILS',
FullName = 'FULL_NAME',
Link = 'LINK',
Links = 'LINKS',
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export enum FieldMetadataType {
Date = 'DATE',
DateTime = 'DATE_TIME',
Email = 'EMAIL',
Emails = 'EMAILS',
FullName = 'FULL_NAME',
Link = 'LINK',
Links = 'LINKS',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const SORTABLE_FIELD_METADATA_TYPES = [
FieldMetadataType.Select,
FieldMetadataType.Phone,
FieldMetadataType.Email,
FieldMetadataType.Emails,
FieldMetadataType.FullName,
FieldMetadataType.Rating,
FieldMetadataType.Currency,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
FieldMetadataType.DateTime,
FieldMetadataType.Text,
FieldMetadataType.Email,
FieldMetadataType.Emails,
FieldMetadataType.Number,
FieldMetadataType.Link,
FieldMetadataType.Links,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,13 @@ ${mapObjectMetadataToGraphQLQuery({
}`;
}

if (fieldType === FieldMetadataType.Emails) {
return `${field.name}
{
primaryEmail
additionalEmails
}`;
}

return '';
};
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export type ActorFilter = {
name?: StringFilter;
};

export type EmailsFilter = {
primaryEmail?: StringFilter;
};

export type LeafFilter =
| UUIDFilter
| StringFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const MultipleFiltersDropdownContent = ({
{[
'TEXT',
'EMAIL',
'EMAILS',
'PHONE',
'FULL_NAME',
'LINK',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type FilterType =
| 'TEXT'
| 'PHONE'
| 'EMAIL'
| 'EMAILS'
| 'DATE_TIME'
| 'DATE'
| 'NUMBER'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const getOperandsForFilterType = (
switch (filterType) {
case 'TEXT':
case 'EMAIL':
case 'EMAILS':
case 'FULL_NAME':
case 'ADDRESS':
case 'PHONE':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -100,5 +102,7 @@ export const FieldDisplay = () => {
<RichTextFieldDisplay />
) : isFieldActor(fieldDefinition) ? (
<ActorFieldDisplay />
) : isFieldEmails(fieldDefinition) ? (
<EmailsFieldDisplay />
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -103,6 +105,8 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldEmails(fieldDefinition) ? (
<EmailsFieldInput onCancel={onCancel} />
) : isFieldFullName(fieldDefinition) ? (
<FullNameFieldInput
onEnter={onEnter}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { isFieldAddress } from '@/object-record/record-field/types/guards/isFiel
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFieldDateValue';
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 { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
Expand Down Expand Up @@ -65,6 +67,9 @@ export const usePersistField = () => {
const fieldIsEmail =
isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist);

const fieldIsEmails =
isFieldEmails(fieldDefinition) && isFieldEmailsValue(valueToPersist);

const fieldIsDateTime =
isFieldDateTime(fieldDefinition) &&
isFieldDateTimeValue(valueToPersist);
Expand Down Expand Up @@ -119,6 +124,7 @@ export const usePersistField = () => {
fieldIsText ||
fieldIsBoolean ||
fieldIsEmail ||
fieldIsEmails ||
fieldIsRating ||
fieldIsNumber ||
fieldIsDateTime ||
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <EmailsDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -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<FieldEmailsValue>(
recordStoreFamilySelector({
recordId,
fieldName: fieldName,
}),
);

const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldEmailsValue>(`${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,
};
};
Original file line number Diff line number Diff line change
@@ -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<FieldEmailsValue | undefined>(
recordId,
fieldName,
);

return {
fieldDefinition,
fieldValue,
hotkeyScope,
};
};
Original file line number Diff line number Diff line change
@@ -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<string[]>(
() =>
[
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 (
<MultiItemFieldInput
items={emails}
onPersist={handlePersistEmails}
onCancel={onCancel}
placeholder="Email"
renderItem={({
value: email,
index,
handleEdit,
handleSetPrimary,
handleDelete,
}) => (
<EmailsFieldMenuItem
key={index}
dropdownId={`${hotkeyScope}-emails-${index}`}
isPrimary={index === 0}
email={email}
onEdit={handleEdit}
onSetAsPrimary={handleSetPrimary}
onDelete={handleDelete}
/>
)}
hotkeyScope={hotkeyScope}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<MultiItemFieldMenuItem
dropdownId={dropdownId}
isPrimary={isPrimary}
value={email}
onEdit={onEdit}
onSetAsPrimary={onSetAsPrimary}
onDelete={onDelete}
DisplayComponent={EmailDisplay}
/>
);
};
Loading

0 comments on commit 7a9a43b

Please sign in to comment.