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}
|
onChange={onChange}
|
||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
options={field.metadata.options}
|
options={field.metadata.options}
|
||||||
clearLabel={field.label}
|
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
/>
|
||||||
) : isFieldFullName(field) ? (
|
) : isFieldFullName(field) ? (
|
||||||
<FormFullNameFieldInput
|
<FormFullNameFieldInput
|
||||||
|
|||||||
@ -27,8 +27,7 @@ export const FormCountryCodeSelectInput = ({
|
|||||||
({ countryName, countryCode, callingCode, Flag }) => ({
|
({ countryName, countryCode, callingCode, Flag }) => ({
|
||||||
label: `${countryName} (+${callingCode})`,
|
label: `${countryName} (+${callingCode})`,
|
||||||
value: countryCode,
|
value: countryCode,
|
||||||
color: 'transparent',
|
Icon: (props: IconComponentProps) =>
|
||||||
icon: (props: IconComponentProps) =>
|
|
||||||
Flag({ width: props.size, height: props.size }),
|
Flag({ width: props.size, height: props.size }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -36,7 +35,7 @@ export const FormCountryCodeSelectInput = ({
|
|||||||
{
|
{
|
||||||
label: 'No country',
|
label: 'No country',
|
||||||
value: '',
|
value: '',
|
||||||
icon: IconCircleOff,
|
Icon: IconCircleOff,
|
||||||
},
|
},
|
||||||
...countryList,
|
...countryList,
|
||||||
];
|
];
|
||||||
@ -62,7 +61,6 @@ export const FormCountryCodeSelectInput = ({
|
|||||||
defaultValue={selectedCountryCode}
|
defaultValue={selectedCountryCode}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
preventDisplayPadding
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,8 +24,7 @@ export const FormCountrySelectInput = ({
|
|||||||
({ countryName, Flag }) => ({
|
({ countryName, Flag }) => ({
|
||||||
label: countryName,
|
label: countryName,
|
||||||
value: countryName,
|
value: countryName,
|
||||||
color: 'transparent',
|
Icon: (props: IconComponentProps) =>
|
||||||
icon: (props: IconComponentProps) =>
|
|
||||||
Flag({ width: props.size, height: props.size }),
|
Flag({ width: props.size, height: props.size }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -33,7 +32,7 @@ export const FormCountrySelectInput = ({
|
|||||||
{
|
{
|
||||||
label: 'No country',
|
label: 'No country',
|
||||||
value: '',
|
value: '',
|
||||||
icon: IconCircleOff,
|
Icon: IconCircleOff,
|
||||||
},
|
},
|
||||||
...countryList,
|
...countryList,
|
||||||
];
|
];
|
||||||
@ -59,8 +58,6 @@ export const FormCountrySelectInput = ({
|
|||||||
defaultValue={selectedCountryName}
|
defaultValue={selectedCountryName}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
VariablePicker={VariablePicker}
|
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 { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
|
||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { IconCircleOff } from 'twenty-ui/display';
|
||||||
|
|
||||||
type FormCurrencyFieldInputProps = {
|
type FormCurrencyFieldInputProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -25,13 +26,22 @@ export const FormCurrencyFieldInput = ({
|
|||||||
readonly,
|
readonly,
|
||||||
}: FormCurrencyFieldInputProps) => {
|
}: FormCurrencyFieldInputProps) => {
|
||||||
const currencies = useMemo(() => {
|
const currencies = useMemo(() => {
|
||||||
return Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
const currencies = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
||||||
([key, { Icon, label }]) => ({
|
([key, { Icon, label }]) => ({
|
||||||
value: key,
|
value: key,
|
||||||
icon: Icon,
|
Icon,
|
||||||
label: `${label} (${key})`,
|
label: `${label} (${key})`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'No currency',
|
||||||
|
value: '',
|
||||||
|
Icon: IconCircleOff,
|
||||||
|
},
|
||||||
|
...currencies,
|
||||||
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAmountMicrosChange = (
|
const handleAmountMicrosChange = (
|
||||||
@ -59,11 +69,8 @@ export const FormCurrencyFieldInput = ({
|
|||||||
defaultValue={defaultValue?.currencyCode ?? ''}
|
defaultValue={defaultValue?.currencyCode ?? ''}
|
||||||
onChange={handleCurrencyCodeChange}
|
onChange={handleCurrencyCodeChange}
|
||||||
options={currencies}
|
options={currencies}
|
||||||
clearLabel={'Currency Code'}
|
|
||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
placeholder="Select a currency"
|
|
||||||
preventDisplayPadding
|
|
||||||
/>
|
/>
|
||||||
<FormNumberFieldInput
|
<FormNumberFieldInput
|
||||||
label="Amount Micros"
|
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 { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
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 { 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 { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useId, useState } from 'react';
|
import { useId, useState } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconChevronDown } from 'twenty-ui/display';
|
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
import { VisibilityHidden } from 'twenty-ui/accessibility';
|
|
||||||
|
|
||||||
type FormSelectFieldInputProps = {
|
type FormSelectFieldInputProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -28,68 +20,22 @@ type FormSelectFieldInputProps = {
|
|||||||
onChange: (value: string | null) => void;
|
onChange: (value: string | null) => void;
|
||||||
VariablePicker?: VariablePickerComponent;
|
VariablePicker?: VariablePickerComponent;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
clearLabel?: string;
|
|
||||||
readonly?: boolean;
|
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 = ({
|
export const FormSelectFieldInput = ({
|
||||||
label,
|
label,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onChange,
|
onChange,
|
||||||
VariablePicker,
|
VariablePicker,
|
||||||
options,
|
options,
|
||||||
clearLabel,
|
|
||||||
readonly,
|
readonly,
|
||||||
preventDisplayPadding,
|
|
||||||
placeholder,
|
|
||||||
}: FormSelectFieldInputProps) => {
|
}: FormSelectFieldInputProps) => {
|
||||||
const inputId = useId();
|
const inputId = useId();
|
||||||
|
|
||||||
const hotkeyScope = InlineCellHotkeyScope.InlineCell;
|
const hotkeyScope = InlineCellHotkeyScope.InlineCell;
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const {
|
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
|
||||||
goBackToPreviousHotkeyScope,
|
|
||||||
} = usePreviousHotkeyScope();
|
|
||||||
|
|
||||||
const [draftValue, setDraftValue] = useState<
|
const [draftValue, setDraftValue] = useState<
|
||||||
| {
|
| {
|
||||||
@ -114,7 +60,7 @@ export const FormSelectFieldInput = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSubmit = (option: string) => {
|
const onSelect = (option: string) => {
|
||||||
setDraftValue({
|
setDraftValue({
|
||||||
type: 'static',
|
type: 'static',
|
||||||
value: option,
|
value: option,
|
||||||
@ -139,38 +85,10 @@ export const FormSelectFieldInput = ({
|
|||||||
goBackToPreviousHotkeyScope();
|
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(
|
const selectedOption = options.find(
|
||||||
(option) => option.value === draftValue.value,
|
(option) => option.value === draftValue.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearField = () => {
|
|
||||||
clearField();
|
|
||||||
|
|
||||||
goBackToPreviousHotkeyScope();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (option: SelectOption) => {
|
|
||||||
onSubmit(option.value);
|
|
||||||
|
|
||||||
resetSelectedItem();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnlinkVariable = () => {
|
const handleUnlinkVariable = () => {
|
||||||
setDraftValue({
|
setDraftValue({
|
||||||
type: 'static',
|
type: 'static',
|
||||||
@ -190,131 +108,43 @@ export const FormSelectFieldInput = ({
|
|||||||
onChange(variableName);
|
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(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
Key.Escape,
|
||||||
() => {
|
() => {
|
||||||
onCancel();
|
onCancel();
|
||||||
resetSelectedItem();
|
|
||||||
},
|
},
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
[onCancel, resetSelectedItem],
|
[onCancel],
|
||||||
);
|
);
|
||||||
|
|
||||||
const optionIds = [
|
|
||||||
`No ${label}`,
|
|
||||||
...filteredOptions.map((option) => option.value),
|
|
||||||
];
|
|
||||||
|
|
||||||
const placeholderText = placeholder ?? label;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
|
|
||||||
<FormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<FormFieldInputInputContainer
|
{draftValue.type === 'static' ? (
|
||||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
<Select
|
||||||
>
|
dropdownId={`${inputId}-select-display`}
|
||||||
{draftValue.type === 'static' ? (
|
options={options}
|
||||||
readonly ? (
|
value={selectedOption?.value}
|
||||||
<StyledDisplayModeReadonlyContainer>
|
onChange={onSelect}
|
||||||
{isDefined(selectedOption) ? (
|
fullWidth
|
||||||
<StyledSelectDisplayContainer>
|
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||||
<SelectDisplay
|
withSearchInput
|
||||||
color={selectedOption.color ?? 'transparent'}
|
disabled={readonly}
|
||||||
label={selectedOption.label}
|
/>
|
||||||
Icon={selectedOption.Icon ?? undefined}
|
) : (
|
||||||
preventPadding={preventDisplayPadding}
|
<FormFieldInputInputContainer
|
||||||
/>
|
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||||
</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>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<VariableChipStandalone
|
<VariableChipStandalone
|
||||||
rawVariableName={draftValue.value}
|
rawVariableName={draftValue.value}
|
||||||
onRemove={readonly ? undefined : handleUnlinkVariable}
|
onRemove={readonly ? undefined : handleUnlinkVariable}
|
||||||
/>
|
/>
|
||||||
)}
|
</FormFieldInputInputContainer>
|
||||||
</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>
|
|
||||||
|
|
||||||
{VariablePicker && !readonly && (
|
{isDefined(VariablePicker) && !readonly && (
|
||||||
<VariablePicker
|
<VariablePicker
|
||||||
inputId={inputId}
|
inputId={inputId}
|
||||||
onVariableSelect={handleVariableTagInsert}
|
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 = {
|
export const WithVariablesAsDefaultValues: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Phone',
|
label: 'Phone',
|
||||||
|
|||||||
@ -10,10 +10,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||||
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
|
||||||
import { IconComponent } from 'twenty-ui/display';
|
import { IconComponent } from 'twenty-ui/display';
|
||||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
|
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
||||||
|
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||||
|
|
||||||
export type SelectSizeVariant = 'small' | 'default';
|
export type SelectSizeVariant = 'small' | 'default';
|
||||||
|
|
||||||
@ -43,6 +43,7 @@ export type SelectProps<Value extends SelectValue> = {
|
|||||||
needIconCheck?: boolean;
|
needIconCheck?: boolean;
|
||||||
callToActionButton?: CallToActionButton;
|
callToActionButton?: CallToActionButton;
|
||||||
dropdownOffset?: DropdownOffset;
|
dropdownOffset?: DropdownOffset;
|
||||||
|
hasRightElement?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||||
@ -75,6 +76,7 @@ export const Select = <Value extends SelectValue>({
|
|||||||
needIconCheck,
|
needIconCheck,
|
||||||
callToActionButton,
|
callToActionButton,
|
||||||
dropdownOffset,
|
dropdownOffset,
|
||||||
|
hasRightElement,
|
||||||
}: SelectProps<Value>) => {
|
}: SelectProps<Value>) => {
|
||||||
const selectContainerRef = useRef<HTMLDivElement>(null);
|
const selectContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -121,6 +123,7 @@ export const Select = <Value extends SelectValue>({
|
|||||||
selectedOption={selectedOption}
|
selectedOption={selectedOption}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
selectSizeVariant={selectSizeVariant}
|
selectSizeVariant={selectSizeVariant}
|
||||||
|
hasRightElement={hasRightElement}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -133,6 +136,7 @@ export const Select = <Value extends SelectValue>({
|
|||||||
selectedOption={selectedOption}
|
selectedOption={selectedOption}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
selectSizeVariant={selectSizeVariant}
|
selectSizeVariant={selectSizeVariant}
|
||||||
|
hasRightElement={hasRightElement}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { SelectSizeVariant } from '@/ui/input/components/Select';
|
import { SelectSizeVariant } from '@/ui/input/components/Select';
|
||||||
import { useTheme } from '@emotion/react';
|
import { css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconChevronDown, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
import { IconChevronDown, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||||
@ -13,6 +13,7 @@ const StyledControlContainer = styled.div<{
|
|||||||
hasIcon: boolean;
|
hasIcon: boolean;
|
||||||
selectSizeVariant?: SelectSizeVariant;
|
selectSizeVariant?: SelectSizeVariant;
|
||||||
textAccent: SelectControlTextAccent;
|
textAccent: SelectControlTextAccent;
|
||||||
|
hasRightElement?: boolean;
|
||||||
}>`
|
}>`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: ${({ hasIcon }) =>
|
grid-template-columns: ${({ hasIcon }) =>
|
||||||
@ -26,7 +27,22 @@ const StyledControlContainer = styled.div<{
|
|||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
|
||||||
|
${({ hasRightElement, theme }) =>
|
||||||
|
!hasRightElement
|
||||||
|
? css`
|
||||||
|
border-right: auto;
|
||||||
|
border-bottom-right-radius: ${theme.border.radius.sm};
|
||||||
|
border-top-right-radius: ${theme.border.radius.sm};
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
border-right: none;
|
||||||
|
border-bottom-right-radius: none;
|
||||||
|
border-top-right-radius: none;
|
||||||
|
`}
|
||||||
|
|
||||||
color: ${({ disabled, theme, textAccent }) =>
|
color: ${({ disabled, theme, textAccent }) =>
|
||||||
disabled
|
disabled
|
||||||
? theme.font.color.tertiary
|
? theme.font.color.tertiary
|
||||||
@ -49,6 +65,7 @@ type SelectControlProps = {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
selectSizeVariant?: SelectSizeVariant;
|
selectSizeVariant?: SelectSizeVariant;
|
||||||
textAccent?: SelectControlTextAccent;
|
textAccent?: SelectControlTextAccent;
|
||||||
|
hasRightElement?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectControl = ({
|
export const SelectControl = ({
|
||||||
@ -56,6 +73,7 @@ export const SelectControl = ({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
selectSizeVariant,
|
selectSizeVariant,
|
||||||
textAccent = 'default',
|
textAccent = 'default',
|
||||||
|
hasRightElement,
|
||||||
}: SelectControlProps) => {
|
}: SelectControlProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -65,6 +83,7 @@ export const SelectControl = ({
|
|||||||
hasIcon={isDefined(selectedOption.Icon)}
|
hasIcon={isDefined(selectedOption.Icon)}
|
||||||
selectSizeVariant={selectSizeVariant}
|
selectSizeVariant={selectSizeVariant}
|
||||||
textAccent={textAccent}
|
textAccent={textAccent}
|
||||||
|
hasRightElement={hasRightElement}
|
||||||
>
|
>
|
||||||
{isDefined(selectedOption.Icon) ? (
|
{isDefined(selectedOption.Icon) ? (
|
||||||
<selectedOption.Icon
|
<selectedOption.Icon
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const StyledWorkflowStepBody = styled.div`
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-block: ${({ theme }) => theme.spacing(4)};
|
padding-block: ${({ theme }) => theme.spacing(4)};
|
||||||
padding-inline: ${({ theme }) => theme.spacing(3)};
|
padding-inline: ${({ theme }) => theme.spacing(3)};
|
||||||
row-gap: ${({ theme }) => theme.spacing(6)};
|
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export { StyledWorkflowStepBody as WorkflowStepBody };
|
export { StyledWorkflowStepBody as WorkflowStepBody };
|
||||||
|
|||||||
@ -20,13 +20,13 @@ import { useDebouncedCallback } from 'use-debounce';
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
useIcons,
|
useIcons,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export type WorkflowEditActionFormBuilderProps = {
|
export type WorkflowEditActionFormBuilderProps = {
|
||||||
action: WorkflowFormAction;
|
action: WorkflowFormAction;
|
||||||
@ -207,7 +207,7 @@ export const WorkflowEditActionFormBuilder = ({
|
|||||||
? field.placeholder
|
? field.placeholder
|
||||||
: getDefaultFormFieldSettings(field.type).placeholder}
|
: getDefaultFormFieldSettings(field.type).placeholder}
|
||||||
</StyledPlaceholder>
|
</StyledPlaceholder>
|
||||||
{!isFieldSelected(field.id) && (
|
{field.type === 'RECORD' && (
|
||||||
<IconChevronDown
|
<IconChevronDown
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
color={theme.font.color.tertiary}
|
color={theme.font.color.tertiary}
|
||||||
@ -260,15 +260,15 @@ export const WorkflowEditActionFormBuilder = ({
|
|||||||
<FormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={false}
|
hasRightElement={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { label, placeholder, name } =
|
const { label, name } = getDefaultFormFieldSettings(
|
||||||
getDefaultFormFieldSettings(FieldMetadataType.TEXT);
|
FieldMetadataType.TEXT,
|
||||||
|
);
|
||||||
|
|
||||||
const newField: WorkflowFormActionField = {
|
const newField: WorkflowFormActionField = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
name,
|
name,
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setFormData([...formData, newField]);
|
setFormData([...formData, newField]);
|
||||||
@ -280,6 +280,8 @@ export const WorkflowEditActionFormBuilder = ({
|
|||||||
input: [...action.settings.input, newField],
|
input: [...action.settings.input, newField],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSelectedField(newField.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledFieldContainer>
|
<StyledFieldContainer>
|
||||||
|
|||||||
@ -43,7 +43,7 @@ const StyledSettingsHeader = styled.div`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||||
grid-template-columns: 1fr 24px;
|
grid-template-columns: 1fr 24px;
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTitleContainer = styled.div`
|
const StyledTitleContainer = styled.div`
|
||||||
@ -110,7 +110,7 @@ export const WorkflowEditActionFormFieldSettings = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = newType as WorkflowFormFieldType;
|
const type = newType as WorkflowFormFieldType;
|
||||||
const { name, label, placeholder, settings } =
|
const { name, label, settings } =
|
||||||
getDefaultFormFieldSettings(type);
|
getDefaultFormFieldSettings(type);
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
@ -118,12 +118,10 @@ export const WorkflowEditActionFormFieldSettings = ({
|
|||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
|
||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
defaultValue={field.type}
|
defaultValue={field.type}
|
||||||
preventDisplayPadding
|
|
||||||
/>
|
/>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
<WorkflowFormFieldSettingsByType
|
<WorkflowFormFieldSettingsByType
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
|
|||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
|
|
||||||
type WorkflowFormFieldSettingsRecordPickerProps = {
|
type WorkflowFormFieldSettingsRecordPickerProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||||
import { WorkflowEditActionFormFieldSettings } from '../WorkflowEditActionFormFieldSettings';
|
import { WorkflowEditActionFormFieldSettings } from '../WorkflowEditActionFormFieldSettings';
|
||||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
|
||||||
|
|
||||||
const meta: Meta<typeof WorkflowEditActionFormFieldSettings> = {
|
const meta: Meta<typeof WorkflowEditActionFormFieldSettings> = {
|
||||||
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFieldSettings',
|
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFieldSettings',
|
||||||
@ -60,7 +60,7 @@ export const TextFieldSettings: Story = {
|
|||||||
const placeholderInput = await canvas.findByText('Enter text');
|
const placeholderInput = await canvas.findByText('Enter text');
|
||||||
expect(placeholderInput).toBeVisible();
|
expect(placeholderInput).toBeVisible();
|
||||||
|
|
||||||
const closeButton = await canvas.findByRole('button');
|
const closeButton = await canvas.findByTestId('close-button');
|
||||||
await userEvent.click(closeButton);
|
await userEvent.click(closeButton);
|
||||||
expect(args.onClose).toHaveBeenCalled();
|
expect(args.onClose).toHaveBeenCalled();
|
||||||
},
|
},
|
||||||
@ -87,7 +87,7 @@ export const NumberFieldSettings: Story = {
|
|||||||
const placeholderInput = await canvas.findByText('Enter number');
|
const placeholderInput = await canvas.findByText('Enter number');
|
||||||
expect(placeholderInput).toBeInTheDocument();
|
expect(placeholderInput).toBeInTheDocument();
|
||||||
|
|
||||||
const closeButton = await canvas.findByRole('button');
|
const closeButton = await canvas.findByTestId('close-button');
|
||||||
await userEvent.click(closeButton);
|
await userEvent.click(closeButton);
|
||||||
expect(args.onClose).toHaveBeenCalled();
|
expect(args.onClose).toHaveBeenCalled();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const getDefaultFormFieldSettings = (type: WorkflowFormFieldType) => {
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
name: 'record',
|
name: 'record',
|
||||||
label: 'Record',
|
label: 'Record',
|
||||||
placeholder: 'Select a record',
|
placeholder: `Select a record`,
|
||||||
settings: {
|
settings: {
|
||||||
objectName: 'company',
|
objectName: 'company',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user