Replace country code by calling code in workflows (#18008)

Long overdue PR: replacing deprecated country code in workflows.
Will allow to use variables.

We keep storing the country code since it allows to display the right
flag in picker when there are multiple countries for one calling code.
But we do not store country for variables.

<img width="477" height="254" alt="Capture d’écran 2026-02-17 à 17 25
23"
src="https://github.com/user-attachments/assets/dc67c41c-33cf-4021-b7bb-490827b2aa3c"
/>
This commit is contained in:
Thomas Trompette
2026-02-17 18:06:09 +01:00
committed by GitHub
parent 610c0ebc9d
commit f768bbe512
5 changed files with 93 additions and 71 deletions
@@ -3,22 +3,27 @@ import { useMemo } from 'react';
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { t } from '@lingui/core/macro';
import type { CountryCode } from 'libphonenumber-js';
import { IconCircleOff, type IconComponentProps } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
export type FormCountryCodeSelectInputUpdatedValue = CountryCode | '';
export type FormCallingCodeSelectInputUpdatedValue = {
callingCode: string;
countryCode: string;
};
export const FormCountryCodeSelectInput = ({
export const FormCallingCodeSelectInput = ({
selectedCountryCode,
selectedCallingCode,
onChange,
label,
readonly = false,
VariablePicker,
}: {
selectedCountryCode: string;
onChange: (countryCode: FormCountryCodeSelectInputUpdatedValue) => void;
selectedCallingCode?: string;
onChange: (value: FormCallingCodeSelectInputUpdatedValue) => void;
label?: string;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
@@ -28,7 +33,7 @@ export const FormCountryCodeSelectInput = ({
const options: SelectOption[] = useMemo(() => {
const countryList = countries.map<SelectOption>(
({ countryName, countryCode, callingCode, Flag }) => ({
label: `${countryName} (+${callingCode})`,
label: `+${callingCode} (${countryName})`,
value: countryCode,
Icon: (props: IconComponentProps) =>
Flag({ width: props.size, height: props.size }),
@@ -36,7 +41,7 @@ export const FormCountryCodeSelectInput = ({
);
return [
{
label: t`No country`,
label: t`No calling code`,
value: '',
Icon: IconCircleOff,
},
@@ -44,24 +49,43 @@ export const FormCountryCodeSelectInput = ({
];
}, [countries]);
const onCountryCodeChange = (countryCode: string | null) => {
const defaultValue = isStandaloneVariableString(selectedCallingCode)
? selectedCallingCode
: selectedCountryCode;
const handleChange = (value: string | null) => {
if (readonly) {
return;
}
if (countryCode === null) {
onChange('');
} else {
onChange(countryCode as CountryCode);
if (value === null) {
onChange({ callingCode: '', countryCode: '' });
return;
}
if (isStandaloneVariableString(value)) {
onChange({ callingCode: value, countryCode: '' });
return;
}
const matchingCountry = countries.find(
(countryItem) => countryItem.countryCode === value,
);
const callingCode = matchingCountry?.callingCode;
onChange({
callingCode: callingCode ? `+${callingCode}` : '',
countryCode: value,
});
};
return (
<FormSelectFieldInput
label={label}
onChange={onCountryCodeChange}
onChange={handleChange}
options={options}
defaultValue={selectedCountryCode}
defaultValue={defaultValue}
readonly={readonly}
VariablePicker={VariablePicker}
/>
@@ -1,15 +1,14 @@
import { t } from '@lingui/core/macro';
import {
FormCountryCodeSelectInput,
type FormCountryCodeSelectInputUpdatedValue,
} from '@/object-record/record-field/ui/form-types/components/FormCountryCodeSelectInput';
FormCallingCodeSelectInput,
type FormCallingCodeSelectInputUpdatedValue,
} from '@/object-record/record-field/ui/form-types/components/FormCallingCodeSelectInput';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormNestedFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormNestedFieldInputContainer';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { getCountryCallingCode } from 'libphonenumber-js';
import { t } from '@lingui/core/macro';
type FormPhoneFieldInputProps = {
label?: string;
@@ -26,19 +25,12 @@ export const FormPhoneFieldInput = ({
readonly,
VariablePicker,
}: FormPhoneFieldInputProps) => {
const handleCountryChange = (
newCountry: FormCountryCodeSelectInputUpdatedValue,
const handleCallingCodeChange = (
newValue: FormCallingCodeSelectInputUpdatedValue,
) => {
let newCallingCode;
if (newCountry === '') {
newCallingCode = '';
} else {
newCallingCode = getCountryCallingCode(newCountry);
}
onChange({
primaryPhoneCountryCode: newCountry,
primaryPhoneCallingCode: newCallingCode,
primaryPhoneCountryCode: newValue.countryCode,
primaryPhoneCallingCode: newValue.callingCode,
primaryPhoneNumber: defaultValue?.primaryPhoneNumber ?? '',
});
};
@@ -55,11 +47,13 @@ export const FormPhoneFieldInput = ({
<FormFieldInputContainer>
{label && <InputLabel>{label}</InputLabel>}
<FormNestedFieldInputContainer>
<FormCountryCodeSelectInput
label={t`Country Code`}
<FormCallingCodeSelectInput
label={t`Calling Code`}
selectedCountryCode={defaultValue?.primaryPhoneCountryCode ?? ''}
onChange={handleCountryChange}
selectedCallingCode={defaultValue?.primaryPhoneCallingCode}
onChange={handleCallingCodeChange}
readonly={readonly}
VariablePicker={VariablePicker}
/>
<FormNumberFieldInput
label={t`Phone Number`}
@@ -0,0 +1,29 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { within } from 'storybook/test';
import { FormCallingCodeSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCallingCodeSelectInput';
const meta: Meta<typeof FormCallingCodeSelectInput> = {
title: 'UI/Data/Field/Form/Input/FormCallingCodeSelectInput',
component: FormCallingCodeSelectInput,
args: {
label: 'Calling Code',
},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormCallingCodeSelectInput>;
export const Default: Story = {
args: {
selectedCountryCode: 'FR',
selectedCallingCode: '+33',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Calling Code');
},
};
@@ -1,28 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { within } from 'storybook/test';
import { FormCountryCodeSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryCodeSelectInput';
const meta: Meta<typeof FormCountryCodeSelectInput> = {
title: 'UI/Data/Field/Form/Input/FormCountryCodeSelectInput',
component: FormCountryCodeSelectInput,
args: {
label: 'Country Code',
},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormCountryCodeSelectInput>;
export const Default: Story = {
args: {
selectedCountryCode: 'FR',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Country Code');
},
};
@@ -22,7 +22,7 @@ type Story = StoryObj<typeof FormPhoneFieldInput>;
const defaultPhoneValue: FieldPhonesValue = {
primaryPhoneNumber: '0612345678',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '33',
primaryPhoneCallingCode: '+33',
};
const FormPhoneFieldInputWithState = ({
@@ -66,7 +66,8 @@ export const WithVariablesAsDefaultValues: Story = {
args: {
label: 'Phone',
defaultValue: {
primaryPhoneCountryCode: `{{${MOCKED_STEP_ID}.name}}`,
primaryPhoneCountryCode: '',
primaryPhoneCallingCode: `{{${MOCKED_STEP_ID}.name}}`,
primaryPhoneNumber: `{{${MOCKED_STEP_ID}.amount.amountMicros}}`,
},
VariablePicker: () => <div>VariablePicker</div>,
@@ -74,12 +75,12 @@ export const WithVariablesAsDefaultValues: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const countryCodeVariable = await canvas.findByText('Name');
expect(countryCodeVariable).toBeVisible();
const callingCodeVariable = await canvas.findByText('Name');
expect(callingCodeVariable).toBeVisible();
const variablePickers = await canvas.findAllByText('VariablePicker');
expect(variablePickers).toHaveLength(1);
expect(variablePickers).toHaveLength(2);
for (const variablePicker of variablePickers) {
expect(variablePicker).toBeVisible();
@@ -115,14 +116,16 @@ export const SelectingVariables: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const countryCodeDefaultValue = await canvas.findByText('No country');
expect(countryCodeDefaultValue).toBeVisible();
const callingCodeDefaultValue = await canvas.findByText('No calling code');
expect(callingCodeDefaultValue).toBeVisible();
const phoneNumberDefaultValue =
await canvas.findByPlaceholderText('Enter phone number');
expect(phoneNumberDefaultValue).toHaveDisplayValue('');
const phoneNumberVariablePicker = await canvas.findByText('Add variable');
const addVariableButtons = await canvas.findAllByText('Add variable');
const phoneNumberVariablePicker = addVariableButtons[1];
await userEvent.click(phoneNumberVariablePicker);
@@ -148,10 +151,10 @@ export const Disabled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const countryInput = await canvas.findByText('No country');
expect(countryInput).toBeVisible();
const callingCodeInput = await canvas.findByText('No calling code');
expect(callingCodeInput).toBeVisible();
await userEvent.click(countryInput);
await userEvent.click(callingCodeInput);
const searchInputInModal = canvas.queryByPlaceholderText('Search');
expect(searchInputInModal).not.toBeInTheDocument();