diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx index 5ab4617ed..eff716fb1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx @@ -1,5 +1,5 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; -import { TextInput } from '@/ui/field/input/components/TextInput'; +import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; import { useCurrencyField } from '../../hooks/useCurrencyField'; @@ -79,10 +79,18 @@ export const CurrencyFieldInput = ({ }); }; + const handleSelect = (newValue: string) => { + setDraftValue({ + amount: draftValue?.amount ?? '', + currencyCode: newValue as CurrencyCode, + }); + }; + return ( - ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts index ee11e822c..72c024f7a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts @@ -1,4 +1,3 @@ -import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; @@ -31,7 +30,7 @@ export const computeDraftValueFromFieldValue = ({ return { amount: fieldValue?.amountMicros ? fieldValue.amountMicros / 1000000 : '', - currenyCode: CurrencyCode.USD, + currencyCode: fieldValue?.currencyCode ?? '', } as unknown as FieldInputDraftValue; } if (isFieldRelation(fieldDefinition)) { diff --git a/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx new file mode 100644 index 000000000..8a72584b5 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx @@ -0,0 +1,152 @@ +import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; +import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton'; +import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle'; + +export const StyledInput = styled.input` + margin: 0; + ${TEXT_INPUT_STYLE} + width: 100%; + padding: ${({ theme }) => `${theme.spacing(0)} ${theme.spacing(1)}`}; +`; + +const StyledContainer = styled.div` + align-items: center; + + border: none; + border-radius: ${({ theme }) => theme.border.radius.sm}; + box-shadow: ${({ theme }) => theme.boxShadow.strong}; + + display: flex; + justify-content: center; +`; + +const StyledIcon = styled.div` + align-items: center; + display: flex; + + & > svg { + padding-left: ${({ theme }) => theme.spacing(1)}; + color: ${({ theme }) => theme.font.color.tertiary}; + height: ${({ theme }) => theme.icon.size.md}px; + width: ${({ theme }) => theme.icon.size.md}px; + } +`; + +export type CurrencyInputProps = { + placeholder?: string; + autoFocus?: boolean; + value: string; + currencyCode: string; + onEnter: (newText: string) => void; + onEscape: (newText: string) => void; + onTab?: (newText: string) => void; + onShiftTab?: (newText: string) => void; + onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void; + onChange?: (newText: string) => void; + onSelect?: (newText: string) => void; + hotkeyScope: string; +}; + +type Currency = { + label: string; + value: string; + Icon: any; +}; + +export const CurrencyInput = ({ + autoFocus, + value, + currencyCode, + placeholder, + onEnter, + onEscape, + onTab, + onShiftTab, + onClickOutside, + onChange, + onSelect, + hotkeyScope, +}: CurrencyInputProps) => { + const theme = useTheme(); + + const [internalText, setInternalText] = useState(value); + const [internalCurrency, setInternalCurrency] = useState( + null, + ); + + const wrapperRef = useRef(null); + + const handleChange = (event: ChangeEvent) => { + setInternalText(event.target.value); + onChange?.(event.target.value); + }; + + const handleCurrencyChange = (currency: Currency) => { + setInternalCurrency(currency); + onSelect?.(currency.value); + }; + + useRegisterInputEvents({ + inputRef: wrapperRef, + inputValue: internalText, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, + hotkeyScope, + }); + + const currencies = useMemo( + () => + Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map( + ([key, { Icon, label }]) => ({ + value: key, + Icon, + label, + }), + ), + [], + ); + + useEffect(() => { + const currency = currencies.find(({ value }) => value === currencyCode); + if (currency) { + setInternalCurrency(currency); + } + }, [currencies, currencyCode]); + + useEffect(() => { + setInternalText(value); + }, [value]); + + const Icon: IconComponent = internalCurrency?.Icon; + + return ( + + + + {Icon && ( + + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx new file mode 100644 index 000000000..305ca9851 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; +import { IconChevronDown } from '@/ui/display/icon'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; + +import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope'; + +import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect'; + +type StyledDropdownButtonProps = { + isUnfolded: boolean; +}; + +export const StyledDropdownButtonContainer = styled.div` + align-items: center; + color: ${({ color }) => color ?? 'none'}; + cursor: pointer; + display: flex; + border-right: ${({ theme }) => `1px solid ${theme.border.color.medium}`}; + height: 32px; + padding-left: ${({ theme }) => theme.spacing(2)}; + padding-right: ${({ theme }) => theme.spacing(2)}; + user-select: none; + &:hover { + filter: brightness(0.95); + } +`; + +const StyledIconContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + justify-content: center; + + svg { + align-items: center; + display: flex; + height: 16px; + justify-content: center; + } +`; + +export type Currency = { + label: string; + value: string; + Icon: any; +}; + +export const CurrencyPickerDropdownButton = ({ + valueCode, + onChange, + currencies, +}: { + valueCode: string; + onChange: (currency: Currency) => void; + currencies: Currency[]; +}) => { + const theme = useTheme(); + + const [selectedCurrency, setSelectedCurrency] = useState(); + + const { isDropdownOpen, closeDropdown } = useDropdown( + CurrencyPickerHotkeyScope.CurrencyPicker, + ); + + const handleChange = (currency: Currency) => { + onChange(currency); + closeDropdown(); + }; + + useEffect(() => { + const currency = currencies.find(({ value }) => value === valueCode); + if (currency) { + setSelectedCurrency(currency); + } + }, [valueCode, currencies]); + + return ( + + + {selectedCurrency ? selectedCurrency.value : CurrencyCode.USD} + + + + } + dropdownComponents={ + + } + dropdownPlacement="bottom-start" + dropdownOffset={{ x: 0, y: 4 }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx new file mode 100644 index 000000000..98f4348f0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx @@ -0,0 +1,71 @@ +import { useMemo, useState } from 'react'; + +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar'; + +import { Currency } from './CurrencyPickerDropdownButton'; + +export const CurrencyPickerDropdownSelect = ({ + currencies, + selectedCurrency, + onChange, +}: { + currencies: Currency[]; + selectedCurrency?: Currency; + onChange: (currency: Currency) => void; +}) => { + const [searchFilter, setSearchFilter] = useState(''); + + const filteredCurrencies = useMemo( + () => + currencies.filter( + ({ value, label }) => + value + .toLocaleLowerCase() + .includes(searchFilter.toLocaleLowerCase()) || + label.toLocaleLowerCase().includes(searchFilter.toLocaleLowerCase()), + ), + [currencies, searchFilter], + ); + + return ( + + setSearchFilter(event.target.value)} + autoFocus + /> + + + {filteredCurrencies.length === 0 ? ( + + ) : ( + <> + {selectedCurrency && ( + onChange(selectedCurrency)} + text={`${selectedCurrency.label} (${selectedCurrency.value})`} + /> + )} + {filteredCurrencies.map((item) => + selectedCurrency?.value === item.value ? null : ( + onChange(item)} + text={`${item.label} (${item.value})`} + /> + ), + )} + + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/types/CurrencyPickerHotkeyScope.ts b/packages/twenty-front/src/modules/ui/input/components/internal/currency/types/CurrencyPickerHotkeyScope.ts new file mode 100644 index 000000000..cfeaebd04 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/types/CurrencyPickerHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum CurrencyPickerHotkeyScope { + CurrencyPicker = 'currency-picker', +} diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectAvatar.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectAvatar.tsx index 59026208d..14756606e 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectAvatar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelectAvatar.tsx @@ -12,7 +12,7 @@ import { import { StyledMenuItemSelect } from './MenuItemSelect'; type MenuItemSelectAvatarProps = { - avatar: ReactNode; + avatar?: ReactNode; selected: boolean; text: string; className?: string;