diff --git a/packages/page-accounts/src/Accounts/Account.tsx b/packages/page-accounts/src/Accounts/Account.tsx index c04af22c35..bb7e02df71 100644 --- a/packages/page-accounts/src/Accounts/Account.tsx +++ b/packages/page-accounts/src/Accounts/Account.tsx @@ -5,10 +5,11 @@ import { DeriveAccountInfo, DeriveBalancesAll } from '@polkadot/api-derive/types'; import { ActionStatus } from '@polkadot/react-components/Status/types'; import { RecoveryConfig } from '@polkadot/types/interfaces'; +import { SortedAccount } from './types'; import React, { useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; -import { AddressInfo, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, Input, InputTags, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components'; +import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, Input, InputTags, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components'; import { useApi, useCall, useToggle } from '@polkadot/react-hooks'; import { Option } from '@polkadot/types'; import keyring from '@polkadot/ui-keyring'; @@ -23,22 +24,31 @@ import RecoverAccount from './modals/RecoverAccount'; import RecoverSetup from './modals/RecoverSetup'; import Transfer from './modals/Transfer'; -interface Props { - address: string; +interface Props extends SortedAccount { className?: string; filter: string; - isFavorite: boolean; toggleFavorite: (address: string) => void; } -function Account ({ address, className, filter, isFavorite, toggleFavorite }: Props): React.ReactElement | null { +function calcVisible (filter: string, name: string, tags: string[]): boolean { + if (filter.length === 0) { + return true; + } + + const _filter = filter.toLowerCase(); + + return tags.reduce((result: boolean, tag: string): boolean => { + return result || tag.toLowerCase().includes(_filter); + }, name.toLowerCase().includes(_filter)); +} + +function Account ({ account: { address, meta }, className, filter, isFavorite, toggleFavorite }: Props): React.ReactElement | null { const { t } = useTranslation(); const api = useApi(); const info = useCall(api.api.derive.accounts.info as any, [address]); const balancesAll = useCall(api.api.derive.balances.all as any, [address]); const recoveryInfo = useCall(api.api.query.recovery?.recoverable, [address], { - transform: (opt: Option): RecoveryConfig | null => - opt.unwrapOr(null) + transform: (opt: Option) => opt.unwrapOr(null) }); const [tags, setTags] = useState([]); const [accName, setAccName] = useState(''); @@ -88,23 +98,16 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr }, [address, _setTags]); useEffect((): void => { - if (filter.length === 0) { - setIsVisible(true); - } else { - const _filter = filter.toLowerCase(); - - setIsVisible( - tags.reduce((result: boolean, tag: string): boolean => { - return result || tag.toLowerCase().includes(_filter); - }, accName.toLowerCase().includes(_filter)) - ); - } + setIsVisible( + calcVisible(filter, accName, tags) + ); }, [accName, filter, tags]); const _onFavorite = useCallback( (): void => toggleFavorite(address), [address, toggleFavorite] ); + const _onGenesisChange = useCallback( (genesisHash: string | null): void => { const account = keyring.getPair(address); @@ -115,6 +118,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr }, [address] ); + const _saveName = useCallback( (): void => { toggleEditName(); @@ -133,6 +137,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr }, [accName, address, toggleEditName] ); + const _saveTags = useCallback( (): void => { toggleEditTags(); @@ -151,6 +156,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr }, [address, tags, toggleEditTags] ); + const _onForget = useCallback( (): void => { if (!address) { @@ -306,6 +312,11 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr /> )} + + {meta.parentAddress && ( + + )} + diff --git a/packages/page-accounts/src/Accounts/index.tsx b/packages/page-accounts/src/Accounts/index.tsx index d137737fa2..8c4fb1f5a4 100644 --- a/packages/page-accounts/src/Accounts/index.tsx +++ b/packages/page-accounts/src/Accounts/index.tsx @@ -4,6 +4,7 @@ import { KeyringAddress } from '@polkadot/ui-keyring/types'; import { ComponentProps as Props } from '../types'; +import { SortedAccount } from './types'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; @@ -19,8 +20,6 @@ import QrModal from './modals/Qr'; import Account from './Account'; import Banner from './Banner'; -type SortedAccount = { address: string; isFavorite: boolean }; - const STORE_FAVS = 'accounts:favorites'; // query the ledger for the address, adding it to the keyring @@ -36,6 +35,53 @@ async function queryLedger (): Promise { } } +function expandList (mapped: SortedAccount[], entry: SortedAccount): SortedAccount[] { + mapped.push(entry); + + entry.children.forEach((entry): void => { + expandList(mapped, entry); + }); + + return mapped; +} + +function sortAccounts (addresses: string[], favorites: string[]): SortedAccount[] { + const mapped = addresses + .map((address) => keyring.getAccount(address)) + .filter((account): account is KeyringAddress => !!account) + .map((account): SortedAccount => ({ + account, + children: [], + isFavorite: favorites.includes(account.address) + })) + .sort((a, b) => (a.account.meta.whenCreated || 0) - (b.account.meta.whenCreated || 0)); + + return mapped + .filter((entry): boolean => { + const parentAddress = entry.account.meta.parentAddress; + + if (parentAddress) { + const parent = mapped.find(({ account: { address } }) => address === parentAddress); + + if (parent) { + parent.children.push(entry); + + return false; + } + } + + return true; + }) + .reduce(expandList, []) + .sort((a, b): number => + a.isFavorite === b.isFavorite + ? 0 + : b.isFavorite + ? 1 + : -1 + ); +} + function Overview ({ className, onStatusChange }: Props): React.ReactElement { const { t } = useTranslation(); const { allAccounts } = useAccounts(); @@ -48,21 +94,7 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement { setSortedAccounts( - allAccounts - .map((address): SortedAccount => ({ address, isFavorite: favorites.includes(address) })) - .sort((a, b): number => { - const accA = keyring.getAccount(a.address) as KeyringAddress; - const accB = keyring.getAccount(b.address) as KeyringAddress; - - return (accA.meta.whenCreated || 0) - (accB.meta.whenCreated || 0); - }) - .sort((a, b): number => - a.isFavorite === b.isFavorite - ? 0 - : b.isFavorite - ? 1 - : -1 - ) + sortAccounts(allAccounts, favorites) ); }, [allAccounts, favorites]); @@ -131,6 +163,7 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement - {sortedAccounts.map(({ address, isFavorite }): React.ReactNode => ( + {sortedAccounts.map(({ account, isFavorite }): React.ReactNode => ( ))} diff --git a/packages/page-accounts/src/Accounts/modals/Derive.tsx b/packages/page-accounts/src/Accounts/modals/Derive.tsx index 5633ebee6e..c5cd89ea4c 100644 --- a/packages/page-accounts/src/Accounts/modals/Derive.tsx +++ b/packages/page-accounts/src/Accounts/modals/Derive.tsx @@ -49,7 +49,7 @@ function createAccount (source: KeyringPair, suri: string, name: string, passwor try { const derived = source.derive(suri); - derived.setMeta({ ...derived.meta, name, tags: [] }); + derived.setMeta({ ...derived.meta, name, parentAddress: source.address, tags: [] }); const result = keyring.addPair(derived, password || ''); const { address } = result.pair; diff --git a/packages/page-accounts/src/Accounts/types.ts b/packages/page-accounts/src/Accounts/types.ts new file mode 100644 index 0000000000..80fc8bc4ab --- /dev/null +++ b/packages/page-accounts/src/Accounts/types.ts @@ -0,0 +1,11 @@ +// Copyright 2017-2020 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { KeyringAddress } from '@polkadot/ui-keyring/types'; + +export interface SortedAccount { + account: KeyringAddress; + children: SortedAccount[]; + isFavorite: boolean; +} diff --git a/packages/react-components/src/AddressToggle.tsx b/packages/react-components/src/AddressToggle.tsx index d75bedf3b2..1a44e791ac 100644 --- a/packages/react-components/src/AddressToggle.tsx +++ b/packages/react-components/src/AddressToggle.tsx @@ -4,7 +4,7 @@ import { DeriveAccountInfo } from '@polkadot/api-derive/types'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { useApi, useCall } from '@polkadot/react-hooks'; @@ -54,7 +54,10 @@ function AddressToggle ({ address, className, filter, isHidden, noToggle, onChan setIsFiltered(getIsFiltered(address, filter, info)); }, [address, filter, info]); - const _onClick = (): void => onChange && onChange(!value); + const _onClick = useCallback( + (): void => onChange && onChange(!value), + [onChange, value] + ); return (
+ onChange( + isTiedToChain + ? api.genesisHash.toHex() + : null + ), + [api, onChange] + ); + if (isDevelopment) { return null; } - const _onChange = (isTiedToChain: boolean): void => - onChange( - isTiedToChain - ? api.genesisHash.toHex() - : null - ); - return ( { - isAddress && queueAction && queueAction({ - account: value, - action: t('clipboard'), - message: t('address copied'), - status: 'queued' - }); - }; + const _onCopy = useCallback( + (): void => { + isAddress && queueAction && queueAction({ + account: value, + action: t('clipboard'), + message: t('address copied'), + status: 'queued' + }); + }, + [isAddress, queueAction, t, value] + ); return (
diff --git a/packages/react-components/src/Dropdown.tsx b/packages/react-components/src/Dropdown.tsx index 82ae6cdfc8..42a3ee117e 100644 --- a/packages/react-components/src/Dropdown.tsx +++ b/packages/react-components/src/Dropdown.tsx @@ -71,11 +71,17 @@ function BaseDropdown