Account row collapsible row details (#5894)

* Retrofitting basic unit tests to Accounts

Extraction of useAccounts and useBalance hooks

* Retrofitting basic unit tests to Accounts

Extraction of useAccounts and useBalance hooks

* Fixing accounts unit tests - basic account balances

* Cleaning up after PR remarks

* Fixing arrangements of balances 1/3

Moving ArrangedAccount around

* Add useStakingInfo hook

* Adopt linter suggestions

* Fixing arrangements of balances 2/3

Linking arranged balances values to mocked balances calls.

* Fixing arrangements of balances 3/3

Formatting utils made consistent

* Details row odd-even + (un)hide

* Improving th/td/footer layout

* Add tags to details row

* Changing layout of account balance with details

* Add total/details balances unit tests

* Add tags unit tests

* Clean up of TODOs, adding comments

* Improve tags unit testing

* Alternate account row details coloring unit test

* Row details toggling unit tests

* Row details toggling unit tests - bad commit fix

* Style address balances (#5897)

* Fix jumping column problem

* Improve balances positioning

* Format sidebar

* Position header

* Extract common formatting of balances in tests.

Thx @vitaliyworks, great hint.

* Revert Sidebar styling

* Use rems

* Lint

* Use useToggle for expanding account row

* Add tags header

* Apply FormatBalance review comments

* Display parent account info above account name (#5947)

* Display parent address

* Improve layout

* Set keyring in tests

* Remove redundant code from components

* Use derivedAccount only in specific test

* Apply lint fixes

* Remove parent column from expanded row

* Apply review suggestions

* Improve styling in ParentAddress component

* Improve layout of parent address and add short address

* Apply review suggestions

* Use map instead of for loops

Co-authored-by: Marcin Wachulski <marcin@ethworks.io>
Co-authored-by: Vitaliy Mysak <vitaliy@ethworks.io>
Co-authored-by: MiZiet <55240109+MiZiet@users.noreply.github.com>
Co-authored-by: MiZiet <michalzietalak@o2.pl>
Co-authored-by: Ewa Kowalska <30404388+ekowalsk@users.noreply.github.com>
Co-authored-by: Ewa Kowalska <ewcia.kowalska@gmail.com>
This commit is contained in:
Marcin Wachulski 2021-08-19 16:42:46 +02:00 committed by GitHub
parent 8c380e6953
commit 466a1be22f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 819 additions and 438 deletions

View File

@ -27,6 +27,7 @@
"You are about to remove this account from your list of available accounts. Once completed, should you need to access it again, you will have to re-create the account either via seed or via a backup file.": "You are about to remove this account from your list of available accounts. Once completed, should you need to access it again, you will have to re-create the account either via seed or via a backup file.",
"You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.": "You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.",
"You are about to remove this contract from your list of available contracts. Once completed, should you need to access it again, you will have to manually add the contract's address in the Instantiate tab.": "You are about to remove this contract from your list of available contracts. Once completed, should you need to access it again, you will have to manually add the contract's address in the Instantiate tab.",
"account balance": "account balance",
"address copied": "address copied",
"available to be unlocked": "available to be unlocked",
"beneficiary": "beneficiary",

View File

@ -16,7 +16,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
import styled, { ThemeContext } from 'styled-components';
import { ApiPromise } from '@polkadot/api';
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components';
import { AddressInfo, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components';
import { useAccountInfo, useApi, useBalancesAll, useBestNumber, useCall, useLedger, useStakingInfo, useToggle } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { BN_ZERO, formatBalance, formatNumber, isFunction } from '@polkadot/util';
@ -47,6 +47,7 @@ interface Props {
proxy?: [ProxyDefinition[], BN];
setBalance: (address: string, value: AccountBalance) => void;
toggleFavorite: (address: string) => void;
isEven: boolean;
}
interface DemocracyUnlockable {
@ -99,8 +100,9 @@ const transformRecovery = {
transform: (opt: Option<RecoveryConfig>) => opt.unwrapOr(null)
};
function Account ({ account: { address, meta }, className = '', delegation, filter, isFavorite, proxy, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
function Account ({ account: { address, meta }, className = '', delegation, filter, isEven, isFavorite, proxy, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
const { theme } = useContext<ThemeDef>(ThemeContext);
const { queueExtrinsic } = useContext(StatusContext);
const api = useApi();
@ -381,278 +383,334 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
}
return (
<tr className={className}>
<td className='favorite'>
<Icon
color={isFavorite ? 'orange' : 'gray'}
icon='star'
onClick={_onFavorite}
/>
</td>
<td className='together'>
{meta.genesisHash
? <Badge color='transparent' />
: isDevelopment
? (
<Badge
className='devBadge'
color='orange'
hover={t<string>('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.')}
icon='wrench'
/>
)
: (
<Badge
color='orange'
hover={
<div>
<p>{t<string>('This account is available on all networks. It is recommended to link to a specific network via the account options ("only this network" option) to limit availability. For accounts from an extension, set the network on the extension.')}</p>
<p>{t<string>('This does not send any transaction, rather is only sets the genesis in the account JSON.')}</p>
</div>
}
icon='exclamation-triangle'
/>
)
}
{recoveryInfo && (
<Badge
color='green'
hover={
<div>
<p>{t<string>('This account is recoverable, with the following friends:')}</p>
<div>
{recoveryInfo.friends.map((friend, index): React.ReactNode => (
<IdentityIcon
key={index}
value={friend}
/>
))}
</div>
<table>
<tbody>
<tr>
<td>{t<string>('threshold')}</td>
<td>{formatNumber(recoveryInfo.threshold)}</td>
</tr>
<tr>
<td>{t<string>('delay')}</td>
<td>{formatNumber(recoveryInfo.delayPeriod)}</td>
</tr>
<tr>
<td>{t<string>('deposit')}</td>
<td>{formatBalance(recoveryInfo.deposit)}</td>
</tr>
</tbody>
</table>
</div>
}
icon='shield'
<>
<tr className={`${className}${isExpanded ? ' noBorder' : ''} ${isEven ? 'isEven' : 'isOdd'}`}>
<td className='favorite'>
<Icon
color={isFavorite ? 'orange' : 'gray'}
icon='star'
onClick={_onFavorite}
/>
)}
{multiInfos && multiInfos.length !== 0 && (
<Badge
color='red'
hover={t<string>('Multisig approvals pending')}
info={multiInfos.length}
/>
)}
{isProxied && !proxyInfo.hasOwned && (
<Badge
color='red'
hover={t<string>('Proxied account has no owned proxies')}
info='0'
/>
)}
{delegation?.accountDelegated && (
<Badge
color='blue'
hover={t<string>('This account has a governance delegation')}
icon='calendar-check'
onClick={toggleDelegate}
/>
)}
{!!proxy?.[0].length && api.api.tx.utility && (
<Badge
color='blue'
hover={t<string>('This account has {{proxyNumber}} proxy set.', {
replace: {
proxyNumber: proxy[0].length
}
})}
icon='arrow-right'
onClick={toggleProxyOverview}
/>
)}
</td>
<td className='address'>
<AddressSmall value={address} />
{isBackupOpen && (
<Backup
address={address}
key='modal-backup-account'
onClose={toggleBackup}
/>
)}
{isDelegateOpen && (
<DelegateModal
key='modal-delegate'
onClose={toggleDelegate}
previousAmount={delegation?.amount}
previousConviction={delegation?.conviction}
previousDelegatedAccount={delegation?.accountDelegated}
previousDelegatingAccount={address}
/>
)}
{isDeriveOpen && (
<Derive
from={address}
key='modal-derive-account'
onClose={toggleDerive}
/>
)}
{isForgetOpen && (
<Forget
address={address}
key='modal-forget-account'
onClose={toggleForget}
onForget={_onForget}
/>
)}
{isIdentityMainOpen && (
<IdentityMain
address={address}
key='modal-identity-main'
onClose={toggleIdentityMain}
/>
)}
{isIdentitySubOpen && (
<IdentitySub
address={address}
key='modal-identity-sub'
onClose={toggleIdentitySub}
/>
)}
{isPasswordOpen && (
<ChangePass
address={address}
key='modal-change-pass'
onClose={togglePassword}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={toggleTransfer}
senderId={address}
/>
)}
{isProxyOverviewOpen && (
<ProxyOverview
key='modal-proxy-overview'
onClose={toggleProxyOverview}
previousProxy={proxy}
proxiedAccount={address}
/>
)}
{isMultisigOpen && multiInfos && (
<MultisigApprove
address={address}
key='multisig-approve'
onClose={toggleMultisig}
ongoing={multiInfos}
threshold={meta.threshold as number}
who={meta.who as string[]}
/>
)}
{isRecoverAccountOpen && (
<RecoverAccount
address={address}
key='recover-account'
onClose={toggleRecoverAccount}
/>
)}
{isRecoverSetupOpen && (
<RecoverSetup
address={address}
key='recover-setup'
onClose={toggleRecoverSetup}
/>
)}
{isUndelegateOpen && (
<UndelegateModal
accountDelegating={address}
key='modal-delegate'
onClose={toggleUndelegate}
/>
)}
</td>
<td className='address media--1400'>
{(meta.parentAddress as string) && (
<AddressMini value={meta.parentAddress} />
)}
</td>
<td className='number'>
<CryptoType accountId={address} />
</td>
<td className='all'>
<div className='tags'>
<Tags value={tags} />
</div>
</td>
<td className='number media--1500'>
{balancesAll?.accountNonce.gt(BN_ZERO) && formatNumber(balancesAll.accountNonce)}
</td>
<td className='number'>
<AddressInfo
address={address}
balancesAll={balancesAll}
withBalance
withBalanceToggle
withExtended={false}
/>
</td>
<td className='button'>
{isFunction(api.api.tx.balances?.transfer) && (
<Button
icon='paper-plane'
label={t<string>('send')}
onClick={toggleTransfer}
/>
)}
<Popup
className={`theme--${theme}`}
isOpen={isSettingsOpen}
onClose={toggleSettings}
trigger={
<Button
icon='ellipsis-v'
isDisabled={!menuItems.length}
onClick={toggleSettings}
/>
</td>
<td className='together'>
{meta.genesisHash
? <Badge color='transparent' />
: isDevelopment
? (
<Badge
className='devBadge'
color='orange'
hover={t<string>('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.')}
icon='wrench'
/>
)
: (
<Badge
color='orange'
hover={
<div>
<p>{t<string>('This account is available on all networks. It is recommended to link to a specific network via the account options ("only this network" option) to limit availability. For accounts from an extension, set the network on the extension.')}</p>
<p>{t<string>('This does not send any transaction, rather is only sets the genesis in the account JSON.')}</p>
</div>
}
icon='exclamation-triangle'
/>
)
}
>
<Menu
onClick={toggleSettings}
text
vertical
{recoveryInfo && (
<Badge
color='green'
hover={
<div>
<p>{t<string>('This account is recoverable, with the following friends:')}</p>
<div>
{recoveryInfo.friends.map((friend, index): React.ReactNode => (
<IdentityIcon
key={index}
value={friend}
/>
))}
</div>
<table>
<tbody>
<tr>
<td>{t<string>('threshold')}</td>
<td>{formatNumber(recoveryInfo.threshold)}</td>
</tr>
<tr>
<td>{t<string>('delay')}</td>
<td>{formatNumber(recoveryInfo.delayPeriod)}</td>
</tr>
<tr>
<td>{t<string>('deposit')}</td>
<td>{formatBalance(recoveryInfo.deposit)}</td>
</tr>
</tbody>
</table>
</div>
}
icon='shield'
/>
)}
{multiInfos && multiInfos.length !== 0 && (
<Badge
color='red'
hover={t<string>('Multisig approvals pending')}
info={multiInfos.length}
/>
)}
{isProxied && !proxyInfo.hasOwned && (
<Badge
color='red'
hover={t<string>('Proxied account has no owned proxies')}
info='0'
/>
)}
{delegation?.accountDelegated && (
<Badge
color='blue'
hover={t<string>('This account has a governance delegation')}
icon='calendar-check'
onClick={toggleDelegate}
/>
)}
{!!proxy?.[0].length && api.api.tx.utility && (
<Badge
color='blue'
hover={t<string>('This account has {{proxyNumber}} proxy set.', {
replace: {
proxyNumber: proxy[0].length
}
})}
icon='arrow-right'
onClick={toggleProxyOverview}
/>
)}
</td>
<td className='address'>
<AddressSmall
parentAddress={meta.parentAddress}
value={address}
withShortAddress
/>
{isBackupOpen && (
<Backup
address={address}
key='modal-backup-account'
onClose={toggleBackup}
/>
)}
{isDelegateOpen && (
<DelegateModal
key='modal-delegate'
onClose={toggleDelegate}
previousAmount={delegation?.amount}
previousConviction={delegation?.conviction}
previousDelegatedAccount={delegation?.accountDelegated}
previousDelegatingAccount={address}
/>
)}
{isDeriveOpen && (
<Derive
from={address}
key='modal-derive-account'
onClose={toggleDerive}
/>
)}
{isForgetOpen && (
<Forget
address={address}
key='modal-forget-account'
onClose={toggleForget}
onForget={_onForget}
/>
)}
{isIdentityMainOpen && (
<IdentityMain
address={address}
key='modal-identity-main'
onClose={toggleIdentityMain}
/>
)}
{isIdentitySubOpen && (
<IdentitySub
address={address}
key='modal-identity-sub'
onClose={toggleIdentitySub}
/>
)}
{isPasswordOpen && (
<ChangePass
address={address}
key='modal-change-pass'
onClose={togglePassword}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={toggleTransfer}
senderId={address}
/>
)}
{isProxyOverviewOpen && (
<ProxyOverview
key='modal-proxy-overview'
onClose={toggleProxyOverview}
previousProxy={proxy}
proxiedAccount={address}
/>
)}
{isMultisigOpen && multiInfos && (
<MultisigApprove
address={address}
key='multisig-approve'
onClose={toggleMultisig}
ongoing={multiInfos}
threshold={meta.threshold as number}
who={meta.who as string[]}
/>
)}
{isRecoverAccountOpen && (
<RecoverAccount
address={address}
key='recover-account'
onClose={toggleRecoverAccount}
/>
)}
{isRecoverSetupOpen && (
<RecoverSetup
address={address}
key='recover-setup'
onClose={toggleRecoverSetup}
/>
)}
{isUndelegateOpen && (
<UndelegateModal
accountDelegating={address}
key='modal-delegate'
onClose={toggleUndelegate}
/>
)}
</td>
<td className='number'>
<CryptoType accountId={address} />
</td>
<td className='number media--1500'>
{balancesAll?.accountNonce.gt(BN_ZERO) && formatNumber(balancesAll.accountNonce)}
</td>
<td className='number'>
<AddressInfo
address={address}
balancesAll={balancesAll}
withBalance={{
available: false,
bonded: false,
locked: false,
redeemable: false,
reserved: false,
total: true,
unlocking: false,
vested: false
}}
withExtended={false}
/>
</td>
<td className='button'>
{isFunction(api.api.tx.balances?.transfer) && (
<Button
icon='paper-plane'
label={t<string>('send')}
onClick={toggleTransfer}
/>
)}
<Popup
className={`theme--${theme}`}
isOpen={isSettingsOpen}
onClose={toggleSettings}
trigger={
<Button
icon='ellipsis-v'
isDisabled={!menuItems.length}
onClick={toggleSettings}
/>
}
>
{menuItems}
</Menu>
</Popup>
</td>
<td className='links media--1400'>
<LinkExternal
className='ui--AddressCard-exporer-link'
data={address}
isLogo
type='address'
/>
</td>
</tr>
<Menu
onClick={toggleSettings}
text
vertical
>
{menuItems}
</Menu>
</Popup>
</td>
<td className='links media--1400'>
<LinkExternal
className='ui--AddressCard-exporer-link'
data={address}
isLogo
type='address'
/>
</td>
<td className='button'>
<div className='table-column-icon'
data-testid='row-toggle'
onClick={toggleIsExpanded}>
<Icon icon={
isExpanded
? 'caret-up'
: 'caret-down'
}
/>
</div>
</td>
</tr>
<tr className={`${className} ${isExpanded ? 'isExpanded' : 'isCollapsed'} ${isEven ? 'isEven' : 'isOdd'}`}>
<td colSpan={2} />
<td>
<div className='tags'
data-testid='tags'>
<Tags
value={tags}
withTitle
/>
</div>
</td>
<td className='media--1500'/>
<td/>
<td>
<AddressInfo
address={address}
balancesAll={balancesAll}
withBalance={{
available: true,
bonded: true,
locked: true,
redeemable: true,
reserved: true,
total: false,
unlocking: true,
vested: true
}}
withExtended={false}
/>
</td>
<td colSpan={4} />
</tr>
</>
);
}
export default React.memo(styled(Account)`
&.isCollapsed {
visibility: collapse;
}
&.isExpanded {
visibility: visible;
}
.tags {
width: 100%;
min-height: 1.5rem;
@ -661,4 +719,15 @@ export default React.memo(styled(Account)`
.devBadge {
opacity: 0.65;
}
.table-column-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.7rem;
height: 1.7rem;
border: 1px solid var(--border-table);
border-radius: 4px;
cursor: pointer;
}
`);

View File

@ -2,20 +2,37 @@
// SPDX-License-Identifier: Apache-2.0
import type { Balance } from '@polkadot/types/interfaces';
import type { AccountOverrides } from '../../test/hooks/default';
import { screen, within } from '@testing-library/react';
import { fireEvent, screen, within } from '@testing-library/react';
import BN from 'bn.js';
import { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types';
import i18next from '@polkadot/react-components/i18n';
import toShortAddress from '@polkadot/react-components/util/toShortAddress';
import { UseAccountInfo } from '@polkadot/react-hooks/types';
import { balanceOf } from '@polkadot/test-support/creation/balance';
import { makeStakingLedger as ledger } from '@polkadot/test-support/creation/stakingInfo/stakingLedger';
import { MemoryStore } from '@polkadot/test-support/keyring';
import { keyring } from '@polkadot/ui-keyring';
import { KeyringJson$Meta } from '@polkadot/ui-keyring/types';
import { AccountsPage } from '../../test/pages/accountsPage';
import { AccountOverrides } from '../../test/hooks/default';
import { AccountsPage, format } from '../../test/pages/accountsPage';
describe('Accounts page', () => {
const aliceAddress = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const bobAddress = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
const charlieAddress = '5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy';
const aliceDerivedAddress = '5Dc96kiTPTfZHmq6yTFSqejJzfUNfQQjneNesRWf9MDppJsd';
const addAccountToKeyring = (address: string, meta: KeyringJson$Meta) => {
keyring.addExternal(address, meta);
};
const forgetAccountFromKeyring = (address: string) => {
keyring.forgetAccount(address);
};
let accountsPage: AccountsPage;
beforeAll(async () => {
@ -25,6 +42,7 @@ describe('Accounts page', () => {
beforeEach(() => {
accountsPage = new AccountsPage();
forgetAccountFromKeyring(aliceDerivedAddress);
});
describe('when no accounts', () => {
@ -61,17 +79,7 @@ describe('Accounts page', () => {
describe('when some accounts exist', () => {
it('the accounts table contains some account rows', async () => {
defaultRender(
{
balance: {
freeBalance: balance(10000)
}
},
{
balance: {
freeBalance: balance(999)
}
});
renderDefaultAccounts(2);
const accountRows = await accountsPage.findAccountRows();
@ -79,44 +87,117 @@ describe('Accounts page', () => {
});
it('account rows display the total balance info', async () => {
defaultRender(
{
balance: {
freeBalance: balance(500)
}
},
{
balance: {
freeBalance: balance(200),
reservedBalance: balance(150)
}
});
renderAccountsWithDefaultAddresses(
anAccountWithBalance({ freeBalance: balance(500) }),
anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
);
const rows = await accountsPage.findAccountRows();
const row1TotalActual = await within(rows[0]).findByTestId('balance-summary');
const row1TotalExpected = accountsPage.format(balance(500));
await rows[0].assertBalancesTotal(balance(500));
await rows[1].assertBalancesTotal(balance(350));
});
expect(row1TotalActual).toHaveTextContent(row1TotalExpected);
it('account rows display the details balance info', async () => {
renderAccountsWithDefaultAddresses(
anAccountWithBalance({ freeBalance: balance(500), lockedBalance: balance(30) }),
anAccountWithBalance({ availableBalance: balance(50), freeBalance: balance(200), reservedBalance: balance(150) })
);
const row2TotalActual = await within(rows[1]).findByTestId('balance-summary');
const row2TotalExpected = accountsPage.format(balance(350));
const rows = await accountsPage.findAccountRows();
expect(row2TotalActual).toHaveTextContent(row2TotalExpected);
await rows[0].assertBalancesDetails([
{ amount: balance(0), name: 'transferrable' },
{ amount: balance(30), name: 'locked' }]);
await rows[1].assertBalancesDetails([
{ amount: balance(50), name: 'transferrable' },
{ amount: balance(150), name: 'reserved' }]);
});
it('derived account displays parent account info', async () => {
addAccountToKeyring(aliceDerivedAddress, { parentAddress: aliceAddress });
renderAccountsForAddresses(
aliceAddress,
aliceDerivedAddress
);
const accountRows = await accountsPage.findAccountRows();
expect(accountRows).toHaveLength(2);
await accountRows[1].assertParentAccountName('ALICE');
});
it('a separate column for parent account is not displayed', async () => {
renderDefaultAccounts(1);
const accountsTable = await accountsPage.findAccountsTable();
assertColumnNotExistInTable('parent', accountsTable);
assertColumnExistsInTable('type', accountsTable);
});
it('account rows display the shorted address', async () => {
renderAccountsForAddresses(
aliceAddress
);
const accountRows = await accountsPage.findAccountRows();
expect(accountRows).toHaveLength(1);
const aliceShortAddress = toShortAddress(aliceAddress);
await accountRows[0].assertShortAddress(aliceShortAddress);
});
it('when account is not tagged, account row details displays no tags info', async () => {
renderDefaultAccounts(1);
const rows = await accountsPage.findAccountRows();
await rows[0].assertTags('no tags');
});
it('when account is tagged, account row details displays tags', async () => {
renderAccountsWithDefaultAddresses(
anAccountWithInfo({ tags: ['my tag', 'Super Tag'] })
);
const rows = await accountsPage.findAccountRows();
await rows[0].assertTags('my tagSuper Tag');
});
it('account details rows keep colouring from their primary rows', async () => {
renderDefaultAccounts(3);
const rows = await accountsPage.findAccountRows();
expect(rows[0].primaryRow).toHaveClass('isOdd');
expect(rows[0].detailsRow).toHaveClass('isOdd');
expect(rows[1].primaryRow).toHaveClass('isEven');
expect(rows[1].detailsRow).toHaveClass('isEven');
expect(rows[2].primaryRow).toHaveClass('isOdd');
expect(rows[2].detailsRow).toHaveClass('isOdd');
});
it('account details rows toggled on icon toggle click', async () => {
renderDefaultAccounts(1);
const row = (await accountsPage.findAccountRows())[0];
const toggle = await within(row.primaryRow).findByTestId('row-toggle');
expect(row.detailsRow).toHaveClass('isCollapsed');
fireEvent.click(toggle);
expect(row.detailsRow).toHaveClass('isExpanded');
});
it('displays some summary', () => {
defaultRender(
{
balance: {
freeBalance: balance(500)
}
},
{
balance: {
freeBalance: balance(200),
reservedBalance: balance(150)
}
});
renderAccountsWithDefaultAddresses(
anAccountWithBalance({ freeBalance: balance(500) }),
anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
);
const summaries = screen.queryAllByTestId(/card-summary:total \w+/i);
@ -124,26 +205,21 @@ describe('Accounts page', () => {
});
it('displays balance summary', async () => {
defaultRender(
{
balance: {
freeBalance: balance(500)
}
},
{
balance: {
freeBalance: balance(200),
reservedBalance: balance(150)
}
});
renderAccountsWithDefaultAddresses(
anAccountWithBalance({ freeBalance: balance(500) }),
anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
);
const summary = await screen.findByTestId(/card-summary:(total )?balance/i);
expect(summary).toHaveTextContent(showBalance(500 + 200 + 150));
});
it('displays transferrable summary', async () => {
defaultRender({ balance: { availableBalance: balance(400) } }, { balance: { availableBalance: balance(600) } });
it('displays transferable summary', async () => {
renderAccountsWithDefaultAddresses(
anAccountWithBalance({ availableBalance: balance(400) }),
anAccountWithBalance({ availableBalance: balance(600) })
);
const summary = await screen.findByTestId(/card-summary:(total )?transferrable/i);
@ -151,7 +227,10 @@ describe('Accounts page', () => {
});
it('displays locked summary', async () => {
defaultRender({ balance: { lockedBalance: balance(400) } }, { balance: { lockedBalance: balance(600) } });
renderAccountsWithDefaultAddresses(
anAccountWithBalance({ lockedBalance: balance(400) }),
anAccountWithBalance({ lockedBalance: balance(600) })
);
const summary = await screen.findByTestId(/card-summary:(total )?locked/i);
@ -159,17 +238,10 @@ describe('Accounts page', () => {
});
it('displays bonded summary', async () => {
defaultRender(
{
staking: {
stakingLedger: ledger(balance(70))
}
},
{
staking: {
stakingLedger: ledger(balance(20))
}
});
renderAccountsWithDefaultAddresses(
anAccountWithStaking({ stakingLedger: ledger(balance(70)) }),
anAccountWithStaking({ stakingLedger: ledger(balance(20)) })
);
const summary = await screen.findByTestId(/card-summary:(total )?bonded/i);
@ -177,43 +249,40 @@ describe('Accounts page', () => {
});
it('displays unbonding summary', async () => {
defaultRender(
{
staking: {
unlocking: [
{
remainingEras: new BN('1000000000'),
value: balance(200)
},
{
remainingEras: new BN('2000000000'),
value: balance(300)
},
{
remainingEras: new BN('3000000000'),
value: balance(400)
}
]
}
},
{
staking: {
unlocking: [
{
remainingEras: new BN('1000000000'),
value: balance(100)
},
{
remainingEras: new BN('2000000000'),
value: balance(200)
},
{
remainingEras: new BN('3000000000'),
value: balance(300)
}
]
}
});
renderAccountsWithDefaultAddresses(
anAccountWithStaking({
unlocking: [
{
remainingEras: new BN('1000000000'),
value: balance(200)
},
{
remainingEras: new BN('2000000000'),
value: balance(300)
},
{
remainingEras: new BN('3000000000'),
value: balance(400)
}
]
}),
anAccountWithStaking({
unlocking: [
{
remainingEras: new BN('1000000000'),
value: balance(100)
},
{
remainingEras: new BN('2000000000'),
value: balance(200)
},
{
remainingEras: new BN('3000000000'),
value: balance(300)
}
]
})
);
const summary = await screen.findByTestId(/card-summary:(total )?unbonding/i);
@ -221,7 +290,10 @@ describe('Accounts page', () => {
});
it('displays redeemable summary', async () => {
defaultRender({ staking: { redeemable: balance(4000) } }, { staking: { redeemable: balance(5000) } });
renderAccountsWithDefaultAddresses(
anAccountWithStaking({ redeemable: balance(4000) }),
anAccountWithStaking({ redeemable: balance(5000) })
);
const summary = await screen.findByTestId(/card-summary:(total )?redeemable/i);
@ -241,22 +313,55 @@ describe('Accounts page', () => {
return balanceOf(amountInt.toString() + decimalsPadded);
};
const showBalance = function (amount: number): string {
return accountsPage.format(balance(amount));
};
const anAccount = (): AccountOverrides => ({});
const anAccountWithBalance = (balance: { [P in keyof DeriveBalancesAll]?: DeriveBalancesAll[P] }) => ({
balance
});
const anAccountWithInfo = (info: { [P in keyof UseAccountInfo]?: UseAccountInfo[P] }) => ({
info
});
const anAccountWithStaking = (staking: { [P in keyof DeriveStakingAccount]?: DeriveStakingAccount[P]}) => ({
staking
});
function assertColumnNotExistInTable (columnName: string, table: HTMLElement) {
expect(within(table).queryByRole('columnheader', { name: columnName })).toBeFalsy();
}
function assertColumnExistsInTable (columnName: string, table: HTMLElement) {
expect(within(table).getByRole('columnheader', { name: columnName })).toBeTruthy();
}
function showBalance (amount: number): string {
return format(balance(amount));
}
const defaultAddresses = [
'5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy',
'5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw'
aliceAddress,
bobAddress,
charlieAddress
];
const defaultRender = function (...overrides: AccountOverrides[]): void {
const accounts: [string, AccountOverrides][] = [];
for (let i = 0; i < overrides.length; i++) {
accounts.push([defaultAddresses[i], overrides[i]]);
}
function renderAccountsWithDefaultAddresses (...overrides: AccountOverrides[]): void {
const accounts = overrides.map((accountProperties, index) =>
[defaultAddresses[index], accountProperties] as [string, AccountOverrides]);
accountsPage.renderPage(accounts);
};
}
function renderAccountsForAddresses (...addresses: string[]): void {
const accounts = addresses.map((address) => [address, anAccount()] as [string, AccountOverrides]);
accountsPage.renderPage(accounts);
}
function renderDefaultAccounts (accountsNumber: number): void {
const accounts = Array.from({ length: accountsNumber },
(_, index) => [defaultAddresses[index], anAccount()] as [string, AccountOverrides]);
accountsPage.renderPage(accounts);
}
});

View File

@ -73,13 +73,12 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
const headerRef = useRef([
[t('accounts'), 'start', 3],
[t('parent'), 'address media--1400'],
[t('type')],
[t('tags'), 'start'],
[t('transactions'), 'media--1500'],
[t('balances'), 'expand'],
[t('balances'), 'balances'],
[],
[undefined, 'media--1400']
[undefined, 'media--1400'],
[]
]);
useEffect((): void => {
@ -235,6 +234,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
account={account}
delegation={delegation}
filter={filterOn}
isEven={!!(index % 2)}
isFavorite={isFavorite}
key={`${index}:${account.address}`}
proxy={proxies?.[index]}

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types';
import type { UseAccountInfo } from '@polkadot/react-hooks/types';
import BN from 'bn.js';
@ -11,6 +12,7 @@ import { makeStakingLedger } from '@polkadot/test-support/creation/stakingInfo/s
export interface Account {
balance: DeriveBalancesAll,
info: UseAccountInfo,
staking: DeriveStakingAccount
}
@ -26,6 +28,9 @@ export interface AccountOverrides {
staking?: {
[P in keyof DeriveStakingAccount]?: DeriveStakingAccount[P];
};
info?: {
[P in keyof UseAccountInfo]?: UseAccountInfo[P];
}
}
export const emptyAccounts: UseAccounts = {
@ -74,6 +79,12 @@ export const defaultStakingAccount: DeriveStakingAccount = {
]
} as any;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
export const defaultAccountInfo: UseAccountInfo = {
flags: {},
tags: []
} as any;
class MockAccountHooks {
public useAccounts: UseAccounts = emptyAccounts;
public accountsMap: AccountsMap = {};
@ -92,6 +103,7 @@ class MockAccountHooks {
for (const [address, props] of accounts) {
const staking = { ...defaultStakingAccount };
const balance = { ...defaultBalanceAccount };
const info = { ...defaultAccountInfo };
// Typescript does not recognize that keys and values from Object.entries are safe,
// so we have to use "any" here.
@ -100,9 +112,12 @@ class MockAccountHooks {
Object.entries(props.balance || balance).forEach(function ([key, value]) { (balance as any)[key] = value; });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
Object.entries(props.staking || staking).forEach(function ([key, value]) { (staking as any)[key] = value; });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
Object.entries(props.info || info).forEach(function ([key, value]) { (info as any)[key] = value; });
this.accountsMap[address] = {
balance: balance,
info: info,
staking: staking
};
}

View File

@ -25,12 +25,60 @@ let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
class NotYetRendered extends Error {
}
export type AccountRow = HTMLElement;
// utility wrapper over an account item in accounts table, serves basic assertions about an account row
export class AccountRow {
public primaryRow: HTMLElement;
public detailsRow: HTMLElement;
constructor (primaryRow: HTMLElement, detailsRow: HTMLElement) {
this.primaryRow = primaryRow;
this.detailsRow = detailsRow;
}
async assertBalancesTotal (expected: Balance): Promise<void> {
const balanceActual = await within(this.primaryRow).findByTestId('balance-summary');
const balanceExpected = format(expected);
expect(balanceActual).toHaveTextContent(balanceExpected);
}
async assertBalancesDetails (expected: {name: string, amount: Balance}[]): Promise<void> {
for (const expectedBalanceDetailsItem of expected) {
const labelElement = await within(this.detailsRow).findByText(expectedBalanceDetailsItem.name);
const balanceElement = labelElement.nextSibling;
const amount = format(expectedBalanceDetailsItem.amount);
expect(balanceElement).toHaveTextContent(amount);
}
}
async assertParentAccountName (expectedParentAccount: string): Promise<void> {
const parentAccount = await within(this.primaryRow).findByTestId('parent');
expect(parentAccount).toHaveTextContent(expectedParentAccount);
}
async assertTags (tagsContent: string): Promise<void> {
const tagsActual = await within(this.detailsRow).findByTestId('tags');
expect(tagsActual).toHaveTextContent(tagsContent);
}
async assertShortAddress (expectedShortAddress: string): Promise<void> {
const actualShortAddress = await within(this.primaryRow).findByTestId('short-address');
expect(actualShortAddress).toHaveTextContent(expectedShortAddress);
}
}
jest.mock('@polkadot/react-hooks/useAccounts', () => ({
useAccounts: () => mockAccountHooks.useAccounts
}));
jest.mock('@polkadot/react-hooks/useAccountInfo', () => ({
useAccountInfo: (address: string) => mockAccountHooks.accountsMap[address].info
}));
jest.mock('@polkadot/react-hooks/useLoadingDelay', () => ({
useLoadingDelay: () => false
}));
@ -113,16 +161,20 @@ export class AccountsPage {
}
async findAccountRows (): Promise<AccountRow[]> {
this.assertRendered();
const table = await this.findAccountsTable();
const tableBody = table.getElementsByTagName('tbody')[0];
const rows = await within(tableBody).findAllByRole('row');
const htmlRows = (await within(tableBody).findAllByRole('row'))
.filter((r) => r.className.startsWith('Account-'));
const rows: AccountRow[] = [];
return rows.filter((r) => r.className.startsWith('Account-'));
}
for (let rowIdx = 0; rowIdx < htmlRows.length; rowIdx = rowIdx + 2) {
const primary = htmlRows[rowIdx];
const details = htmlRows[rowIdx + 1];
format (amount: Balance | BN): string {
return formatBalance(amount, { decimals: 12, forceUnit: '-', withUnit: true });
rows.push(new AccountRow(primary, details));
}
return rows;
}
private assertRendered () {
@ -131,3 +183,7 @@ export class AccountsPage {
}
}
}
export const format = (amount: Balance | BN): string => {
return formatBalance(amount, { decimals: 12, forceUnit: '-', withUnit: true });
};

View File

@ -81,6 +81,11 @@ const DEFAULT_PREFS = {
validatorPayment: true
};
// auxiliary component that helps aligning balances details, fills up the space when no icon for a balance is specified
function IconVoid (): React.ReactElement {
return <span className='icon-void'>&nbsp;</span>;
}
function lookupLock (lookup: Record<string, string>, lockId: LockIdentifier): string {
const lockHex = lockId.toHex();
@ -222,10 +227,11 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
!withBalanceToggle && balancesAll && balanceDisplay.total && allItems.push(
<React.Fragment key={0}>
<Label label={t<string>('total')} />
<Label label={t<string>('account balance')} />
<FormatBalance
className='result'
formatIndex={formatIndex}
labelPost={<IconVoid/>}
value={balancesAll.freeBalance.add(balancesAll.reservedBalance)}
/>
</React.Fragment>
@ -236,6 +242,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<FormatBalance
className='result'
formatIndex={formatIndex}
labelPost={<IconVoid/>}
value={(balancesAll as DeriveBalancesAll).availableBalance}
/>
</React.Fragment>
@ -246,7 +253,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<FormatBalance
className='result'
formatIndex={formatIndex}
label={
labelPost={
<Icon
icon='info-circle'
tooltip={`${address}-vested-trigger`}
@ -286,7 +293,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<FormatBalance
className='result'
formatIndex={formatIndex}
label={
labelPost={
<Icon
icon='info-circle'
tooltip={`${address}-locks-trigger`}
@ -314,6 +321,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<FormatBalance
className='result'
formatIndex={formatIndex}
labelPost={<IconVoid/>}
value={balancesAll.reservedBalance}
/>
</React.Fragment>
@ -324,6 +332,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<FormatBalance
className='result'
formatIndex={formatIndex}
labelPost={<IconVoid/>}
value={ownBonded}
>
{otherBonded.length !== 0 && (
@ -331,6 +340,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<FormatBalance
formatIndex={formatIndex}
key={index}
labelPost={<IconVoid/>}
value={bonded}
/>
)})</>
@ -353,7 +363,10 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
<React.Fragment key={7}>
<Label label={t<string>('unbonding')} />
<div className='result'>
<StakingUnbonding stakingInfo={stakingInfo} />
<StakingUnbonding
iconPosition='right'
stakingInfo={stakingInfo}
/>
</div>
</React.Fragment>
);
@ -490,12 +503,11 @@ export default withMulti(
white-space: nowrap;
&:not(.ui--AddressInfo-expander) {
justify-content: center;
justify-content: flex-end;
}
.column {
justify-content: start;
max-width: 260px;
&.column--expander {
width: 17.5rem;
@ -513,12 +525,14 @@ export default withMulti(
&:not(.column--expander) {
flex: 1;
display: grid;
column-gap: 1.5rem;
row-gap: 0.571rem;
opacity: 1;
label {
grid-column: 1;
padding-right: 0.5rem;
text-align: right;
text-align: left;
vertical-align: middle;
.help.circle.icon {
@ -528,12 +542,19 @@ export default withMulti(
.result {
grid-column: 2;
text-align: right;
.icon {
margin-left: 0;
margin-right: 0.25rem;
.ui--Icon,
.icon-void {
margin-left: 0.25rem;
margin-right: 0;
padding-right: 0 !important;
}
.icon-void {
float: right;
width: 1em;
}
}
}
}

View File

@ -8,6 +8,8 @@ import styled from 'styled-components';
import AccountName from './AccountName';
import IdentityIcon from './IdentityIcon';
import ParentAddress from './ParentAddress';
import { toShortAddress } from './util';
interface Props {
children?: React.ReactNode;
@ -15,38 +17,85 @@ interface Props {
defaultName?: string;
onClickName?: () => void;
overrideName?: React.ReactNode;
parentAddress?: string;
withSidebar?: boolean;
withShortAddress?: boolean;
toggle?: unknown;
value?: string | Address | AccountId | null | Uint8Array;
}
function AddressSmall ({ children, className = '', defaultName, onClickName, overrideName, toggle, value, withSidebar = true }: Props): React.ReactElement<Props> {
function AddressSmall ({ children, className = '', defaultName, onClickName, overrideName, parentAddress, toggle, value, withShortAddress = false, withSidebar = true }: Props): React.ReactElement<Props> {
return (
<div className={`ui--AddressSmall ${className}`}>
<IdentityIcon value={value as Uint8Array} />
<AccountName
className={withSidebar ? 'withSidebar' : ''}
defaultName={defaultName}
onClick={onClickName}
override={overrideName}
toggle={toggle}
value={value}
withSidebar={withSidebar}
>
{children}
</AccountName>
<div>
<IdentityIcon value={value as Uint8Array} />
</div>
<div className='addressContainer'>
<div className='additionalInfo parentAccountName'>
{parentAddress && <ParentAddress address={parentAddress}/>}
</div>
<AccountName
className={`accountName ${withSidebar ? 'withSidebar' : ''}`}
defaultName={defaultName}
onClick={onClickName}
override={overrideName}
toggle={toggle}
value={value}
withSidebar={withSidebar}
>
{children}
</AccountName>
<div className='additionalInfo shortAddress'
data-testid='short-address'>
{withShortAddress && toShortAddress(value)}
</div>
</div>
</div>
);
}
export default React.memo(styled(AddressSmall)`
white-space: nowrap;
display: flex;
align-items: center;
.ui--IdentityIcon {
margin-right: 0.75rem;
vertical-align: middle;
}
.additionalInfo {
display: flex;
flex-direction: column;
align-self: center;
}
.parentAccountName {
grid-area: parentAccountName;
}
.accountName {
grid-area: accountName;
}
.shortAddress {
grid-area: shortAddress;
color: #8B8B8B;
font-size: 0.75rem;
}
.addressContainer {
border: 0.031rem;
height: 3.438rem;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 33% 33% 33%;
grid-template-areas:
"parentAccountName"
"accountName"
"shortAddress";
}
.ui--AccountName {
max-width: 26rem;
overflow: hidden;

View File

@ -0,0 +1,42 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styled from 'styled-components';
import AccountName from '@polkadot/react-components/AccountName';
import { Icon } from '@polkadot/react-components/index';
interface ParentAccountProps {
address: string,
className?: string
}
function ParentAddress ({ address, className }: ParentAccountProps): React.ReactElement<ParentAccountProps> {
return (
<div className={className}
data-testid='parent'>
<Icon className='parent-icon'
icon='code-branch'/>
<AccountName
value={address}
withSidebar
>
</AccountName>
</div>
);
}
export default React.memo(styled(ParentAddress)`
align-items: center;
color: #8B8B8B;
font-size: 0.75rem;
display: flex;
& .parent-icon {
font-size: 0.625rem;
margin-right: 0.3rem;
margin-left: 0.15rem;
}
`);

View File

@ -16,6 +16,7 @@ import Tooltip from './Tooltip';
import { useTranslation } from './translate';
interface Props {
iconPosition: 'left' | 'right';
className?: string;
stakingInfo?: DeriveStakingAccount;
}
@ -41,7 +42,7 @@ function extractTotals (stakingInfo?: DeriveStakingAccount, progress?: DeriveSes
return [mapped, total];
}
function StakingUnbonding ({ className = '', stakingInfo }: Props): React.ReactElement<Props> | null {
function StakingUnbonding ({ className = '', iconPosition = 'left', stakingInfo }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const progress = useCall<DeriveSessionProgress>(api.derive.session.progress);
const { t } = useTranslation();
@ -59,10 +60,13 @@ function StakingUnbonding ({ className = '', stakingInfo }: Props): React.ReactE
return (
<div className={className}>
<Icon
icon='clock'
tooltip={trigger}
/>
{iconPosition === 'left' && (
<Icon
className='left'
icon='clock'
tooltip={trigger}
/>
)}
<FormatBalance value={total} />
<Tooltip
text={mapped.map(([{ value }, eras, blocks], index): React.ReactNode => (
@ -86,6 +90,13 @@ function StakingUnbonding ({ className = '', stakingInfo }: Props): React.ReactE
))}
trigger={trigger}
/>
{iconPosition === 'right' && (
<Icon
className='right'
icon='clock'
tooltip={trigger}
/>
)}
</div>
);
}
@ -93,11 +104,16 @@ function StakingUnbonding ({ className = '', stakingInfo }: Props): React.ReactE
export default React.memo(styled(StakingUnbonding)`
white-space: nowrap;
.ui--Icon {
.ui--Icon.left {
margin-left: 0;
margin-right: 0.25rem;
}
.ui--Icon.right {
margin-left: 0.25rem;
margin-right: 0;
}
.ui--FormatBalance {
display: inline-block;
}

View File

@ -124,6 +124,11 @@ export default React.memo(styled(Head)`
&.start {
text-align: left;
}
&.balances {
text-align: right;
padding-right: 15.571rem;
}
}
tr {

View File

@ -9,7 +9,7 @@ import React, { useMemo } from 'react';
import styled from 'styled-components';
import { useApi } from '@polkadot/react-hooks';
import { formatBalance } from '@polkadot/util';
import { formatBalance, isString } from '@polkadot/util';
import { useTranslation } from './translate';
@ -20,7 +20,7 @@ interface Props {
formatIndex?: number;
isShort?: boolean;
label?: React.ReactNode;
labelPost?: string;
labelPost?: LabelPost;
value?: Compact<any> | BN | string | null | 'all';
valueFormatted?: string;
withCurrency?: boolean;
@ -31,6 +31,8 @@ interface Props {
const M_LENGTH = 6 + 1;
const K_LENGTH = 3 + 1;
type LabelPost = string | React.ReactNode
function getFormat (registry: Registry, formatIndex = 0): [number, string] {
const decimals = registry.chainDecimals;
const tokens = registry.chainTokens;
@ -45,18 +47,18 @@ function getFormat (registry: Registry, formatIndex = 0): [number, string] {
];
}
function createElement (prefix: string, postfix: string, unit: string, label = '', isShort = false): React.ReactNode {
function createElement (prefix: string, postfix: string, unit: string, label: LabelPost = '', isShort = false): React.ReactNode {
return <>{`${prefix}${isShort ? '' : '.'}`}{!isShort && <span className='ui--FormatBalance-postfix'>{`0000${postfix || ''}`.slice(-4)}</span>}<span className='ui--FormatBalance-unit'> {unit}</span>{label}</>;
}
function splitFormat (value: string, label?: string, isShort?: boolean): React.ReactNode {
function splitFormat (value: string, label?: LabelPost, isShort?: boolean): React.ReactNode {
const [prefix, postfixFull] = value.split('.');
const [postfix, unit] = postfixFull.split(' ');
return createElement(prefix, postfix, unit, label, isShort);
}
function applyFormat (value: Compact<any> | BN | string, [decimals, token]: [number, string], withCurrency = true, withSi?: boolean, _isShort?: boolean, labelPost?: string): React.ReactNode {
function applyFormat (value: Compact<any> | BN | string, [decimals, token]: [number, string], withCurrency = true, withSi?: boolean, _isShort?: boolean, labelPost?: LabelPost): React.ReactNode {
const [prefix, postfix] = formatBalance(value, { decimals, forceUnit: '-', withSi: false }).split('.');
const isShort = _isShort || (withSi && prefix.length >= K_LENGTH);
const unitPost = withCurrency ? token : '';
@ -84,17 +86,17 @@ function FormatBalance ({ children, className = '', format, formatIndex, isShort
// labelPost here looks messy, however we ensure we have one less text node
return (
<div className={`ui--FormatBalance ${className}`}>
{label ? <>{label}&nbsp;</> : ''}
<span
className='ui--FormatBalance-value'
data-testid='balance-summary'>{
{label ? <>{label}&nbsp;</> : ''}<span className='ui--FormatBalance-value'
data-testid='balance-summary' >{
valueFormatted
? splitFormat(valueFormatted, labelPost, isShort)
: value
? value === 'all'
? t<string>('everything{{labelPost}}', { replace: { labelPost } })
: applyFormat(value, formatInfo, withCurrency, withSi, isShort, labelPost)
: `-${labelPost || ''}`
: isString(labelPost)
? `-${labelPost}`
: labelPost
}</span>{children}
</div>
);