From 43e9d7c90f7378bc84d341fcd9ab37938088672e Mon Sep 17 00:00:00 2001 From: Gustav Larsson Date: Tue, 28 Jan 2025 19:49:10 +0100 Subject: [PATCH] Fix(Expense form): Update payout method form inputs (#10954) --- components/ComboSelect.tsx | 123 ++++++++ components/CurrencyPicker.tsx | 61 ++++ components/FormField.tsx | 14 +- components/PayoutMethodLabel.tsx | 2 +- .../expenses/PayoutBankInformationForm.js | 292 +++++++----------- components/expenses/PayoutMethodForm.js | 133 ++++---- .../form/PayoutMethodSection.tsx | 139 ++++----- components/ui/Textarea.tsx | 2 + lang/ca.json | 2 +- lang/cs.json | 2 +- lang/de.json | 2 +- lang/en.json | 2 +- lang/es.json | 2 +- lang/fr.json | 2 +- lang/he.json | 2 +- lang/it.json | 2 +- lang/ja.json | 2 +- lang/ko.json | 2 +- lang/nl.json | 2 +- lang/pl.json | 2 +- lang/pt-BR.json | 2 +- lang/pt.json | 2 +- lang/ru.json | 2 +- lang/sk-SK.json | 2 +- lang/sv-SE.json | 2 +- lang/uk.json | 2 +- lang/zh.json | 2 +- test/cypress/integration/27-expenses.test.js | 2 + 28 files changed, 442 insertions(+), 364 deletions(-) create mode 100644 components/ComboSelect.tsx create mode 100644 components/CurrencyPicker.tsx diff --git a/components/ComboSelect.tsx b/components/ComboSelect.tsx new file mode 100644 index 00000000000..c39421c43c5 --- /dev/null +++ b/components/ComboSelect.tsx @@ -0,0 +1,123 @@ +'use client'; + +import * as React from 'react'; +import { isString } from 'lodash'; +import { Check, ChevronDown } from 'lucide-react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { cn } from '@/lib/utils'; + +import { Button } from '@/components/ui/Button'; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/Command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'; + +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/Select'; + +const Messages = defineMessages({ + loading: { + id: 'Select.Loading', + defaultMessage: 'Loading...', + }, + noOptions: { + id: 'Select.NoOptions', + defaultMessage: 'Nothing found', + }, + placeholder: { + id: 'Select.Placeholder', + defaultMessage: 'No selection', + }, + inputPlaceholder: { + id: 'search.placeholder', + defaultMessage: 'Search...', + }, +}); + +type ComboSelectProps = { + options: Array<{ value: string; label: string | React.JSX.Element; keywords?: string[] }>; + value: string | undefined; + onChange: (val: string) => void; + id?: string; + disabled?: boolean; + error?: boolean; + loading?: boolean; + className?: string; + placeholder?: string; + inputPlaceholder?: string; + isSearchable?: boolean; + 'data-cy'?: string; +}; + +export function ComboSelect(props: ComboSelectProps) { + const [open, setOpen] = React.useState(false); + const intl = useIntl(); + const isSearchable = props.isSearchable ?? props.options?.length > 8; + const placeholder = props.loading + ? intl.formatMessage(Messages.loading) + : (props.placeholder ?? intl.formatMessage(Messages.placeholder)); + const selectedOption = props.options.find(option => option.value === props.value); + + const onChange = (val: string) => { + props.onChange(val); + setOpen(false); + }; + + if (!isSearchable) { + return ( + + ); + } + return ( + + + + + + + + + {intl.formatMessage(Messages.noOptions)} + + {props.options.map(option => ( + + + {option.label} + + ))} + + + + + + ); +} diff --git a/components/CurrencyPicker.tsx b/components/CurrencyPicker.tsx new file mode 100644 index 00000000000..b89085a657d --- /dev/null +++ b/components/CurrencyPicker.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { getEmojiByCurrencyCode } from 'country-currency-emoji-flags'; +import { truncate } from 'lodash'; +import { useIntl } from 'react-intl'; + +import { Currency } from '../lib/constants/currency'; +import { getIntlDisplayNames } from '../lib/i18n'; + +import { ComboSelect } from './ComboSelect'; + +const generateCurrencyOptions = (intl, availableCurrencies) => { + const currencyDisplayNames = getIntlDisplayNames(intl.locale, 'currency'); + return availableCurrencies.map(currency => { + const currencyName = currencyDisplayNames.of(currency); + const emoji = getEmojiByCurrencyCode(currency); + return { + value: currency, + label: ( +
+ {emoji && {emoji}} +   + + {currency} + {` `} + {truncate(currencyName, { length: 30 })} + +
+ ), + }; + }); +}; + +export default function CurrencyPicker({ + availableCurrencies = Currency, + value, + onChange, + ...props +}: { + /** A list of currencies presented in the currency picker */ + availableCurrencies: string[]; + onChange: (selectedCurrency: string) => void; + value?: string; +} & Omit, 'options'>) { + const intl = useIntl(); + const currencyOptions = React.useMemo( + () => generateCurrencyOptions(intl, availableCurrencies), + [intl, availableCurrencies], + ); + return ( + 10} + options={currencyOptions} + value={value} + onChange={onChange} + {...props} + /> + ); +} diff --git a/components/FormField.tsx b/components/FormField.tsx index 2a0e5c6611b..ad03b851b4b 100644 --- a/components/FormField.tsx +++ b/components/FormField.tsx @@ -6,6 +6,7 @@ import { useIntl } from 'react-intl'; import { isOCError } from '../lib/errors'; import { formatFormErrorMessage, RICH_ERROR_MESSAGES } from '../lib/form-utils'; +import { cn } from '@/lib/utils'; import PrivateInfoIcon from './icons/PrivateInfoIcon'; import { Input } from './ui/Input'; @@ -21,6 +22,8 @@ export function FormField({ children, error: customError, isPrivate, + validate, + className, ...props }: { label?: string; @@ -37,13 +40,16 @@ export function FormField({ htmlFor?: string; error?: string; isPrivate?: boolean; + validate?: any; + className?: string; + onFocus?: () => void; }) { const intl = useIntl(); const htmlFor = props.htmlFor || `input-${name}`; const { schema } = useContext(FormikZodContext); return ( - + {({ field, form, meta }) => { const hasError = Boolean(meta.error && (meta.touched || form.submitCount)) || Boolean(customError); const error = customError || meta.error; @@ -62,6 +68,7 @@ export function FormField({ required: props.required, error: hasError, placeholder, + onFocus: props.onFocus, }, value => value !== undefined, ), @@ -74,10 +81,11 @@ export function FormField({ ) { fieldAttributes.required = true; } + return ( -
+
{label && ( -