Display child when parent has been removed (#349)

* Display child when parent has been removed

* Update packages/extension-ui/src/Popup/index.tsx

* Small styling updates
This commit is contained in:
Jaco Greeff
2020-05-31 10:51:29 +02:00
committed by GitHub
parent dceb530b30
commit db0b7e0eb3
11 changed files with 133 additions and 103 deletions
+3 -1
View File
@@ -1,11 +1,13 @@
# CHANGELOG
## 0.26.0-beta.11
## 0.26.0-beta.x
- Add the ability to import JSON keystore files (Thanks to https://github.com/shawntabrizi)
- updated to derivation documentation (Thanks to https://github.com/EthWorks)
- Adjust allowed mnemonic seed strengths, 12, 15, 18, 21 & 24 all allowed
- Remove duplication with Default/Substrate prefixes in dropdown (equivalent, only generic displayed)
- Display child accounts when no parent has been found (orphans)
- Display derived suri alongside parent account names
## 0.25.1 May 14, 2020
@@ -344,7 +344,8 @@ export default class Extension {
const childPair = this.derive(parentAddress, suri, parentPassword, {
genesisHash,
name,
parentAddress
parentAddress,
suri
});
keyring.addPair(childPair, password);
@@ -29,6 +29,7 @@ export interface AccountJson extends KeyringPair$Meta {
isExternal?: boolean;
name?: string;
parentAddress?: string;
suri?: string;
whenCreated?: number;
}
@@ -33,19 +33,19 @@ describe('Account component', () => {
wrapper = mountAccountComponent({ isExternal: false });
wrapper.find('Details').simulate('click');
expect(wrapper.find('MenuItem').length).toBe(4);
expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('MenuItem').at(1).text()).toBe('Derive New Account');
expect(wrapper.find('MenuItem').at(2).text()).toBe('Export Account');
expect(wrapper.find('MenuItem').at(3).text()).toBe('Forget Account');
expect(wrapper.find('a.menuItem').length).toBe(4);
expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Derive New Account');
expect(wrapper.find('a.menuItem').at(2).text()).toBe('Export Account');
expect(wrapper.find('a.menuItem').at(3).text()).toBe('Forget Account');
});
it('does not show Export option if account is external', () => {
wrapper = mountAccountComponent({ isExternal: true });
wrapper.find('Details').simulate('click');
expect(wrapper.find('MenuItem').length).toBe(2);
expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('MenuItem').at(1).text()).toBe('Forget Account');
expect(wrapper.find('a.menuItem').length).toBe(2);
expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('a.menuItem').at(1).text()).toBe('Forget Account');
});
});
@@ -5,7 +5,7 @@
import { AccountJson } from '@polkadot/extension-base/background/types';
import { ThemeProps } from '../../types';
import React, { useCallback, useContext, useState } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import styled from 'styled-components';
import { ActionContext, Address, Link } from '../../components';
@@ -13,12 +13,11 @@ import { editAccount } from '../../messaging';
import { Name } from '../../partials';
interface Props extends AccountJson {
address: string;
parentName?: string;
className?: string;
parentName?: string;
}
function Account ({ address, className, isExternal, parentName }: Props): React.ReactElement<Props> {
function Account ({ address, className, isExternal, parentName, suri }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
const [isEditing, setEditing] = useState(false);
const [editedName, setName] = useState<string | null>(null);
@@ -27,48 +26,63 @@ function Account ({ address, className, isExternal, parentName }: Props): React.
const _saveChanges = useCallback((): void => {
if (editedName && editedName !== name) {
editAccount(address, editedName)
.then((): void => onAction())
.catch((error: Error) => console.error(error));
.then(() => onAction())
.catch(console.error);
}
_toggleEdit();
}, [editedName, address, _toggleEdit, onAction]);
const _actions = useMemo(() => (
<>
<Link
className='menuItem'
onClick={_toggleEdit}
>
Rename
</Link>
{!isExternal && (
<Link
className='menuItem'
to={`/account/derive/${address}/locked`}
>
Derive New Account
</Link>
)}
<div className='divider' />
{!isExternal && (
<Link
className='menuItem'
isDanger
to={`/account/export/${address}`}
>
Export Account
</Link>
)}
<Link
className='menuItem'
isDanger
to={`/account/forget/${address}`}
>
Forget Account
</Link>
</>
), [_toggleEdit, address, isExternal]);
return (
<div className={className}>
<Address
actions={(
<>
<MenuGroup>
<MenuItem onClick={_toggleEdit}>Rename</MenuItem>
{!isExternal && (
<MenuItem to={`/account/derive/${address}/locked`}>Derive New Account</MenuItem>
)}
</MenuGroup>
{!isExternal && (
<MenuItem
isDanger
to={`/account/export/${address}`}
>
Export Account
</MenuItem>
)}
<MenuItem
isDanger
to={`/account/forget/${address}`}
>
Forget Account
</MenuItem>
</>
)}
actions={_actions}
address={address}
className='address'
name={editedName}
parentName={parentName}
suri={suri}
>
{isEditing && (
<Name
address={address}
className='edit-name'
className='editName'
isFocused
label={' '}
onBlur={_saveChanges}
@@ -80,33 +94,31 @@ function Account ({ address, className, isExternal, parentName }: Props): React.
);
}
const MenuGroup = styled.div`
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid ${({ theme }: ThemeProps): string => theme.boxBorderColor};
`;
const MenuItem = styled(Link)`
padding: 4px 16px;
display: block;
border-radius: 8px;
font-weight: 600;
font-size: 15px;
line-height: 20px;
`;
MenuItem.displayName = 'MenuItem';
export default styled(Account)`
${Address} {
export default styled(Account)(({ theme }: ThemeProps) => `
.address {
margin-bottom: 8px;
}
.edit-name {
.divider {
padding-top: 16px;
margin-bottom: 16px;
border-bottom: 1px solid ${theme.inputBorderColor};
}
.editName {
position: absolute;
flex: 1;
left: 80px;
top: 6px;
width: 315px;
}
`;
.menuItem {
border-radius: 8px;
display: block;
font-weight: 600;
font-size: 15px;
line-height: 20px;
padding: 4px 16px;
}
`);
@@ -10,12 +10,13 @@ interface Props extends AccountWithChildren {
parentName?: string;
}
function AccountsTree ({ parentName, ...account }: Props): React.ReactElement<Props> {
export default function AccountsTree ({ parentName, suri, ...account }: Props): React.ReactElement<Props> {
return (
<>
<Account
{...account}
parentName={parentName}
suri={suri}
/>
{account?.children?.map((child, index) => (
<AccountsTree
@@ -27,5 +28,3 @@ function AccountsTree ({ parentName, ...account }: Props): React.ReactElement<Pr
</>
);
}
export default AccountsTree;
@@ -14,28 +14,38 @@ interface Props {
isLocked?: boolean;
}
interface AddressState {
address: string;
}
interface PathState extends AddressState {
suri: string;
}
interface ConfirmState {
account: PathState;
parentPassword: string;
}
function Derive ({ isLocked }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
const { address: parentAddress } = useParams<{ address: string }>();
const [account, setAccount] = useState<null | { address: string; suri: string }>(null);
const { address: parentAddress } = useParams<AddressState>();
const [account, setAccount] = useState<null | PathState>(null);
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null);
const [parentPassword, setParentPassword] = useState<string | null>(null);
const _onCreate = useCallback(async () => {
const _onCreate = useCallback(() => {
if (!account || !name || !password || !parentPassword) {
return;
}
await deriveAccount(parentAddress, account.suri, parentPassword, name, password);
onAction('/');
deriveAccount(parentAddress, account.suri, parentPassword, name, password)
.then(() => onAction('/'))
.catch(console.error);
}, [account, name, password, onAction, parentAddress, parentPassword]);
const _onDerivationConfirmed = useCallback(({ account, parentPassword }: {
account: { address: string; suri: string };
parentPassword: string;
}) => {
const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => {
setAccount(account);
setParentPassword(parentPassword);
}, []);
+1 -2
View File
@@ -53,8 +53,7 @@ async function requestMediaAccess (cameraOn: boolean): Promise<boolean> {
function initAccountContext (accounts: AccountJson[]): AccountsContext {
const hierarchy = buildHierarchy(accounts);
const isNotExternal = (account: AccountJson): boolean => !account.isExternal;
const master = hierarchy.find(isNotExternal);
const master = hierarchy.find((account) => !account.isExternal);
return {
accounts,
@@ -33,6 +33,7 @@ interface Props {
genesisHash?: string | null;
actions?: React.ReactNode;
parentName?: string | null;
suri?: string;
}
interface Recoded {
@@ -69,7 +70,7 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain:
const ACCOUNTS_SCREEN_HEIGHT = 500;
function Address ({ actions, address, children, className, genesisHash, name, parentName }: Props): React.ReactElement<Props> {
function Address ({ actions, address, children, className, genesisHash, name, parentName, suri }: 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 });
@@ -119,7 +120,7 @@ function Address ({ actions, address, children, className, genesisHash, name, pa
<>
<Banner>
<ArrowLabel/>
<ParentName data-field='parent'>{parentName}</ParentName>
<ParentName data-field='parent'>{parentName}{suri || ''}</ParentName>
</Banner>
<DisplacedName>{displayedName}</DisplacedName>
</>
@@ -38,20 +38,20 @@ function Link ({ children, className, onClick, to }: Props): React.ReactElement<
);
}
export default styled(Link)`
display: flex;
export default styled(Link)(({ isDanger, theme }: Props) => `
align-items: center;
color: ${({ isDanger, theme }: Props): string => isDanger ? theme.textColorDanger : theme.textColor};
color: ${isDanger ? theme.textColorDanger : theme.textColor};
display: flex;
opacity: 0.9;
text-decoration: none;
vertical-align: middle;
&:hover {
color: ${({ isDanger, theme }: Props): string => isDanger ? theme.textColorDanger : theme.textColor};
color: ${isDanger ? theme.textColorDanger : theme.textColor};
opacity: 1.0;
}
&:visited {
color: ${({ isDanger, theme }: Props): string => isDanger ? theme.textColorDanger : theme.textColor};
color: ${isDanger ? theme.textColorDanger : theme.textColor};
}
`;
`);
@@ -4,28 +4,33 @@
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;
const compareByCreationTime = (a: AccountJson, b: AccountJson): number => (a.whenCreated || Infinity) - (b.whenCreated || Infinity);
type ChildFilter = (account: AccountJson) => AccountWithChildren;
export const accountWithChildren = (allAccounts: AccountJson[]) => (account: AccountJson): AccountWithChildren => {
const children = allAccounts
.filter(isChild(account))
.map(accountWithChildren(allAccounts))
.sort(compareByCreationTime);
function compareByCreationTime (a: AccountJson, b: AccountJson): number {
return (a.whenCreated || Infinity) - (b.whenCreated || Infinity);
}
if (children.length === 0) {
return account;
}
export function accountWithChildren (accounts: AccountJson[]): ChildFilter {
return (account: AccountJson): AccountWithChildren => {
const children = accounts
.filter(({ parentAddress }) => account.address === parentAddress)
.map(accountWithChildren(accounts))
.sort(compareByCreationTime);
return {
...account,
children
return children.length === 0
? account
: { children, ...account };
};
};
}
export const buildHierarchy = (accounts: AccountJson[]): AccountWithChildren[] =>
accounts
.filter(isRoot)
export function buildHierarchy (accounts: AccountJson[]): AccountWithChildren[] {
return accounts
.filter(({ parentAddress }) =>
// it is a parent
!parentAddress ||
// we don't have a parent for this one
!accounts.some(({ address }) => parentAddress === address)
)
.map(accountWithChildren(accounts))
.sort(compareByCreationTime);
}