Show labels of account parent (#306)
* Show derived accounts indentation * Add parent labels * Update branch * styling * CR requests * Slightly reword derive documentation * Remove derivation path from account metadata
This commit is contained in:
parent
5e13f5e57f
commit
53d3ebce37
@ -151,7 +151,7 @@ That is, hard-derivation paths are prefixed with `//`, soft paths with `/`, and
|
|||||||
|
|
||||||
The extension will still ask you to enter a password for this account. As before, this password only encrypts the private key on disk. It is not required to be the same password as the one that encrypts the mnemonic phrase.
|
The extension will still ask you to enter a password for this account. As before, this password only encrypts the private key on disk. It is not required to be the same password as the one that encrypts the mnemonic phrase.
|
||||||
|
|
||||||
Accounts can also be derived from existing accounts – `Derive New Account` option in account's dropdown menu should be selected. After providing the password of the derived account, along with name and password of the new account, enter derivation path in the following format:
|
Accounts can also be derived from existing accounts – `Derive New Account` option in account's dropdown menu should be selected. After providing the password of the parent account, along with name and password of the derived account, enter derivation path in the following format:
|
||||||
|
|
||||||
```
|
```
|
||||||
//<hard>/<soft>
|
//<hard>/<soft>
|
||||||
|
|||||||
@ -303,7 +303,11 @@ export default class Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private derivationCreate ({ genesisHash, name, parentAddress, parentPassword, password, suri }: RequestDeriveCreate): boolean {
|
private derivationCreate ({ genesisHash, name, parentAddress, parentPassword, password, suri }: RequestDeriveCreate): boolean {
|
||||||
const childPair = this.derive(parentAddress, suri, parentPassword, { genesisHash, name, parentAddress });
|
const childPair = this.derive(parentAddress, suri, parentPassword, {
|
||||||
|
genesisHash,
|
||||||
|
name,
|
||||||
|
parentAddress
|
||||||
|
});
|
||||||
|
|
||||||
keyring.addPair(childPair, password);
|
keyring.addPair(childPair, password);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import { InjectedAccount, MetadataDef, ProviderList, ProviderMeta, InjectedMetadataKnown } from '@polkadot/extension-inject/types';
|
import { InjectedAccount, MetadataDef, ProviderList, ProviderMeta, InjectedMetadataKnown } from '@polkadot/extension-inject/types';
|
||||||
import { JsonRpcResponse } from '@polkadot/rpc-provider/types';
|
import { JsonRpcResponse } from '@polkadot/rpc-provider/types';
|
||||||
import { KeypairType } from '@polkadot/util-crypto/types';
|
import { KeypairType } from '@polkadot/util-crypto/types';
|
||||||
import { KeyringPair } from '@polkadot/keyring/types';
|
import { KeyringPair, KeyringPair$Meta } from '@polkadot/keyring/types';
|
||||||
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
|
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
|
||||||
import { TypeRegistry } from '@polkadot/types';
|
import { TypeRegistry } from '@polkadot/types';
|
||||||
|
|
||||||
@ -23,11 +23,21 @@ type NullKeys<T> = { [K in keyof T]: IsNull<T, K> }[keyof T];
|
|||||||
|
|
||||||
export type SeedLengths = 12 | 24;
|
export type SeedLengths = 12 | 24;
|
||||||
|
|
||||||
export interface AccountJson {
|
export interface AccountJson extends KeyringPair$Meta {
|
||||||
address: string;
|
address: string;
|
||||||
genesisHash?: string | null;
|
genesisHash?: string | null;
|
||||||
isExternal?: boolean;
|
isExternal?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
parentAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountWithChildren = AccountJson & {
|
||||||
|
children?: AccountWithChildren[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountsContext = {
|
||||||
|
accounts: AccountJson[];
|
||||||
|
hierarchy: AccountWithChildren[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizeRequest {
|
export interface AuthorizeRequest {
|
||||||
|
|||||||
@ -13,10 +13,11 @@ import { Name } from '../../partials';
|
|||||||
|
|
||||||
interface Props extends AccountJson {
|
interface Props extends AccountJson {
|
||||||
address: string;
|
address: string;
|
||||||
|
parentName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Account ({ address, className, isExternal }: Props): React.ReactElement<Props> {
|
function Account ({ address, className, isExternal, parentName }: Props): React.ReactElement<Props> {
|
||||||
const onAction = useContext(ActionContext);
|
const onAction = useContext(ActionContext);
|
||||||
const [isEditing, setEditing] = useState(false);
|
const [isEditing, setEditing] = useState(false);
|
||||||
const [editedName, setName] = useState<string | null>(null);
|
const [editedName, setName] = useState<string | null>(null);
|
||||||
@ -61,6 +62,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
|
|||||||
)}
|
)}
|
||||||
address={address}
|
address={address}
|
||||||
name={editedName}
|
name={editedName}
|
||||||
|
parentName={parentName}
|
||||||
>
|
>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<Name
|
<Name
|
||||||
|
|||||||
27
packages/extension-ui/src/Popup/Accounts/AccountsTree.tsx
Normal file
27
packages/extension-ui/src/Popup/Accounts/AccountsTree.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AccountWithChildren } from '@polkadot/extension-base/background/types';
|
||||||
|
import Account from './Account';
|
||||||
|
|
||||||
|
interface Props extends AccountWithChildren {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountsTree ({ parentName, ...account }: Props): React.ReactElement<Props> {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Account
|
||||||
|
{...account}
|
||||||
|
parentName={parentName}
|
||||||
|
/>
|
||||||
|
{account?.children?.map((child, index) => (
|
||||||
|
<AccountsTree
|
||||||
|
key={`${index}:${child.address}`}
|
||||||
|
{...child}
|
||||||
|
parentName={account.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountsTree;
|
||||||
@ -3,21 +3,21 @@
|
|||||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import QrImage from '../../assets/qr.svg';
|
|
||||||
|
|
||||||
import { AccountContext, Button, Header, MediaContext, AddAccount, ButtonArea, Svg, ButtonWithSubtitle } from '../../components';
|
|
||||||
import Account from './Account';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import QrImage from '../../assets/qr.svg';
|
||||||
|
import AccountsTree from './AccountsTree';
|
||||||
|
import { AccountContext, Button, Header, MediaContext, AddAccount, ButtonArea, Svg, ButtonWithSubtitle } from '../../components';
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
export default function Accounts (): React.ReactElement<Props> {
|
export default function Accounts (): React.ReactElement<Props> {
|
||||||
const accounts = useContext(AccountContext);
|
const { hierarchy } = useContext(AccountContext);
|
||||||
const mediaAllowed = useContext(MediaContext);
|
const mediaAllowed = useContext(MediaContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(accounts.length === 0)
|
{(hierarchy.length === 0)
|
||||||
? <AddAccount />
|
? <AddAccount />
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
@ -26,8 +26,8 @@ export default function Accounts (): React.ReactElement<Props> {
|
|||||||
text={'Accounts'}
|
text={'Accounts'}
|
||||||
/>
|
/>
|
||||||
<AccountsArea>
|
<AccountsArea>
|
||||||
{accounts.map((json, index): React.ReactNode => (
|
{hierarchy.map((json, index): React.ReactNode => (
|
||||||
<Account
|
<AccountsTree
|
||||||
{...json}
|
{...json}
|
||||||
key={`${index}:${json.address}`}
|
key={`${index}:${json.address}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import Adapter from 'enzyme-adapter-react-16';
|
|||||||
import { configure, mount, ReactWrapper } from 'enzyme';
|
import { configure, mount, ReactWrapper } from 'enzyme';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import * as messaging from '@polkadot/extension-ui/messaging';
|
import * as messaging from '@polkadot/extension-ui/messaging';
|
||||||
import { AccountJson, SigningRequest } from '@polkadot/extension-base/background/types';
|
import { SigningRequest } from '@polkadot/extension-base/background/types';
|
||||||
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
|
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
|
||||||
import { AccountContext, ActionContext, Address, Button, Input, SigningReqContext, themes } from '@polkadot/extension-ui/components';
|
import { ActionContext, Address, Button, Input, SigningReqContext, themes } from '@polkadot/extension-ui/components';
|
||||||
import { ThemeProvider } from 'styled-components';
|
import { ThemeProvider } from 'styled-components';
|
||||||
import Signing from '.';
|
import Signing from '.';
|
||||||
import TransactionIndex from './TransactionIndex';
|
import TransactionIndex from './TransactionIndex';
|
||||||
@ -25,17 +25,6 @@ describe.skip('Signing requests', () => {
|
|||||||
let wrapper: ReactWrapper;
|
let wrapper: ReactWrapper;
|
||||||
let onActionStub: jest.Mock;
|
let onActionStub: jest.Mock;
|
||||||
let signRequests: SigningRequest[] = [];
|
let signRequests: SigningRequest[] = [];
|
||||||
const accounts: AccountJson[] = [{
|
|
||||||
address: '5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5',
|
|
||||||
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
|
|
||||||
isExternal: false,
|
|
||||||
name: 'acc1'
|
|
||||||
}, {
|
|
||||||
address: '5E9nq1yGJJFiP8C75ryD9J2R62q2cesz6NumLnuXRgmuN5DG',
|
|
||||||
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
|
|
||||||
isExternal: false,
|
|
||||||
name: 'acc2'
|
|
||||||
}];
|
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
@ -54,11 +43,9 @@ describe.skip('Signing requests', () => {
|
|||||||
const mountComponent = async (): Promise<void> => {
|
const mountComponent = async (): Promise<void> => {
|
||||||
wrapper = mount(
|
wrapper = mount(
|
||||||
<ActionContext.Provider value={onActionStub}>
|
<ActionContext.Provider value={onActionStub}>
|
||||||
<AccountContext.Provider value={accounts}>
|
<ThemeProvider theme={themes.dark}>
|
||||||
<ThemeProvider theme={themes.dark}>
|
<MockRequestsProvider />
|
||||||
<MockRequestsProvider />
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
|
||||||
</AccountContext.Provider>
|
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
);
|
);
|
||||||
await act(flushAllPromises);
|
await act(flushAllPromises);
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { Loading } from '../components';
|
|||||||
import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SigningReqContext } from '../components/contexts';
|
import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SigningReqContext } from '../components/contexts';
|
||||||
import ToastProvider from '../components/Toast/ToastProvider';
|
import ToastProvider from '../components/Toast/ToastProvider';
|
||||||
import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging';
|
import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging';
|
||||||
|
import { buildHierarchy } from '../utils/buildHierarchy';
|
||||||
import Accounts from './Accounts';
|
import Accounts from './Accounts';
|
||||||
import Authorize from './Authorize';
|
import Authorize from './Authorize';
|
||||||
import CreateAccount from './CreateAccount';
|
import CreateAccount from './CreateAccount';
|
||||||
@ -96,7 +97,7 @@ export default function Popup (): React.ReactElement<{}> {
|
|||||||
return (
|
return (
|
||||||
<Loading>{accounts && authRequests && metaRequests && signRequests && (
|
<Loading>{accounts && authRequests && metaRequests && signRequests && (
|
||||||
<ActionContext.Provider value={_onAction}>
|
<ActionContext.Provider value={_onAction}>
|
||||||
<AccountContext.Provider value={accounts}>
|
<AccountContext.Provider value={{ accounts, hierarchy: buildHierarchy(accounts) }}>
|
||||||
<AuthorizeReqContext.Provider value={authRequests}>
|
<AuthorizeReqContext.Provider value={authRequests}>
|
||||||
<MediaContext.Provider value={cameraOn && mediaAllowed}>
|
<MediaContext.Provider value={cameraOn && mediaAllowed}>
|
||||||
<MetadataReqContext.Provider value={metaRequests}>
|
<MetadataReqContext.Provider value={metaRequests}>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// This software may be modified and distributed under the terms
|
// This software may be modified and distributed under the terms
|
||||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||||
|
|
||||||
import { AccountJson } from '@polkadot/extension-base/background/types';
|
import { AccountJson, AccountWithChildren } from '@polkadot/extension-base/background/types';
|
||||||
import { Chain } from '@polkadot/extension-chains/types';
|
import { Chain } from '@polkadot/extension-chains/types';
|
||||||
|
|
||||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
@ -29,6 +29,7 @@ interface Props {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
genesisHash?: string | null;
|
genesisHash?: string | null;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
|
parentName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Recoded {
|
interface Recoded {
|
||||||
@ -47,7 +48,7 @@ function findAccount (accounts: AccountJson[], publicKey: Uint8Array): AccountJs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// recodes an supplied address using the prefix/genesisHash, include the actual saved account & chain
|
// recodes an supplied address using the prefix/genesisHash, include the actual saved account & chain
|
||||||
function recodeAddress (address: string, accounts: AccountJson[], chain: Chain | null): Recoded {
|
function recodeAddress (address: string, accounts: AccountWithChildren[], chain: Chain | null): Recoded {
|
||||||
// decode and create a shortcut for the encoded address
|
// decode and create a shortcut for the encoded address
|
||||||
const publicKey = decodeAddress(address);
|
const publicKey = decodeAddress(address);
|
||||||
|
|
||||||
@ -65,8 +66,8 @@ function recodeAddress (address: string, accounts: AccountJson[], chain: Chain |
|
|||||||
|
|
||||||
const ACCOUNTS_SCREEN_HEIGHT = 500;
|
const ACCOUNTS_SCREEN_HEIGHT = 500;
|
||||||
|
|
||||||
function Address ({ actions, address, children, className, genesisHash, name }: Props): React.ReactElement<Props> {
|
function Address ({ actions, address, children, className, genesisHash, name, parentName }: Props): React.ReactElement<Props> {
|
||||||
const accounts = useContext(AccountContext);
|
const { accounts } = useContext(AccountContext);
|
||||||
const chain = useMetadata(genesisHash);
|
const chain = useMetadata(genesisHash);
|
||||||
const [{ account, formatted, prefix }, setRecoded] = useState<Recoded>({ account: null, formatted: null, prefix: 42 });
|
const [{ account, formatted, prefix }, setRecoded] = useState<Recoded>({ account: null, formatted: null, prefix: 42 });
|
||||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||||
@ -111,9 +112,8 @@ function Address ({ actions, address, children, className, genesisHash, name }:
|
|||||||
<Name>{name || (account && account.name) || '<unknown>'}</Name>
|
<Name>{name || (account && account.name) || '<unknown>'}</Name>
|
||||||
<CopyAddress>
|
<CopyAddress>
|
||||||
<FullAddress>{formatted || '<unknown>'}</FullAddress>
|
<FullAddress>{formatted || '<unknown>'}</FullAddress>
|
||||||
{chain?.genesisHash && (
|
{chain?.genesisHash && <ChainBanner>{chain.name}</ChainBanner>}
|
||||||
<Banner>{chain.name}</Banner>
|
{parentName && <ParentBanner>↳ {parentName}</ParentBanner>}
|
||||||
)}
|
|
||||||
<CopyToClipboard text={(formatted && formatted) || ''} >
|
<CopyToClipboard text={(formatted && formatted) || ''} >
|
||||||
<Svg
|
<Svg
|
||||||
onClick={_onCopy}
|
onClick={_onCopy}
|
||||||
@ -241,7 +241,6 @@ const ActiveActionsIcon = styled(Svg).attrs(() => ({
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Banner = styled.div`
|
const Banner = styled.div`
|
||||||
background: ${({ theme }): string => theme.primaryColor};
|
|
||||||
border-radius: 0 0 8px 8px;
|
border-radius: 0 0 8px 8px;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -252,6 +251,14 @@ const Banner = styled.div`
|
|||||||
top: 0;
|
top: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ChainBanner = styled(Banner)`
|
||||||
|
background: ${({ theme }): string => theme.primaryColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ParentBanner = styled(Banner)`
|
||||||
|
background: ${({ theme }): string => theme.parentLabelColor};
|
||||||
|
`;
|
||||||
|
|
||||||
const MovableMenu = styled(Menu) <{ isMoved: boolean }>`
|
const MovableMenu = styled(Menu) <{ isMoved: boolean }>`
|
||||||
${({ isMoved }): string => isMoved ? 'bottom: 50px' : ''};
|
${({ isMoved }): string => isMoved ? 'bottom: 50px' : ''};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// This software may be modified and distributed under the terms
|
// This software may be modified and distributed under the terms
|
||||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||||
|
|
||||||
import { AccountJson, AuthorizeRequest, MetadataRequest, SigningRequest } from '@polkadot/extension-base/background/types';
|
import { AccountsContext, AuthorizeRequest, MetadataRequest, SigningRequest } from '@polkadot/extension-base/background/types';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AvailableThemes } from '.';
|
import { AvailableThemes } from '.';
|
||||||
@ -10,7 +10,10 @@ import { AvailableThemes } from '.';
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const noop = (): void => undefined;
|
const noop = (): void => undefined;
|
||||||
|
|
||||||
const AccountContext = React.createContext<AccountJson[]>([]);
|
const AccountContext = React.createContext<AccountsContext>({
|
||||||
|
accounts: [],
|
||||||
|
hierarchy: []
|
||||||
|
});
|
||||||
const ActionContext = React.createContext<(to?: string) => void>(noop);
|
const ActionContext = React.createContext<(to?: string) => void>(noop);
|
||||||
const AuthorizeReqContext = React.createContext<AuthorizeRequest[]>([]);
|
const AuthorizeReqContext = React.createContext<AuthorizeRequest[]>([]);
|
||||||
const MediaContext = React.createContext<boolean>(false);
|
const MediaContext = React.createContext<boolean>(false);
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const darkTheme = {
|
|||||||
labelFontSize: '13px',
|
labelFontSize: '13px',
|
||||||
labelLineHeight: '18px',
|
labelLineHeight: '18px',
|
||||||
lineHeight: '26px',
|
lineHeight: '26px',
|
||||||
|
parentLabelColor: '#4A7463',
|
||||||
popupBackground: '#38393F',
|
popupBackground: '#38393F',
|
||||||
primaryColor: '#FF7D01',
|
primaryColor: '#FF7D01',
|
||||||
readonlyInputBackground: '#1A1B20',
|
readonlyInputBackground: '#1A1B20',
|
||||||
@ -59,6 +60,7 @@ const lightTheme: Theme = {
|
|||||||
inputBackground: '#FFFFFF',
|
inputBackground: '#FFFFFF',
|
||||||
inputBorderColor: '#DDE1EB',
|
inputBorderColor: '#DDE1EB',
|
||||||
labelColor: '#333333',
|
labelColor: '#333333',
|
||||||
|
parentLabelColor: '#215B4F',
|
||||||
popupBackground: '#FFFFFF',
|
popupBackground: '#FFFFFF',
|
||||||
readonlyInputBackground: '#FFF',
|
readonlyInputBackground: '#FFF',
|
||||||
subTextColor: '#454545',
|
subTextColor: '#454545',
|
||||||
|
|||||||
@ -19,7 +19,7 @@ interface Props {
|
|||||||
const isNameValid = isNotShorterThan(3, 'Account name is too short');
|
const isNameValid = isNotShorterThan(3, 'Account name is too short');
|
||||||
|
|
||||||
export default function Name ({ address, className, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
|
export default function Name ({ address, className, isFocused, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
|
||||||
const accounts = useContext(AccountContext);
|
const { accounts } = useContext(AccountContext);
|
||||||
const account = accounts.find((account): boolean => account.address === address);
|
const account = accounts.find((account): boolean => account.address === address);
|
||||||
const startValue = account && account.name;
|
const startValue = account && account.name;
|
||||||
|
|
||||||
|
|||||||
59
packages/extension-ui/src/utils/buildHierarchy.test.ts
Normal file
59
packages/extension-ui/src/utils/buildHierarchy.test.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2019-2020 @polkadot/extension-ui 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 { AccountJson, AccountWithChildren } from '@polkadot/extension-base/background/types';
|
||||||
|
import { buildHierarchy } from './buildHierarchy';
|
||||||
|
|
||||||
|
const testHierarchy = (accounts: AccountJson[], expected: AccountWithChildren[]): void => {
|
||||||
|
expect(buildHierarchy(accounts)).toEqual(expected);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Use Account Hierarchy', () => {
|
||||||
|
const acc = (address: string, parentAddress?: string, derivationPath?: string): {
|
||||||
|
address: string;
|
||||||
|
derivationPath?: string;
|
||||||
|
parentAddress?: string;
|
||||||
|
} => ({
|
||||||
|
address,
|
||||||
|
derivationPath,
|
||||||
|
parentAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
test('for empty account list, returns empty list', () => {
|
||||||
|
testHierarchy([], []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns one account', () => {
|
||||||
|
testHierarchy([acc('a')], [acc('a')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('puts child account into children field of parent: single child', () => {
|
||||||
|
testHierarchy([acc('a'), acc('b', 'a')], [
|
||||||
|
{ address: 'a', children: [acc('b', 'a')] }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('puts child account into children field of parent: more children', () => {
|
||||||
|
testHierarchy([acc('a'), acc('b', 'a'), acc('c', 'a')], [
|
||||||
|
{ address: 'a', children: [acc('b', 'a'), acc('c', 'a')] }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('puts child account into children field of parent: 2 roots', () => {
|
||||||
|
testHierarchy([acc('a'), acc('b', 'a'), acc('c', 'a'), acc('d')], [
|
||||||
|
{ address: 'a', children: [acc('b', 'a'), acc('c', 'a')] },
|
||||||
|
acc('d')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles grandchildren', () => {
|
||||||
|
testHierarchy([acc('a'), acc('b', 'a'), acc('c', 'b')], [{
|
||||||
|
address: 'a',
|
||||||
|
children: [{
|
||||||
|
...acc('b', 'a'),
|
||||||
|
children: [acc('c', 'b')]
|
||||||
|
}]
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
packages/extension-ui/src/utils/buildHierarchy.ts
Normal file
28
packages/extension-ui/src/utils/buildHierarchy.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2019-2020 @polkadot/extension-ui 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 { AccountJson, AccountWithChildren } from '@polkadot/extension-base/background/types';
|
||||||
|
|
||||||
|
const isRoot = (account: AccountJson): boolean => !account.parentAddress;
|
||||||
|
const isChild = (parent: AccountJson) => (account: AccountJson): boolean => account.parentAddress === parent.address;
|
||||||
|
|
||||||
|
export const accountWithChildren = (allAccounts: AccountJson[]) => (account: AccountJson): AccountWithChildren => {
|
||||||
|
const children = allAccounts
|
||||||
|
.filter(isChild(account))
|
||||||
|
.map(accountWithChildren(allAccounts));
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
children
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildHierarchy = (accounts: AccountJson[]): AccountWithChildren[] =>
|
||||||
|
accounts
|
||||||
|
.filter(isRoot)
|
||||||
|
.map(accountWithChildren(accounts));
|
||||||
Loading…
Reference in New Issue
Block a user