From 2882e0f78974b31cb17b46e8d5506871023f624a Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 2 Jan 2025 17:19:11 +0100 Subject: [PATCH 1/2] Build phone number field --- .../components/FormFieldInput.tsx | 12 +++- .../components/FormCountryCodeSelectInput.tsx | 63 ++++++++++++++++++ .../components/FormCountrySelectInput.tsx | 8 +-- .../form-types/components/FormFieldHint.tsx | 10 +++ .../components/FormNumberFieldInput.tsx | 5 ++ .../components/FormPhoneFieldInput.tsx | 64 +++++++++++++++++++ .../components/FormSelectFieldInput.tsx | 1 + .../display/components/SelectDisplay.tsx | 18 +++++- .../src/display/tag/components/Tag.tsx | 6 +- 9 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index bfa0a901c703..1b38f7016a00 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -1,11 +1,13 @@ import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput'; +import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput'; import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; +import { FormPhoneFieldInput } from '@/object-record/record-field/form-types/components/FormPhoneFieldInput'; import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput'; import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; @@ -19,6 +21,7 @@ import { FieldLinksValue, FieldMetadata, FieldMultiSelectValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; @@ -29,12 +32,12 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; +import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; import { JsonValue } from 'type-fest'; -import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; type FormFieldInputProps = { field: FieldDefinition; @@ -109,6 +112,13 @@ export const FormFieldInput = ({ onPersist={onPersist} VariablePicker={VariablePicker} /> + ) : isFieldPhones(field) ? ( + ) : isFieldDate(field) ? ( void; + readonly?: boolean; + VariablePicker?: VariablePickerComponent; +}) => { + const countries = useCountries(); + + const options: SelectOption[] = useMemo(() => { + const countryList = countries.map( + ({ countryName, countryCode, callingCode, Flag }) => ({ + label: `${countryName} (+${callingCode})`, + value: countryCode, + color: 'transparent', + icon: (props: IconComponentProps) => + Flag({ width: props.size, height: props.size }), + }), + ); + return [ + { + label: 'No country', + value: '', + icon: IconCircleOff, + }, + ...countryList, + ]; + }, [countries]); + + const onChange = (countryCode: string | null) => { + if (readonly) { + return; + } + + if (countryCode === null) { + onPersist(''); + } else { + onPersist(countryCode); + } + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx index 6cd9d7ef658a..866ac1da2368 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx @@ -13,7 +13,7 @@ export const FormCountrySelectInput = ({ VariablePicker, }: { selectedCountryName: string; - onPersist: (countryCode: string) => void; + onPersist: (country: string) => void; readonly?: boolean; VariablePicker?: VariablePickerComponent; }) => { @@ -39,15 +39,15 @@ export const FormCountrySelectInput = ({ ]; }, [countries]); - const onChange = (countryCode: string | null) => { + const onChange = (country: string | null) => { if (readonly) { return; } - if (countryCode === null) { + if (country === null) { onPersist(''); } else { - onPersist(countryCode); + onPersist(country); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx new file mode 100644 index 000000000000..b172a457b263 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +const StyledFormFieldHint = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + margin-top: ${({ theme }) => theme.spacing(1)}; +`; + +export const FormFieldHint = StyledFormFieldHint; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx index 2ce19596571b..55be9d9c3763 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx @@ -1,3 +1,4 @@ +import { FormFieldHint } from '@/object-record/record-field/form-types/components/FormFieldHint'; import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer'; import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer'; import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer'; @@ -24,6 +25,7 @@ type FormNumberFieldInputProps = { defaultValue: number | string | undefined; onPersist: (value: number | null | string) => void; VariablePicker?: VariablePickerComponent; + hint?: string; }; export const FormNumberFieldInput = ({ @@ -32,6 +34,7 @@ export const FormNumberFieldInput = ({ defaultValue, onPersist, VariablePicker, + hint, }: FormNumberFieldInputProps) => { const inputId = useId(); @@ -125,6 +128,8 @@ export const FormNumberFieldInput = ({ /> ) : null} + + {hint ? {hint} : null} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx new file mode 100644 index 000000000000..b362d71120f0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx @@ -0,0 +1,64 @@ +import { FormCountryCodeSelectInput } from '@/object-record/record-field/form-types/components/FormCountryCodeSelectInput'; +import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer'; +import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer'; +import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; +import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; +import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; +import { InputLabel } from '@/ui/input/components/InputLabel'; +import { CountryCode, getCountryCallingCode } from 'libphonenumber-js'; + +type FormPhoneFieldInputProps = { + label?: string; + defaultValue?: FieldPhonesValue; + onPersist: (value: FieldPhonesValue) => void; + VariablePicker?: VariablePickerComponent; + readonly?: boolean; +}; + +export const FormPhoneFieldInput = ({ + label, + defaultValue, + onPersist, + readonly, + VariablePicker, +}: FormPhoneFieldInputProps) => { + const handleCountryChange = (newCountry: string) => { + const newCallingCode = getCountryCallingCode(newCountry as CountryCode); + + onPersist({ + primaryPhoneCountryCode: newCountry, + primaryPhoneCallingCode: newCallingCode, + primaryPhoneNumber: '', + }); + }; + + const handleNumberChange = (number: string | number | null) => { + onPersist({ + primaryPhoneCountryCode: defaultValue?.primaryPhoneCountryCode || '', + primaryPhoneCallingCode: defaultValue?.primaryPhoneCallingCode || '', + primaryPhoneNumber: number ? `${number}` : '', + }); + }; + + return ( + + {label && {label}} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx index 4f985aa922d6..353fe487c02a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx @@ -225,6 +225,7 @@ export const FormSelectFieldInput = ({ color={selectedOption.color ?? 'transparent'} label={selectedOption.label} Icon={selectedOption.icon ?? undefined} + isUsedInForm /> ) : null} diff --git a/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx index 13ac37aa3c7d..eb0a87219154 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx @@ -4,8 +4,22 @@ type SelectDisplayProps = { color: ThemeColor | 'transparent'; label: string; Icon?: IconComponent; + isUsedInForm?: boolean; }; -export const SelectDisplay = ({ color, label, Icon }: SelectDisplayProps) => { - return ; +export const SelectDisplay = ({ + color, + label, + Icon, + isUsedInForm, +}: SelectDisplayProps) => { + return ( + + ); }; diff --git a/packages/twenty-ui/src/display/tag/components/Tag.tsx b/packages/twenty-ui/src/display/tag/components/Tag.tsx index 864beced455d..0a9156771648 100644 --- a/packages/twenty-ui/src/display/tag/components/Tag.tsx +++ b/packages/twenty-ui/src/display/tag/components/Tag.tsx @@ -21,6 +21,7 @@ const StyledTag = styled.h3<{ weight: TagWeight; variant: TagVariant; preventShrink?: boolean; + preventPadding?: boolean; }>` align-items: center; background: ${({ color, theme }) => { @@ -52,7 +53,7 @@ const StyledTag = styled.h3<{ height: ${spacing5}; margin: 0; overflow: hidden; - padding: 0 ${spacing2}; + padding: ${({ preventPadding }) => (preventPadding ? '0' : `0 ${spacing2}`)}; border: ${({ variant, theme }) => variant === 'outline' || variant === 'border' ? `1px ${variant === 'border' ? 'solid' : 'dashed'} ${theme.border.color.strong}` @@ -91,6 +92,7 @@ type TagProps = { weight?: TagWeight; variant?: TagVariant; preventShrink?: boolean; + preventPadding?: boolean; }; // TODO: Find a way to have ellipsis and shrinkable tag in tag list while keeping good perf for table cells @@ -103,6 +105,7 @@ export const Tag = ({ weight = 'regular', variant = 'solid', preventShrink, + preventPadding, }: TagProps) => { const { theme } = useContext(ThemeContext); @@ -115,6 +118,7 @@ export const Tag = ({ weight={weight} variant={variant} preventShrink={preventShrink} + preventPadding={preventPadding} > {isDefined(Icon) ? ( From 6ea4c2d30faafb9f883be0d0daf5fbf29bef2ec3 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 2 Jan 2025 17:36:58 +0100 Subject: [PATCH 2/2] Add tests --- .../FormCountryCodeSelectInput.stories.tsx | 26 ++++++++++++++ .../FormPhoneFieldInput.stories.tsx | 34 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx new file mode 100644 index 000000000000..5a49cc9ce0fd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/test'; + +import { FormCountryCodeSelectInput } from '../FormCountryCodeSelectInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormCountryCodeSelectInput', + component: FormCountryCodeSelectInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + selectedCountryCode: 'FR', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Country Code'); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx new file mode 100644 index 000000000000..eec784fa0bce --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/test'; + +import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; +import { FormPhoneFieldInput } from '../FormPhoneFieldInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormPhoneFieldInput', + component: FormPhoneFieldInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +const defaultPhoneValue: FieldPhonesValue = { + primaryPhoneNumber: '0612345678', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '33', +}; + +export const Default: Story = { + args: { + label: 'Phone', + defaultValue: defaultPhoneValue, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Phone'); + }, +};