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:
+3
-1
@@ -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);
|
||||
}, []);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user