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:
@ -2,5 +2,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const FIELD_NOT_OVERWRITTEN_AT_DRAFT = [
|
||||
FieldMetadataType.Address,
|
||||
FieldMetadataType.Phones,
|
||||
FieldMetadataType.Links,
|
||||
];
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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) &&
|
||||
|
||||
Reference in New Issue
Block a user