diff --git a/apps/trading/components/accounts-container/accounts-container.tsx b/apps/trading/components/accounts-container/accounts-container.tsx index 79bc4329c..a66a0c625 100644 --- a/apps/trading/components/accounts-container/accounts-container.tsx +++ b/apps/trading/components/accounts-container/accounts-container.tsx @@ -5,7 +5,7 @@ import { useWithdrawalDialog } from '@vegaprotocol/withdraws'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { Splash } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet } from '@vegaprotocol/wallet'; -import { AccountManager } from '@vegaprotocol/accounts'; +import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts'; import { useDepositDialog } from '@vegaprotocol/deposits'; export const AccountsContainer = () => { @@ -13,6 +13,7 @@ export const AccountsContainer = () => { const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const openWithdrawalDialog = useWithdrawalDialog((store) => store.open); const openDepositDialog = useDepositDialog((store) => store.open); + const openTransferDialog = useTransferDialog((store) => store.open); const onClickAsset = useCallback( (assetId?: string) => { @@ -41,7 +42,10 @@ export const AccountsContainer = () => { /> {!isReadOnly && ( -
+
+ diff --git a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx index 1109b61be..cb80f07f6 100644 --- a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx +++ b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx @@ -13,11 +13,13 @@ import { DropdownMenuTrigger, Icon, Drawer, + DropdownMenuSeparator, } from '@vegaprotocol/ui-toolkit'; import type { PubKey } from '@vegaprotocol/wallet'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { Networks, useEnvironment } from '@vegaprotocol/environment'; import { WalletIcon } from '../icons/wallet'; +import { useTransferDialog } from '@vegaprotocol/accounts'; const MobileWalletButton = ({ isConnected, @@ -30,6 +32,7 @@ const MobileWalletButton = ({ const openVegaWalletDialog = useVegaWalletDialogStore( (store) => store.openVegaWalletDialog ); + const openTransferDialog = useTransferDialog((store) => store.open); const { VEGA_ENV } = useEnvironment(); const isYellow = VEGA_ENV === Networks.TESTNET; const [drawerOpen, setDrawerOpen] = useState(false); @@ -115,7 +118,16 @@ const MobileWalletButton = ({ /> ))}
-
+
+ @@ -131,6 +143,7 @@ export const VegaWalletConnectButton = () => { const openVegaWalletDialog = useVegaWalletDialogStore( (store) => store.openVegaWalletDialog ); + const openTransferDialog = useTransferDialog((store) => store.open); const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet(); const isConnected = pubKey !== null; @@ -171,6 +184,10 @@ export const VegaWalletConnectButton = () => { ))} + + openTransferDialog(true)}> + {t('Transfer')} + {t('Disconnect')} diff --git a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx index bd985164a..723b41ce1 100644 --- a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx +++ b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; +import first from 'lodash/first'; import compact from 'lodash/compact'; import type { BatchMarketInstructionSubmissionBody, @@ -10,13 +11,12 @@ import type { VegaStoredTxState, WithdrawalBusEventFieldsFragment, } from '@vegaprotocol/wallet'; -import { isBatchMarketInstructionsTransaction } from '@vegaprotocol/wallet'; import { + isTransferTransaction, + isBatchMarketInstructionsTransaction, ClientErrors, useReconnectVegaWallet, WalletError, -} from '@vegaprotocol/wallet'; -import { isOrderAmendmentTransaction, isOrderCancellationTransaction, isOrderSubmissionTransaction, @@ -32,13 +32,13 @@ import { Size, t, toBigNum, + truncateByChars, } from '@vegaprotocol/react-helpers'; import { useAssetsDataProvider } from '@vegaprotocol/assets'; import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3'; import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment'; import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders'; import { useMarketList } from '@vegaprotocol/market-list'; -import first from 'lodash/first'; import type { Side } from '@vegaprotocol/types'; import { OrderStatusMapping } from '@vegaprotocol/types'; @@ -87,12 +87,14 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => { const cancelOrder = isOrderCancellationTransaction(tx.body); const editOrder = isOrderAmendmentTransaction(tx.body); const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body); + const transfer = isTransferTransaction(tx.body); return ( withdraw || submitOrder || cancelOrder || editOrder || - batchMarketInstructions + batchMarketInstructions || + transfer ); }; @@ -388,6 +390,26 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => { return
{t('Batch market instruction')}
; } + if (isTransferTransaction(tx.body)) { + const { amount, to, asset } = tx.body.transfer; + const transferAsset = assets?.find((a) => a.id === asset); + // only render if we have an asset to avoid unformatted amounts showing + if (transferAsset) { + const value = addDecimalsFormatNumber(amount, transferAsset.decimals); + return ( +
+

{t('Transfer')}

+

+ {t('To')} {truncateByChars(to)} +

+

+ {value} {transferAsset.symbol} +

+
+ ); + } + } + return null; }; @@ -433,6 +455,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => { }) ); const explorerLink = useLinks(DApp.Explorer); + if (isWithdrawTransaction(tx.body)) { const completeWithdrawalButton = tx.withdrawal && (
@@ -490,6 +513,16 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => { ); } + if (isTransferTransaction(tx.body)) { + return ( +
+

{t('Transfer complete')}

+

{t('Your transaction has been confirmed ')}

+ +
+ ); + } + return (

{t('Confirmed')}

diff --git a/apps/trading/pages/dialogs-container.tsx b/apps/trading/pages/dialogs-container.tsx index c03576ac4..6f0bbeb77 100644 --- a/apps/trading/pages/dialogs-container.tsx +++ b/apps/trading/pages/dialogs-container.tsx @@ -8,6 +8,7 @@ import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws'; import { DepositDialog } from '@vegaprotocol/deposits'; import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3'; import { WelcomeDialog } from '../components/welcome-dialog'; +import { TransferDialog } from '@vegaprotocol/accounts'; const DialogsContainer = () => { const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore(); @@ -25,6 +26,7 @@ const DialogsContainer = () => { + ); }; diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts index d3b35a103..ce1402607 100644 --- a/libs/accounts/src/lib/index.ts +++ b/libs/accounts/src/lib/index.ts @@ -7,3 +7,4 @@ export * from './breakdown-table'; export * from './use-account-balance'; export * from './get-settlement-account'; export * from './use-market-account-balance'; +export * from './transfer-dialog'; diff --git a/libs/accounts/src/lib/transfer-container.tsx b/libs/accounts/src/lib/transfer-container.tsx new file mode 100644 index 000000000..a8fe25e9e --- /dev/null +++ b/libs/accounts/src/lib/transfer-container.tsx @@ -0,0 +1,68 @@ +import * as Schema from '@vegaprotocol/types'; +import { + addDecimal, + NetworkParams, + t, + truncateByChars, + useDataProvider, + useNetworkParam, +} from '@vegaprotocol/react-helpers'; +import type { Transfer } from '@vegaprotocol/wallet'; +import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet'; +import { useCallback, useMemo } from 'react'; +import { accountsDataProvider } from './accounts-data-provider'; +import { TransferForm } from './transfer-form'; +import { useTransferDialog } from './transfer-dialog'; +import { Lozenge } from '@vegaprotocol/ui-toolkit'; + +export const TransferContainer = () => { + const { pubKey, pubKeys } = useVegaWallet(); + const open = useTransferDialog((store) => store.open); + const { param } = useNetworkParam(NetworkParams.transfer_fee_factor); + const { data } = useDataProvider({ + dataProvider: accountsDataProvider, + variables: { partyId: pubKey }, + skip: !pubKey, + }); + const create = useVegaTransactionStore((store) => store.create); + + const transfer = useCallback( + (transfer: Transfer) => { + create({ transfer }); + open(false); + }, + [create, open] + ); + + const assets = useMemo(() => { + if (!data) return []; + return data + .filter( + (account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL + ) + .map((account) => ({ + id: account.asset.id, + symbol: account.asset.symbol, + name: account.asset.name, + decimals: account.asset.decimals, + balance: addDecimal(account.balance, account.asset.decimals), + })); + }, [data]); + + return ( + <> +

+ {t('Transfer funds to another Vega key from')}{' '} + {truncateByChars(pubKey || '')}{' '} + {t('If you are at all unsure, stop and seek advice.')} +

+ pk.publicKey) : null} + assets={assets} + feeFactor={param} + submitTransfer={transfer} + /> + + ); +}; diff --git a/libs/accounts/src/lib/transfer-dialog.tsx b/libs/accounts/src/lib/transfer-dialog.tsx new file mode 100644 index 000000000..998eaf253 --- /dev/null +++ b/libs/accounts/src/lib/transfer-dialog.tsx @@ -0,0 +1,28 @@ +import { t } from '@vegaprotocol/react-helpers'; +import { Dialog } from '@vegaprotocol/ui-toolkit'; +import { create } from 'zustand'; +import { TransferContainer } from './transfer-container'; + +interface State { + isOpen: boolean; +} + +interface Actions { + open: (open?: boolean) => void; +} + +export const useTransferDialog = create((set) => ({ + isOpen: false, + open: (open = true) => { + set(() => ({ isOpen: open })); + }, +})); + +export const TransferDialog = () => { + const { isOpen, open } = useTransferDialog(); + return ( + + + + ); +}; diff --git a/libs/accounts/src/lib/transfer-form.spec.tsx b/libs/accounts/src/lib/transfer-form.spec.tsx new file mode 100644 index 000000000..eb977217a --- /dev/null +++ b/libs/accounts/src/lib/transfer-form.spec.tsx @@ -0,0 +1,177 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import BigNumber from 'bignumber.js'; +import { AddressField, TransferFee, TransferForm } from './transfer-form'; +import { AccountType } from '@vegaprotocol/types'; +import { formatNumber, removeDecimal } from '@vegaprotocol/react-helpers'; + +describe('TransferForm', () => { + const submit = () => fireEvent.submit(screen.getByTestId('transfer-form')); + const amount = '100'; + const pubKey = + '70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680'; + const asset = { + id: 'asset-0', + symbol: 'ASSET 0', + name: 'Asset 0', + decimals: 2, + balance: '1000', + }; + const props = { + pubKey, + pubKeys: [ + pubKey, + 'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce', + ], + assets: [asset], + feeFactor: '0.001', + submitTransfer: jest.fn(), + }; + + it('validates fields and submits', async () => { + render(); + + // check current pubkey not shown + const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key'); + expect(keySelect.children).toHaveLength(2); + expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([ + '', + props.pubKeys[1], + ]); + + submit(); + expect(await screen.findAllByText('Required')).toHaveLength(3); + + // Select a pubkey + fireEvent.change(screen.getByLabelText('Vega key'), { + target: { value: props.pubKeys[1] }, + }); + + // Select asset + fireEvent.change( + // Bypass RichSelect and target hidden native select + // eslint-disable-next-line + document.querySelector('select[name="asset"]')!, + { target: { value: asset.id } } + ); + + // assert rich select as updated + expect(await screen.findByTestId('select-asset')).toHaveTextContent( + asset.name + ); + expect(screen.getByTestId('asset-balance')).toHaveTextContent( + formatNumber(asset.balance, asset.decimals) + ); + + // Test amount validation + fireEvent.change(screen.getByLabelText('Amount'), { + target: { value: '0.00000001' }, + }); + expect( + await screen.findByText('Value is below minimum') + ).toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText('Amount'), { + target: { value: '9999999' }, + }); + expect( + await screen.findByText(/cannot transfer more/i) + ).toBeInTheDocument(); + + // set valid amount + fireEvent.change(screen.getByLabelText('Amount'), { + target: { value: amount }, + }); + expect(screen.getByTestId('transfer-fee')).toHaveTextContent( + new BigNumber(props.feeFactor).times(amount).toFixed() + ); + + submit(); + + await waitFor(() => { + expect(props.submitTransfer).toHaveBeenCalledTimes(1); + expect(props.submitTransfer).toHaveBeenCalledWith({ + fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL, + toAccountType: AccountType.ACCOUNT_TYPE_GENERAL, + to: props.pubKeys[1], + asset: asset.id, + amount: removeDecimal(amount, asset.decimals), + oneOff: {}, + }); + }); + }); + + it('validates a manually entered address', async () => { + render(); + submit(); + expect(await screen.findAllByText('Required')).toHaveLength(3); + const toggle = screen.getByText('Enter manually'); + fireEvent.click(toggle); + // has switched to input + expect(toggle).toHaveTextContent('Select from wallet'); + expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text'); + fireEvent.change(screen.getByLabelText('Vega key'), { + target: { value: 'invalid-address' }, + }); + await waitFor(() => { + const errors = screen.getAllByTestId('input-error-text'); + expect(errors[0]).toHaveTextContent('Invalid Vega key'); + }); + + // same pubkey + fireEvent.change(screen.getByLabelText('Vega key'), { + target: { value: pubKey }, + }); + + await waitFor(() => { + const errors = screen.getAllByTestId('input-error-text'); + expect(errors[0]).toHaveTextContent('Vega key is the same'); + }); + }); +}); + +describe('AddressField', () => { + const props = { + pubKeys: ['pubkey-1', 'pubkey-2'], + select:
select
, + input:
input
, + onChange: jest.fn(), + }; + + it('toggles content and calls onChange', async () => { + const mockOnChange = jest.fn(); + render(); + + // select should be shown as multiple pubkeys provided + expect(screen.getByText('select')).toBeInTheDocument(); + expect(screen.queryByText('input')).not.toBeInTheDocument(); + fireEvent.click(screen.getByText('Enter manually')); + expect(screen.queryByText('select')).not.toBeInTheDocument(); + expect(screen.getByText('input')).toBeInTheDocument(); + expect(mockOnChange).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByText('Select from wallet')); + expect(screen.getByText('select')).toBeInTheDocument(); + expect(screen.queryByText('input')).not.toBeInTheDocument(); + expect(mockOnChange).toHaveBeenCalledTimes(2); + }); + + it('Does not provide select option if there is only a single key', () => { + render(); + expect(screen.getByText('input')).toBeInTheDocument(); + expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument(); + }); +}); + +describe('TransferFee', () => { + const props = { + amount: '200', + feeFactor: '0.001', + }; + it('calculates and renders the transfer fee', () => { + render(); + + const expected = new BigNumber(props.amount) + .times(props.feeFactor) + .toFixed(); + expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected); + }); +}); diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx new file mode 100644 index 000000000..53d1bf020 --- /dev/null +++ b/libs/accounts/src/lib/transfer-form.tsx @@ -0,0 +1,296 @@ +import { + t, + minSafe, + maxSafe, + required, + vegaPublicKey, + addDecimal, + formatNumber, +} from '@vegaprotocol/react-helpers'; +import { + Button, + FormGroup, + Input, + InputError, + Option, + RichSelect, + Select, + Tooltip, +} from '@vegaprotocol/ui-toolkit'; +import type { Transfer } from '@vegaprotocol/wallet'; +import { normalizeTransfer } from '@vegaprotocol/wallet'; +import BigNumber from 'bignumber.js'; +import type { ReactNode } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +interface FormFields { + toAddress: string; + asset: string; + amount: string; +} + +interface TransferFormProps { + pubKey: string | null; + pubKeys: string[] | null; + assets: Array<{ + id: string; + symbol: string; + name: string; + decimals: number; + balance: string; + }>; + feeFactor: string | null; + submitTransfer: (transfer: Transfer) => void; +} + +export const TransferForm = ({ + pubKey, + pubKeys, + assets, + feeFactor, + submitTransfer, +}: TransferFormProps) => { + const { + control, + register, + watch, + handleSubmit, + setValue, + formState: { errors }, + } = useForm(); + + const amount = watch('amount'); + const assetId = watch('asset'); + + const asset = useMemo(() => { + return assets.find((a) => a.id === assetId); + }, [assets, assetId]); + + const onSubmit = useCallback( + (fields: FormFields) => { + if (!asset) { + throw new Error('Submitted transfer with no asset selected'); + } + const transfer = normalizeTransfer(fields.toAddress, fields.amount, { + id: asset.id, + decimals: asset.decimals, + }); + submitTransfer(transfer); + }, + [asset, submitTransfer] + ); + + const min = useMemo(() => { + // Min viable amount given asset decimals EG for WEI 0.000000000000000001 + const minViableAmount = asset + ? new BigNumber(addDecimal('1', asset.decimals)) + : new BigNumber(0); + return minViableAmount; + }, [asset]); + + const max = useMemo(() => { + const maxAmount = asset ? new BigNumber(asset.balance) : new BigNumber(0); + return maxAmount; + }, [asset]); + + return ( +
+ + setValue('toAddress', '')} + select={ + + } + input={ + { + if (value === pubKey) { + return t('Vega key is the same as current key'); + } + return true; + }, + }, + })} + /> + } + /> + {errors.toAddress?.message && ( + + {errors.toAddress.message} + + )} + + + ( + { + field.onChange(value); + }} + placeholder={t('Please select')} + value={field.value} + > + {assets.map((a) => ( + + ))} + + )} + /> + {errors.asset?.message && ( + {errors.asset.message} + )} + + + {asset.symbol} + } + {...register('amount', { + validate: { + required, + minSafe: (value) => minSafe(new BigNumber(min))(value), + maxSafe: (v) => { + const value = new BigNumber(v); + if (value.isGreaterThan(max)) { + return t( + 'You cannot transfer more than your available collateral' + ); + } + return maxSafe(max)(v); + }, + }, + })} + /> + {errors.amount?.message && ( + {errors.amount.message} + )} + + + + + ); +}; + +export const TransferFee = ({ + amount, + feeFactor, +}: { + amount: string; + feeFactor: string | null; +}) => { + if (!feeFactor || !amount) return null; + + // using toFixed without an argument will always return a + // number in normal notation without rounding, formatting functions + // arent working in a way which won't round the decimal places + const value = new BigNumber(amount).times(feeFactor).toFixed(); + + return ( +
+
+ +
{t('Transfer fee')}
+
+
+
+ {value} +
+
+ ); +}; + +interface AddressInputProps { + pubKeys: string[] | null; + select: ReactNode; + input: ReactNode; + onChange: () => void; +} + +export const AddressField = ({ + pubKeys, + select, + input, + onChange, +}: AddressInputProps) => { + const [isInput, setIsInput] = useState(() => { + if (pubKeys && pubKeys.length <= 1) { + return true; + } + return false; + }); + + return ( + <> + {isInput ? input : select} + {pubKeys && pubKeys.length > 1 && ( + + )} + + ); +}; diff --git a/libs/react-helpers/src/hooks/use-network-params.spec.tsx b/libs/react-helpers/src/hooks/use-network-params.spec.tsx index cac3f004e..a70320ff3 100644 --- a/libs/react-helpers/src/hooks/use-network-params.spec.tsx +++ b/libs/react-helpers/src/hooks/use-network-params.spec.tsx @@ -2,6 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import type { NetworkParamsKey } from './use-network-params'; +import { toRealKey } from './use-network-params'; import { NetworkParams, useNetworkParam, @@ -18,7 +19,7 @@ describe('useNetworkParam', () => { request: { query: NetworkParamDocument, variables: { - key: arg, + key: toRealKey(arg), }, }, result: { diff --git a/libs/react-helpers/src/hooks/use-network-params.ts b/libs/react-helpers/src/hooks/use-network-params.ts index 8f8081ae0..bd598c85e 100644 --- a/libs/react-helpers/src/hooks/use-network-params.ts +++ b/libs/react-helpers/src/hooks/use-network-params.ts @@ -104,10 +104,11 @@ export const NetworkParams = { market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume', market_liquidity_targetstake_triggering_ratio: 'market_liquidity_targetstake_triggering_ratio', + transfer_fee_factor: 'transfer_fee_factor', } as const; type Params = typeof NetworkParams; -export type NetworkParamsKey = Params[keyof Params]; +export type NetworkParamsKey = keyof Params; type Result = { [key in keyof Params]: string; }; @@ -120,7 +121,7 @@ export const useNetworkParams = (params?: T) => { return compact(data.networkParametersConnection.edges) .map((p) => ({ ...p.node, - key: p.node.key.split('.').join('_'), + key: toInternalKey(p.node.key), })) .filter((p) => { if (params === undefined || params.length === 0) return true; @@ -143,7 +144,7 @@ export const useNetworkParams = (params?: T) => { export const useNetworkParam = (param: NetworkParamsKey) => { const { data, loading, error } = useNetworkParamQuery({ variables: { - key: param, + key: toRealKey(param), }, }); @@ -153,3 +154,11 @@ export const useNetworkParam = (param: NetworkParamsKey) => { error, }; }; + +export const toRealKey = (key: NetworkParamsKey) => { + return key.split('_').join('.'); +}; + +export const toInternalKey = (key: string) => { + return key.split('.').join('_'); +}; diff --git a/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx index 8c000a171..3c9a4f533 100644 --- a/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx +++ b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx @@ -151,7 +151,7 @@ export const DropdownMenuSeparator = forwardRef< {...separatorProps} ref={forwardedRef} className={classNames( - 'h-px my-1 mx-2 bg-neutral-700 dark:bg-black', + 'h-px my-1 mx-2 bg-neutral-400 dark:bg-neutral-300', className )} /> diff --git a/libs/ui-toolkit/src/components/select/select.tsx b/libs/ui-toolkit/src/components/select/select.tsx index 13691624f..8fb5f0fae 100644 --- a/libs/ui-toolkit/src/components/select/select.tsx +++ b/libs/ui-toolkit/src/components/select/select.tsx @@ -15,7 +15,7 @@ export interface SelectProps extends SelectHTMLAttributes { export const Select = forwardRef( ({ className, hasError, ...props }, ref) => ( -
+