Skip to content

Commit

Permalink
Add address composite form field (#9022)
Browse files Browse the repository at this point in the history
- Create FormCountrySelectInput using the existing FormSelectFieldInput
- Create AddressFormFieldInput component
- Fix FormSelectFieldInput dropdown + add leftIcon

<img width="554" alt="Capture d’écran 2024-12-11 à 15 56 32"
src="https://github.com/user-attachments/assets/c3019f29-af76-44e1-96bd-a0c6283674e1"
/>
  • Loading branch information
thomtrp authored Dec 11, 2024
1 parent 2c4a77a commit 4d9facb
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput';
import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput';
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
Expand All @@ -6,9 +7,11 @@ import { FormTextFieldInput } from '@/object-record/record-field/form-types/comp
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import {
FieldAddressValue,
FieldFullNameValue,
FieldMetadata,
} 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';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
Expand Down Expand Up @@ -57,8 +60,9 @@ export const FormFieldInput = ({
label={field.label}
defaultValue={defaultValue as string | undefined}
onPersist={onPersist}
field={field}
VariablePicker={VariablePicker}
options={field.metadata.options}
clearLabel={field.label}
/>
) : isFieldFullName(field) ? (
<FormFullNameFieldInput
Expand All @@ -67,5 +71,12 @@ export const FormFieldInput = ({
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : isFieldAddress(field) ? (
<FormAddressFieldInput
label={field.label}
defaultValue={defaultValue as FieldAddressValue}
onPersist={onPersist}
VariablePicker={VariablePicker}
/>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { FormCountrySelectInput } from '@/object-record/record-field/form-types/components/FormCountrySelectInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer';
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
import { InputLabel } from '@/ui/input/components/InputLabel';

type FormAddressFieldInputProps = {
label?: string;
defaultValue: FieldAddressDraftValue | null;
onPersist: (value: FieldAddressValue) => void;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
};

export const FormAddressFieldInput = ({
label,
defaultValue,
onPersist,
readonly,
VariablePicker,
}: FormAddressFieldInputProps) => {
const handleChange =
(field: keyof FieldAddressDraftValue) => (updatedAddressPart: string) => {
const updatedAddress = {
addressStreet1: defaultValue?.addressStreet1 ?? '',
addressStreet2: defaultValue?.addressStreet2 ?? '',
addressCity: defaultValue?.addressCity ?? '',
addressState: defaultValue?.addressState ?? '',
addressPostcode: defaultValue?.addressPostcode ?? '',
addressCountry: defaultValue?.addressCountry ?? '',
addressLat: defaultValue?.addressLat ?? null,
addressLng: defaultValue?.addressLng ?? null,
[field]: updatedAddressPart,
};
onPersist(updatedAddress);
};

return (
<StyledFormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}
<StyledFormCompositeFieldInputContainer>
<FormTextFieldInput
label="Address 1"
defaultValue={defaultValue?.addressStreet1 ?? ''}
onPersist={handleChange('addressStreet1')}
readonly={readonly}
VariablePicker={VariablePicker}
placeholder="Street address"
/>
<FormTextFieldInput
label="Address 2"
defaultValue={defaultValue?.addressStreet2 ?? ''}
onPersist={handleChange('addressStreet2')}
readonly={readonly}
VariablePicker={VariablePicker}
placeholder="Street address 2"
/>
<FormTextFieldInput
label="City"
defaultValue={defaultValue?.addressCity ?? ''}
onPersist={handleChange('addressCity')}
readonly={readonly}
VariablePicker={VariablePicker}
placeholder="City"
/>
<FormTextFieldInput
label="State"
defaultValue={defaultValue?.addressState ?? ''}
onPersist={handleChange('addressState')}
readonly={readonly}
VariablePicker={VariablePicker}
placeholder="State"
/>
<FormTextFieldInput
label="Post Code"
defaultValue={defaultValue?.addressPostcode ?? ''}
onPersist={handleChange('addressPostcode')}
readonly={readonly}
VariablePicker={VariablePicker}
placeholder="Post Code"
/>
<FormCountrySelectInput
selectedCountryName={defaultValue?.addressCountry ?? ''}
onPersist={handleChange('addressCountry')}
readonly={readonly}
VariablePicker={VariablePicker}
/>
</StyledFormCompositeFieldInputContainer>
</StyledFormFieldInputContainer>
);
};
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 FormCountrySelectInput = ({
selectedCountryName,
onPersist,
readonly = false,
VariablePicker,
}: {
selectedCountryName: string;
onPersist: (countryCode: string) => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
}) => {
const countries = useCountries();

const options: SelectOption[] = useMemo(() => {
const countryList = countries.map<SelectOption>(
({ countryName, Flag }) => ({
label: countryName,
value: countryName,
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"
onPersist={onChange}
options={options}
defaultValue={selectedCountryName}
VariablePicker={VariablePicker}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { StyledFormFieldInputInputContainer } from '@/object-record/record-field
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldSelectMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { SINGLE_RECORD_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleRecordSelectBaseList';
import { SelectOption } from '@/spreadsheet-import/types';
Expand All @@ -21,11 +19,12 @@ import { Key } from 'ts-key-enum';
import { isDefined, VisibilityHidden } from 'twenty-ui';

type FormSelectFieldInputProps = {
field: FieldDefinition<FieldSelectMetadata>;
label?: string;
defaultValue: string | undefined;
onPersist: (value: number | null | string) => void;
onPersist: (value: string | null) => void;
VariablePicker?: VariablePickerComponent;
options: SelectOption[];
clearLabel?: string;
};

const StyledDisplayModeContainer = styled.button`
Expand All @@ -44,12 +43,19 @@ const StyledDisplayModeContainer = styled.button`
}
`;

const StyledSelectInputContainer = styled.div`
position: absolute;
z-index: 1;
top: ${({ theme }) => theme.spacing(8)};
`;

export const FormSelectFieldInput = ({
label,
field,
defaultValue,
onPersist,
VariablePicker,
options,
clearLabel,
}: FormSelectFieldInputProps) => {
const inputId = useId();

Expand Down Expand Up @@ -124,7 +130,7 @@ export const FormSelectFieldInput = ({
onPersist(null);
};

const selectedOption = field.metadata.options.find(
const selectedOption = options.find(
(option) => option.value === draftValue.value,
);

Expand Down Expand Up @@ -193,7 +199,7 @@ export const FormSelectFieldInput = ({
);

const optionIds = [
`No ${field.label}`,
`No ${label}`,
...filteredOptions.map((option) => option.value),
];

Expand All @@ -215,29 +221,12 @@ export const FormSelectFieldInput = ({

{isDefined(selectedOption) ? (
<SelectDisplay
color={selectedOption.color}
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
/>
) : null}
</StyledDisplayModeContainer>

{draftValue.editingMode === 'edit' ? (
<SelectInput
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope}
onEnter={handleSelectEnter}
onOptionSelected={handleSubmit}
options={field.metadata.options}
onCancel={onCancel}
defaultOption={selectedOption}
onFilterChange={setFilteredOptions}
onClear={
field.metadata.isNullable ? handleClearField : undefined
}
clearLabel={field.label}
/>
) : null}
</>
) : (
<VariableChip
Expand All @@ -246,13 +235,31 @@ export const FormSelectFieldInput = ({
/>
)}
</StyledFormFieldInputInputContainer>

{VariablePicker ? (
<StyledSelectInputContainer>
{draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && (
<SelectInput
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope}
onEnter={handleSelectEnter}
onOptionSelected={handleSubmit}
options={options}
onCancel={onCancel}
defaultOption={selectedOption}
onFilterChange={setFilteredOptions}
onClear={handleClearField}
clearLabel={clearLabel}
/>
)}
</StyledSelectInputContainer>

{VariablePicker && (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}
)}
</StyledFormFieldInputRowContainer>
</StyledFormFieldInputContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { FormAddressFieldInput } from '../FormAddressFieldInput';

const meta: Meta<typeof FormAddressFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormAddressFieldInput',
component: FormAddressFieldInput,
args: {},
argTypes: {},
};

export default meta;

type Story = StoryObj<typeof FormAddressFieldInput>;

export const Default: Story = {
args: {
label: 'Address',
defaultValue: {
addressStreet1: '123 Main St',
addressStreet2: 'Apt 123',
addressCity: 'Springfield',
addressState: 'IL',
addressCountry: 'US',
addressPostcode: '12345',
addressLat: 39.781721,
addressLng: -89.650148,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await canvas.findByText('123 Main St');
await canvas.findByText('Address');
await canvas.findByText('Post Code');
},
};
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 { FormCountrySelectInput } from '../FormCountrySelectInput';

const meta: Meta<typeof FormCountrySelectInput> = {
title: 'UI/Data/Field/Form/Input/FormCountrySelectInput',
component: FormCountrySelectInput,
args: {},
argTypes: {},
};

export default meta;

type Story = StoryObj<typeof FormCountrySelectInput>;

export const Default: Story = {
args: {
selectedCountryName: 'Canada',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await canvas.findByText('Country');
await canvas.findByText('Canada');
},
};
Loading

0 comments on commit 4d9facb

Please sign in to comment.