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