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:
@@ -303,7 +303,11 @@ export default class Extension {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { InjectedAccount, MetadataDef, ProviderList, ProviderMeta, InjectedMetadataKnown } from '@polkadot/extension-inject/types';
|
||||
import { JsonRpcResponse } from '@polkadot/rpc-provider/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 { 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 interface AccountJson {
|
||||
export interface AccountJson extends KeyringPair$Meta {
|
||||
address: string;
|
||||
genesisHash?: string | null;
|
||||
isExternal?: boolean;
|
||||
name?: string;
|
||||
parentAddress?: string;
|
||||
}
|
||||
|
||||
export type AccountWithChildren = AccountJson & {
|
||||
children?: AccountWithChildren[];
|
||||
}
|
||||
|
||||
export type AccountsContext = {
|
||||
accounts: AccountJson[];
|
||||
hierarchy: AccountWithChildren[];
|
||||
}
|
||||
|
||||
export interface AuthorizeRequest {
|
||||
|
||||
@@ -13,10 +13,11 @@ import { Name } from '../../partials';
|
||||
|
||||
interface Props extends AccountJson {
|
||||
address: string;
|
||||
parentName?: 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 [isEditing, setEditing] = useState(false);
|
||||
const [editedName, setName] = useState<string | null>(null);
|
||||
@@ -61,6 +62,7 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
|
||||
)}
|
||||
address={address}
|
||||
name={editedName}
|
||||
parentName={parentName}
|
||||
>
|
||||
{isEditing && (
|
||||
<Name
|
||||
|
||||
@@ -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.
|
||||
|
||||
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 QrImage from '../../assets/qr.svg';
|
||||
import AccountsTree from './AccountsTree';
|
||||
import { AccountContext, Button, Header, MediaContext, AddAccount, ButtonArea, Svg, ButtonWithSubtitle } from '../../components';
|
||||
|
||||
type Props = {};
|
||||
|
||||
export default function Accounts (): React.ReactElement<Props> {
|
||||
const accounts = useContext(AccountContext);
|
||||
const { hierarchy } = useContext(AccountContext);
|
||||
const mediaAllowed = useContext(MediaContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(accounts.length === 0)
|
||||
{(hierarchy.length === 0)
|
||||
? <AddAccount />
|
||||
: (
|
||||
<>
|
||||
@@ -26,8 +26,8 @@ export default function Accounts (): React.ReactElement<Props> {
|
||||
text={'Accounts'}
|
||||
/>
|
||||
<AccountsArea>
|
||||
{accounts.map((json, index): React.ReactNode => (
|
||||
<Account
|
||||
{hierarchy.map((json, index): React.ReactNode => (
|
||||
<AccountsTree
|
||||
{...json}
|
||||
key={`${index}:${json.address}`}
|
||||
/>
|
||||
|
||||
@@ -8,9 +8,9 @@ import Adapter from 'enzyme-adapter-react-16';
|
||||
import { configure, mount, ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
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 { 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 Signing from '.';
|
||||
import TransactionIndex from './TransactionIndex';
|
||||
@@ -25,17 +25,6 @@ describe.skip('Signing requests', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
let onActionStub: jest.Mock;
|
||||
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();
|
||||
|
||||
@@ -54,11 +43,9 @@ describe.skip('Signing requests', () => {
|
||||
const mountComponent = async (): Promise<void> => {
|
||||
wrapper = mount(
|
||||
<ActionContext.Provider value={onActionStub}>
|
||||
<AccountContext.Provider value={accounts}>
|
||||
<ThemeProvider theme={themes.dark}>
|
||||
<MockRequestsProvider />
|
||||
</ThemeProvider>
|
||||
</AccountContext.Provider>
|
||||
<ThemeProvider theme={themes.dark}>
|
||||
<MockRequestsProvider />
|
||||
</ThemeProvider>
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
await act(flushAllPromises);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Loading } from '../components';
|
||||
import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SigningReqContext } from '../components/contexts';
|
||||
import ToastProvider from '../components/Toast/ToastProvider';
|
||||
import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging';
|
||||
import { buildHierarchy } from '../utils/buildHierarchy';
|
||||
import Accounts from './Accounts';
|
||||
import Authorize from './Authorize';
|
||||
import CreateAccount from './CreateAccount';
|
||||
@@ -96,7 +97,7 @@ export default function Popup (): React.ReactElement<{}> {
|
||||
return (
|
||||
<Loading>{accounts && authRequests && metaRequests && signRequests && (
|
||||
<ActionContext.Provider value={_onAction}>
|
||||
<AccountContext.Provider value={accounts}>
|
||||
<AccountContext.Provider value={{ accounts, hierarchy: buildHierarchy(accounts) }}>
|
||||
<AuthorizeReqContext.Provider value={authRequests}>
|
||||
<MediaContext.Provider value={cameraOn && mediaAllowed}>
|
||||
<MetadataReqContext.Provider value={metaRequests}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// 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 React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
@@ -29,6 +29,7 @@ interface Props {
|
||||
children?: React.ReactNode;
|
||||
genesisHash?: string | null;
|
||||
actions?: React.ReactNode;
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
const publicKey = decodeAddress(address);
|
||||
|
||||
@@ -65,8 +66,8 @@ function recodeAddress (address: string, accounts: AccountJson[], chain: Chain |
|
||||
|
||||
const ACCOUNTS_SCREEN_HEIGHT = 500;
|
||||
|
||||
function Address ({ actions, address, children, className, genesisHash, name }: Props): React.ReactElement<Props> {
|
||||
const accounts = useContext(AccountContext);
|
||||
function Address ({ actions, address, children, className, genesisHash, name, parentName }: Props): React.ReactElement<Props> {
|
||||
const { accounts } = useContext(AccountContext);
|
||||
const chain = useMetadata(genesisHash);
|
||||
const [{ account, formatted, prefix }, setRecoded] = useState<Recoded>({ account: null, formatted: null, prefix: 42 });
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||
@@ -111,9 +112,8 @@ function Address ({ actions, address, children, className, genesisHash, name }:
|
||||
<Name>{name || (account && account.name) || '<unknown>'}</Name>
|
||||
<CopyAddress>
|
||||
<FullAddress>{formatted || '<unknown>'}</FullAddress>
|
||||
{chain?.genesisHash && (
|
||||
<Banner>{chain.name}</Banner>
|
||||
)}
|
||||
{chain?.genesisHash && <ChainBanner>{chain.name}</ChainBanner>}
|
||||
{parentName && <ParentBanner>↳ {parentName}</ParentBanner>}
|
||||
<CopyToClipboard text={(formatted && formatted) || ''} >
|
||||
<Svg
|
||||
onClick={_onCopy}
|
||||
@@ -241,7 +241,6 @@ const ActiveActionsIcon = styled(Svg).attrs(() => ({
|
||||
`;
|
||||
|
||||
const Banner = styled.div`
|
||||
background: ${({ theme }): string => theme.primaryColor};
|
||||
border-radius: 0 0 8px 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
@@ -252,6 +251,14 @@ const Banner = styled.div`
|
||||
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 }>`
|
||||
${({ isMoved }): string => isMoved ? 'bottom: 50px' : ''};
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// 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 { AvailableThemes } from '.';
|
||||
@@ -10,7 +10,10 @@ import { AvailableThemes } from '.';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
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 AuthorizeReqContext = React.createContext<AuthorizeRequest[]>([]);
|
||||
const MediaContext = React.createContext<boolean>(false);
|
||||
|
||||
@@ -34,6 +34,7 @@ const darkTheme = {
|
||||
labelFontSize: '13px',
|
||||
labelLineHeight: '18px',
|
||||
lineHeight: '26px',
|
||||
parentLabelColor: '#4A7463',
|
||||
popupBackground: '#38393F',
|
||||
primaryColor: '#FF7D01',
|
||||
readonlyInputBackground: '#1A1B20',
|
||||
@@ -59,6 +60,7 @@ const lightTheme: Theme = {
|
||||
inputBackground: '#FFFFFF',
|
||||
inputBorderColor: '#DDE1EB',
|
||||
labelColor: '#333333',
|
||||
parentLabelColor: '#215B4F',
|
||||
popupBackground: '#FFFFFF',
|
||||
readonlyInputBackground: '#FFF',
|
||||
subTextColor: '#454545',
|
||||
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
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> {
|
||||
const accounts = useContext(AccountContext);
|
||||
const { accounts } = useContext(AccountContext);
|
||||
const account = accounts.find((account): boolean => account.address === address);
|
||||
const startValue = account && account.name;
|
||||
|
||||
|
||||
@@ -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')]
|
||||
}]
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user