Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflow phone number field #9324

Merged
merged 2 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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<FieldMetadata>;
Expand Down Expand Up @@ -109,6 +112,13 @@ export const FormFieldInput = ({
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : isFieldPhones(field) ? (
<FormPhoneFieldInput
label={field.label}
defaultValue={defaultValue as FieldPhonesValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : isFieldDate(field) ? (
<FormDateFieldInput
label={field.label}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useMemo } from 'react';
import { IconCircleOff, IconComponentProps } from 'twenty-ui';

import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { SelectOption } from '@/spreadsheet-import/types';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';

export const FormCountryCodeSelectInput = ({
selectedCountryCode,
onPersist,
readonly = false,
VariablePicker,
}: {
selectedCountryCode: string;
onPersist: (countryCode: string) => void;
thomtrp marked this conversation as resolved.
Show resolved Hide resolved
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
}) => {
const countries = useCountries();

const options: SelectOption[] = useMemo(() => {
const countryList = countries.map<SelectOption>(
({ 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 (
<FormSelectFieldInput
label="Country Code"
onPersist={onChange}
options={options}
defaultValue={selectedCountryCode}
VariablePicker={VariablePicker}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const FormCountrySelectInput = ({
VariablePicker,
}: {
selectedCountryName: string;
onPersist: (countryCode: string) => void;
onPersist: (country: string) => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
}) => {
Expand All @@ -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);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,6 +25,7 @@ type FormNumberFieldInputProps = {
defaultValue: number | string | undefined;
onPersist: (value: number | null | string) => void;
VariablePicker?: VariablePickerComponent;
hint?: string;
};

export const FormNumberFieldInput = ({
Expand All @@ -32,6 +34,7 @@ export const FormNumberFieldInput = ({
defaultValue,
onPersist,
VariablePicker,
hint,
}: FormNumberFieldInputProps) => {
const inputId = useId();

Expand Down Expand Up @@ -125,6 +128,8 @@ export const FormNumberFieldInput = ({
/>
) : null}
</FormFieldInputRowContainer>

{hint ? <FormFieldHint>{hint}</FormFieldHint> : null}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

</FormFieldInputContainer>
);
};
Original file line number Diff line number Diff line change
@@ -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);
thomtrp marked this conversation as resolved.
Show resolved Hide resolved

onPersist({
primaryPhoneCountryCode: newCountry,
primaryPhoneCallingCode: newCallingCode,
primaryPhoneNumber: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: resetting phone number on country change could lead to data loss - consider preserving it

});
};

const handleNumberChange = (number: string | number | null) => {
onPersist({
primaryPhoneCountryCode: defaultValue?.primaryPhoneCountryCode || '',
primaryPhoneCallingCode: defaultValue?.primaryPhoneCallingCode || '',
primaryPhoneNumber: number ? `${number}` : '',
});
};
Comment on lines +35 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: handleNumberChange should validate phone number format before persisting


return (
<FormFieldInputContainer>
{label && <InputLabel>{label}</InputLabel>}
<FormNestedFieldInputContainer>
<FormCountryCodeSelectInput
selectedCountryCode={defaultValue?.primaryPhoneCountryCode || ''}
onPersist={handleCountryChange}
readonly={readonly}
VariablePicker={VariablePicker}
/>
<FormNumberFieldInput
label="Phone Number"
defaultValue={defaultValue?.primaryPhoneNumber || ''}
onPersist={handleNumberChange}
VariablePicker={VariablePicker}
placeholder="Enter phone number"
hint="Without calling code"
/>
</FormNestedFieldInputContainer>
</FormFieldInputContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const FormSelectFieldInput = ({
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
isUsedInForm
/>
) : null}
</StyledDisplayModeContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';

import { FormCountryCodeSelectInput } from '../FormCountryCodeSelectInput';

const meta: Meta<typeof FormCountryCodeSelectInput> = {
title: 'UI/Data/Field/Form/Input/FormCountryCodeSelectInput',
component: FormCountryCodeSelectInput,
args: {},
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');
},
};
Original file line number Diff line number Diff line change
@@ -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<typeof FormPhoneFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormPhoneFieldInput',
component: FormPhoneFieldInput,
args: {},
argTypes: {},
};

export default meta;

type Story = StoryObj<typeof FormPhoneFieldInput>;

const defaultPhoneValue: FieldPhonesValue = {
primaryPhoneNumber: '0612345678',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '33',
};

export const Default: Story = {
args: {
label: 'Phone',
defaultValue: defaultPhoneValue,
},
Comment on lines +24 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing required onPersist prop in story args. This will cause runtime errors since FormPhoneFieldInput requires it.

play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await canvas.findByText('Phone');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@ type SelectDisplayProps = {
color: ThemeColor | 'transparent';
label: string;
Icon?: IconComponent;
isUsedInForm?: boolean;
};

export const SelectDisplay = ({ color, label, Icon }: SelectDisplayProps) => {
return <Tag preventShrink color={color} text={label} Icon={Icon} />;
export const SelectDisplay = ({
color,
label,
Icon,
isUsedInForm,
}: SelectDisplayProps) => {
return (
<Tag
preventShrink
color={color}
text={label}
Icon={Icon}
preventPadding={isUsedInForm}
/>
);
};
6 changes: 5 additions & 1 deletion packages/twenty-ui/src/display/tag/components/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const StyledTag = styled.h3<{
weight: TagWeight;
variant: TagVariant;
preventShrink?: boolean;
preventPadding?: boolean;
}>`
align-items: center;
background: ${({ color, theme }) => {
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -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
Expand All @@ -103,6 +105,7 @@ export const Tag = ({
weight = 'regular',
variant = 'solid',
preventShrink,
preventPadding,
}: TagProps) => {
const { theme } = useContext(ThemeContext);

Expand All @@ -115,6 +118,7 @@ export const Tag = ({
weight={weight}
variant={variant}
preventShrink={preventShrink}
preventPadding={preventPadding}
>
{isDefined(Icon) ? (
<StyledIconContainer>
Expand Down
Loading