Support for multiple values in the Phone field (#6882)
### Description - This is the first PR on Phones field; - We are introducing new field type(Phones) - We are Forbidding creation of Phone field - We Added support for filtering and sorting on Phones 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** 1. Checkout to TWNTY-6260 branch 2. Reset database using "npx nx database:reset twenty-server" command 3. Add custom field of type Phones in settings/data-model **Loom Video:**\ <https://www.loom.com/share/3c981260be254dcf851256d020a20ab0?sid=58507361-3a3b-452c-9de8-b5b1abda70ac> ### Refs #6260 Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
91187dcf82
commit
846953b0f4
@ -2527,6 +2527,7 @@ export enum FieldMetadataType {
|
|||||||
Number = 'NUMBER',
|
Number = 'NUMBER',
|
||||||
Numeric = 'NUMERIC',
|
Numeric = 'NUMERIC',
|
||||||
Phone = 'PHONE',
|
Phone = 'PHONE',
|
||||||
|
Phones = 'PHONES',
|
||||||
Position = 'POSITION',
|
Position = 'POSITION',
|
||||||
Rating = 'RATING',
|
Rating = 'RATING',
|
||||||
RawJson = 'RAW_JSON',
|
RawJson = 'RAW_JSON',
|
||||||
|
|||||||
@ -368,6 +368,7 @@ export enum FieldMetadataType {
|
|||||||
Number = 'NUMBER',
|
Number = 'NUMBER',
|
||||||
Numeric = 'NUMERIC',
|
Numeric = 'NUMERIC',
|
||||||
Phone = 'PHONE',
|
Phone = 'PHONE',
|
||||||
|
Phones = 'PHONES',
|
||||||
Position = 'POSITION',
|
Position = 'POSITION',
|
||||||
Rating = 'RATING',
|
Rating = 'RATING',
|
||||||
RawJson = 'RAW_JSON',
|
RawJson = 'RAW_JSON',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
@ -273,6 +273,7 @@ export enum FieldMetadataType {
|
|||||||
Number = 'NUMBER',
|
Number = 'NUMBER',
|
||||||
Numeric = 'NUMERIC',
|
Numeric = 'NUMERIC',
|
||||||
Phone = 'PHONE',
|
Phone = 'PHONE',
|
||||||
|
Phones = 'PHONES',
|
||||||
Position = 'POSITION',
|
Position = 'POSITION',
|
||||||
Rating = 'RATING',
|
Rating = 'RATING',
|
||||||
RawJson = 'RAW_JSON',
|
RawJson = 'RAW_JSON',
|
||||||
|
|||||||
@ -15,4 +15,5 @@ export const SORTABLE_FIELD_METADATA_TYPES = [
|
|||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.Actor,
|
FieldMetadataType.Actor,
|
||||||
FieldMetadataType.Links,
|
FieldMetadataType.Links,
|
||||||
|
FieldMetadataType.Phones,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.Rating,
|
FieldMetadataType.Rating,
|
||||||
FieldMetadataType.Actor,
|
FieldMetadataType.Actor,
|
||||||
|
FieldMetadataType.Phones,
|
||||||
].includes(field.type)
|
].includes(field.type)
|
||||||
) {
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
@ -83,6 +84,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
|
|||||||
return 'EMAILS';
|
return 'EMAILS';
|
||||||
case FieldMetadataType.Phone:
|
case FieldMetadataType.Phone:
|
||||||
return 'PHONE';
|
return 'PHONE';
|
||||||
|
case FieldMetadataType.Phones:
|
||||||
|
return 'PHONES';
|
||||||
case FieldMetadataType.Relation:
|
case FieldMetadataType.Relation:
|
||||||
return 'RELATION';
|
return 'RELATION';
|
||||||
case FieldMetadataType.Select:
|
case FieldMetadataType.Select:
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordG
|
|||||||
import {
|
import {
|
||||||
FieldEmailsValue,
|
FieldEmailsValue,
|
||||||
FieldLinksValue,
|
FieldLinksValue,
|
||||||
|
FieldPhonesValue,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { OrderBy } from '@/types/OrderBy';
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
@ -54,6 +55,14 @@ export const getOrderByForFieldMetadataType = (
|
|||||||
} satisfies { [key in keyof FieldEmailsValue]?: OrderBy },
|
} satisfies { [key in keyof FieldEmailsValue]?: OrderBy },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
case FieldMetadataType.Phones:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
[field.name]: {
|
||||||
|
primaryPhoneNumber: direction ?? 'AscNullsLast',
|
||||||
|
} satisfies { [key in keyof FieldPhonesValue]?: OrderBy },
|
||||||
|
},
|
||||||
|
];
|
||||||
default:
|
default:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -164,5 +164,14 @@ ${mapObjectMetadataToGraphQLQuery({
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldType === FieldMetadataType.Phones) {
|
||||||
|
return `${field.name}
|
||||||
|
{
|
||||||
|
primaryPhoneNumber
|
||||||
|
primaryPhoneCountryCode
|
||||||
|
additionalPhones
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -98,6 +98,11 @@ export type EmailsFilter = {
|
|||||||
primaryEmail?: StringFilter;
|
primaryEmail?: StringFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PhonesFilter = {
|
||||||
|
primaryPhoneNumber?: StringFilter;
|
||||||
|
primaryPhoneCountryCode?: StringFilter;
|
||||||
|
};
|
||||||
|
|
||||||
export type LeafFilter =
|
export type LeafFilter =
|
||||||
| UUIDFilter
|
| UUIDFilter
|
||||||
| StringFilter
|
| StringFilter
|
||||||
@ -110,6 +115,7 @@ export type LeafFilter =
|
|||||||
| AddressFilter
|
| AddressFilter
|
||||||
| LinksFilter
|
| LinksFilter
|
||||||
| ActorFilter
|
| ActorFilter
|
||||||
|
| PhonesFilter
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export type AndObjectRecordFilter = {
|
export type AndObjectRecordFilter = {
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
'LINKS',
|
'LINKS',
|
||||||
'ADDRESS',
|
'ADDRESS',
|
||||||
'ACTOR',
|
'ACTOR',
|
||||||
|
'PHONES',
|
||||||
].includes(filterDefinitionUsedInDropdown.type) && (
|
].includes(filterDefinitionUsedInDropdown.type) && (
|
||||||
<ObjectFilterDropdownTextSearchInput />
|
<ObjectFilterDropdownTextSearchInput />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export type FilterType =
|
export type FilterType =
|
||||||
| 'TEXT'
|
| 'TEXT'
|
||||||
| 'PHONE'
|
| 'PHONE'
|
||||||
|
| 'PHONES'
|
||||||
| 'EMAIL'
|
| 'EMAIL'
|
||||||
| 'EMAILS'
|
| 'EMAILS'
|
||||||
| 'DATE_TIME'
|
| 'DATE_TIME'
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export const getOperandsForFilterType = (
|
|||||||
case 'LINK':
|
case 'LINK':
|
||||||
case 'LINKS':
|
case 'LINKS':
|
||||||
case 'ACTOR':
|
case 'ACTOR':
|
||||||
|
case 'PHONES':
|
||||||
return [
|
return [
|
||||||
ViewFilterOperand.Contains,
|
ViewFilterOperand.Contains,
|
||||||
ViewFilterOperand.DoesNotContain,
|
ViewFilterOperand.DoesNotContain,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/displ
|
|||||||
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
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 { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay';
|
||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||||
|
import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay';
|
||||||
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
||||||
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
||||||
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
|
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
|
||||||
@ -13,6 +14,7 @@ import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFiel
|
|||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
|
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
@ -104,5 +106,7 @@ export const FieldDisplay = () => {
|
|||||||
<ActorFieldDisplay />
|
<ActorFieldDisplay />
|
||||||
) : isFieldEmails(fieldDefinition) ? (
|
) : isFieldEmails(fieldDefinition) ? (
|
||||||
<EmailsFieldDisplay />
|
<EmailsFieldDisplay />
|
||||||
|
) : isFieldPhones(fieldDefinition) ? (
|
||||||
|
<PhonesFieldDisplay />
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { EmailsFieldInput } from '@/object-record/record-field/meta-types/input/
|
|||||||
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
|
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 { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
|
||||||
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
||||||
|
import { PhonesFieldInput } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
|
||||||
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
||||||
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput';
|
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput';
|
||||||
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
||||||
@ -16,6 +17,7 @@ import { isFieldEmails } from '@/object-record/record-field/types/guards/isField
|
|||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
|
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
@ -89,6 +91,8 @@ export const FieldInput = ({
|
|||||||
onTab={onTab}
|
onTab={onTab}
|
||||||
onShiftTab={onShiftTab}
|
onShiftTab={onShiftTab}
|
||||||
/>
|
/>
|
||||||
|
) : isFieldPhones(fieldDefinition) ? (
|
||||||
|
<PhonesFieldInput onCancel={onCancel} />
|
||||||
) : isFieldText(fieldDefinition) ? (
|
) : isFieldText(fieldDefinition) ? (
|
||||||
<TextFieldInput
|
<TextFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldL
|
|||||||
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||||
|
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
|
import { isFieldPhonesValue } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
@ -104,6 +106,9 @@ export const usePersistField = () => {
|
|||||||
const fieldIsPhone =
|
const fieldIsPhone =
|
||||||
isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist);
|
isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist);
|
||||||
|
|
||||||
|
const fieldIsPhones =
|
||||||
|
isFieldPhones(fieldDefinition) && isFieldPhonesValue(valueToPersist);
|
||||||
|
|
||||||
const fieldIsSelect =
|
const fieldIsSelect =
|
||||||
isFieldSelect(fieldDefinition) && isFieldSelectValue(valueToPersist);
|
isFieldSelect(fieldDefinition) && isFieldSelectValue(valueToPersist);
|
||||||
|
|
||||||
@ -130,6 +135,7 @@ export const usePersistField = () => {
|
|||||||
fieldIsDateTime ||
|
fieldIsDateTime ||
|
||||||
fieldIsDate ||
|
fieldIsDate ||
|
||||||
fieldIsPhone ||
|
fieldIsPhone ||
|
||||||
|
fieldIsPhones ||
|
||||||
fieldIsLink ||
|
fieldIsLink ||
|
||||||
fieldIsLinks ||
|
fieldIsLinks ||
|
||||||
fieldIsCurrency ||
|
fieldIsCurrency ||
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
|
import { usePhonesFieldDisplay } from '@/object-record/record-field/meta-types/hooks/usePhonesFieldDisplay';
|
||||||
|
import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay';
|
||||||
|
|
||||||
|
export const PhonesFieldDisplay = () => {
|
||||||
|
const { fieldValue } = usePhonesFieldDisplay();
|
||||||
|
|
||||||
|
const { isFocused } = useFieldFocus();
|
||||||
|
|
||||||
|
return <PhonesDisplay value={fieldValue} isFocused={isFocused} />;
|
||||||
|
};
|
||||||
@ -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 { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
|
import { phonesSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
|
||||||
|
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 usePhonesField = () => {
|
||||||
|
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||||
|
|
||||||
|
assertFieldMetadata(FieldMetadataType.Phones, isFieldPhones, fieldDefinition);
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
const [fieldValue, setFieldValue] = useRecoilState<FieldPhonesValue>(
|
||||||
|
recordStoreFamilySelector({
|
||||||
|
recordId,
|
||||||
|
fieldName: fieldName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { setDraftValue, getDraftValueSelector } =
|
||||||
|
useRecordFieldInput<FieldPhonesValue>(`${recordId}-${fieldName}`);
|
||||||
|
|
||||||
|
const draftValue = useRecoilValue(getDraftValueSelector());
|
||||||
|
|
||||||
|
const persistField = usePersistField();
|
||||||
|
|
||||||
|
const persistPhonesField = (nextValue: FieldPhonesValue) => {
|
||||||
|
try {
|
||||||
|
persistField(phonesSchema.parse(nextValue));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldDefinition,
|
||||||
|
fieldValue,
|
||||||
|
draftValue,
|
||||||
|
setDraftValue,
|
||||||
|
setFieldValue,
|
||||||
|
hotkeyScope,
|
||||||
|
persistPhonesField,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
|
|
||||||
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
|
||||||
|
export const usePhonesFieldDisplay = () => {
|
||||||
|
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
const fieldValue = useRecordFieldValue<FieldPhonesValue | undefined>(
|
||||||
|
recordId,
|
||||||
|
fieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldDefinition,
|
||||||
|
fieldValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/us
|
|||||||
import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem';
|
import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||||
|
|
||||||
type EmailsFieldInputProps = {
|
type EmailsFieldInputProps = {
|
||||||
@ -34,6 +35,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
|
|||||||
onPersist={handlePersistEmails}
|
onPersist={handlePersistEmails}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
|
fieldMetadataType={FieldMetadataType.Emails}
|
||||||
renderItem={({
|
renderItem={({
|
||||||
value: email,
|
value: email,
|
||||||
index,
|
index,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useLinksField } from '@/object-record/record-field/meta-types/hooks/use
|
|||||||
import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem';
|
import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
|
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
|
||||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
|
|||||||
onPersist={handlePersistLinks}
|
onPersist={handlePersistLinks}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
|
fieldMetadataType={FieldMetadataType.Links}
|
||||||
validateInput={(input) => absoluteUrlSchema.safeParse(input).success}
|
validateInput={(input) => absoluteUrlSchema.safeParse(input).success}
|
||||||
formatInput={(input) => ({ url: input, label: '' })}
|
formatInput={(input) => ({ url: input, label: '' })}
|
||||||
renderItem={({
|
renderItem={({
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { IconCheck, IconPlus } from 'twenty-ui';
|
import { IconCheck, IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
import {
|
||||||
|
DropdownMenuInput,
|
||||||
|
DropdownMenuInputProps,
|
||||||
|
} from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||||
import { toSpliced } from '~/utils/array/toSpliced';
|
import { toSpliced } from '~/utils/array/toSpliced';
|
||||||
|
|
||||||
@ -35,6 +40,8 @@ type MultiItemFieldInputProps<T> = {
|
|||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
|
fieldMetadataType: FieldMetadataType;
|
||||||
|
renderInput?: DropdownMenuInputProps['renderInput'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultiItemFieldInput = <T,>({
|
export const MultiItemFieldInput = <T,>({
|
||||||
@ -46,6 +53,8 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
formatInput,
|
formatInput,
|
||||||
renderItem,
|
renderItem,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
|
fieldMetadataType,
|
||||||
|
renderInput,
|
||||||
}: MultiItemFieldInputProps<T>) => {
|
}: MultiItemFieldInputProps<T>) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const handleDropdownClose = () => {
|
const handleDropdownClose = () => {
|
||||||
@ -70,9 +79,25 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditButtonClick = (index: number) => {
|
const handleEditButtonClick = (index: number) => {
|
||||||
const item = items[index] as { label: string; url: string };
|
let item;
|
||||||
|
switch (fieldMetadataType) {
|
||||||
|
case FieldMetadataType.Links:
|
||||||
|
item = items[index] as { label: string; url: string };
|
||||||
|
setInputValue(item.url || '');
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.Phones:
|
||||||
|
item = items[index] as PhoneRecord;
|
||||||
|
setInputValue(item.countryCode + item.number);
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.Emails:
|
||||||
|
item = items[index] as string;
|
||||||
|
setInputValue(item);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported field type: ${fieldMetadataType}`);
|
||||||
|
}
|
||||||
|
|
||||||
setItemToEditIndex(index);
|
setItemToEditIndex(index);
|
||||||
setInputValue(item.url || '');
|
|
||||||
setIsInputDisplayed(true);
|
setIsInputDisplayed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,6 +157,16 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
hotkeyScope={hotkeyScope}
|
hotkeyScope={hotkeyScope}
|
||||||
|
renderInput={
|
||||||
|
renderInput
|
||||||
|
? (props) =>
|
||||||
|
renderInput({
|
||||||
|
...props,
|
||||||
|
onChange: (newValue) =>
|
||||||
|
setInputValue(newValue as unknown as string),
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onChange={(event) => setInputValue(event.target.value)}
|
onChange={(event) => setInputValue(event.target.value)}
|
||||||
onEnter={handleSubmitInput}
|
onEnter={handleSubmitInput}
|
||||||
rightComponent={
|
rightComponent={
|
||||||
|
|||||||
@ -0,0 +1,134 @@
|
|||||||
|
import { usePhonesField } from '@/object-record/record-field/meta-types/hooks/usePhonesField';
|
||||||
|
import { PhonesFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/PhonesFieldMenuItem';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { E164Number, parsePhoneNumber } from 'libphonenumber-js';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import ReactPhoneNumberInput from 'react-phone-number-input';
|
||||||
|
import 'react-phone-number-input/style.css';
|
||||||
|
import { isDefined, TEXT_INPUT_STYLE } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||||
|
|
||||||
|
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
height: 32px;
|
||||||
|
${TEXT_INPUT_STYLE}
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.PhoneInputInput {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
|
||||||
|
&::placeholder,
|
||||||
|
&::-webkit-input-placeholder {
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||||
|
`;
|
||||||
|
|
||||||
|
type PhonesFieldInputProps = {
|
||||||
|
onCancel?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
|
||||||
|
const { persistPhonesField, hotkeyScope, fieldValue } = usePhonesField();
|
||||||
|
|
||||||
|
const phones = useMemo<{ number: string; countryCode: string }[]>(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
fieldValue.primaryPhoneNumber
|
||||||
|
? {
|
||||||
|
number: fieldValue.primaryPhoneNumber,
|
||||||
|
countryCode: fieldValue.primaryPhoneCountryCode,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
...(fieldValue.additionalPhones ?? []),
|
||||||
|
].filter(isDefined),
|
||||||
|
[
|
||||||
|
fieldValue.primaryPhoneNumber,
|
||||||
|
fieldValue.primaryPhoneCountryCode,
|
||||||
|
fieldValue.additionalPhones,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePersistPhones = (
|
||||||
|
updatedPhones: { number: string; countryCode: string }[],
|
||||||
|
) => {
|
||||||
|
const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones;
|
||||||
|
persistPhonesField({
|
||||||
|
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
|
||||||
|
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
|
||||||
|
additionalPhones: nextAdditionalPhones,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiItemFieldInput
|
||||||
|
items={phones}
|
||||||
|
onPersist={handlePersistPhones}
|
||||||
|
onCancel={onCancel}
|
||||||
|
placeholder="Phone"
|
||||||
|
fieldMetadataType={FieldMetadataType.Phones}
|
||||||
|
formatInput={(input) => {
|
||||||
|
const phone = parsePhoneNumber(input);
|
||||||
|
if (phone !== undefined) {
|
||||||
|
return {
|
||||||
|
number: phone.nationalNumber,
|
||||||
|
countryCode: `+${phone.countryCallingCode}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
number: '',
|
||||||
|
countryCode: '',
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
renderItem={({
|
||||||
|
value: phone,
|
||||||
|
index,
|
||||||
|
handleEdit,
|
||||||
|
handleSetPrimary,
|
||||||
|
handleDelete,
|
||||||
|
}) => (
|
||||||
|
<PhonesFieldMenuItem
|
||||||
|
key={index}
|
||||||
|
dropdownId={`${hotkeyScope}-phones-${index}`}
|
||||||
|
isPrimary={index === 0}
|
||||||
|
phone={phone}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onSetAsPrimary={handleSetPrimary}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderInput={({ value, onChange, autoFocus, placeholder }) => {
|
||||||
|
return (
|
||||||
|
<StyledCustomPhoneInput
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value as E164Number}
|
||||||
|
onChange={onChange as unknown as (newValue: E164Number) => void}
|
||||||
|
international={true}
|
||||||
|
withCountryCallingCode={true}
|
||||||
|
countrySelectComponent={PhoneCountryPickerDropdownButton}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
hotkeyScope={hotkeyScope}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { PhoneDisplay } from '@/ui/field/display/components/PhoneDisplay';
|
||||||
|
import { MultiItemFieldMenuItem } from './MultiItemFieldMenuItem';
|
||||||
|
|
||||||
|
type PhonesFieldMenuItemProps = {
|
||||||
|
dropdownId: string;
|
||||||
|
isPrimary?: boolean;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onSetAsPrimary?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
phone: { number: string; countryCode: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PhonesFieldMenuItem = ({
|
||||||
|
dropdownId,
|
||||||
|
isPrimary,
|
||||||
|
onEdit,
|
||||||
|
onSetAsPrimary,
|
||||||
|
onDelete,
|
||||||
|
phone,
|
||||||
|
}: PhonesFieldMenuItemProps) => {
|
||||||
|
return (
|
||||||
|
<MultiItemFieldMenuItem
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
isPrimary={isPrimary}
|
||||||
|
value={phone.countryCode + phone.number}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onSetAsPrimary={onSetAsPrimary}
|
||||||
|
onDelete={onDelete}
|
||||||
|
DisplayComponent={PhoneDisplay}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
FieldLinkValue,
|
FieldLinkValue,
|
||||||
FieldMultiSelectValue,
|
FieldMultiSelectValue,
|
||||||
FieldNumberValue,
|
FieldNumberValue,
|
||||||
|
FieldPhonesValue,
|
||||||
FieldPhoneValue,
|
FieldPhoneValue,
|
||||||
FieldRatingValue,
|
FieldRatingValue,
|
||||||
FieldRelationFromManyValue,
|
FieldRelationFromManyValue,
|
||||||
@ -20,12 +21,18 @@ import {
|
|||||||
FieldSelectValue,
|
FieldSelectValue,
|
||||||
FieldTextValue,
|
FieldTextValue,
|
||||||
FieldUUidValue,
|
FieldUUidValue,
|
||||||
|
PhoneRecord,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
|
||||||
export type FieldTextDraftValue = string;
|
export type FieldTextDraftValue = string;
|
||||||
export type FieldNumberDraftValue = string;
|
export type FieldNumberDraftValue = string;
|
||||||
export type FieldDateTimeDraftValue = string;
|
export type FieldDateTimeDraftValue = string;
|
||||||
export type FieldPhoneDraftValue = string;
|
export type FieldPhoneDraftValue = string;
|
||||||
|
export type FieldPhonesDraftValue = {
|
||||||
|
primaryPhoneNumber: string;
|
||||||
|
primaryPhoneCountryCode: string;
|
||||||
|
additionalPhones?: PhoneRecord[] | null;
|
||||||
|
};
|
||||||
export type FieldEmailDraftValue = string;
|
export type FieldEmailDraftValue = string;
|
||||||
export type FieldEmailsDraftValue = {
|
export type FieldEmailsDraftValue = {
|
||||||
primaryEmail: string;
|
primaryEmail: string;
|
||||||
@ -75,32 +82,34 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldBooleanValue
|
? FieldBooleanValue
|
||||||
: FieldValue extends FieldPhoneValue
|
: FieldValue extends FieldPhoneValue
|
||||||
? FieldPhoneDraftValue
|
? FieldPhoneDraftValue
|
||||||
: FieldValue extends FieldEmailValue
|
: FieldValue extends FieldPhonesValue
|
||||||
? FieldEmailDraftValue
|
? FieldPhonesDraftValue
|
||||||
: FieldValue extends FieldEmailsValue
|
: FieldValue extends FieldEmailValue
|
||||||
? FieldEmailsDraftValue
|
? FieldEmailDraftValue
|
||||||
: FieldValue extends FieldLinkValue
|
: FieldValue extends FieldEmailsValue
|
||||||
? FieldLinkDraftValue
|
? FieldEmailsDraftValue
|
||||||
: FieldValue extends FieldLinksValue
|
: FieldValue extends FieldLinkValue
|
||||||
? FieldLinksDraftValue
|
? FieldLinkDraftValue
|
||||||
: FieldValue extends FieldCurrencyValue
|
: FieldValue extends FieldLinksValue
|
||||||
? FieldCurrencyDraftValue
|
? FieldLinksDraftValue
|
||||||
: FieldValue extends FieldFullNameValue
|
: FieldValue extends FieldCurrencyValue
|
||||||
? FieldFullNameDraftValue
|
? FieldCurrencyDraftValue
|
||||||
: FieldValue extends FieldRatingValue
|
: FieldValue extends FieldFullNameValue
|
||||||
? FieldRatingValue
|
? FieldFullNameDraftValue
|
||||||
: FieldValue extends FieldSelectValue
|
: FieldValue extends FieldRatingValue
|
||||||
? FieldSelectDraftValue
|
? FieldRatingValue
|
||||||
: FieldValue extends FieldMultiSelectValue
|
: FieldValue extends FieldSelectValue
|
||||||
? FieldMultiSelectDraftValue
|
? FieldSelectDraftValue
|
||||||
: FieldValue extends FieldRelationToOneValue
|
: FieldValue extends FieldMultiSelectValue
|
||||||
? FieldRelationDraftValue
|
? FieldMultiSelectDraftValue
|
||||||
: FieldValue extends FieldRelationFromManyValue
|
: FieldValue extends FieldRelationToOneValue
|
||||||
? FieldRelationManyDraftValue
|
? FieldRelationDraftValue
|
||||||
: FieldValue extends FieldAddressValue
|
: FieldValue extends FieldRelationFromManyValue
|
||||||
? FieldAddressDraftValue
|
? FieldRelationManyDraftValue
|
||||||
: FieldValue extends FieldJsonValue
|
: FieldValue extends FieldAddressValue
|
||||||
? FieldJsonDraftValue
|
? FieldAddressDraftValue
|
||||||
: FieldValue extends FieldActorValue
|
: FieldValue extends FieldJsonValue
|
||||||
? FieldActorDraftValue
|
? FieldJsonDraftValue
|
||||||
: never;
|
: FieldValue extends FieldActorValue
|
||||||
|
? FieldActorDraftValue
|
||||||
|
: never;
|
||||||
|
|||||||
@ -157,6 +157,11 @@ export type FieldActorMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldPhonesMetadata = {
|
||||||
|
objectMetadataNameSingular?: string;
|
||||||
|
fieldName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldMetadata =
|
export type FieldMetadata =
|
||||||
| FieldBooleanMetadata
|
| FieldBooleanMetadata
|
||||||
| FieldCurrencyMetadata
|
| FieldCurrencyMetadata
|
||||||
@ -230,3 +235,11 @@ export type FieldActorValue = {
|
|||||||
workspaceMemberId?: string;
|
workspaceMemberId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PhoneRecord = { number: string; countryCode: string };
|
||||||
|
|
||||||
|
export type FieldPhonesValue = {
|
||||||
|
primaryPhoneNumber: string;
|
||||||
|
primaryPhoneCountryCode: string;
|
||||||
|
additionalPhones?: PhoneRecord[] | null;
|
||||||
|
};
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
FieldMultiSelectMetadata,
|
FieldMultiSelectMetadata,
|
||||||
FieldNumberMetadata,
|
FieldNumberMetadata,
|
||||||
FieldPhoneMetadata,
|
FieldPhoneMetadata,
|
||||||
|
FieldPhonesMetadata,
|
||||||
FieldRatingMetadata,
|
FieldRatingMetadata,
|
||||||
FieldRawJsonMetadata,
|
FieldRawJsonMetadata,
|
||||||
FieldRelationMetadata,
|
FieldRelationMetadata,
|
||||||
@ -55,21 +56,23 @@ type AssertFieldMetadataFunction = <
|
|||||||
? FieldNumberMetadata
|
? FieldNumberMetadata
|
||||||
: E extends 'PHONE'
|
: E extends 'PHONE'
|
||||||
? FieldPhoneMetadata
|
? FieldPhoneMetadata
|
||||||
: E extends 'RELATION'
|
: E extends 'PHONES'
|
||||||
? FieldRelationMetadata
|
? FieldPhonesMetadata
|
||||||
: E extends 'TEXT'
|
: E extends 'RELATION'
|
||||||
? FieldTextMetadata
|
? FieldRelationMetadata
|
||||||
: E extends 'UUID'
|
: E extends 'TEXT'
|
||||||
? FieldUuidMetadata
|
? FieldTextMetadata
|
||||||
: E extends 'ADDRESS'
|
: E extends 'UUID'
|
||||||
? FieldAddressMetadata
|
? FieldUuidMetadata
|
||||||
: E extends 'RAW_JSON'
|
: E extends 'ADDRESS'
|
||||||
? FieldRawJsonMetadata
|
? FieldAddressMetadata
|
||||||
: E extends 'RICH_TEXT'
|
: E extends 'RAW_JSON'
|
||||||
? FieldTextMetadata
|
? FieldRawJsonMetadata
|
||||||
: E extends 'ACTOR'
|
: E extends 'RICH_TEXT'
|
||||||
? FieldActorMetadata
|
? FieldTextMetadata
|
||||||
: never,
|
: E extends 'ACTOR'
|
||||||
|
? FieldActorMetadata
|
||||||
|
: never,
|
||||||
>(
|
>(
|
||||||
fieldType: E,
|
fieldType: E,
|
||||||
fieldTypeGuard: (
|
fieldTypeGuard: (
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldMetadata, FieldPhonesMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const isFieldPhones = (
|
||||||
|
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||||
|
): field is FieldDefinition<FieldPhonesMetadata> =>
|
||||||
|
field.type === FieldMetadataType.Phones;
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FieldPhonesValue } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const phonesSchema = z.object({
|
||||||
|
primaryPhoneNumber: z.string(),
|
||||||
|
primaryPhoneCountryCode: z.string(),
|
||||||
|
additionalPhones: z
|
||||||
|
.array(z.object({ number: z.string(), countryCode: z.string() }))
|
||||||
|
.nullable(),
|
||||||
|
}) satisfies z.ZodType<FieldPhonesValue>;
|
||||||
|
|
||||||
|
export const isFieldPhonesValue = (
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is FieldPhonesValue => phonesSchema.safeParse(fieldValue).success;
|
||||||
@ -6,6 +6,7 @@ import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guar
|
|||||||
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
|
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
@ -31,7 +32,8 @@ export const getFieldButtonIcon = (
|
|||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
||||||
'workspaceMember') ||
|
'workspaceMember') ||
|
||||||
isFieldLinks(fieldDefinition) ||
|
isFieldLinks(fieldDefinition) ||
|
||||||
isFieldEmails(fieldDefinition)
|
isFieldEmails(fieldDefinition) ||
|
||||||
|
isFieldPhones(fieldDefinition)
|
||||||
) {
|
) {
|
||||||
return IconPencil;
|
return IconPencil;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
|
|||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
import { isFieldPhone } from '@/object-record/record-field/types/guards/isFieldPhone';
|
import { isFieldPhone } from '@/object-record/record-field/types/guards/isFieldPhone';
|
||||||
|
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
|
||||||
|
import { isFieldPhonesValue } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
|
||||||
import { isFieldPosition } from '@/object-record/record-field/types/guards/isFieldPosition';
|
import { isFieldPosition } from '@/object-record/record-field/types/guards/isFieldPosition';
|
||||||
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
@ -128,6 +130,13 @@ export const isFieldValueEmpty = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldPhones(fieldDefinition)) {
|
||||||
|
return (
|
||||||
|
!isFieldPhonesValue(fieldValue) ||
|
||||||
|
isValueEmpty(fieldValue.primaryPhoneNumber)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
LinksFilter,
|
LinksFilter,
|
||||||
NotObjectRecordFilter,
|
NotObjectRecordFilter,
|
||||||
OrObjectRecordFilter,
|
OrObjectRecordFilter,
|
||||||
|
PhonesFilter,
|
||||||
RecordGqlOperationFilter,
|
RecordGqlOperationFilter,
|
||||||
StringFilter,
|
StringFilter,
|
||||||
URLFilter,
|
URLFilter,
|
||||||
@ -282,6 +283,26 @@ export const isRecordMatchingFilter = ({
|
|||||||
value: record[filterKey].primaryEmail,
|
value: record[filterKey].primaryEmail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Phones: {
|
||||||
|
const phonesFilter = filterValue as PhonesFilter;
|
||||||
|
|
||||||
|
const keys: (keyof PhonesFilter)[] = [
|
||||||
|
'primaryPhoneNumber',
|
||||||
|
'primaryPhoneCountryCode',
|
||||||
|
];
|
||||||
|
|
||||||
|
return keys.some((key) => {
|
||||||
|
const value = phonesFilter[key];
|
||||||
|
if (value === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isMatchingStringFilter({
|
||||||
|
stringFilter: value,
|
||||||
|
value: record[filterKey][key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
case FieldMetadataType.Relation: {
|
case FieldMetadataType.Relation: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
|
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
|
||||||
|
|||||||
@ -52,6 +52,19 @@ const applyEmptyFilters = (
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'PHONES': {
|
||||||
|
const phonesFilter = generateILikeFiltersForCompositeFields(
|
||||||
|
'',
|
||||||
|
correspondingField.name,
|
||||||
|
['primaryPhoneNumber', 'primaryPhoneCountryCode'],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
emptyRecordFilter = {
|
||||||
|
and: phonesFilter,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'CURRENCY':
|
case 'CURRENCY':
|
||||||
emptyRecordFilter = {
|
emptyRecordFilter = {
|
||||||
or: [
|
or: [
|
||||||
@ -870,6 +883,43 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'PHONES': {
|
||||||
|
const phonesFilters = generateILikeFiltersForCompositeFields(
|
||||||
|
rawUIFilter.value,
|
||||||
|
correspondingField.name,
|
||||||
|
['primaryPhoneNumber', 'primaryPhoneCountryCode'],
|
||||||
|
);
|
||||||
|
switch (rawUIFilter.operand) {
|
||||||
|
case ViewFilterOperand.Contains:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
or: phonesFilters,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ViewFilterOperand.DoesNotContain:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
and: phonesFilters.map((filter) => {
|
||||||
|
return {
|
||||||
|
not: filter,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
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:
|
default:
|
||||||
throw new Error('Unknown filter type');
|
throw new Error('Unknown filter type');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,13 @@ export const generateEmptyFieldValue = (
|
|||||||
name: '',
|
name: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Phones: {
|
||||||
|
return {
|
||||||
|
primaryPhoneNumber: '',
|
||||||
|
primaryPhoneCountryCode: '',
|
||||||
|
additionalPhones: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Unhandled FieldMetadataType');
|
throw new Error('Unhandled FieldMetadataType');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,6 +130,15 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = {
|
|||||||
exampleValue: '+1234-567-890',
|
exampleValue: '+1234-567-890',
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
},
|
},
|
||||||
|
[FieldMetadataType.Phones]: {
|
||||||
|
label: 'Phones',
|
||||||
|
Icon: IconPhone,
|
||||||
|
exampleValue: {
|
||||||
|
primaryPhoneNumber: '234-567-890',
|
||||||
|
primaryPhoneCountryCode: '+1',
|
||||||
|
},
|
||||||
|
category: 'Basic',
|
||||||
|
},
|
||||||
[FieldMetadataType.Rating]: {
|
[FieldMetadataType.Rating]: {
|
||||||
label: 'Rating',
|
label: 'Rating',
|
||||||
Icon: IconTwentyStar,
|
Icon: IconTwentyStar,
|
||||||
|
|||||||
@ -92,6 +92,7 @@ const previewableTypes = [
|
|||||||
FieldMetadataType.MultiSelect,
|
FieldMetadataType.MultiSelect,
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Phone,
|
FieldMetadataType.Phone,
|
||||||
|
FieldMetadataType.Phones,
|
||||||
FieldMetadataType.Rating,
|
FieldMetadataType.Rating,
|
||||||
FieldMetadataType.RawJson,
|
FieldMetadataType.RawJson,
|
||||||
FieldMetadataType.Relation,
|
FieldMetadataType.Relation,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const PhoneDisplay = ({ value }: PhoneDisplayProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const URI = parsedPhoneNumber.getURI();
|
const URI = parsedPhoneNumber.getURI();
|
||||||
const formattedNational = parsedPhoneNumber?.formatNational();
|
const formatedPhoneNumber = parsedPhoneNumber.formatInternational();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContactLink
|
<ContactLink
|
||||||
@ -33,7 +33,7 @@ export const PhoneDisplay = ({ value }: PhoneDisplayProps) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formattedNational || value}
|
{formatedPhoneNumber || value}
|
||||||
</ContactLink>
|
</ContactLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { THEME_COMMON } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { FieldPhonesValue } 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 { parsePhoneNumber } from 'libphonenumber-js';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type PhonesDisplayProps = {
|
||||||
|
value?: FieldPhonesValue;
|
||||||
|
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 PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
|
||||||
|
const phones = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
value?.primaryPhoneNumber
|
||||||
|
? {
|
||||||
|
number: value.primaryPhoneNumber,
|
||||||
|
countryCode: value.primaryPhoneCountryCode,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
...(value?.additionalPhones ?? []),
|
||||||
|
]
|
||||||
|
.filter(isDefined)
|
||||||
|
.map(({ number, countryCode }) => {
|
||||||
|
return {
|
||||||
|
number,
|
||||||
|
countryCode,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
value?.primaryPhoneNumber,
|
||||||
|
value?.primaryPhoneCountryCode,
|
||||||
|
value?.additionalPhones,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return isFocused ? (
|
||||||
|
<ExpandableList isChipCountDisplayed>
|
||||||
|
{phones.map(({ number, countryCode }, index) => {
|
||||||
|
const parsedPhone = parsePhoneNumber(countryCode + number);
|
||||||
|
const URI = parsedPhone.getURI();
|
||||||
|
return (
|
||||||
|
<RoundedLink
|
||||||
|
key={index}
|
||||||
|
href={URI}
|
||||||
|
label={parsedPhone.formatInternational()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ExpandableList>
|
||||||
|
) : (
|
||||||
|
<StyledContainer>
|
||||||
|
{phones.map(({ number, countryCode }, index) => {
|
||||||
|
const parsedPhone = parsePhoneNumber(countryCode + number);
|
||||||
|
const URI = parsedPhone.getURI();
|
||||||
|
return (
|
||||||
|
<RoundedLink
|
||||||
|
key={index}
|
||||||
|
href={URI}
|
||||||
|
label={parsedPhone.formatInternational()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { forwardRef, InputHTMLAttributes, ReactNode, useRef } from 'react';
|
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { forwardRef, InputHTMLAttributes, ReactNode, useRef } from 'react';
|
||||||
|
import 'react-phone-number-input/style.css';
|
||||||
import { RGBA, TEXT_INPUT_STYLE } from 'twenty-ui';
|
import { RGBA, TEXT_INPUT_STYLE } from 'twenty-ui';
|
||||||
|
|
||||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
@ -43,7 +44,9 @@ const StyledRightContainer = styled.div`
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type DropdownMenuInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
type HTMLInputProps = InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
|
export type DropdownMenuInputProps = HTMLInputProps & {
|
||||||
hotkeyScope?: string;
|
hotkeyScope?: string;
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
onEnter?: () => void;
|
onEnter?: () => void;
|
||||||
@ -51,6 +54,12 @@ type DropdownMenuInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
|||||||
onShiftTab?: () => void;
|
onShiftTab?: () => void;
|
||||||
onTab?: () => void;
|
onTab?: () => void;
|
||||||
rightComponent?: ReactNode;
|
rightComponent?: ReactNode;
|
||||||
|
renderInput?: (props: {
|
||||||
|
value: HTMLInputProps['value'];
|
||||||
|
onChange: HTMLInputProps['onChange'];
|
||||||
|
autoFocus: HTMLInputProps['autoFocus'];
|
||||||
|
placeholder: HTMLInputProps['placeholder'];
|
||||||
|
}) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownMenuInput = forwardRef<
|
export const DropdownMenuInput = forwardRef<
|
||||||
@ -71,6 +80,7 @@ export const DropdownMenuInput = forwardRef<
|
|||||||
onShiftTab,
|
onShiftTab,
|
||||||
onTab,
|
onTab,
|
||||||
rightComponent,
|
rightComponent,
|
||||||
|
renderInput,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@ -90,14 +100,23 @@ export const DropdownMenuInput = forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledInputContainer className={className}>
|
<StyledInputContainer className={className}>
|
||||||
<StyledInput
|
{renderInput ? (
|
||||||
autoFocus={autoFocus}
|
renderInput({
|
||||||
value={value}
|
value,
|
||||||
placeholder={placeholder}
|
onChange,
|
||||||
onChange={onChange}
|
autoFocus,
|
||||||
ref={combinedRef}
|
placeholder,
|
||||||
withRightComponent={!!rightComponent}
|
})
|
||||||
/>
|
) : (
|
||||||
|
<StyledInput
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={combinedRef}
|
||||||
|
withRightComponent={!!rightComponent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!!rightComponent && (
|
{!!rightComponent && (
|
||||||
<StyledRightContainer>{rightComponent}</StyledRightContainer>
|
<StyledRightContainer>{rightComponent}</StyledRightContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -166,6 +166,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
FieldMetadataType.RichText,
|
FieldMetadataType.RichText,
|
||||||
FieldMetadataType.Actor,
|
FieldMetadataType.Actor,
|
||||||
FieldMetadataType.Email,
|
FieldMetadataType.Email,
|
||||||
|
FieldMetadataType.Phone,
|
||||||
] as const
|
] as const
|
||||||
).filter(isDefined);
|
).filter(isDefined);
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency';
|
|||||||
export const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress';
|
export const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress';
|
||||||
export const FIELD_ACTOR_MOCK_NAME = 'fieldActor';
|
export const FIELD_ACTOR_MOCK_NAME = 'fieldActor';
|
||||||
export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName';
|
export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName';
|
||||||
|
export const FIELD_PHONES_MOCK_NAME = 'fieldPhones';
|
||||||
|
|
||||||
export const fieldNumberMock = {
|
export const fieldNumberMock = {
|
||||||
name: 'fieldNumber',
|
name: 'fieldNumber',
|
||||||
@ -221,6 +222,7 @@ const fieldActorMock = {
|
|||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldEmailsMock = {
|
const fieldEmailsMock = {
|
||||||
name: 'fieldEmails',
|
name: 'fieldEmails',
|
||||||
type: FieldMetadataType.EMAILS,
|
type: FieldMetadataType.EMAILS,
|
||||||
@ -228,10 +230,24 @@ const fieldEmailsMock = {
|
|||||||
defaultValue: [{ primaryEmail: '', additionalEmails: {} }],
|
defaultValue: [{ primaryEmail: '', additionalEmails: {} }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fieldPhonesMock = {
|
||||||
|
name: FIELD_PHONES_MOCK_NAME,
|
||||||
|
type: FieldMetadataType.PHONES,
|
||||||
|
isNullable: false,
|
||||||
|
defaultValue: [
|
||||||
|
{
|
||||||
|
primaryPhoneNumber: '',
|
||||||
|
primaryPhoneCountryCode: '',
|
||||||
|
additionalPhones: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export const fields = [
|
export const fields = [
|
||||||
fieldUuidMock,
|
fieldUuidMock,
|
||||||
fieldTextMock,
|
fieldTextMock,
|
||||||
fieldPhoneMock,
|
fieldPhoneMock,
|
||||||
|
fieldPhonesMock,
|
||||||
fieldEmailMock,
|
fieldEmailMock,
|
||||||
fieldEmailsMock,
|
fieldEmailsMock,
|
||||||
fieldDateTimeMock,
|
fieldDateTimeMock,
|
||||||
|
|||||||
@ -152,5 +152,14 @@ export const mapFieldMetadataToGraphqlQuery = (
|
|||||||
additionalEmails
|
additionalEmails
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
} else if (fieldType === FieldMetadataType.PHONES) {
|
||||||
|
return `
|
||||||
|
${field.name}
|
||||||
|
{
|
||||||
|
primaryPhoneNumber
|
||||||
|
primaryPhoneCountryCode
|
||||||
|
additionalPhones
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -33,6 +33,20 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldPhone: {
|
fieldPhone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
fieldPhones: {
|
||||||
|
properties: {
|
||||||
|
additionalPhones: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
primaryPhoneCountryCode: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
primaryPhoneNumber: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
fieldEmail: {
|
fieldEmail: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'email',
|
format: 'email',
|
||||||
@ -195,6 +209,20 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldPhone: {
|
fieldPhone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
fieldPhones: {
|
||||||
|
properties: {
|
||||||
|
additionalPhones: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
primaryPhoneCountryCode: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
primaryPhoneNumber: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
fieldEmail: {
|
fieldEmail: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'email',
|
format: 'email',
|
||||||
@ -356,6 +384,20 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldPhone: {
|
fieldPhone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
fieldPhones: {
|
||||||
|
properties: {
|
||||||
|
additionalPhones: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
primaryPhoneCountryCode: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
primaryPhoneNumber: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
fieldEmail: {
|
fieldEmail: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'email',
|
format: 'email',
|
||||||
|
|||||||
@ -137,6 +137,7 @@ const getSchemaComponentsProperties = ({
|
|||||||
case FieldMetadataType.ADDRESS:
|
case FieldMetadataType.ADDRESS:
|
||||||
case FieldMetadataType.ACTOR:
|
case FieldMetadataType.ACTOR:
|
||||||
case FieldMetadataType.EMAILS:
|
case FieldMetadataType.EMAILS:
|
||||||
|
case FieldMetadataType.PHONES:
|
||||||
itemProperty = {
|
itemProperty = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: compositeTypeDefinitions
|
properties: compositeTypeDefinitions
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/
|
|||||||
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.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 { 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';
|
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||||
|
import { phonesCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
export const compositeTypeDefinitions = new Map<
|
export const compositeTypeDefinitions = new Map<
|
||||||
@ -20,4 +21,5 @@ export const compositeTypeDefinitions = new Map<
|
|||||||
[FieldMetadataType.ADDRESS, addressCompositeType],
|
[FieldMetadataType.ADDRESS, addressCompositeType],
|
||||||
[FieldMetadataType.ACTOR, actorCompositeType],
|
[FieldMetadataType.ACTOR, actorCompositeType],
|
||||||
[FieldMetadataType.EMAILS, emailsCompositeType],
|
[FieldMetadataType.EMAILS, emailsCompositeType],
|
||||||
|
[FieldMetadataType.PHONES, phonesCompositeType],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
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 phonesCompositeType: CompositeType = {
|
||||||
|
type: FieldMetadataType.PHONES,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'primaryPhoneNumber',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
hidden: false,
|
||||||
|
isRequired: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'primaryPhoneCountryCode',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
hidden: false,
|
||||||
|
isRequired: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'additionalPhones',
|
||||||
|
type: FieldMetadataType.RAW_JSON,
|
||||||
|
hidden: false,
|
||||||
|
isRequired: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PhonesMetadata = {
|
||||||
|
primaryPhoneNumber: string;
|
||||||
|
primaryPhoneCountryCode: string;
|
||||||
|
additionalPhones: object | null;
|
||||||
|
};
|
||||||
@ -185,3 +185,17 @@ export class FieldMetadataDefaultValueEmails {
|
|||||||
@IsObject()
|
@IsObject()
|
||||||
additionalEmails: string[] | null;
|
additionalEmails: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValuePhones {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsQuotedString()
|
||||||
|
primaryPhoneNumber: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsQuotedString()
|
||||||
|
primaryPhoneCountryCode: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsObject()
|
||||||
|
additionalPhones: object | null;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export enum FieldMetadataType {
|
|||||||
UUID = 'UUID',
|
UUID = 'UUID',
|
||||||
TEXT = 'TEXT',
|
TEXT = 'TEXT',
|
||||||
PHONE = 'PHONE',
|
PHONE = 'PHONE',
|
||||||
|
PHONES = 'PHONES',
|
||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
EMAILS = 'EMAILS',
|
EMAILS = 'EMAILS',
|
||||||
DATE_TIME = 'DATE_TIME',
|
DATE_TIME = 'DATE_TIME',
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
FieldMetadataDefaultValueLinks,
|
FieldMetadataDefaultValueLinks,
|
||||||
FieldMetadataDefaultValueNowFunction,
|
FieldMetadataDefaultValueNowFunction,
|
||||||
FieldMetadataDefaultValueNumber,
|
FieldMetadataDefaultValueNumber,
|
||||||
|
FieldMetadataDefaultValuePhones,
|
||||||
FieldMetadataDefaultValueRawJson,
|
FieldMetadataDefaultValueRawJson,
|
||||||
FieldMetadataDefaultValueRichText,
|
FieldMetadataDefaultValueRichText,
|
||||||
FieldMetadataDefaultValueString,
|
FieldMetadataDefaultValueString,
|
||||||
@ -27,6 +28,7 @@ type FieldMetadataDefaultValueMapping = {
|
|||||||
| FieldMetadataDefaultValueUuidFunction;
|
| FieldMetadataDefaultValueUuidFunction;
|
||||||
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
|
||||||
|
[FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones;
|
||||||
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails;
|
[FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails;
|
||||||
[FieldMetadataType.DATE_TIME]:
|
[FieldMetadataType.DATE_TIME]:
|
||||||
|
|||||||
@ -47,6 +47,12 @@ export function generateDefaultValue(
|
|||||||
primaryLinkUrl: "''",
|
primaryLinkUrl: "''",
|
||||||
secondaryLinks: null,
|
secondaryLinks: null,
|
||||||
};
|
};
|
||||||
|
case FieldMetadataType.PHONES:
|
||||||
|
return {
|
||||||
|
primaryPhoneNumber: "''",
|
||||||
|
primaryPhoneCountryCode: "''",
|
||||||
|
additionalPhones: null,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,8 @@ export const isCompositeFieldMetadataType = (
|
|||||||
| FieldMetadataType.ADDRESS
|
| FieldMetadataType.ADDRESS
|
||||||
| FieldMetadataType.LINKS
|
| FieldMetadataType.LINKS
|
||||||
| FieldMetadataType.ACTOR
|
| FieldMetadataType.ACTOR
|
||||||
| FieldMetadataType.EMAILS => {
|
| FieldMetadataType.EMAILS
|
||||||
|
| FieldMetadataType.PHONES => {
|
||||||
return [
|
return [
|
||||||
FieldMetadataType.LINK,
|
FieldMetadataType.LINK,
|
||||||
FieldMetadataType.CURRENCY,
|
FieldMetadataType.CURRENCY,
|
||||||
@ -18,5 +19,6 @@ export const isCompositeFieldMetadataType = (
|
|||||||
FieldMetadataType.LINKS,
|
FieldMetadataType.LINKS,
|
||||||
FieldMetadataType.ACTOR,
|
FieldMetadataType.ACTOR,
|
||||||
FieldMetadataType.EMAILS,
|
FieldMetadataType.EMAILS,
|
||||||
|
FieldMetadataType.PHONES,
|
||||||
].includes(type);
|
].includes(type);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
FieldMetadataDefaultValueLinks,
|
FieldMetadataDefaultValueLinks,
|
||||||
FieldMetadataDefaultValueNowFunction,
|
FieldMetadataDefaultValueNowFunction,
|
||||||
FieldMetadataDefaultValueNumber,
|
FieldMetadataDefaultValueNumber,
|
||||||
|
FieldMetadataDefaultValuePhones,
|
||||||
FieldMetadataDefaultValueRawJson,
|
FieldMetadataDefaultValueRawJson,
|
||||||
FieldMetadataDefaultValueString,
|
FieldMetadataDefaultValueString,
|
||||||
FieldMetadataDefaultValueStringArray,
|
FieldMetadataDefaultValueStringArray,
|
||||||
@ -55,6 +56,7 @@ export const defaultValueValidatorsMap = {
|
|||||||
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
|
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
|
||||||
[FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor],
|
[FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor],
|
||||||
[FieldMetadataType.EMAILS]: [FieldMetadataDefaultValueEmails],
|
[FieldMetadataType.EMAILS]: [FieldMetadataDefaultValueEmails],
|
||||||
|
[FieldMetadataType.PHONES]: [FieldMetadataDefaultValuePhones],
|
||||||
};
|
};
|
||||||
|
|
||||||
type ValidationResult = {
|
type ValidationResult = {
|
||||||
|
|||||||
@ -24,7 +24,8 @@ export type CompositeFieldMetadataType =
|
|||||||
| FieldMetadataType.FULL_NAME
|
| FieldMetadataType.FULL_NAME
|
||||||
| FieldMetadataType.LINK
|
| FieldMetadataType.LINK
|
||||||
| FieldMetadataType.LINKS
|
| FieldMetadataType.LINKS
|
||||||
| FieldMetadataType.EMAILS;
|
| FieldMetadataType.EMAILS
|
||||||
|
| FieldMetadataType.PHONES;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {
|
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {
|
||||||
|
|||||||
@ -101,6 +101,10 @@ export class WorkspaceMigrationFactory {
|
|||||||
FieldMetadataType.EMAILS,
|
FieldMetadataType.EMAILS,
|
||||||
{ factory: this.compositeColumnActionFactory },
|
{ factory: this.compositeColumnActionFactory },
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
FieldMetadataType.PHONES,
|
||||||
|
{ factory: this.compositeColumnActionFactory },
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user