mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +03:00
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:
parent
39a9cd0d51
commit
0527bc296e
@ -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) &&
|
||||
|
@ -5,17 +5,10 @@ import {
|
||||
StyledSettingsOptionCardTitle,
|
||||
} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase';
|
||||
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { Select, SelectValue } from '@/ui/input/components/Select';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
const StyledSettingsOptionCardSelect = styled(Select)`
|
||||
margin-left: auto;
|
||||
width: 120px;
|
||||
`;
|
||||
|
||||
type SelectValue = string | number | boolean | null;
|
||||
|
||||
type SettingsOptionCardContentSelectProps<Value extends SelectValue> = {
|
||||
Icon?: IconComponent;
|
||||
title: React.ReactNode;
|
||||
@ -23,7 +16,7 @@ type SettingsOptionCardContentSelectProps<Value extends SelectValue> = {
|
||||
divider?: boolean;
|
||||
disabled?: boolean;
|
||||
value: Value;
|
||||
onChange: (value: SelectValue) => void;
|
||||
onChange: (value: Value) => void;
|
||||
options: {
|
||||
value: Value;
|
||||
label: string;
|
||||
@ -34,6 +27,10 @@ type SettingsOptionCardContentSelectProps<Value extends SelectValue> = {
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const SettingsOptionCardContentSelect = <Value extends SelectValue>({
|
||||
Icon,
|
||||
title,
|
||||
@ -60,16 +57,18 @@ export const SettingsOptionCardContentSelect = <Value extends SelectValue>({
|
||||
{description}
|
||||
</StyledSettingsOptionCardDescription>
|
||||
</div>
|
||||
<StyledSettingsOptionCardSelect
|
||||
className={selectClassName}
|
||||
dropdownWidth={fullWidth ? 'auto' : 120}
|
||||
disabled={disabled}
|
||||
dropdownId={dropdownId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
<StyledSelectContainer>
|
||||
<Select<Value>
|
||||
className={selectClassName}
|
||||
dropdownWidth={fullWidth ? 'auto' : 120}
|
||||
disabled={disabled}
|
||||
dropdownId={dropdownId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
</StyledSelectContainer>
|
||||
</StyledSettingsOptionCardContent>
|
||||
);
|
||||
};
|
||||
|
@ -91,7 +91,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
||||
exampleValue: {
|
||||
primaryPhoneNumber: '234-567-890',
|
||||
primaryPhoneCountryCode: '+1',
|
||||
additionalPhones: [{ number: '234-567-890', countryCode: '+1' }],
|
||||
additionalPhones: [{ number: '234-567-890', callingCode: '+1' }],
|
||||
},
|
||||
subFields: [
|
||||
'primaryPhoneNumber',
|
||||
@ -151,7 +151,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
||||
},
|
||||
exampleValue: {
|
||||
addressStreet1: '456 Oak Street',
|
||||
addressStreet2: 'Unit 3B',
|
||||
addressStreet2: '',
|
||||
addressCity: 'Springfield',
|
||||
addressState: 'California',
|
||||
addressCountry: 'United States',
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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 });
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -4,28 +4,37 @@ import { ContactLink } from 'twenty-ui';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type PhoneDisplayProps = {
|
||||
value: string | null | undefined;
|
||||
interface PhoneDisplayProps {
|
||||
value: PhoneDisplayValueProps;
|
||||
}
|
||||
type PhoneDisplayValueProps = {
|
||||
number: string | null | undefined;
|
||||
callingCode: string | null | undefined;
|
||||
};
|
||||
|
||||
// TODO: see if we can find a faster way to format the phone number
|
||||
export const PhoneDisplay = ({ value }: PhoneDisplayProps) => {
|
||||
if (!isDefined(value)) {
|
||||
return <ContactLink href="#">{value}</ContactLink>;
|
||||
}
|
||||
export const PhoneDisplay = ({
|
||||
value: { number, callingCode },
|
||||
}: PhoneDisplayProps) => {
|
||||
if (!isDefined(number)) return <ContactLink href="#">{number}</ContactLink>;
|
||||
|
||||
const callingCodeSanitized = callingCode?.replace('+', '');
|
||||
|
||||
let parsedPhoneNumber: PhoneNumber | null = null;
|
||||
|
||||
try {
|
||||
// TODO: parse according to locale not hard coded FR
|
||||
parsedPhoneNumber = parsePhoneNumber(value, 'FR');
|
||||
parsedPhoneNumber = parsePhoneNumber(number, {
|
||||
defaultCallingCode: callingCodeSanitized || '1',
|
||||
});
|
||||
} catch (error) {
|
||||
return <ContactLink href="#">{value}</ContactLink>;
|
||||
if (!(error instanceof Error))
|
||||
return <ContactLink href="#">{number}</ContactLink>;
|
||||
if (error.message === 'NOT_A_NUMBER')
|
||||
return <ContactLink href="#">{`+${callingCodeSanitized}`}</ContactLink>;
|
||||
return <ContactLink href="#">{number}</ContactLink>;
|
||||
}
|
||||
|
||||
const URI = parsedPhoneNumber.getURI();
|
||||
const formatedPhoneNumber = parsedPhoneNumber.formatInternational();
|
||||
|
||||
return (
|
||||
<ContactLink
|
||||
href={URI}
|
||||
@ -33,7 +42,7 @@ export const PhoneDisplay = ({ value }: PhoneDisplayProps) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{formatedPhoneNumber || value}
|
||||
{formatedPhoneNumber || number}
|
||||
</ContactLink>
|
||||
);
|
||||
};
|
||||
|
@ -36,16 +36,16 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
|
||||
value?.primaryPhoneNumber
|
||||
? {
|
||||
number: value.primaryPhoneNumber,
|
||||
countryCode: value.primaryPhoneCountryCode,
|
||||
callingCode: value.primaryPhoneCountryCode,
|
||||
}
|
||||
: null,
|
||||
...parseAdditionalPhones(value?.additionalPhones),
|
||||
]
|
||||
.filter(isDefined)
|
||||
.map(({ number, countryCode }) => {
|
||||
.map(({ number, callingCode }) => {
|
||||
return {
|
||||
number,
|
||||
countryCode,
|
||||
callingCode,
|
||||
};
|
||||
}),
|
||||
[
|
||||
@ -65,9 +65,9 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
|
||||
|
||||
return isFocused ? (
|
||||
<ExpandableList isChipCountDisplayed>
|
||||
{phones.map(({ number, countryCode }, index) => {
|
||||
{phones.map(({ number, callingCode }, index) => {
|
||||
const { parsedPhone, invalidPhone } =
|
||||
parsePhoneNumberOrReturnInvalidValue(countryCode + number);
|
||||
parsePhoneNumberOrReturnInvalidValue(`+${callingCode}` + number);
|
||||
const URI = parsedPhone?.getURI();
|
||||
return (
|
||||
<RoundedLink
|
||||
@ -82,9 +82,9 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
|
||||
</ExpandableList>
|
||||
) : (
|
||||
<StyledContainer>
|
||||
{phones.map(({ number, countryCode }, index) => {
|
||||
{phones.map(({ number, callingCode }, index) => {
|
||||
const { parsedPhone, invalidPhone } =
|
||||
parsePhoneNumberOrReturnInvalidValue(countryCode + number);
|
||||
parsePhoneNumberOrReturnInvalidValue(`+${callingCode}` + number);
|
||||
const URI = parsedPhone?.getURI();
|
||||
return (
|
||||
<RoundedLink
|
||||
|
@ -26,7 +26,9 @@ type CallToActionButton = {
|
||||
Icon?: IconComponent;
|
||||
};
|
||||
|
||||
export type SelectProps<Value extends string | number | boolean | null> = {
|
||||
export type SelectValue = string | number | boolean | null;
|
||||
|
||||
export type SelectProps<Value extends SelectValue> = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
selectSizeVariant?: SelectSizeVariant;
|
||||
@ -57,7 +59,7 @@ const StyledLabel = styled.span`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const Select = <Value extends string | number | boolean | null>({
|
||||
export const Select = <Value extends SelectValue>({
|
||||
className,
|
||||
disabled: disabledFromProps,
|
||||
selectSizeVariant,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IconComponentProps } from 'twenty-ui';
|
||||
import { IconCircleOff, IconComponentProps } from 'twenty-ui';
|
||||
|
||||
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
@ -15,12 +15,20 @@ export const CountrySelect = ({
|
||||
const countries = useCountries();
|
||||
|
||||
const options: SelectOption<string>[] = useMemo(() => {
|
||||
return countries.map<SelectOption<string>>(({ countryName, Flag }) => ({
|
||||
label: countryName,
|
||||
value: countryName,
|
||||
Icon: (props: IconComponentProps) =>
|
||||
Flag({ width: props.size, height: props.size }), // TODO : improve this ?
|
||||
}));
|
||||
const countryList = countries.map<SelectOption<string>>(
|
||||
({ countryName, Flag }) => ({
|
||||
label: countryName,
|
||||
value: countryName,
|
||||
Icon: (props: IconComponentProps) =>
|
||||
Flag({ width: props.size, height: props.size }), // TODO : improve this ?
|
||||
}),
|
||||
);
|
||||
countryList.unshift({
|
||||
label: 'No country',
|
||||
value: '',
|
||||
Icon: IconCircleOff,
|
||||
});
|
||||
return countryList;
|
||||
}, [countries]);
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as Flags from 'country-flag-icons/react/3x2';
|
||||
import { CountryCallingCode } from 'libphonenumber-js';
|
||||
import { CountryCallingCode, CountryCode } from 'libphonenumber-js';
|
||||
|
||||
export type Country = {
|
||||
countryCode: string;
|
||||
countryCode: CountryCode;
|
||||
countryName: string;
|
||||
callingCode: CountryCallingCode;
|
||||
Flag: Flags.FlagComponent;
|
||||
|
@ -6,3 +6,20 @@ export const stripSimpleQuotesFromString = <Input extends string>(
|
||||
(simpleQuotesStringSchema.safeParse(value).success
|
||||
? value.slice(1, -1)
|
||||
: value) as Input extends `'${infer Output}'` ? Output : Input;
|
||||
|
||||
export const stripSimpleQuotesFromStringRecursive = (obj: any): any => {
|
||||
if (typeof obj === 'string') {
|
||||
return stripSimpleQuotesFromString(obj);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(stripSimpleQuotesFromStringRecursive);
|
||||
} else if (typeof obj === 'object' && obj !== null) {
|
||||
const newObj: any = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key) === true) {
|
||||
newObj[key] = stripSimpleQuotesFromStringRecursive(obj[key]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ enum ValueType {
|
||||
NUMBER = 'number',
|
||||
}
|
||||
|
||||
class SettingsValidation {
|
||||
class NumberSettingsValidation {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ -32,7 +32,9 @@ class SettingsValidation {
|
||||
@IsOptional()
|
||||
@IsEnum(ValueType)
|
||||
type?: 'percentage' | 'number';
|
||||
}
|
||||
|
||||
class TextSettingsValidation {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ -55,17 +57,19 @@ export class FieldMetadataValidationService<
|
||||
}) {
|
||||
switch (fieldType) {
|
||||
case FieldMetadataType.NUMBER:
|
||||
await this.validateSettings(NumberSettingsValidation, settings);
|
||||
break;
|
||||
case FieldMetadataType.TEXT:
|
||||
await this.validateSettings(settings);
|
||||
await this.validateSettings(TextSettingsValidation, settings);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async validateSettings(settings: any) {
|
||||
private async validateSettings(validator: any, settings: any) {
|
||||
try {
|
||||
const settingsInstance = plainToInstance(SettingsValidation, settings);
|
||||
const settingsInstance = plainToInstance(validator, settings);
|
||||
|
||||
await validateOrReject(settingsInstance);
|
||||
} catch (error) {
|
||||
|
@ -0,0 +1,59 @@
|
||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
|
||||
import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/field-metadata-validation.service';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
|
||||
describe('FieldMetadataValidationService', () => {
|
||||
let service: FieldMetadataValidationService;
|
||||
|
||||
beforeAll(() => {
|
||||
service = new FieldMetadataValidationService();
|
||||
});
|
||||
|
||||
it('should validate NUMBER settings successfully', async () => {
|
||||
const settings = { decimals: 2, type: 'number' } as FieldMetadataSettings;
|
||||
|
||||
await expect(
|
||||
service.validateSettingsOrThrow({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
settings,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw an error for invalid NUMBER settings', async () => {
|
||||
const settings = { type: 'invalidType' } as FieldMetadataSettings;
|
||||
|
||||
await expect(
|
||||
service.validateSettingsOrThrow({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
settings,
|
||||
}),
|
||||
).rejects.toThrow(FieldMetadataException);
|
||||
});
|
||||
|
||||
it('should validate TEXT settings successfully', async () => {
|
||||
const settings = { displayedMaxRows: 10 } as FieldMetadataSettings;
|
||||
|
||||
await expect(
|
||||
service.validateSettingsOrThrow({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
settings,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw an error for invalid TEXT settings', async () => {
|
||||
const settings = {
|
||||
displayedMaxRows: 'NotANumber',
|
||||
} as FieldMetadataSettings;
|
||||
|
||||
await expect(
|
||||
service.validateSettingsOrThrow({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
settings,
|
||||
}),
|
||||
).rejects.toThrow(FieldMetadataException);
|
||||
});
|
||||
});
|
@ -172,7 +172,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
|
||||
description: 'Additional Phones',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
placeholder: '{ number: "", countryCode: "" }',
|
||||
placeholder: '{ number: "", callingCode: "" }',
|
||||
list: true,
|
||||
};
|
||||
return [primaryPhoneNumber, primaryPhoneCountryCode, additionalPhones];
|
||||
|
Loading…
Reference in New Issue
Block a user