Style identity and balance sections in sidebar (#6096)

* Add subs modal

* Add tests

* Add Translations

* Improve closing modal

* Improve tests

* Improve styles

* Style balances & identity

* Optimize imports

* Improve styles

* Set greater width to balance section

* Rename subs modal toggle

* Change 'amount' to 'count' in test name

* Set make modal header fixed while content is scrollable

* Style multisig

* Update multisig styles

* Capitalize headers

* Move sidebar tests to Sidebar.spec.tsx

* Remove duplications in tests

* Add UserIcon component

* Revert subs modal changes and add scrollable section in Sidebar

* Remove unused styles

* Use accountsPage clearAccount method

* Wrap subs into Expander

* Add useSubidentities hook

* Adjust api mock in tests

* WIP - split judgements

* Revert mistaken commit

Co-authored-by: Krzysztof Jelski <krzysztof@ethworks.io>
Co-authored-by: Ewa Kowalska <ewcia.kowalska@gmail.com>
This commit is contained in:
MiZiet 2021-09-19 08:49:23 +02:00 committed by GitHub
parent ff06dc72d7
commit 42ef182879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 529 additions and 300 deletions

View File

@ -29,6 +29,7 @@
--bg-table: #3b3d3f;
--bg-tabs: #38393f;
--bg-toggle: #58595f;
--bg-sidebar: #1a1b20;
--border-table: #343536;
--border-tabs: #343536;
--color-checkbox: rgba(78, 78, 78, 0.66);
@ -49,6 +50,7 @@
--bg-table: #ffffff;
--bg-tabs: #ffffff;
--bg-toggle: #e4e5e6;
--bg-sidebar: #fafafa;
--border-table: #f1efed;
--border-tabs: #f1efed;
--color-checkbox: rgba(34, 36, 38, 0.15);

View File

@ -109,7 +109,6 @@
"Nominator": "Nominator",
"Normal transfer without keep-alive checks": "Normal transfer without keep-alive checks",
"One or more extensions are detected in your browser, however no accounts has been injected.": "One or more extensions are detected in your browser, however no accounts has been injected.",
"Owned": "Owned",
"Password are ignored for hex seed": "Password are ignored for hex seed",
"Pending call hashes": "Pending call hashes",
"Please make sure to save this file in a secure location as it is required, together with your password, to restore your account.": "Please make sure to save this file in a secure location as it is required, together with your password, to restore your account.",
@ -337,10 +336,8 @@
"somebody@example.com": "somebody@example.com",
"sort by": "sort by",
"stash address": "stash address",
"sub": "sub",
"sub name": "sub name",
"subs": "subs",
"tags": "tags",
"the account to make recoverable": "the account to make recoverable",
"the account to recover to": "the account to recover to",
"threshold": "threshold",

View File

@ -1,21 +1,18 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { screen } from '@testing-library/react';
import BN from 'bn.js';
import i18next from '@polkadot/react-components/i18n';
import toShortAddress from '@polkadot/react-components/util/toShortAddress';
import { AddressFlags } from '@polkadot/react-hooks/types';
import { anAccountWithBalance, anAccountWithBalanceAndMeta, anAccountWithInfo, anAccountWithMeta, anAccountWithStaking } from '@polkadot/test-support/creation/account';
import { makeStakingLedger as ledger } from '@polkadot/test-support/creation/stakingInfo/stakingLedger';
import { alice, MemoryStore } from '@polkadot/test-support/keyring';
import { Table } from '@polkadot/test-support/pagesElements';
import { Sidebar } from '@polkadot/test-support/pagesElements/Sidebar';
import { balance, showBalance } from '@polkadot/test-support/utils/balance';
import { keyring } from '@polkadot/ui-keyring';
import { AccountRow } from '../../test/pageElements/AccountRow';
import { AccountsPage } from '../../test/pages/accountsPage';
describe('Accounts page', () => {
@ -368,121 +365,4 @@ describe('Accounts page', () => {
});
});
});
describe('sidebar', () => {
const initialName = 'INITIAL_NAME';
const newName = 'NEW_NAME';
const defaultTag = 'Default';
const nameInputNotFoundError = 'Unable to find an element by: [data-testid="name-input"]';
let accountRows: AccountRow[];
let sideBar: Sidebar;
describe('changes name', () => {
beforeEach(async () => {
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
await sideBar.changeAccountName(newName);
});
it('within keyring', () => {
const changedAccount = keyring.getAccount(alice);
expect(changedAccount?.meta?.name).toEqual(newName);
});
it('within sidebar', async () => {
await sideBar.assertAccountName(newName);
});
it('within account row', async () => {
await accountRows[0].assertAccountName(newName);
});
});
it('cannot be edited if edit button has not been pressed', async () => {
accountsPage.renderDefaultAccounts(1);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
await sideBar.clickByText('no tags');
expect(sideBar.queryByRole('combobox')).toBeFalsy();
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
});
it('when isEditable is false account name is not editable', async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithInfo({ flags: { isEditable: false } as AddressFlags })
);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar.edit();
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
});
describe('on edit cancel', () => {
beforeEach(async () => {
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
await sideBar.assertTags('no tags');
sideBar.edit();
});
it('restores tags and name to state from keyring', async () => {
await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag);
sideBar.cancel();
await sideBar.assertTags('no tags');
await sideBar.assertAccountName(initialName);
});
it('Cancel button disappears', () => {
sideBar.cancel();
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
});
});
describe('outside click', () => {
beforeEach(async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithMeta({ name: 'alice' }),
anAccountWithMeta({ name: 'bob' })
);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar.edit();
});
it('cancels editing', async () => {
await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag);
fireEvent.click(await screen.findByText('accounts'));
await sideBar.assertTags('no tags');
await sideBar.assertAccountName('ALICE');
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
});
it('within sidebar does not cancel editing', async () => {
await sideBar.clickByText('Tags');
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeTruthy();
});
it('cancels editing and changes name when opening sidebar for another account', async () => {
await waitFor(() => sideBar.assertAccountInput('alice'));
sideBar = await accountRows[1].openSidebar();
await sideBar.assertAccountName('BOB');
});
});
});
});

