Default address country 🗺️ & Phone prefix ☎️ (#8614)

# Default address 🗺️ country & Phone ☎️ country

We add the ability to add a Default address country and a default Phone
country for fields in the Data model.

fix #8081

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2024-12-02 13:34:05 +01:00
committed by GitHub
parent 39a9cd0d51
commit 0527bc296e
28 changed files with 617 additions and 108 deletions

View File

@ -2,5 +2,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
export const FIELD_NOT_OVERWRITTEN_AT_DRAFT = [
FieldMetadataType.Address,
FieldMetadataType.Phones,
FieldMetadataType.Links,
];

View File

@ -35,8 +35,8 @@ export const useNumberField = () => {
const persistNumberField = (newValue: string) => {
if (fieldDefinition?.metadata?.settings?.type === 'percentage') {
newValue = newValue.replaceAll('%', '');
if (!canBeCastAsNumberOrNull(newValue)) {
const newValueEscaped = newValue.replaceAll('%', '');
if (!canBeCastAsNumberOrNull(newValueEscaped)) {
return;
}
const castedValue = castAsNumberOrNull(newValue);

View File

@ -111,7 +111,7 @@ export const MultiItemFieldInput = <T,>({
break;
case FieldMetadataType.Phones:
item = items[index] as PhoneRecord;
setInputValue(item.countryCode + item.number);
setInputValue(`+${item.callingCode}` + item.number);
break;
case FieldMetadataType.Emails:
item = items[index] as string;

View File

@ -5,12 +5,14 @@ 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 { TEXT_INPUT_STYLE, isDefined } from 'twenty-ui';
import { MultiItemFieldInput } from './MultiItemFieldInput';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
font-family: ${({ theme }) => theme.font.family};
@ -48,33 +50,41 @@ type PhonesFieldInputProps = {
};
export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
const { persistPhonesField, hotkeyScope, fieldValue } = usePhonesField();
const { persistPhonesField, hotkeyScope, draftValue, fieldDefinition } =
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 phones = useMemo<{ number: string; callingCode: string }[]>(() => {
if (!isDefined(draftValue)) {
return [];
}
return [
draftValue.primaryPhoneNumber
? {
number: draftValue.primaryPhoneNumber,
callingCode: draftValue.primaryPhoneCountryCode,
}
: null,
...(draftValue.additionalPhones ?? []),
].filter(isDefined);
}, [draftValue]);
const defaultCallingCode =
stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
) ?? '+1';
// TODO : improve once we store the real country code
const defaultCountry = useCountries().find(
(obj) => obj.callingCode === defaultCallingCode,
)?.countryCode;
const handlePersistPhones = (
updatedPhones: { number: string; countryCode: string }[],
updatedPhones: { number: string; callingCode: string }[],
) => {
const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones;
persistPhonesField({
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
primaryPhoneCountryCode: nextPrimaryPhone?.callingCode ?? '',
additionalPhones: nextAdditionalPhones,
});
};
@ -93,12 +103,12 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
if (phone !== undefined) {
return {
number: phone.nationalNumber,
countryCode: `+${phone.countryCallingCode}`,
callingCode: `${phone.countryCallingCode}`,
};
}
return {
number: '',
countryCode: '',
callingCode: '',
};
}}
renderItem={({
@ -128,6 +138,7 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
international={true}
withCountryCallingCode={true}
countrySelectComponent={PhoneCountryPickerDropdownButton}
defaultCountry={defaultCountry}
/>
);
}}

View File

@ -7,7 +7,7 @@ type PhonesFieldMenuItemProps = {
onEdit?: () => void;
onSetAsPrimary?: () => void;
onDelete?: () => void;
phone: { number: string; countryCode: string };
phone: { number: string; callingCode: string };
};
export const PhonesFieldMenuItem = ({
@ -22,7 +22,7 @@ export const PhonesFieldMenuItem = ({
<MultiItemFieldMenuItem
dropdownId={dropdownId}
isPrimary={isPrimary}
value={phone.countryCode + phone.number}
value={{ number: phone.number, callingCode: phone.callingCode }}
onEdit={onEdit}
onSetAsPrimary={onSetAsPrimary}
onDelete={onDelete}

View File

@ -10,13 +10,13 @@ import { CurrencyCode } from './CurrencyCode';
export type FieldUuidMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldBooleanMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldTextMetadata = {
@ -61,13 +61,13 @@ export type FieldLinkMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldLinksMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldCurrencyMetadata = {
@ -75,66 +75,66 @@ export type FieldCurrencyMetadata = {
fieldName: string;
placeHolder: string;
isPositive?: boolean;
settings?: Record<string, never>;
settings?: null;
};
export type FieldFullNameMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldEmailMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldEmailsMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldPhoneMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldRatingMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldAddressMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldRawJsonMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
placeHolder: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldRichTextMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldPositionMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldRelationMetadata = {
@ -146,7 +146,7 @@ export type FieldRelationMetadata = {
relationType?: RelationDefinitionType;
targetFieldMetadataName?: string;
useEditButton?: boolean;
settings?: Record<string, never>;
settings?: null;
};
export type FieldSelectMetadata = {
@ -154,39 +154,39 @@ export type FieldSelectMetadata = {
fieldName: string;
options: { label: string; color: ThemeColor; value: string }[];
isNullable: boolean;
settings?: Record<string, never>;
settings?: null;
};
export type FieldMultiSelectMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
options: { label: string; color: ThemeColor; value: string }[];
settings?: Record<string, never>;
settings?: null;
};
export type FieldActorMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldArrayMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
values: { label: string; value: string }[];
settings?: Record<string, never>;
settings?: null;
};
export type FieldPhonesMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldTsVectorMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
settings?: Record<string, never>;
settings?: null;
};
export type FieldMetadata =
@ -265,7 +265,7 @@ export type FieldActorValue = {
export type FieldArrayValue = string[];
export type PhoneRecord = { number: string; countryCode: string };
export type PhoneRecord = { number: string; callingCode: string };
export type FieldPhonesValue = {
primaryPhoneNumber: string;

View File

@ -2,7 +2,7 @@ import { z } from 'zod';
import { FieldAddressValue } from '../FieldMetadata';
const addressSchema = z.object({
export const addressSchema = z.object({
addressStreet1: z.string(),
addressStreet2: z.string().nullable(),
addressCity: z.string().nullable(),

View File

@ -6,7 +6,7 @@ export const phonesSchema = z.object({
primaryPhoneNumber: z.string(),
primaryPhoneCountryCode: z.string(),
additionalPhones: z
.array(z.object({ number: z.string(), countryCode: z.string() }))
.array(z.object({ number: z.string(), callingCode: z.string() }))
.nullable(),
}) satisfies z.ZodType<FieldPhonesValue>;

View File

@ -1,10 +1,12 @@
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldNumberValue } from '@/object-record/record-field/types/guards/isFieldNumberValue';
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
@ -12,6 +14,7 @@ import { computeEmptyDraftValue } from '@/object-record/record-field/utils/compu
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
type computeDraftValueFromFieldValueParams<FieldValue> = {
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>;
@ -42,6 +45,38 @@ export const computeDraftValueFromFieldValue = <FieldValue>({
} as unknown as FieldInputDraftValue<FieldValue>;
}
if (isFieldAddress(fieldDefinition)) {
if (
isFieldValueEmpty({ fieldValue, fieldDefinition }) &&
!!fieldDefinition?.defaultValue?.addressCountry
) {
return {
...fieldValue,
addressCountry: stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.addressCountry,
),
} as unknown as FieldInputDraftValue<FieldValue>;
}
return fieldValue as FieldInputDraftValue<FieldValue>;
}
if (isFieldPhones(fieldDefinition)) {
if (
isFieldValueEmpty({ fieldValue, fieldDefinition }) &&
!!fieldDefinition?.defaultValue?.primaryPhoneCountryCode
) {
return {
...fieldValue,
primaryPhoneCountryCode: stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
),
} as unknown as FieldInputDraftValue<FieldValue>;
}
return fieldValue as FieldInputDraftValue<FieldValue>;
}
if (
isFieldNumber(fieldDefinition) &&
isFieldNumberValue(fieldValue) &&