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

Add address composite form field #9022

Merged
merged 3 commits into from
Dec 11, 2024
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,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}
Comment on lines +64 to +65
Copy link
Contributor

Choose a reason for hiding this comment

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

Dissociating the field definition from the input is excellent. I'm unsure if it will "scale" well as the other fields make extensive use of the useNumberField-like functions and get access to the field definition that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let's see. If it becomes too complex we will still be able to come back to full field!

/>
) : 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('');
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know much about addresses, but are we sure we want to store an empty string vs. null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep default values have to be string

} 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
Loading