View File

@ -4,7 +4,7 @@
import React from 'react';
import styled from 'styled-components';
import { AddressInfo, Icon } from '@polkadot/react-components';
import { AddressInfo } from '@polkadot/react-components';
import { useTranslation } from '../translate';
@ -21,18 +21,14 @@ function Balances ({ address, className }: Props): React.ReactElement<Props> | n
return (
<section className={className}>
<div className='ui--AddressMenu-sectionHeader'>
<div>
<Icon icon='sort-amount-down' />
&nbsp;
{t<string>('balance')}
</div>
{t<string>('balance')}
</div>
<AddressInfo
address={address}
className='balanceExpander'
withBalance={WITH_BALANCE}
withBalanceToggle
withExtended={false}
withLabel
/>
</section>
);
@ -40,10 +36,14 @@ function Balances ({ address, className }: Props): React.ReactElement<Props> | n
export default React.memo(styled(Balances)`
.balanceExpander {
.column.column--expander {
justify-content: flex-start;
.column {
width: auto;
max-width: 18.57rem;
label {
text-align: left;
color: inherit;
font-size: 0.93rem;
font-weight: var(--font-weight-normal);

View File

@ -2,42 +2,55 @@
// SPDX-License-Identifier: Apache-2.0
import type { AddressIdentity } from '@polkadot/react-hooks/types';
import type { AccountId, BalanceOf } from '@polkadot/types/interfaces';
import React from 'react';
import React, { useMemo } from 'react';
import { AddressMini, AvatarItem, Expander, Icon, IconLink, Tag } from '@polkadot/react-components';
import { useApi, useCall, useRegistrars, useToggle } from '@polkadot/react-hooks';
import { AddressMini, AvatarItem, Expander, IconLink, Tag } from '@polkadot/react-components';
import { useApi, useRegistrars, useSubidentities, useToggle } from '@polkadot/react-hooks';
import { isHex } from '@polkadot/util';
import { useTranslation } from '../translate';
import RegistrarJudgement from './RegistrarJudgement';
import UserIcon from './UserIcon';
interface Props {
address: string;
identity?: AddressIdentity;
}
const SUBS_DISPLAY_THRESHOLD = 4;
function Identity ({ address, identity }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { isRegistrar, registrars } = useRegistrars();
const [isJudgementOpen, toggleIsJudgementOpen] = useToggle();
const subs = useCall<[BalanceOf, AccountId[]]>(api.query.identity?.subsOf, [address])?.[1];
const subs = useSubidentities(address);
const subsList = useMemo(() =>
subs?.map((sub) =>
<AddressMini
className='subs'
isPadded={false}
key={sub.toString()}
value={sub}
/>
)
, [subs]
);
if (!identity || !identity.isExistent || !api.query.identity?.identityOf) {
return null;
}
return (
<section>
<section
className='withDivider'
data-testid='identity-section'
>
<div className='ui--AddressMenu-section ui--AddressMenu-identity'>
<div className='ui--AddressMenu-sectionHeader'>
<div>
<Icon icon='address-card' />
&nbsp;
{t<string>('identity')}
</div>
{t<string>('identity')}
<Tag
color={
identity.isBad
@ -74,10 +87,7 @@ function Identity ({ address, identity }: Props): React.ReactElement<Props> | nu
// ? <img src={identity.image} />
// : <i className='icon user ui--AddressMenu-identityIcon' />
//
<Icon
className='ui--AddressMenu-identityIcon'
icon='user'
/>
<UserIcon />
}
subtitle={identity.legal}
title={identity.display}
@ -162,26 +172,28 @@ function Identity ({ address, identity }: Props): React.ReactElement<Props> | nu
</div>
)}
{!!subs?.length && (
<div className='tr subs'>
{subs.length > 1
? <div className='th top'>{t<string>('subs')}</div>
: <div className='th'>{t<string>('sub')}</div>
}
<div className='td'>
<Expander summary={`(${subs.length})`}>
<div className='body column'>
{subs.map((sub) =>
<AddressMini
className='subs'
isPadded={false}
key={sub.toString()}
value={sub}
/>
)}
</div>
</Expander>
<div className='tr'>
<div className='th top'>{t<string>('subs')}</div>
<div
className='td'
data-testid='subs'
>
{subs.length > SUBS_DISPLAY_THRESHOLD
? (
<Expander summary={subs.length}>
{subsList}
</Expander>
)
: (
<>
<div className='subs-number'>{subs.length}</div>
{subsList}
</>
)
}
</div>
</div>)}
</div>
)}
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@ import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types';
import React from 'react';
import { AddressMini, Icon, Static } from '@polkadot/react-components';
import { AddressMini } from '@polkadot/react-components';
import { useTranslation } from '../translate';
@ -24,31 +24,29 @@ function Multisig ({ isMultisig, meta }: Props): React.ReactElement<Props> | nul
const { threshold, who } = meta;
return (
<section>
<section className='ui--AddressMenu-multisig withDivider'>
<div className='ui--AddressMenu-sectionHeader'>
<div>
<Icon icon='handshake' />
&nbsp;
{t<string>('multisig')}
{t<string>('multisig')}
</div>
<div className='ui--AddressMenu-multisigTable'>
<div className='tr'>
<div className='th'>{t<string>('threshold')}</div>
<div className='td'>
{threshold}/{(who as string[]).length}
</div>
</div>
<div className='tr'>
<div className='th signatories'>{t<string>('signatories')}</div>
<div className='td'>
{(who as string[])?.map((address) => (
<AddressMini
key={address}
value={address}
/>
))}
</div>
</div>
</div>
<Static
isFull
label={t<string>('threshold')}
>
{threshold}/{(who as string[]).length}
</Static>
<Static
isFull
label={t<string>('signatories')}
>
{(who as string[])?.map((address) => (
<AddressMini
key={address}
value={address}
/>
))}
</Static>
</section>
);
}

View File

@ -0,0 +1,184 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import { AddressFlags } from '@polkadot/react-hooks/types';
import { anAccount, anAccountWithInfo, anAccountWithMeta } from '@polkadot/test-support/creation/account';
import { alice, bob, MemoryStore } from '@polkadot/test-support/keyring';
import { Sidebar } from '@polkadot/test-support/pagesElements/Sidebar';
import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks';
import { keyring } from '@polkadot/ui-keyring';
import { AccountRow } from '../../test/pageElements/AccountRow';
import { AccountsPage } from '../../test/pages/accountsPage';
describe('Sidebar', () => {
let accountsPage: AccountsPage;
let accountRows: AccountRow[];
let sideBar: Sidebar;
beforeAll(() => {
if (keyring.getAccounts().length === 0) {
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
}
});
beforeEach(() => {
accountsPage = new AccountsPage();
accountsPage.clearAccounts();
});
describe('editing', () => {
const initialName = 'INITIAL_NAME';
const newName = 'NEW_NAME';
const defaultTag = 'Default';
const nameInputNotFoundError = 'Unable to find an element by: [data-testid="name-input"]';
describe('changes name', () => {
beforeEach(async () => {
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
await sideBar.changeAccountName(newName);
});
it('within keyring', () => {
const changedAccount = keyring.getAccount(alice);
expect(changedAccount?.meta?.name).toEqual(newName);
});
it('within sidebar', async () => {
await sideBar.assertAccountName(newName);
});
it('within account row', async () => {
await accountRows[0].assertAccountName(newName);
});
});
it('cannot be edited if edit button has not been pressed', async () => {
accountsPage.renderDefaultAccounts(1);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
await sideBar.clickByText('no tags');
expect(sideBar.queryByRole('combobox')).toBeFalsy();
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
});
it('when isEditable is false account name is not editable', async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithInfo({ flags: { isEditable: false } as AddressFlags })
);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar.edit();
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
});
describe('on edit cancel', () => {
beforeEach(async () => {
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
await sideBar.assertTags('no tags');
sideBar.edit();
});
it('restores tags and name to state from keyring', async () => {
await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag);
sideBar.cancel();
await sideBar.assertTags('no tags');
await sideBar.assertAccountName(initialName);
});
it('Cancel button disappears', () => {
sideBar.cancel();
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
});
});
describe('outside click', () => {
beforeEach(async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithMeta({ name: 'alice' }),
anAccountWithMeta({ name: 'bob' })
);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
sideBar.edit();
});
it('cancels editing', async () => {
await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag);
fireEvent.click(await screen.findByText('accounts'));
await sideBar.assertTags('no tags');
await sideBar.assertAccountName('ALICE');
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
});
it('within sidebar does not cancel editing', async () => {
await sideBar.clickByText('Tags');
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeTruthy();
});
it('cancels editing and changes name when opening sidebar for another account', async () => {
await waitFor(() => sideBar.assertAccountInput('alice'));
sideBar = await accountRows[1].openSidebar();
await sideBar.assertAccountName('BOB');
});
});
});
describe('identity section', () => {
it('does not display subs when account has zero subs', async () => {
accountsPage.render([[alice, anAccount()]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
const subs = await sideBar.findSubs();
expect(subs).toHaveLength(0);
});
it('displays count of subs and account names', async () => {
mockApiHooks.setSubs([bob]);
accountsPage.render([[alice, anAccount()], [bob, anAccountWithMeta({ name: 'Bob' })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
const subs = await sideBar.findSubs();
const subsNumber = subs[0].childNodes[0];
const subAccount = subs[0].childNodes[1];
expect(subsNumber).toHaveClass('subs-number');
expect(subsNumber).toHaveTextContent('1');
expect(subAccount).toHaveTextContent('BOB');
});
it('displays picked sub in sidebar', async () => {
mockApiHooks.setSubs([bob]);
accountsPage.render([[alice, anAccount()], [bob, anAccountWithMeta({ name: 'Bob' })]]);
accountRows = await accountsPage.getAccountRows();
sideBar = await accountRows[0].openSidebar();
const subs = await sideBar.findSubs();
const subAccount = subs[0].childNodes[1];
fireEvent.click(await within(subAccount as HTMLElement).findByTestId('account-name'));
await sideBar.assertAccountName('BOB');
});
});
});

View File

@ -47,16 +47,18 @@ function FullSidebar ({ address, className = '', dataTestId, onClose, onUpdateNa
sidebarRef={ref}
/>
</div>
<Balances address={address} />
<Identity
address={address}
identity={identity}
/>
<Multisig
isMultisig={flags.isMultisig}
meta={meta}
/>
<section>
<div className='ui--ScrollSection'>
<Balances address={address} />
<Identity
address={address}
identity={identity}
/>
<Multisig
isMultisig={flags.isMultisig}
meta={meta}
/>
</div>
<section className='ui--LinkSection'>
<LinkExternal
data={address}
isLogo
@ -71,9 +73,11 @@ function FullSidebar ({ address, className = '', dataTestId, onClose, onUpdateNa
export default React.memo(styled(FullSidebar)`
display: flex;
flex-direction: column;
background-color: var(--bg-sidebar);
max-width: 30.42rem;
min-width: 30.42rem;
overflow-y: hidden;
padding: 0 0 3.286rem;
input {
width: auto !important;
@ -86,7 +90,6 @@ export default React.memo(styled(FullSidebar)`
display: flex;
flex-direction: column;
justify-content: center;
margin: -1rem -1rem 1rem -1rem;
padding: 1.35rem 1rem 1rem 1rem;
}
@ -135,40 +138,61 @@ export default React.memo(styled(FullSidebar)`
}
section {
&:not(:last-child) {
margin-bottom: 1.4rem;
}
position: relative;
& :last-child {
margin-top: auto;
&:not(:last-child) {
margin-bottom: 1rem;
}
.ui--AddressMenu-sectionHeader {
display: inline-flex;
color: var(--color-text);
margin-bottom: 0.4rem;
display: flex;
justify-content: space-between;
align-items: center;
text-transform: capitalize;
margin-bottom: 0.57rem;
width: 100%;
& > :first-child {
flex: 1;
color: var(--color-text);
font-size: 1.143rem;
}
&.withDivider {
padding-top: 1rem;
::before {
position: absolute;
top: 0;
left: 0;
content: '';
width: 100%;
height: 1px;
background-color: var(--border-table);
}
}
}
.ui--AddressMenu-identity {
.ui--AddressMenu-identityTable {
.ui--AddressMenu-identity,
.ui--AddressMenu-multisig {
.ui--AddressMenu-identityTable,
.ui--AddressMenu-multisigTable {
font-size: 0.93rem;
margin-top: 0.3rem;
margin-top: 0.6rem;
.tr {
padding: 0.25rem 0;
display: inline-flex;
align-items: center;
width: 100%;
.th {
text-transform: uppercase;
color: var(--color-label);
font-weight: var(--font-weight-normal);
text-align: right;
flex-basis: 20%;
text-align: left;
flex-basis: 25%;
font-size: 0.714rem;
&.top {
align-self: flex-start;
@ -182,13 +206,46 @@ export default React.memo(styled(FullSidebar)`
text-overflow: ellipsis;
}
}
.ui--AddressMini, .subs-number {
margin-bottom: 0.4rem;
padding: 0;
}
.subs-number {
font-size: 1rem;
margin-bottom: 0.714rem;
}
}
.parent, .subs {
.parent {
padding: 0 !important;
}
}
&& .column {
align-items: center;
.ui--FormatBalance:first-of-type {
margin-bottom: 0.4rem;
}
label:first-of-type {
margin-bottom: 0.4rem;
color: var(--color-text);
}
label {
color: var(--color-label);
text-transform: uppercase;
font-size: 0.714rem;
}
.ui--FormatBalance, label {
line-height: 1rem;
}
}
.ui--AddressMenu-buttons {
.ui--Button-Group {
margin-bottom: 0;
@ -221,7 +278,7 @@ export default React.memo(styled(FullSidebar)`
.inline-icon {
cursor: pointer;
margin: 0 0 0 0.5rem;
color: ${colorLink};
color: ${colorLink};
}
.name--input {
@ -238,4 +295,25 @@ export default React.memo(styled(FullSidebar)`
opacity: 60%;
}
}
.ui--AddressMenu-multisig .th.signatories {
align-self: flex-start;
}
.ui--ScrollSection {
padding: 1rem;
overflow: auto;
}
.ui--LinkSection {
border-top: 1px solid var(--border-table);
padding: 0.5rem 0 0.571rem;
width: 100%;
position: absolute;
bottom: 0;
span {
margin: 0 0.5rem;
}
}
`);

File diff suppressed because one or more lines are too long

View File

@ -2,14 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
import type { TFunction } from 'i18next';
import type { Data, Option, Vec } from '@polkadot/types';
import type { AccountId, Balance } from '@polkadot/types/interfaces';
import type { Data, Option } from '@polkadot/types';
import type { AccountId } from '@polkadot/types/interfaces';
import type { ITuple } from '@polkadot/types/types';
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Columar, Input, InputAddress, Modal, Spinner, TxButton } from '@polkadot/react-components';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
import { useAccounts, useApi, useCall, useSubidentities } from '@polkadot/react-hooks';
import { u8aToString } from '@polkadot/util';
import { useTranslation } from '../translate';
@ -80,17 +80,13 @@ function IdentitySub ({ address, index, name, setAddress, setName, t }: SubProps
const IdentitySubMemo = React.memo(IdentitySub);
const transformIds = {
transform: ([, ids]: ITuple<[Balance, Vec<AccountId>]>) => ids.map((a) => a.toString())
};
const transformInfo = { withParams: true };
function IdentitySubModal ({ address, className, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
const queryIds = useCall<string[]>(api.query.identity.subsOf, [address], transformIds);
const queryIds = useSubidentities(address);
const queryInfos = useCall<[[string[]], Option<ITuple<[AccountId, Data]>>[]]>(queryIds && queryIds.length !== 0 && api.query.identity.superOf.multi, [queryIds], transformInfo);
const [infos, setInfos] = useState<[string, string][] | undefined>();

View File

@ -59,7 +59,7 @@ interface Props {
withExtended?: boolean | CryptoActiveType;
withHexSessionId?: (string | null)[];
withValidatorPrefs?: boolean | ValidatorPrefsType;
withoutLabel?: boolean;
withLabel?: boolean;
}
const DEFAULT_BALANCES: BalanceActiveType = {
@ -222,12 +222,12 @@ function renderValidatorPrefs ({ stakingInfo, withValidatorPrefs = false }: Prop
);
}
function createBalanceItems (formatIndex: number, lookup: Record<string, string>, t: TFunction, { address, balanceDisplay, balancesAll, bestNumber, democracyLocks, isAllLocked, otherBonded, ownBonded, stakingInfo, votingOf, withBalanceToggle }: { address: string; balanceDisplay: BalanceActiveType; balancesAll?: DeriveBalancesAll | DeriveBalancesAccountData; bestNumber: BlockNumber; democracyLocks?: DeriveDemocracyLock[]; isAllLocked: boolean; otherBonded: BN[]; ownBonded: BN; stakingInfo?: DeriveStakingAccount; votingOf?: Voting; withBalanceToggle: boolean }): React.ReactNode {
function createBalanceItems (formatIndex: number, lookup: Record<string, string>, t: TFunction, { address, balanceDisplay, balancesAll, bestNumber, democracyLocks, isAllLocked, otherBonded, ownBonded, stakingInfo, votingOf, withBalanceToggle, withLabel }: { address: string; balanceDisplay: BalanceActiveType; balancesAll?: DeriveBalancesAll | DeriveBalancesAccountData; bestNumber: BlockNumber; democracyLocks?: DeriveDemocracyLock[]; isAllLocked: boolean; otherBonded: BN[]; ownBonded: BN; stakingInfo?: DeriveStakingAccount; votingOf?: Voting; withBalanceToggle: boolean, withLabel: boolean }): React.ReactNode {
const allItems: React.ReactNode[] = [];
!withBalanceToggle && balancesAll && balanceDisplay.total && allItems.push(
<React.Fragment key={0}>
<Label label='' />
<Label label={withLabel ? t<string>('total') : ''} />
<FormatBalance
className='result'
formatIndex={formatIndex}
@ -423,7 +423,7 @@ function createBalanceItems (formatIndex: number, lookup: Record<string, string>
}
function renderBalances (props: Props, lookup: Record<string, string>, bestNumber: BlockNumber | undefined, t: TFunction): React.ReactNode[] {
const { address, balancesAll, democracyLocks, stakingInfo, votingOf, withBalance = true, withBalanceToggle = false } = props;
const { address, balancesAll, democracyLocks, stakingInfo, votingOf, withBalance = true, withBalanceToggle = false, withLabel = false } = props;
const balanceDisplay = withBalance === true
? DEFAULT_BALANCES
: withBalance || false;
@ -434,7 +434,7 @@ function renderBalances (props: Props, lookup: Record<string, string>, bestNumbe
const [ownBonded, otherBonded] = calcBonded(stakingInfo, balanceDisplay.bonded);
const isAllLocked = !!balancesAll && balancesAll.lockedBreakdown.some(({ amount }): boolean => amount?.isMax());
const baseOpts = { address, balanceDisplay, bestNumber, democracyLocks, isAllLocked, otherBonded, ownBonded, votingOf, withBalanceToggle };
const baseOpts = { address, balanceDisplay, bestNumber, democracyLocks, isAllLocked, otherBonded, ownBonded, votingOf, withBalanceToggle, withLabel };
const items = [createBalanceItems(0, lookup, t, { ...baseOpts, balancesAll, stakingInfo })];
withBalanceToggle && balancesAll?.additional.length && balancesAll.additional.forEach((balancesAll, index): void => {

View File

@ -27,6 +27,7 @@ interface Props {
label?: React.ReactNode;
labelBalance?: React.ReactNode;
nameExtra?: React.ReactNode;
onNameClick?: () => void;
summary?: React.ReactNode;
type?: KeyringItemType;
value?: AccountId | AccountIndex | Address | string | null | Uint8Array;
@ -39,7 +40,7 @@ interface Props {
withShrink?: boolean;
}
function AddressMini ({ balance, bonded, children, className = '', iconInfo, isHighlight, isPadded = true, label, labelBalance, nameExtra, summary, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false, withSidebar = true }: Props): React.ReactElement<Props> | null {
function AddressMini ({ balance, bonded, children, className = '', iconInfo, isHighlight, isPadded = true, label, labelBalance, nameExtra, onNameClick, summary, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false, withSidebar = true }: Props): React.ReactElement<Props> | null {
if (!value) {
return null;
}
@ -50,7 +51,7 @@ function AddressMini ({ balance, bonded, children, className = '', iconInfo, isH
<label className='ui--AddressMini-label'>{label}</label>
)}
<div className='ui--AddressMini-icon'>
<IdentityIcon value={value as Uint8Array} />
<IdentityIcon value={value} />
{iconInfo && (
<div className='ui--AddressMini-icon-info'>
{iconInfo}
@ -59,7 +60,10 @@ function AddressMini ({ balance, bonded, children, className = '', iconInfo, isH
</div>
<div className='ui--AddressMini-info'>
{withAddress && (
<div className='ui--AddressMini-address'>
<div
className='ui--AddressMini-address'
onClick={onNameClick}
>
{withName
? (
<AccountName

View File

@ -39,57 +39,41 @@ export default React.memo(styled(AvatarItem)`
align-items: center;
.ui--AvatarItem-icon {
width: 2.4rem;
height: 2.4rem;
margin-right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
}
}
> * {
border-radius: 50%;
width: 100%;
height: 100%;
}
.ui--AvatarItem-details {
.ui--AvatarItem-title {
font-weight: 600;
font-size: 1rem;
}
.ui--AvatarItem-subtitle {
font-weight: var(--font-weight-normal);
font-size: 1rem;
}
}
&.big {
.ui--AvatarItem-icon {
width: 3.4rem;
height: 3.4rem;
margin-right: 0.6rem;
> .ui--Icon {
color: white;
line-height: 2.4rem;
margin-right: 0 !important;
font-size: 1.6rem;
line-height: 3.4rem;
}
}
.ui--AvatarItem-details {
flex: 1;
.ui--AvatarItem-title {
font-weight: var(--font-weight-normal);
font-size: 1rem;
}
.ui--AvatarItem-subtitle {
color: rgba(100, 100, 100, 0.6);
font-size: 1rem;
}
}
&.big {
.ui--AvatarItem-icon {
width: 3.4rem;
height: 3.4rem;
margin-right: 0.6rem;
> .ui--Icon {
font-size: 1.6rem;
line-height: 3.4rem;
}
}
.ui--AvatarItem-details {
.ui--AvatarItem-name {
font-size: 1.4rem;
line-height: 1.4rem;
}
.ui--AvatarItem-name {
font-size: 1.4rem;
line-height: 1.4rem;
}
}
}

View File

@ -8,7 +8,7 @@ import styled from 'styled-components';
function Content ({ children, className = '' }: ContentProps) {
return (
<div className={className}>
<div className={`${className} ui--Modal__Content`}>
{children}
</div>
);

View File

@ -8,9 +8,9 @@ import styled from 'styled-components';
import Button from '../Button';
function Header ({ className, header, onClose }: HeaderProps) {
function Header ({ className = '', header, onClose }: HeaderProps) {
return (
<div className={className}>
<div className={`${className} ui--Modal__Header`}>
{header && (
<h1>{header}</h1>
)}

View File

@ -15,8 +15,10 @@ export interface ColumnsProps {
hint?: React.ReactNode;
}
type ModalSize = 'large' | 'medium' | 'small'
export interface ModalProps {
size?: 'large' | 'medium' | 'small';
size?: ModalSize;
children: React.ReactNode;
className?: string;
header?: React.ReactNode;
@ -32,7 +34,7 @@ export interface ActionsProps {
}
export interface BodyProps {
size: 'large' | 'medium' | 'small'
size: ModalSize;
}
export interface ContentProps {

View File

@ -42,6 +42,7 @@ export { useIsParasLinked, useParaEndpoints } from './useParaEndpoints';
export { usePassword } from './usePassword';
export { useRegistrars } from './useRegistrars';
export { useSavedFlags } from './useSavedFlags';
export { useSubidentities } from './useSubidentities';
export { useStakingInfo } from './useStakingInfo';
export { useStepper } from './useStepper';
export { useSudo } from './useSudo';

View File

@ -34,7 +34,7 @@ interface QueryMapFn extends QueryTrackFn {
};
}
type TrackFn = RpcPromiseResult<AnyFunction> | QueryTrackFn;
export type TrackFn = RpcPromiseResult<AnyFunction> | QueryTrackFn;
type CallFn = (...params: unknown[]) => Promise<VoidFn>;

View File

@ -0,0 +1,13 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { AccountId, BalanceOf } from '@polkadot/types/interfaces';
import { useApi } from './useApi';
import { useCall } from './useCall';
export function useSubidentities (address: string): AccountId[] | undefined {
const { api } = useApi();
return useCall<[BalanceOf, AccountId[]]>(api.query.identity?.subsOf, [address])?.[1];
}

View File

@ -4,3 +4,4 @@
export * from './addresses';
export * from './MemoryStore';
export * from './signers';
export * from './addresses';

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
import { render, RenderResult, screen } from '@testing-library/react';
import BN from 'bn.js';
import React, { Suspense } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
@ -15,8 +14,8 @@ import { ApiProps } from '@polkadot/react-api/types';
import { QueueProvider } from '@polkadot/react-components/Status/Context';
import { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
import { UseAccountInfo } from '@polkadot/react-hooks/types';
import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks';
import { TypeRegistry } from '@polkadot/types/create';
import { BlockNumber } from '@polkadot/types/interfaces';
import { keyring } from '@polkadot/ui-keyring';
import { alice, bob, charlie } from '../keyring';
@ -25,10 +24,6 @@ import { AccountOverrides, mockAccountHooks } from '../utils/accountDefaults';
let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
function noop (): void {
// ignore
}
class NotYetRendered extends Error {
}
@ -48,6 +43,7 @@ jest.mock('@polkadot/react-hooks/useAccountInfo', () => {
? {
...actual.useAccountInfo(address),
flags: { ...actual.useAccountInfo(address).flags, ...(mockInfo.info.flags) },
identity: { ...actual.useAccountInfo(address).identity, ...(mockInfo.info.identity) },
tags: [...actual.useAccountInfo(address).tags, ...(mockInfo.info.tags)]
}
: actual.useAccountInfo(address);
@ -71,6 +67,10 @@ jest.mock('@polkadot/react-hooks/useBestNumber', () => ({
useBestNumber: () => 1
}));
jest.mock('@polkadot/react-hooks/useSubidentities', () => ({
useSubidentities: () => mockApiHooks.subs
}));
export abstract class Page {
private renderResult?: RenderResult
protected readonly defaultAddresses = [alice, bob, charlie];
@ -87,28 +87,32 @@ export abstract class Page {
keyring.addExternal(address, meta);
});
const noop = () => Promise.resolve(() => { /**/ });
const mockApi: ApiProps = {
api: {
derive: {
accounts: {
info: () => Promise.resolve(() => { /**/
})
info: noop
},
balances: {
all: () => ({
accountNonce: mockAccountHooks.nonce,
additional: [],
freeBalance: 0,
lockedBreakdown: [],
reservedBalance: 0
})
all: noop
},
chain: {
bestNumber: () => new BN(1) as BlockNumber
democracy: {
locks: noop
},
staking: {
account: noop
}
},
genesisHash: new TypeRegistry().createType('Hash', POLKADOT_GENESIS),
query: {},
query: {
democracy: {
votingOf: noop
},
identity: {
identityOf: noop
}
},
registry: { chainDecimals: [12], chainTokens: ['Unit'] },
tx: {
council: {

View File

@ -1,7 +1,7 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { fireEvent, within } from '@testing-library/react';
import { fireEvent, screen, within } from '@testing-library/react';
export class Sidebar {
public sidebar: HTMLElement
@ -37,7 +37,8 @@ export class Sidebar {
}
async assertAccountName (expectedAccountName: string): Promise<void> {
const sideBarName = await this.findByTestId('account-name');
const sideBarAddressSection = await this.findByTestId('sidebar-address-menu');
const sideBarName = await within(sideBarAddressSection).findByTestId('account-name');
expect(sideBarName).toHaveTextContent(expectedAccountName);
}
@ -108,6 +109,21 @@ export class Sidebar {
return within(this.sidebar).queryByTestId(testId);
}
async findSubs (): Promise<HTMLElement[]> {
const identitySection = await this.findByTestId('identity-section');
return within(identitySection).queryAllByTestId('subs');
}
async openSubsModal (): Promise<HTMLElement> {
const identitySection = await this.findByTestId('identity-section');
const showSubsButton = await within(identitySection).findByText('Show list');
fireEvent.click(showSubsButton);
return screen.findByTestId('modal');
}
private clickButton (buttonName: string) {
const button = this.getByRole('button', { name: buttonName });

View File

@ -84,6 +84,7 @@ export const defaultMeta: KeyringJson$Meta = {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
export const defaultAccountInfo: UseAccountInfo = {
flags: {},
identity: { email: 'user@email.com', isExistent: true, judgements: [] },
tags: []
} as any;

View File

@ -0,0 +1,11 @@
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
class MockApiHooks {
public subs: string[] | undefined = []
public setSubs (subs: string[] | undefined) {
this.subs = subs;
}
}
export const mockApiHooks = new MockApiHooks();