diff --git a/packages/page-assets/src/Balances/Account.tsx b/packages/page-assets/src/Balances/Account.tsx index 03b65dd3043..4dc9547aae0 100644 --- a/packages/page-assets/src/Balances/Account.tsx +++ b/packages/page-assets/src/Balances/Account.tsx @@ -32,11 +32,11 @@ interface Props { siFormat: [number, string]; } -function Account ({ account: { balance, isFrozen, reason, sufficient }, accountId, assetId, className, minBalance, siFormat }: Props): React.ReactElement { +function Account ({ account: { balance, isFrozen, reason, sufficient }, accountId, assetId, minBalance, siFormat }: Props): React.ReactElement { const { t } = useTranslation(); return ( - + <> @@ -62,7 +62,7 @@ function Account ({ account: { balance, isFrozen, reason, sufficient }, accountI siFormat={siFormat} /> - + ); } diff --git a/packages/page-assets/src/Balances/Asset.tsx b/packages/page-assets/src/Balances/Asset.tsx new file mode 100644 index 00000000000..50228820282 --- /dev/null +++ b/packages/page-assets/src/Balances/Asset.tsx @@ -0,0 +1,83 @@ +// Copyright 2017-2025 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AssetInfoComplete } from '@polkadot/react-hooks/types'; + +import React, { useMemo } from 'react'; + +import { styled } from '@polkadot/react-components'; +import { formatNumber } from '@polkadot/util'; + +import Account from './Account.js'; +import useBalances from './useBalances.js'; + +interface Props { + asset: AssetInfoComplete, + className?: string; + searchValue: string; +} + +const Asset = ({ asset: { details, id, metadata }, className, searchValue }: Props) => { + const balances = useBalances(id); + + const siFormat = useMemo( + (): [number, string] => metadata + ? [metadata.decimals.toNumber(), metadata.symbol.toUtf8().toUpperCase()] + : [0, 'NONE'], + [metadata] + ); + + const shouldShowAsset = useMemo(() => metadata.name.toUtf8().toLowerCase().includes(searchValue) || + formatNumber(id).toString().replaceAll(',', '').includes(searchValue), [id, metadata.name, searchValue]); + + if (!balances?.length || !shouldShowAsset) { + return <>; + } + + return balances.map(({ account, accountId }, index) => { + return ( + + + {index === 0 && <>{metadata.name.toUtf8()} ({formatNumber(id)})} + + + + ); + }); +}; + +const BASE_BORDER = 0.125; +const BORDER_TOP = `${BASE_BORDER * 3}rem solid var(--bg-page)`; +const BORDER_RADIUS = `${BASE_BORDER * 4}rem`; + +const StyledTr = styled.tr<{isFirstItem: boolean; isLastItem: boolean}>` + td { + border-top: ${(props) => props.isFirstItem && BORDER_TOP}; + border-radius: 0rem !important; + + &:first-child { + padding-block: 1rem !important; + border-top-left-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important; + border-bottom-left-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important; + } + + &:last-child { + border-top-right-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important; + border-bottom-right-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important; + } + } +`; + +export default Asset; diff --git a/packages/page-assets/src/Balances/index.tsx b/packages/page-assets/src/Balances/index.tsx index d9bcb9a17ab..58c2d6e0e03 100644 --- a/packages/page-assets/src/Balances/index.tsx +++ b/packages/page-assets/src/Balances/index.tsx @@ -1,17 +1,14 @@ // Copyright 2017-2025 @polkadot/app-assets authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DropdownItemProps } from 'semantic-ui-react'; import type { AssetInfo, AssetInfoComplete } from '@polkadot/react-hooks/types'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { Dropdown, styled, Table } from '@polkadot/react-components'; -import { formatNumber } from '@polkadot/util'; +import { Input, styled, Table } from '@polkadot/react-components'; import { useTranslation } from '../translate.js'; -import Account from './Account.js'; -import useBalances from './useBalances.js'; +import Asset from './Asset.js'; interface Props { className?: string; @@ -20,97 +17,61 @@ interface Props { function Balances ({ className, infos = [] }: Props): React.ReactElement { const { t } = useTranslation(); - const [selectedAssetValue, setSelectedAssetValue] = useState('0'); - const [info, setInfo] = useState(null); - const balances = useBalances(info?.id); + const [{ searchValue }, onApplySearch] = useState({ searchValue: '' }); const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([ + [t('asset'), 'start'], [t('accounts'), 'start'], [t('frozen'), 'start'], [t('sufficient'), 'start'], - [], + [t('free balance'), 'start'], [] ]); - const completeInfos = useMemo( + const onChangeInput = useCallback((e: string) => { + onApplySearch({ searchValue: e }); + }, []); + + const completeAssets = useMemo( () => infos .filter((i): i is AssetInfoComplete => !!(i.details && i.metadata) && !i.details.supply.isZero()) .sort((a, b) => a.id.cmp(b.id)), [infos] ); - const assetOptions = useMemo( - () => completeInfos.map(({ id, metadata }) => ({ - text: `${metadata.name.toUtf8()} (${formatNumber(id)})`, - value: id.toString() - })), - [completeInfos] - ); - - const siFormat = useMemo( - (): [number, string] => info - ? [info.metadata.decimals.toNumber(), info.metadata.symbol.toUtf8().toUpperCase()] - : [0, 'NONE'], - [info] - ); - - const onSearch = useCallback( - (options: DropdownItemProps[], value: string): DropdownItemProps[] => - options.filter((options) => { - const { text: optText, value: optValue } = options as { text: string, value: number }; - - return parseInt(value) === optValue || optText.includes(value); - }), - [] - ); - - useEffect((): void => { - const info = completeInfos.find(({ id }) => id.toString() === selectedAssetValue); - - // if no info found (usually happens on first load), select the first one automatically - if (!info) { - setInfo(completeInfos.at(0) ?? null); - setSelectedAssetValue(completeInfos.at(0)?.id?.toString() ?? '0'); - } else { - setInfo(info); - } - }, [completeInfos, selectedAssetValue]); - return ( + - ) - : undefined - } + empty={t('No accounts with balances found for the asset')} header={headerRef.current} > - {info && balances?.map(({ account, accountId }) => ( - - ))} + {completeAssets.map((asset) => { + return ( + + ); + })}
); } const StyledDiv = styled.div` + input { + max-width: 250px !important; + } + table { overflow: auto; } diff --git a/packages/react-hooks/src/useAssetInfos.ts b/packages/react-hooks/src/useAssetInfos.ts index c984bc5f81f..8880d373e04 100644 --- a/packages/react-hooks/src/useAssetInfos.ts +++ b/packages/react-hooks/src/useAssetInfos.ts @@ -52,7 +52,7 @@ function useAssetInfosImpl (ids?: BN[]): AssetInfo[] | undefined { const { api } = useApi(); const { allAccounts } = useAccounts(); - const isReady = useMemo(() => !!api.tx.assets?.setMetadata && !!api.tx.assets?.transferKeepAlive, [api.tx.assets?.setMetadata, api.tx.assets?.transferKeepAlive]); + const isReady = useMemo(() => !!ids?.length && !!api.tx.assets?.setMetadata && !!api.tx.assets?.transferKeepAlive, [api.tx.assets?.setMetadata, api.tx.assets?.transferKeepAlive, ids?.length]); const metadata = useCall<[[BN[]], PalletAssetsAssetMetadata[]]>(isReady && api.query.assets.metadata.multi, [ids], QUERY_OPTS); const details = useCall<[[BN[]], Option[]]>(isReady && api.query.assets.asset.multi, [ids], QUERY_OPTS);