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

@ -0,0 +1,86 @@
import { Controller, useFormContext } from 'react-hook-form';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { addressSchema as addressFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { IconMap } from 'twenty-ui';
import { z } from 'zod';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
type SettingsDataModelFieldAddressFormProps = {
disabled?: boolean;
defaultCountry?: string;
fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
>;
};
export const settingsDataModelFieldAddressFormSchema = z.object({
defaultValue: addressFieldDefaultValueSchema,
});
export type SettingsDataModelFieldTextFormValues = z.infer<
typeof settingsDataModelFieldAddressFormSchema
>;
export const SettingsDataModelFieldAddressForm = ({
disabled,
fieldMetadataItem,
}: SettingsDataModelFieldAddressFormProps) => {
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
const countries = useCountries()
.sort((a, b) => a.countryName.localeCompare(b.countryName))
.map((country) => ({
label: country.countryName,
value: country.countryName,
}));
countries.unshift({
label: 'No country',
value: '',
});
const defaultDefaultValue = {
addressStreet1: "''",
addressStreet2: null,
addressCity: null,
addressState: null,
addressPostcode: null,
addressCountry: null,
addressLat: null,
addressLng: null,
};
return (
<Controller
name="defaultValue"
defaultValue={{
...defaultDefaultValue,
...fieldMetadataItem?.defaultValue,
}}
control={control}
render={({ field: { onChange, value } }) => {
const defaultCountry = value?.addressCountry || '';
return (
<SettingsOptionCardContentSelect<string>
Icon={IconMap}
dropdownId="selectDefaultCountry"
title="Default Country"
description="The default country for new addresses"
value={stripSimpleQuotesFromString(defaultCountry)}
onChange={(newCountry) =>
onChange({
...value,
addressCountry: applySimpleQuotesToString(newCountry),
})
}
disabled={disabled}
options={countries}
fullWidth={true}
/>
);
}}
/>
);
};

View File

@ -0,0 +1,45 @@
import styled from '@emotion/styled';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { SettingsDataModelFieldAddressForm } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm';
import {
SettingsDataModelFieldPreviewCard,
SettingsDataModelFieldPreviewCardProps,
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
type SettingsDataModelFieldAddressSettingsFormCardProps = {
disabled?: boolean;
fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue'
>;
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
flex: 1 1 100%;
`;
export const SettingsDataModelFieldAddressSettingsFormCard = ({
disabled,
fieldMetadataItem,
objectMetadataItem,
}: SettingsDataModelFieldAddressSettingsFormCardProps) => {
return (
<SettingsDataModelPreviewFormCard
preview={
<StyledFieldPreviewCard
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
}
form={
<SettingsDataModelFieldAddressForm
disabled={disabled}
fieldMetadataItem={fieldMetadataItem}
/>
}
/>
);
};

View File

@ -5,6 +5,8 @@ import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
import { settingsDataModelFieldAddressFormSchema } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm';
import { SettingsDataModelFieldAddressSettingsFormCard } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressSettingsFormCard';
import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
import { SettingsDataModelFieldBooleanSettingsFormCard } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard';
import { settingsDataModelFieldtextFormSchema } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm';
@ -15,6 +17,8 @@ import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fiel
import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard';
import { settingsDataModelFieldNumberFormSchema } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm';
import { SettingsDataModelFieldNumberSettingsFormCard } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard';
import { settingsDataModelFieldPhonesFormSchema } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm';
import { SettingsDataModelFieldPhonesSettingsFormCard } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesSettingsFormCard';
import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm';
import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard';
import {
@ -64,6 +68,14 @@ const textFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Text) })
.merge(settingsDataModelFieldtextFormSchema);
const addressFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Address) })
.merge(settingsDataModelFieldAddressFormSchema);
const phonesFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Phones) })
.merge(settingsDataModelFieldPhonesFormSchema);
const otherFieldsFormSchema = z.object({
type: z.enum(
Object.keys(
@ -76,6 +88,8 @@ const otherFieldsFormSchema = z.object({
FieldMetadataType.Date,
FieldMetadataType.DateTime,
FieldMetadataType.Number,
FieldMetadataType.Address,
FieldMetadataType.Phones,
FieldMetadataType.Text,
]),
) as [FieldMetadataType, ...FieldMetadataType[]],
@ -94,6 +108,8 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
multiSelectFieldFormSchema,
numberFieldFormSchema,
textFieldFormSchema,
addressFieldFormSchema,
phonesFieldFormSchema,
otherFieldsFormSchema,
],
);
@ -195,6 +211,24 @@ export const SettingsDataModelFieldSettingsFormCard = ({
);
}
if (fieldMetadataItem.type === FieldMetadataType.Address) {
return (
<SettingsDataModelFieldAddressSettingsFormCard
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
);
}
if (fieldMetadataItem.type === FieldMetadataType.Phones) {
return (
<SettingsDataModelFieldPhonesSettingsFormCard
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
);
}
if (
fieldMetadataItem.type === FieldMetadataType.Select ||
fieldMetadataItem.type === FieldMetadataType.MultiSelect

View File

@ -0,0 +1,80 @@
import { Controller, useFormContext } from 'react-hook-form';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { IconMap } from 'twenty-ui';
import { z } from 'zod';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
type SettingsDataModelFieldPhonesFormProps = {
disabled?: boolean;
defaultCountryCode?: string;
fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
>;
};
export const settingsDataModelFieldPhonesFormSchema = z.object({
defaultValue: phonesFieldDefaultValueSchema,
});
export type SettingsDataModelFieldTextFormValues = z.infer<
typeof settingsDataModelFieldPhonesFormSchema
>;
export const SettingsDataModelFieldPhonesForm = ({
disabled,
fieldMetadataItem,
}: SettingsDataModelFieldPhonesFormProps) => {
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
const countries = useCountries()
.sort((a, b) => a.countryName.localeCompare(b.countryName))
.map((country) => ({
label: `${country.countryName} (+${country.callingCode})`,
value: `${country.callingCode}`,
}));
countries.unshift({ label: 'No country', value: '' });
const defaultDefaultValue = {
primaryPhoneNumber: "''",
primaryPhoneCountryCode: "''",
additionalPhones: null,
};
const fieldMetadataItemDefaultValue = fieldMetadataItem?.defaultValue;
return (
<Controller
name="defaultValue"
defaultValue={{
...defaultDefaultValue,
...fieldMetadataItemDefaultValue,
}}
control={control}
render={({ field: { onChange, value } }) => {
return (
<SettingsOptionCardContentSelect<string>
Icon={IconMap}
dropdownId="selectDefaultCountryCode"
title="Default Country Code"
description="The default country code for new phone numbers."
value={stripSimpleQuotesFromString(value?.primaryPhoneCountryCode)}
onChange={(newPhoneCountryCode) =>
onChange({
...value,
primaryPhoneCountryCode:
applySimpleQuotesToString(newPhoneCountryCode),
})
}
disabled={disabled}
options={countries}
fullWidth={true}
/>
);
}}
/>
);
};

View File

@ -0,0 +1,46 @@
import styled from '@emotion/styled';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { SettingsDataModelFieldPhonesForm } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm';
import {
SettingsDataModelFieldPreviewCard,
SettingsDataModelFieldPreviewCardProps,
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
type SettingsDataModelFieldPhonesSettingsFormCardProps = {
disabled?: boolean;
fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue'
>;
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
flex: 1 1 100%;
`;
export const SettingsDataModelFieldPhonesSettingsFormCard = ({
disabled,
fieldMetadataItem,
objectMetadataItem,
}: SettingsDataModelFieldPhonesSettingsFormCardProps) => {
return (
<SettingsDataModelPreviewFormCard
preview={
<StyledFieldPreviewCard
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
}
form={
<SettingsDataModelFieldPhonesForm
disabled={disabled}
fieldMetadataItem={fieldMetadataItem}
/>
}
/>
);
};

View File

@ -2,9 +2,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRelationFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue';
import { getAddressFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue';
import { getCurrencyFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue';
import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue';
import { getMultiSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue';
import { getPhonesFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
import { getSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -45,6 +47,10 @@ export const useFieldPreviewValue = ({
return getSelectFieldPreviewValue({ fieldMetadataItem });
case FieldMetadataType.MultiSelect:
return getMultiSelectFieldPreviewValue({ fieldMetadataItem });
case FieldMetadataType.Address:
return getAddressFieldPreviewValue({ fieldMetadataItem });
case FieldMetadataType.Phones:
return getPhonesFieldPreviewValue({ fieldMetadataItem });
default:
return getFieldPreviewValue({ fieldMetadataItem });
}

View File

@ -0,0 +1,34 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
export const getAddressFieldPreviewValue = ({
fieldMetadataItem,
}: {
fieldMetadataItem: Pick<
FieldMetadataItem,
'defaultValue' | 'options' | 'type'
>;
}): FieldAddressValue | null => {
if (fieldMetadataItem.type !== FieldMetadataType.Address) return null;
const addressFieldTypeConfig = getSettingsFieldTypeConfig(
FieldMetadataType.Address,
);
const placeholderDefaultValue = addressFieldTypeConfig.exampleValue;
const addressCountry =
fieldMetadataItem.defaultValue?.addressCountry &&
fieldMetadataItem.defaultValue.addressCountry !== ''
? stripSimpleQuotesFromString(
fieldMetadataItem.defaultValue?.addressCountry,
)
: null;
return {
...placeholderDefaultValue,
addressCountry: addressCountry,
};
};

View File

@ -0,0 +1,33 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
export const getPhonesFieldPreviewValue = ({
fieldMetadataItem,
}: {
fieldMetadataItem: Pick<
FieldMetadataItem,
'defaultValue' | 'options' | 'type'
>;
}): FieldPhonesValue | null => {
if (fieldMetadataItem.type !== FieldMetadataType.Phones) return null;
const phonesFieldTypeConfig = getSettingsFieldTypeConfig(
FieldMetadataType.Phones,
);
const placeholderDefaultValue = phonesFieldTypeConfig.exampleValue;
const primaryPhoneCountryCode =
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode &&
fieldMetadataItem.defaultValue.primaryPhoneCountryCode !== ''
? `+${stripSimpleQuotesFromString(
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode,
)}`
: null;
return {
...placeholderDefaultValue,
primaryPhoneCountryCode,
};
};