Refacto form select input (#11426)
- small fixes on form design - refacto form select input to use the existing select component
This commit is contained in:
@ -92,9 +92,7 @@ export const FormFieldInput = ({
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
options={field.metadata.options}
|
||||
clearLabel={field.label}
|
||||
readonly={readonly}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
) : isFieldFullName(field) ? (
|
||||
<FormFullNameFieldInput
|
||||
|
||||
@ -27,8 +27,7 @@ export const FormCountryCodeSelectInput = ({
|
||||
({ countryName, countryCode, callingCode, Flag }) => ({
|
||||
label: `${countryName} (+${callingCode})`,
|
||||
value: countryCode,
|
||||
color: 'transparent',
|
||||
icon: (props: IconComponentProps) =>
|
||||
Icon: (props: IconComponentProps) =>
|
||||
Flag({ width: props.size, height: props.size }),
|
||||
}),
|
||||
);
|
||||
@ -36,7 +35,7 @@ export const FormCountryCodeSelectInput = ({
|
||||
{
|
||||
label: 'No country',
|
||||
value: '',
|
||||
icon: IconCircleOff,
|
||||
Icon: IconCircleOff,
|
||||
},
|
||||
...countryList,
|
||||
];
|
||||
@ -62,7 +61,6 @@ export const FormCountryCodeSelectInput = ({
|
||||
defaultValue={selectedCountryCode}
|
||||
readonly={readonly}
|
||||
VariablePicker={VariablePicker}
|
||||
preventDisplayPadding
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -24,8 +24,7 @@ export const FormCountrySelectInput = ({
|
||||
({ countryName, Flag }) => ({
|
||||
label: countryName,
|
||||
value: countryName,
|
||||
color: 'transparent',
|
||||
icon: (props: IconComponentProps) =>
|
||||
Icon: (props: IconComponentProps) =>
|
||||
Flag({ width: props.size, height: props.size }),
|
||||
}),
|
||||
);
|
||||
@ -33,7 +32,7 @@ export const FormCountrySelectInput = ({
|
||||
{
|
||||
label: 'No country',
|
||||
value: '',
|
||||
icon: IconCircleOff,
|
||||
Icon: IconCircleOff,
|
||||
},
|
||||
...countryList,
|
||||
];
|
||||
@ -59,8 +58,6 @@ export const FormCountrySelectInput = ({
|
||||
defaultValue={selectedCountryName}
|
||||
readonly={readonly}
|
||||
VariablePicker={VariablePicker}
|
||||
placeholder="Select a country"
|
||||
preventDisplayPadding
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import { FormFieldCurrencyValue } from '@/object-record/record-field/types/Field
|
||||
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { useMemo } from 'react';
|
||||
import { IconCircleOff } from 'twenty-ui/display';
|
||||
|
||||
type FormCurrencyFieldInputProps = {
|
||||
label?: string;
|
||||
@ -25,13 +26,22 @@ export const FormCurrencyFieldInput = ({
|
||||
readonly,
|
||||
}: FormCurrencyFieldInputProps) => {
|
||||
const currencies = useMemo(() => {
|
||||
return Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
||||
const currencies = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
||||
([key, { Icon, label }]) => ({
|
||||
value: key,
|
||||
icon: Icon,
|
||||
Icon,
|
||||
label: `${label} (${key})`,
|
||||
}),
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'No currency',
|
||||
value: '',
|
||||
Icon: IconCircleOff,
|
||||
},
|
||||
...currencies,
|
||||
];
|
||||
}, []);
|
||||
|
||||
const handleAmountMicrosChange = (
|
||||
@ -59,11 +69,8 @@ export const FormCurrencyFieldInput = ({
|
||||
defaultValue={defaultValue?.currencyCode ?? ''}
|
||||
onChange={handleCurrencyCodeChange}
|
||||
options={currencies}
|
||||
clearLabel={'Currency Code'}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
placeholder="Select a currency"
|
||||
preventDisplayPadding
|
||||
/>
|
||||
<FormNumberFieldInput
|
||||
label="Amount Micros"
|
||||
|
||||
@ -3,24 +3,16 @@ import { FormFieldInputInputContainer } from '@/object-record/record-field/form-
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay';
|
||||
import { SelectInput } from '@/ui/field/input/components/SelectInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useId, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronDown } from 'twenty-ui/display';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
import { VisibilityHidden } from 'twenty-ui/accessibility';
|
||||
|
||||
type FormSelectFieldInputProps = {
|
||||
label?: string;
|
||||
@ -28,68 +20,22 @@ type FormSelectFieldInputProps = {
|
||||
onChange: (value: string | null) => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
options: SelectOption[];
|
||||
clearLabel?: string;
|
||||
readonly?: boolean;
|
||||
preventDisplayPadding?: boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const StyledDisplayModeReadonlyContainer = styled.div`
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
font-family: inherit;
|
||||
padding-inline: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDisplayModeContainer = styled(StyledDisplayModeReadonlyContainer)`
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&[data-open='true'] {
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledPlaceholder = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSelectInputContainer = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledSelectDisplayContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FormSelectFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
onChange,
|
||||
VariablePicker,
|
||||
options,
|
||||
clearLabel,
|
||||
readonly,
|
||||
preventDisplayPadding,
|
||||
placeholder,
|
||||
}: FormSelectFieldInputProps) => {
|
||||
const inputId = useId();
|
||||
|
||||
const hotkeyScope = InlineCellHotkeyScope.InlineCell;
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const [draftValue, setDraftValue] = useState<
|
||||
| {
|
||||
@ -114,7 +60,7 @@ export const FormSelectFieldInput = ({
|
||||
},
|
||||
);
|
||||
|
||||
const onSubmit = (option: string) => {
|
||||
const onSelect = (option: string) => {
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: option,
|
||||
@ -139,38 +85,10 @@ export const FormSelectFieldInput = ({
|
||||
goBackToPreviousHotkeyScope();
|
||||
};
|
||||
|
||||
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const clearField = () => {
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
editingMode: 'view',
|
||||
value: '',
|
||||
});
|
||||
|
||||
onChange(null);
|
||||
};
|
||||
|
||||
const selectedOption = options.find(
|
||||
(option) => option.value === draftValue.value,
|
||||
);
|
||||
|
||||
const handleClearField = () => {
|
||||
clearField();
|
||||
|
||||
goBackToPreviousHotkeyScope();
|
||||
};
|
||||
|
||||
const handleSubmit = (option: SelectOption) => {
|
||||
onSubmit(option.value);
|
||||
|
||||
resetSelectedItem();
|
||||
};
|
||||
|
||||
const handleUnlinkVariable = () => {
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
@ -190,131 +108,43 @@ export const FormSelectFieldInput = ({
|
||||
onChange(variableName);
|
||||
};
|
||||
|
||||
const handleDisplayModeClick = () => {
|
||||
if (draftValue.type !== 'static') {
|
||||
throw new Error(
|
||||
'This function can only be called when editing a static value.',
|
||||
);
|
||||
}
|
||||
|
||||
setDraftValue({
|
||||
...draftValue,
|
||||
editingMode: 'edit',
|
||||
});
|
||||
|
||||
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
|
||||
};
|
||||
|
||||
const handleSelectEnter = (itemId: string) => {
|
||||
const option = filteredOptions.find((option) => option.value === itemId);
|
||||
if (isDefined(option)) {
|
||||
onSubmit(option.value);
|
||||
resetSelectedItem();
|
||||
}
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
onCancel();
|
||||
resetSelectedItem();
|
||||
},
|
||||
hotkeyScope,
|
||||
[onCancel, resetSelectedItem],
|
||||
[onCancel],
|
||||
);
|
||||
|
||||
const optionIds = [
|
||||
`No ${label}`,
|
||||
...filteredOptions.map((option) => option.value),
|
||||
];
|
||||
|
||||
const placeholderText = placeholder ?? label;
|
||||
|
||||
return (
|
||||
<FormFieldInputContainer>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInputContainer
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
>
|
||||
{draftValue.type === 'static' ? (
|
||||
readonly ? (
|
||||
<StyledDisplayModeReadonlyContainer>
|
||||
{isDefined(selectedOption) ? (
|
||||
<StyledSelectDisplayContainer>
|
||||
<SelectDisplay
|
||||
color={selectedOption.color ?? 'transparent'}
|
||||
label={selectedOption.label}
|
||||
Icon={selectedOption.Icon ?? undefined}
|
||||
preventPadding={preventDisplayPadding}
|
||||
/>
|
||||
</StyledSelectDisplayContainer>
|
||||
) : (
|
||||
<StyledPlaceholder />
|
||||
)}
|
||||
<IconChevronDown
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
</StyledDisplayModeReadonlyContainer>
|
||||
) : (
|
||||
<StyledDisplayModeContainer
|
||||
data-open={draftValue.editingMode === 'edit'}
|
||||
onClick={handleDisplayModeClick}
|
||||
>
|
||||
<VisibilityHidden>Edit</VisibilityHidden>
|
||||
|
||||
{isDefined(selectedOption) ? (
|
||||
<StyledSelectDisplayContainer>
|
||||
<SelectDisplay
|
||||
color={selectedOption.color ?? 'transparent'}
|
||||
label={selectedOption.label}
|
||||
Icon={selectedOption.Icon ?? undefined}
|
||||
preventPadding={preventDisplayPadding}
|
||||
/>
|
||||
</StyledSelectDisplayContainer>
|
||||
) : (
|
||||
<StyledPlaceholder>{placeholderText}</StyledPlaceholder>
|
||||
)}
|
||||
<IconChevronDown
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</StyledDisplayModeContainer>
|
||||
)
|
||||
) : (
|
||||
{draftValue.type === 'static' ? (
|
||||
<Select
|
||||
dropdownId={`${inputId}-select-display`}
|
||||
options={options}
|
||||
value={selectedOption?.value}
|
||||
onChange={onSelect}
|
||||
fullWidth
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
withSearchInput
|
||||
disabled={readonly}
|
||||
/>
|
||||
) : (
|
||||
<FormFieldInputInputContainer
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
>
|
||||
<VariableChipStandalone
|
||||
rawVariableName={draftValue.value}
|
||||
onRemove={readonly ? undefined : handleUnlinkVariable}
|
||||
/>
|
||||
)}
|
||||
</FormFieldInputInputContainer>
|
||||
<StyledSelectInputContainer>
|
||||
{!readonly &&
|
||||
draftValue.type === 'static' &&
|
||||
draftValue.editingMode === 'edit' && (
|
||||
<OverlayContainer>
|
||||
<SelectInput
|
||||
selectableListComponentInstanceId={
|
||||
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
|
||||
}
|
||||
selectableItemIdArray={optionIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={handleSelectEnter}
|
||||
onOptionSelected={handleSubmit}
|
||||
options={options}
|
||||
onCancel={onCancel}
|
||||
defaultOption={selectedOption}
|
||||
onFilterChange={setFilteredOptions}
|
||||
onClear={handleClearField}
|
||||
clearLabel={clearLabel}
|
||||
/>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</StyledSelectInputContainer>
|
||||
</FormFieldInputInputContainer>
|
||||
)}
|
||||
|
||||
{VariablePicker && !readonly && (
|
||||
{isDefined(VariablePicker) && !readonly && (
|
||||
<VariablePicker
|
||||
inputId={inputId}
|
||||
onVariableSelect={handleVariableTagInsert}
|
||||
|
||||
@ -37,77 +37,6 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectCountryCode: Story = {
|
||||
args: {
|
||||
label: 'Phone',
|
||||
onChange: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const defaultEmptyOption = await canvas.findByText('No country');
|
||||
expect(defaultEmptyOption).toBeVisible();
|
||||
|
||||
await userEvent.click(defaultEmptyOption);
|
||||
|
||||
const searchInput = await canvas.findByPlaceholderText('Search');
|
||||
expect(searchInput).toBeVisible();
|
||||
|
||||
await userEvent.type(searchInput, 'France');
|
||||
|
||||
const franceOption = await canvas.findByText(/France/);
|
||||
|
||||
await userEvent.click(franceOption);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onChange).toHaveBeenCalledWith({
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '33',
|
||||
});
|
||||
});
|
||||
|
||||
expect(args.onChange).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectEmptyCountryCode: Story = {
|
||||
args: {
|
||||
label: 'Phone',
|
||||
onChange: fn(),
|
||||
defaultValue: {
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '33',
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const defaultSelectedOption = await canvas.findByText(/France/);
|
||||
expect(defaultSelectedOption).toBeVisible();
|
||||
|
||||
await userEvent.click(defaultSelectedOption);
|
||||
|
||||
const searchInput = await canvas.findByPlaceholderText('Search');
|
||||
expect(searchInput).toBeVisible();
|
||||
|
||||
const emptyOption = await canvas.findByText('No country');
|
||||
|
||||
await userEvent.click(emptyOption);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onChange).toHaveBeenCalledWith({
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: '',
|
||||
primaryPhoneCallingCode: '',
|
||||
});
|
||||
});
|
||||
|
||||
expect(args.onChange).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithVariablesAsDefaultValues: Story = {
|
||||
args: {
|
||||
label: 'Phone',
|
||||
|
||||
Reference in New Issue
Block a user